الگوی طراحی نما (Facade) در C#

توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.

توضیح دوات 2: Facade یک کلمه فرانسوی به معنای نما (مثلا نمای ساختمان) یا ظاهر است. در انگلیسی، کلمات وارداتی معمولا به صورت شکل تلفظ اصلی حفظ می‌شوند، هر چند در طول زمان ممکن است تغییرات کوچکی بکنند. در فرانسه: فاساد (آ اول به صورت کوتاه و دومی بلند) و در انگلیسی: فساد (آ به صورت بلند تلفظ می‌شود). برای دانستن بیشتر در این مورد به گوگل مراجعه کنید.

الگوی طراحی Facade یک الگوی ساختاری است که یک رابط ساده‌شده برای یک کتابخانه، فریم‌ورک یا هر مجموعه پیچیده‌ای از کلاس‌ها ارائه می‌دهد.

ایده اصلی پشت الگوی طراحی Facade این است که مجموعه‌ای از رابط‌ها برای یک سیستم پیچیده ایجاد شود که استفاده از آن آسان‌تر باشد. Facade کار با یک زیربخش را ساده‌تر می‌کند، زیرا یک رابط سطح بالا برای آن ارائه می‌دهد. این رابط سطح بالا برای پوشش تمام موارد استفاده سیستم پیچیده طراحی نشده است، بلکه فقط موارد مهم را در بر می‌گیرد. هرچند الگوی طراحی Facade کار با رابط را آسان‌تر می‌کند، اما معمولاً امکان استفاده مستقیم از سیستم پیچیده همچنان وجود دارد.

می‌توانید کد نمونه این مطلب را در GitHub پیدا کنید.

مفهوم‌سازی مسئله

معمولاً برنامه‌ها برای انجام وظایف خاص از چندین کتابخانه شخص ثالث یا رابط‌های برنامه‌نویسی کاربردی (APIs) استفاده می‌کنند. این کتابخانه‌ها و APIها ممکن است توسط گروه‌ها یا تیم‌های مختلفی ایجاد شده باشند و بنابراین رابط‌ها، ساختارهای داده و روش‌های کاری متفاوتی داشته باشند.

زمانی که یک برنامه باید به‌صورت همزمان از چندین کتابخانه یا API استفاده کند، مدیریت آن بسیار پیچیده و دشوار می‌شود. هر کتابخانه ممکن است مجموعه‌ای از کلاس‌ها و متدهای خود را داشته باشد که باید به شیوه خاصی استفاده شوند، و ممکن است تعاملات پیچیده‌ای میان کتابخانه‌ها وجود داشته باشد که نیاز به مدیریت دارند.

با پیچیده‌تر شدن زیربخش، تغییر و نگهداری کد نیز سخت‌تر می‌شود. هنگام ایجاد تغییرات، ممکن است باگ‌ها و خطاهایی به وجود بیاید و تست و رفع مشکلات کد دشوار شود. همچنین، زمان لازم برای مدیریت تعاملات اجزای زیربخش می‌تواند عملکرد کد را تحت تأثیر قرار دهد.

در حالت عادی، باید تمام این اشیاء را مقداردهی اولیه کنیم، تمام وابستگی‌ها را دنبال کنیم، متدها را به ترتیب صحیح اجرا کنیم و غیره. این کار باعث می‌شود که کد ما به‌شدت به کد کتابخانه وابسته باشد. وابستگی زیاد زمانی اتفاق می‌افتد که دو یا چند بخش به‌شدت به یکدیگر وابسته باشند. این امر تغییر یا جایگزینی یک بخش را بدون تغییر دیگر بخش‌ها دشوار می‌کند.

با ایجاد یک Facade، می‌توانیم راه ساده‌تری برای تعامل با زیربخش ارائه دهیم و در عین حال پیچیدگی آن را پنهان کنیم. این کار نه تنها استفاده از سیستم را برای کاربران آسان‌تر می‌کند، بلکه نگهداری و به‌روزرسانی سیستم را نیز تسهیل می‌کند. همچنین، پنهان کردن زیربخش پشت یک Facade کنترل و مدیریت دسترسی به عملکردهای آن را آسان‌تر می‌کند و تضمین می‌کند که عملکردها به درستی و ایمن استفاده می‌شوند.

