java-recipes

ホーム デザインパターン › DP-10

DP-10: Facade パターン

複数のサブシステムを組み合わせた複雑な処理を、シンプルな窓口(ファサード)の陰に隠蔽するパターンです。 呼び出し側はサブシステムの内部構造を知らなくても、窓口のメソッドを呼ぶだけで処理が完結します。

Facade パターンとは

「ファサード(Facade)」は建築用語で「建物の正面」を意味します。 建物の内部がどれだけ複雑な構造をしていても、外から見える正面はシンプルに整えられています。 Facade パターンも同様に、複数のサブシステムが連携する複雑な処理を、 シンプルなインターフェースの陰に隠してしまうパターンです。

主なユースケース

  • ライブラリのラッパー: 外部ライブラリの複雑な API を自社のシンプルなクラスで包んで使いやすくする
  • 複数 API の統合: 認証・テンプレート・SMTP・ログなど複数の機能をひとつの「メール送信」メソッドにまとめる
  • レガシーシステムの隠蔽: 古いシステムの複雑な呼び出し手順を新しい Facade で包み、移行期間中もシンプルに使えるようにする

Java 標準ライブラリの実例: java.util.logging

Java の標準ログライブラリ(java.util.logging)は Facade パターンの実例です。 内部では Handler(出力先管理)・Formatter(フォーマット)・Filter(フィルタリング)が連携していますが、 利用者は Loggerinfo()warning()だけ呼べばログを記録できます。

// Logger(Facade)を使うだけで、Handler/Formatter/Filter を意識しなくて良い
Logger logger = Logger.getLogger(MyApp.class.getName());
logger.info("処理が完了しました");   // ← これだけでOK
logger.warning("注意が必要です");

サンプルコード

FacadePatternSample.java
import java.util.HashMap;
import java.util.Map;

public class FacadePatternSample {

    // ---- サブシステム1: SMTP クライアント ----
    static class SmtpClient {
        private String connectedHost;

        /** SMTP サーバーに接続する */
        public void connect(String host) {
            this.connectedHost = host;
            System.out.println("[SMTP] " + host + " に接続しました");
        }

        /** 認証を行う */
        public void authenticate(String user, String password) {
            System.out.println("[SMTP] ユーザー " + user + " で認証しました");
        }

        /** メールを送信する */
        public void send(String to, String subject, String body) {
            System.out.println("[SMTP] 送信先: " + to);
            System.out.println("[SMTP] 件名: " + subject);
            System.out.println("[SMTP] 本文: " + body);
            System.out.println("[SMTP] 送信完了");
        }

        /** 接続を切断する */
        public void disconnect() {
            System.out.println("[SMTP] " + connectedHost + " から切断しました");
        }
    }

    // ---- サブシステム2: テンプレートエンジン ----
    static class TemplateEngine {

        /** テンプレートを読み込む */
        public String loadTemplate(String name) {
            if ("welcome".equals(name)) {
                return "こんにちは、{{userName}} さん!\nご登録ありがとうございます。";
            }
            return "テンプレートが見つかりません: " + name;
        }

        /** テンプレート内のプレースホルダーを変数で置換する */
        public String render(String template, Map<String, String> vars) {
            String result = template;
            for (Map.Entry<String, String> entry : vars.entrySet()) {
                result = result.replace("{{" + entry.getKey() + "}}", entry.getValue());
            }
            return result;
        }
    }

    // ---- サブシステム3: 監査ログ ----
    static class AuditLogger {

        public void logSend(String to, String subject) {
            System.out.println("[AUDIT] メール送信: to=" + to + ", subject=" + subject);
        }

        public void logError(String message) {
            System.out.println("[AUDIT] エラー: " + message);
        }
    }

    // ---- Facade: 複雑なサブシステムをシンプルな窓口で隠蔽する ----
    static class EmailFacade {
        private final SmtpClient smtpClient;
        private final TemplateEngine templateEngine;
        private final AuditLogger auditLogger;
        private final String smtpHost;
        private final String smtpUser;
        private final String smtpPassword;

        public EmailFacade(String smtpHost, String smtpUser, String smtpPassword) {
            this.smtpClient = new SmtpClient();
            this.templateEngine = new TemplateEngine();
            this.auditLogger = new AuditLogger();
            this.smtpHost = smtpHost;
            this.smtpUser = smtpUser;
            this.smtpPassword = smtpPassword;
        }

