C#でのFacadeパターン

Facade(ファサード)は、ライブラリ、フレームワーク、あるいは他の複雑なクラス群に対して、単純化されたインターフェースを提供する構造デザインパターンです。

ファサードパターンの主な考え方は、複雑なシステムを操作するための一連のインターフェースを作り、使いやすくすることです。ファサードはより上位レベルのインターフェースを提供することで、サブシステムの利用を容易にします。この上位レベルのインターフェースは、複雑なシステムのすべての使用ケースをサポートするためのものではなく、主要なものだけをサポートすることを意図しています。ファサードパターンがインターフェースを容易にする一方で、複雑なシステムを直接使うこともしばしば可能です。

この投稿のサンプルコードは、GitHubで確認できます。

問題の概念化

アプリケーションは特定の作業を行うために、多数のサードパーティライブラリやアプリケーションプログラミングインターフェース(API)を使用することが一般的です。これらのライブラリやAPIはさまざまなグループやチームによって作られたもので、異なるインターフェース、データ構造、動作方法を持っています。

アプリが同時にいくつかのライブラリやAPIを使用する必要がある場合、とても複雑になり、管理が困難になることがあります。各ライブラリにはそれぞれのクラスやメソッドがあり、一定の方法で使う必要がありますし、ライブラリ間での複雑な相互作用を管理する必要があります。

サブシステムが複雑になるにつれて、コードの変更やメンテナンスが困難になります。変更時にはバグやエラーが発生する可能性があり、コードのテストや修正が困難になることがあります。また、サブシステムの部品が互いに作用し合う管理にかかる時間が、コードのパフォーマンスに影響を及ぼすこともあります。

通常、これらのオブジェクトを全て初期化し、依存関係を追跡し、正しい順序でメソッドを実行する必要があります。これにより、コードがライブラリコードと密接に結合することになります。密接な結合とは、2つ以上の部品が互いに強く依存し合っている状態を指します。これは、部品の一方を変えることなくもう一方を変更することを困難にします。

ファサードを作成することで、ユーザーにサブシステムとの対話をより簡単にする方法を提供することができます。これにより、ユーザーがシステムを使いやすくなり、メンテナンスや更新が容易になります。また、ファサードの背後にサブシステムを隠すことで、その機能へのアクセスを容易に制御・管理し、正しく安全に使用されていることを確認することにも貢献します。

ファサードパターンの構造化

基本的な実装では、ファサードパターンには4つの主要な参加者がいます:

  • ファサードファサードはサブシステムの機能の特定の側面への迅速なアクセスを提供します。クライアントのリクエストをどのようにルーティングし、すべての動作部品を操作するかを理解しています。
  • 追加ファサード:単一のファサードに無関係の機能がたくさんあって、また別の複雑な構造を生み出してしまわないようにするために、追加ファサードクラスが作られることがあります。クライアントも他のファサードも、追加ファサードを使用できます。
  • 複雑なサブシステム複雑なサブシステムは、数十の異なるオブジェクトで構成されています。それらを全て役立つように動かすためには、サブシステムの実装の詳細に深く入り込む必要があります。たとえば、正しい順序でオブジェクトを初期化し、正しいフォーマットでデータを提供しなければなりません。サブシステムのクラスはファサードの存在を知りません。それらはシステム内で、直接的にお互いに働きかけます。
  • クライアント:サブシステムのオブジェクトを直接呼び出すのではなく、クライアントはファサードを利用します。

ファサードパターンの動作を示すために、私たちは少し複雑なサブシステムを作成します。例えば、MySql/MS/Mongoデータベースへのアクセス用の一連のインターフェースや、HTMLレポート、PDFレポートなど、さまざまな種類のレポートを生成するためのアプリケーションがあるとします。それにより、さまざまなタイプのデータベースを操作するための異なるインターフェースが必要になります。これらのインターフェースは、必要なデータベース接続を取得してレポートを生成するために、今やクライアントアプリケーションによって利用されることになります。しかし、複雑さが増すと、またはインターフェースの動作の名前が混乱すると、クライアントアプリケーションはそれを管理するのが難しくなります。したがって、クライアントアプリケーションを支援するために、ファサードデザインパターンを使用して、既存のインターフェースの上にラッパーインターフェースを提供できます。

まず、私たちの複雑なサブシステム参加者を作成することにしましょう。サブシステムには、異なるデータベースプロバイダーに対応した3つのヘルパーメソッドがあります。ヘルパー機能には、データベースに接続して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");
    }
}

MSSQLMongoDB接続のために似たようなクラスもあります。上記のように、すべてのメソッドはモックされています。

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");
    }
}

今、ファサードなしでレポートを生成するには、まず接続を取得し、レポートの対象となるテーブルとメソッドに接続を引数として使い、ファイルシステムへのアクセスを確認したり、ファイルやおそらくディレクトリを作成する必要があるかも知れません。レポートの生成には、多くの動作部品が期待通りに機能するように慎重な調整が必要です。そして最悪のことに、データベースからレポートが必要なたびに、この振り付けを実行しなければなりません。そして、これは単一のDBプロバイダーのワークフローであることを覚えておいてください。

以下は、ファサードなしでのレポート生成の例です:

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という単一のメソッドを定義しています。このメソッドは3つの引数を取ります:レポートを作成するために使用するデータベースコネクタの種類を表すDBConnector列挙値、作成するレポートの種類を表すReportType列挙値、およびレポートが作成されるテーブルの名前を表す文字列値。

GenerateReportには2つの辞書が含まれています。最初

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/kalkwst/facade-pattern-in-c-2ho0