ساختار الگوی طراحی Facade

در پیاده‌سازی پایه‌ای، الگوی طراحی Facade شامل چهار نقش اصلی است:

  1. Facade: این نقش دسترسی سریع به یک جنبه خاص از عملکرد زیربخش را فراهم می‌کند. این نقش درخواست مشتری را هدایت کرده و نحوه کار با تمام اجزای متحرک را درک می‌کند.
  2. Additional Facade: برای جلوگیری از پیچیده شدن یک Facade با ویژگی‌های نامرتبط که ممکن است به یک ساختار پیچیده دیگر منجر شود، می‌توان یک کلاس Additional Facade ایجاد کرد. مشتریان و دیگر Facadeها می‌توانند از این Facade اضافی استفاده کنند.
  3. Complex Subsystem: زیربخش پیچیده از ده‌ها شیء مختلف تشکیل شده است. برای اینکه این اشیاء کاری مفید انجام دهند، باید به عمق جزئیات پیاده‌سازی زیربخش رفت، مانند مقداردهی اولیه اشیاء به ترتیب صحیح و ارائه داده‌ها در فرمت مناسب. وجود Facade برای کلاس‌های زیربخش ناشناخته است. این کلاس‌ها در سیستم کار می‌کنند و مستقیماً با یکدیگر تعامل دارند.
  4. Client: به‌جای فراخوانی مستقیم اشیاء زیربخش، Client از Facade استفاده می‌کند.

دیاگرام کلاس الگوی طراحی Facade

برای نشان دادن نحوه کار الگوی Facade، قصد داریم یک زیربخش نسبتاً پیچیده ایجاد کنیم. فرض کنید یک برنامه داریم که مجموعه‌ای از رابط‌ها برای دسترسی به پایگاه داده‌های MySql/MS/Mongo و تولید انواع مختلف گزارش‌ها مانند گزارش‌های HTML، PDF و غیره ارائه می‌دهد. در نتیجه، رابط‌های مختلفی برای کار با انواع مختلف پایگاه داده خواهیم داشت. این رابط‌ها اکنون می‌توانند توسط یک برنامه مشتری برای دریافت اتصال پایگاه داده موردنیاز و تولید گزارش‌ها استفاده شوند. با این حال، با افزایش پیچیدگی یا گیج‌کننده شدن نام رفتارهای رابط، برنامه‌های مشتری در مدیریت آن دچار مشکل می‌شوند. بنابراین، برای کمک به برنامه مشتری، می‌توانیم از الگوی طراحی Facade استفاده کرده و یک رابط پوششی بر روی رابط موجود ارائه دهیم.

ابتدا، نقش زیربخش پیچیده خود را ایجاد خواهیم کرد. زیربخش ما شامل سه متد کمکی است که هر یک مربوط به یک ارائه‌دهنده پایگاه داده مختلف هستند. یک تابع کمکی شامل قابلیت اتصال به پایگاه داده و تولید گزارش‌ها در قالب PDF و HTML خواهد بود.

با در نظر گرفتن طبیعت پیچیده و طاقت‌فرسای عملیات موردنیاز، اجرای کامل این موارد برای اهداف نمایشی به یک کار دشوار و غیرعملی تبدیل می‌شود. به جای آن، از شبیه‌سازی‌های پیشرفته و دقیق استفاده خواهیم کرد و از نیاز به نصب مجموعه بزرگی از بسته‌های NuGet، راه‌اندازی و پیکربندی چندین پایگاه داده، ایجاد داده‌های قابل‌قبول و طراحی یک مکانیزم صادرکننده قوی اجتناب خواهیم کرد. ورود به چنین پروژه‌ای اساساً به منزله یک پروژه مستقل و جامع خواهد بود.

به عبارت دیگر، این عملیات فراتر از اهداف ما در اینجا هستند. بنابراین، به جای ورود به این مسیر پرپیچ‌وخم، تمرکز خود را روی عملکرد اصلی گذاشته و از شبیه‌سازی‌های کاربردی استفاده خواهیم کرد.