        /**
         * ウェルカムメールを送信する。
         * 呼び出し側はこのメソッドだけ呼べば良い。
         */
        public void sendWelcomeEmail(String to, String userName) {
            try {
                // 1. テンプレートを読み込んでレンダリングする
                String template = templateEngine.loadTemplate("welcome");
                Map<String, String> vars = new HashMap<String, String>();
                vars.put("userName", userName);
                String body = templateEngine.render(template, vars);

                // 2. SMTP に接続して認証する
                smtpClient.connect(smtpHost);
                smtpClient.authenticate(smtpUser, smtpPassword);

                // 3. メールを送信して切断する
                String subject = "ご登録ありがとうございます";
                smtpClient.send(to, subject, body);
                smtpClient.disconnect();

                // 4. 監査ログを記録する
                auditLogger.logSend(to, subject);

            } catch (Exception e) {
                auditLogger.logError("ウェルカムメール送信失敗: " + e.getMessage());
                throw e;
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Facade パターン: メール送信システム ===");

        System.out.println("\n--- Facade なし: 呼び出し側が全サブシステムを直接操作する ---");
        SmtpClient smtp = new SmtpClient();
        TemplateEngine engine = new TemplateEngine();
        AuditLogger logger = new AuditLogger();

        // 呼び出し側が手順を全て知る必要がある(複雑!)
        String template = engine.loadTemplate("welcome");
        Map<String, String> vars = new HashMap<String, String>();
        vars.put("userName", "田中");
        String body = engine.render(template, vars);
        smtp.connect("smtp.example.com");
        smtp.authenticate("user@example.com", "password");
        smtp.send("tanaka@example.com", "ご登録ありがとうございます", body);
        smtp.disconnect();
        logger.logSend("tanaka@example.com", "ご登録ありがとうございます");

        System.out.println("\n--- Facade あり: EmailFacade.sendWelcomeEmail() の1行で完結 ---");
        EmailFacade facade = new EmailFacade(
                "smtp.example.com", "user@example.com", "password");
        // 呼び出し側はこれだけ!複雑な手順を知らなくて良い
        facade.sendWelcomeEmail("yamada@example.com", "山田");
    }
}

Java 8 版では各サブシステム(SmtpClient・TemplateEngine・AuditLogger)が独立したクラスとして定義されています。EmailFacade がそれらを組み合わせ、呼び出し側は sendWelcomeEmail() だけ知っていれば良い構造になっています。

よくあるミス・注意点

⚠️ Facade に全ロジックを詰め込んで「神クラス」になる

Facade は「窓口」であり、各処理の実装は必ずサブシステムに委譲(いじょう)すべきです。 「とりあえず Facade に書けばいい」と考えると、Facade クラスが何千行にも膨れ上がり、 メンテナンスが困難な「神クラス(God Class)」になってしまいます。 Facade はあくまでも呼び出しの調整役に留め、ビジネスロジックは適切なサブシステムに置きましょう。

⚠️ サブシステムへの直接アクセスを禁止してしまう

Facade はあくまで「便利な窓口」であり、サブシステムを隠す「壁」ではありません。 高度な使い方をしたい利用者がサブシステムに直接アクセスできる余地を残しておきましょう。 例えば、SmtpClientpackage-private にして外から使えなくすると、 細かい制御が必要なケースに対応できなくなります。

⚠️ Adapter パターンと混同する

Adapter パターンは「互換性のないインターフェース同士をつなぐ」ためのパターンで、 主に1対1の変換を行います。 Facade パターンは「複数のサブシステムをまとめてシンプルな窓口を作る」ためのパターンです。 「既存インターフェースに合わせる → Adapter」「複雑な処理をまとめる → Facade」と覚えましょう。

テストする観点

  • 各サブシステム(SmtpClient・TemplateEngine・AuditLogger)が独立してテストできること
  • Facade が各サブシステムへ正しく委譲していること(sendWelcomeEmail を呼んだとき SmtpClient.send が呼ばれるか)
  • テンプレートのプレースホルダーが正しく置換されること({{userName}} が実際の名前に変わるか)
  • 送信中に例外が発生したとき、AuditLogger にエラーログが記録され、例外が再スローされること
  • Java 17 版: EmailConfig record のアクセサ(host()・user()・password())が正しい値を返すこと
  • Java 21 版: EmailRequest の各サブタイプ(Welcome・Password・Notification)に対して正しい件名・本文が生成されること

GitHub でソースコードを見る →