ابتدا کلاس MySQLHelper خود را ایجاد می‌کنیم:

				
					public class MySqlHelper
{
    public static SqlConnection GetMySqlDBConnection(string connectionString)
    {
        // Generate a mysql db connection using connection parameters
        Console.WriteLine("Successfully created a MySQL database connection.");
        return new SqlConnection();
    }

    public static void GenerateMySqlPDFReport(SqlConnection connection, string tableName)
    {
        // Get data from the table and generate a pdf report
        Console.WriteLine($"Successfully generated a pdf report with the contents of {tableName}");
    }

    public static void GenerateMySqlHTMLReport(SqlConnection connection, string tableName)
    {
        // Get data from the table and generate an html report
        Console.WriteLine($"Successfully generated a report with the contents of {tableName} and exported them to HTML");
    }
}

				
			

همان‌طور که مشاهده می‌کنید، کلاس کمکی ما سه متد شبیه‌سازی‌شده دارد. متد GetMySqlDBConnection یک اتصال ایجاد می‌کند. معمولاً برای این کار به کتابخانه MySql.Data.MySqlClient و کلاس MySqlConnection نیاز است، اما برای ساده‌سازی، از یک دستور ساده در کنسول استفاده کرده‌ایم. متد GenerateMySqlPDFReport یک فایل PDF شامل داده‌های یک جدول مشخص ایجاد می‌کند. باز هم، برای این کار معمولاً به یک بسته NuGet مانند iTextSharp نیاز است، اما به‌منظور ساده‌سازی فقط یک پیام ثبت می‌کنیم. در نهایت، متد GenerateMySqlHTMLReport یک گزارش HTML شامل داده‌های جدول مشخص‌شده تولید می‌کند. بار دیگر، برای این کار معمولاً به چیزی مانند DataTables نیاز است، اما ما از این مرحله گذر می‌کنیم.

ما همچنین کلاس‌های مشابهی برای اتصالات MSSQL و MongoDB داریم. مانند موارد بالا، تمام متدها شبیه‌سازی شده‌اند.

				
					public class MSSqlHelper
{
    public static SqlConnection GetDBConnection(string connectionString)
    {
        // Generate a MSSql db connection using connection parameters
        Console.WriteLine("Successfully created a MSSql database connection.");
        return new SqlConnection();
    }

    public static void GenerateMSSqlPDFReport(SqlConnection connection, string tableName)
    {
        // Get data from the table and generate a pdf report
        Console.WriteLine($"Successfully generated a pdf report with the contents of {tableName}");
    }

    public static void GenerateMSSqlHTMLReport(SqlConnection connection, string tableName)
    {
        // Get data from the table and generate an html report
        Console.WriteLine($"Successfully generated a report with the contents of {tableName} and exported them to HTML");
    }
}

				
			

و هلپر Mongo:

				
					public class MongoDBHelper
{
    public static SqlConnection GetDBConnection(string connectionString)
    {
        // Generate a mongodb connection using connection parameters
        Console.WriteLine("Successfully created a MySQL database connection.");
        return new SqlConnection();
    }

    public static void GenerateMongoPDFReport(SqlConnection connection, string tableName)
    {
        // Get data from the table and generate a pdf report
        Console.WriteLine($"Successfully generated a pdf report with the contents of {tableName}");
    }

    public static void GenerateMongoHTMLReport(SqlConnection connection, string tableName)
    {
        // Get data from the table and generate an html report
        Console.WriteLine($"Successfully generated a report with the contents of {tableName} and exported them to HTML");
    }
}
				
			

اکنون، بدون استفاده از Facade، برای تولید یک گزارش باید ابتدا یک اتصال ایجاد کنیم، سپس از این اتصال به‌عنوان یک آرگومان برای متد، همراه با نام جدول موردنظر برای گزارش استفاده کنیم و همین‌طور ادامه دهیم. ممکن است همچنین نیاز داشته باشیم تا دسترسی به سیستم فایل را بررسی کنیم، فایل و احتمالاً دایرکتوری مربوطه را ایجاد کنیم. تولید گزارش نیازمند هماهنگی دقیق میان اجزای متعدد است تا همه چیز مطابق انتظار کار کند و بدتر از آن، این فرآیند باید هر بار که به یک گزارش از پایگاه داده نیاز داریم، تکرار شود. این نکته را هم در نظر بگیرید که این فقط برای یک DB Provider است.

در ادامه مثالی از نحوه تولید گزارش بدون استفاده از Facade آمده است:

				
					SqlConnection connection;  

// Generate reports without facade  
connection = MySqlHelper.GetMySqlDBConnection("connectionString");  
MySqlHelper.GenerateMySqlHTMLReport(connection, "sample-MySQL");  
MySqlHelper.GenerateMySqlPDFReport(connection, "sample-MySQL");  

connection = MSSqlHelper.GetDBConnection("connectionString");  
MSSqlHelper.GenerateMSSqlHTMLReport(connection, "sample-MSSql");  
MSSqlHelper.GenerateMSSqlPDFReport(connection, "sample-MSSql");  

connection = MongoDBHelper.GetDBConnection("connectionString");  
MongoDBHelper.GenerateMongoHTMLReport(connection, "sample-Mongo");  
MongoDBHelper.GenerateMongoPDFReport(connection, "sample-Mongo");
				
			

حالا شرکت‌کننده Facade را پیاده‌سازی می‌کنیم:

				
					public class HelperFacade
{
    public static void GenerateReport(DBConnector connector, ReportType reportType, string tableName)
    {
        var connectorRegistry = new Dictionary<DBConnector, Func<string, IDbConnection>>()
        {
            { DBConnector.MYSQL, MySqlHelper.GetDBConnection },
            { DBConnector.MSSQL, MSSqlHelper.GetDBConnection },
            { DBConnector.MONGO, MongoDBHelper.GetDBConnection }
        };

        var generatorRegistry = new Dictionary<ReportType, Action<IDbConnection, DBConnector, string>>()
        {
            { ReportType.HTML, GenerateHTMLReport },
            { ReportType.PDF, GeneratePDFReport }
        };

        if (!connectorRegistry.ContainsKey(connector) || !generatorRegistry.ContainsKey(reportType))
        {
            throw new ArgumentException("Invalid connector or report type.");
        }

        var getConnectionMethod = connectorRegistry[connector];
        var generateReportMethod = generatorRegistry[reportType];

        using (var connection = getConnectionMethod("connectionString"))
        {
            generateReportMethod(connection, connector, tableName);
        }
    }

    private static void GenerateHTMLReport(IDbConnection connection, DBConnector connector, string tableName)
    {
        switch (connector)
        {
            case DBConnector.MYSQL:
                MySqlHelper.GenerateMySqlHTMLReport(new SqlConnection(), tableName);
                break;
            case DBConnector.MSSQL:
                MSSqlHelper.GenerateMSSqlHTMLReport(new SqlConnection(), tableName);
                break;
            case DBConnector.MONGO:
                MongoDBHelper.GenerateMongoHTMLReport(new SqlConnection(), tableName);
                break;
            default:
                throw new ArgumentException("Invalid database connection.");
        }
    }

    private static void GeneratePDFReport(IDbConnection connection, DBConnector connector, string tableName)
    {
        switch (connector)
        {
            case DBConnector.MYSQL:
                MySqlHelper.GenerateMySqlPDFReport(new SqlConnection(), tableName);
                break;
            case DBConnector.MSSQL:
                MSSqlHelper.GenerateMSSqlPDFReport(new SqlConnection(), tableName);
                break;
            case DBConnector.MONGO:
                MongoDBHelper.GenerateMongoPDFReport(new SqlConnection(), tableName);
                break;
            default:
                throw new ArgumentException("Invalid database connection.");
        }
    }

    public enum DBConnector
    {
        MYSQL, MSSQL, MONGO
    }

    public enum ReportType
    {
        HTML, PDF
    }
}   
				
			

کلاس HelperFacade یک متد به نام GenerateReport تعریف می‌کند. این متد سه آرگومان دریافت می‌کند: یک مقدار از نوع enum به نام DBConnector که نوع اتصال‌دهنده پایگاه داده را برای ایجاد گزارش مشخص می‌کند، یک مقدار از نوع enum به نام ReportType که نوع گزارش موردنظر را تعیین می‌کند، و یک مقدار رشته‌ای که نام جدولی را مشخص می‌کند که گزارش از آن ساخته خواهد شد.

متد GenerateReport شامل دو دیکشنری است. دیکشنری اول که connectorRegistry نام دارد، هر مقدار از enum DBConnector را به یک متد که یک شیء اتصال پایگاه داده را برای آن نوع اتصال‌دهنده برمی‌گرداند، نگاشت می‌کند. دیکشنری دوم که generatorRegistry نام دارد، هر مقدار از enum ReportType را به یک متد که گزارشی از آن نوع تولید می‌کند، نگاشت می‌کند.

اگر مقادیر نوع اتصال‌دهنده و نوع گزارش معتبر باشند، این متد متد مربوط به اتصال پایگاه داده را از دیکشنری connectorRegistry و متد مربوط به تولید گزارش را از دیکشنری generatorRegistry دریافت می‌کند. سپس، متد به کمک متد اتصال دریافت‌شده به پایگاه داده متصل می‌شود. سپس شیء اتصال، نوع اتصال‌دهنده و نام جدول را به متد تولید گزارش ارسال می‌کند تا گزارش موردنظر ساخته شود.

متدهای خصوصی

کلاس HelperFacade همچنین دارای دو متد خصوصی استاتیک به نام‌های GenerateHTMLReport و GeneratePDFReport است. این متدها به یک شیء از نوع IDbConnection، یک مقدار از enum DBConnector و نام جدولی که گزارش از آن ساخته خواهد شد نیاز دارند. متد GenerateReport این متدها را برای تولید گزارشی که بر اساس مقدار ReportType درخواست شده است، فراخوانی می‌کند.

در نهایت، کلاس HelperFacade دارای دو نوع enum به نام‌های DBConnector و ReportType است. این enumها برای مشخص کردن نوع اتصال‌دهنده پایگاه داده و نوع گزارش موردنظر استفاده می‌شوند.

اکنون بیایید از متد HelperFacade برای تولید چند گزارش استفاده کنیم.

				
					HelperFacade.GenerateReport(HelperFacade.DBConnector.MYSQL, HelperFacade.ReportType.HTML, "sample-MySQL");  
HelperFacade.GenerateReport(HelperFacade.DBConnector.MONGO, HelperFacade.ReportType.PDF, "sample-Mongo");
				
			

همان‌طور که مشاهده می‌کنید، استفاده از الگوی Facade یک روش بسیار ساده‌تر و تمیزتر برای جلوگیری از قرار دادن منطق زیاد در سمت کلاینت است.

آیا Facade یک الگوی نامناسب است؟

برخی توسعه‌دهندگان الگوی طراحی Facade را یک الگوی نامناسب می‌دانند، زیرا ممکن است به یک پایگاه کد پیچیده و به‌هم‌پیوسته منجر شود. این الگو امکان دسترسی به یک زیرسیستم پیچیده را از طریق یک رابط واحد فراهم می‌کند که ممکن است باعث شود Facade مسئولیت‌های زیادی بر عهده بگیرد. این امر می‌تواند نگهداری و تغییر پایگاه کد را دشوارتر کند، زیرا تغییرات در زیرسیستم ممکن است نیازمند تغییرات در رابط Facade باشد. همچنین، الگوی طراحی Facade ممکن است بخش‌های مهمی از زیرسیستم را پنهان کند و باعث شود توسعه‌دهندگان نتوانند به‌خوبی متوجه نحوه عملکرد آن شوند. این مسئله می‌تواند به تصمیمات طراحی ضعیف و هدررفت زمان منجر شود.

از سوی دیگر، الگوی Facade یک الگوی طراحی مفید است که با ساده‌سازی رابط، استفاده از یک زیرسیستم پیچیده را آسان‌تر و قابل درک‌تر می‌کند. این الگو می‌تواند تغییرات زیرسیستم را از بقیه برنامه پنهان کند و یک رابط ساده و یکپارچه ارائه دهد که پیچیدگی زیرسیستم را مخفی می‌سازد. همچنین، الگوی Facade می‌تواند وابستگی کد کلاینت به زیرسیستم را کاهش دهد و آزمایش و نگهداری را آسان‌تر کند. اگر به‌درستی از این الگو استفاده شود، می‌تواند طراحی کلی برنامه را بهبود بخشد و نگهداری آن را آسان‌تر کند. بنابراین، این الگو می‌تواند گزینه‌ای مناسب برای بررسی باشد.

مزایا و معایب الگوی طراحی Facade

مزایا

  • پیچیدگی رابط یک زیرسیستم پیچیده را کاهش داده و استفاده و درک آن را آسان‌تر می‌کند.
  • یک رابط ساده و یکپارچه ارائه می‌دهد که پیچیدگی زیرسیستم را در بر می‌گیرد.
  • طراحی کلی و نگهداری برنامه را بهبود می‌بخشد.

معایب

  • ممکن است طراحی ضعیف و تلاش‌های بیهوده به همراه داشته باشد، اگر جزئیات مهم زیرسیستم پنهان شوند.
  • ممکن است به‌راحتی تبدیل به یک “شیء همه‌کاره” شود.

ارتباط با سایر الگوها

  • الگوی Facade یک رابط جدید برای یک شیء موجود ایجاد می‌کند، در حالی که Adapter تلاش می‌کند رابط موجود را قابل استفاده کند. معمولاً Adapter فقط یک شیء را پوشش می‌دهد، اما Facade با یک گروه کامل از اشیاء در یک زیرسیستم کار می‌کند.
  • می‌توانید به جای Facade از Abstract Factory استفاده کنید، اگر فقط می‌خواهید نحوه ساخت اشیاء زیرسیستم از دید کد کلاینت پنهان شود.
  • Flyweight نشان می‌دهد چگونه تعداد زیادی اشیاء کوچک ایجاد کنید، در حالی که Facade نشان می‌دهد چگونه یک شیء واحد نماینده یک زیرسیستم کامل باشد.
  • هر دو الگوهای Facade و Mediator تلاش می‌کنند راه‌هایی برای همکاری نزدیک کلاس‌های مرتبط ایجاد کنند:
    • Facade رابط گروهی از اشیاء در یک زیرسیستم را ساده‌تر می‌کند، اما عملکرد جدیدی اضافه نمی‌کند. زیرسیستم از وجود Facade اطلاعی ندارد و اشیاء زیرسیستم می‌توانند مستقیماً با یکدیگر ارتباط برقرار کنند.
    • Mediator تضمین می‌کند که تمام اجزای سیستم بتوانند با یکدیگر ارتباط برقرار کنند. در این حالت، اجزا مستقیماً با یکدیگر ارتباط ندارند و فقط از شیء Mediator آگاه هستند.
  • معمولاً یک شیء Facade واحد کافی است تا یک کلاس Facade به یک Singleton تبدیل شود.
  • هر دو الگوهای Facade و Proxy یک موجودیت پیچیده را پوشش می‌دهند و آن را به طور مستقل راه‌اندازی می‌کنند. با این حال، بر خلاف Facade، Proxy و شیء سرویس آن دارای یک رابط یکسان هستند که امکان جایگزینی آنها را فراهم می‌کند.

نکات پایانی

در این مقاله، مفهوم الگوی طراحی Facade، زمان استفاده از آن و مزایا و معایب استفاده از این الگوی طراحی را بررسی کردیم. همچنین چند مورد استفاده از این الگو و ارتباط آن با سایر الگوهای طراحی کلاسیک را توضیح دادیم.

شایان ذکر است که الگوی Facade، همراه با سایر الگوهای طراحی ارائه‌شده توسط Gang of Four، راه‌حلی جهانی و نهایی برای طراحی برنامه نیست. مهندسان باید با دقت تصمیم بگیرند که چه زمانی از یک الگوی خاص استفاده کنند. در نهایت، این الگوها زمانی مفید هستند که به‌عنوان یک ابزار دقیق استفاده شوند، نه یک ابزار کلی و سنگین.

©دوات با هدف دسترس‌پذیر کردن دانش انگلیسی در حوزه صنعت نرم‌افزار وجود آمده است. در این راستا از هوش مصنوعی برای ترجمه گلچینی از مقالات مطرح و معتبر استفاده می‌شود. با ما در تماس باشید و انتقادات و پیشنهادات خود را از طریق صفحه «تماس با ما» در میان بگذارید.