- 技術 💻
Facadeパターンとは|Adapter・Mediatorとの違いと3言語の実装比較
結論
Facade(ファサード)パターンとは、いくつものクラスを決まった順番で呼ぶ処理を1つのクラスにまとめ、呼び出す側には簡単な入口(窓口)だけを見せるデザインパターンです。呼び出す側が知るクラスの数が減り、手順の変更や失敗時の戻し処理を1か所で直せます。
この記事でわかること
- 注文処理をFacadeあり・なしで書き、Java・Python・TypeScriptで動かした結果
- コード量・依存の数・変更の影響・テストの偽物の数を比べた実測
- Adapter・Mediator・Proxy・LaravelのFacadeとの違いと判断表
呼び出す側(クライアント)は Facade だけを呼び、Facade が在庫・決済・通知のクラスを正しい順番で呼びます。在庫・決済・通知のクラスは Facade の存在を知りません。
flowchart TD
W[画面の注文] --> F[OrderFacade<br>placeOrder]
B[バッチの注文] --> F
F --> I[在庫<br>確保・戻す]
F --> P[決済<br>与信・確定]
F --> N[通知<br>メール]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| Facadeパターンとは | 複雑な仕組みに、簡単な入口を1つ用意するパターン(GoFの構造パターン) | Facadeパターンとは |
| ファサードとは(意味) | 建物の正面(façade)。中の複雑さを見せない「顔」 | Facadeパターンとは |
| Facadeパターンのメリット | 呼び出し側の依存が減る・手順を1か所で直せる・偽物1つでテストできる | コード量・依存の数・変更の影響 |
| Facadeパターンのデメリット | 窓口が太りやすい(God Object)。中の複雑さは消えず、移るだけ | よくある誤用:God Object化 |
| Facade と Adapter の違い | Facadeは複数をまとめて簡単にする。Adapterは1つの形を合わせる | Adapter・Mediator・Proxyとの違い |
| Facade と Mediator の違い | Facadeは一方通行の窓口。Mediatorは部品どうしのやり取りを仲介する | Adapter・Mediator・Proxyとの違い |
| Laravel の Facade は同じもの? | 名前は同じだが別物。サービスコンテナのオブジェクトへの「静的なプロキシ」 | LaravelのFacade |
| Java・Python・TypeScript での書き方 | 窓口のクラスを1つ作り、中のクラスは公開範囲を絞る | 各言語の節 |
| 使う・使わないの判断 | 同じ手順を2か所以上から呼ぶか、が最初の目安 | 判断表 |
確認環境:Ubuntu 24.04.5 LTS の上で、同じ題材(注文処理:在庫・決済・通知)を Facade なし・Facade ありの2通りで書き、OpenJDK 21.0.12.1(javac でコンパイルして java で実行)、Python 3.13.18(標準ライブラリの unittest と unittest.mock でテスト)、Node.js 22.23.4 と TypeScript 5.9.3(作業場所に npm で入れた tsc でコンパイルし、node:test でテスト。@types/node 22.20.5)で実行しました。コード量は空行を除いた行数、変更の影響は diff で数えています。Laravel は動かしておらず、Laravel 13.x の公式ドキュメント(Facades・Mocking)で確かめた範囲だけを書いています。GoF のパターンの説明と他のパターンとの関係は refactoring.guru と Wikipedia、SLF4J の説明は公式サイトで確かめました。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Facadeパターンとは|Adapter・Mediatorとの違いと3言語の実装比較」
Facadeパターンとは(意味と一言の定義)
この節では、Facadeパターンの意味、名前の由来、身近にある Facade の例が分かります。
Facadeパターンは、GoF(Gang of Four)の23のデザインパターンのうち「構造に関するパターン」の1つで、複雑な仕組み(サブシステム)に、簡単な入口を1つ用意するパターンです。 refactoring.guru は「ライブラリー、フレームワーク、その他のクラスの複雑な集合に対して、簡素化されたインターフェースを提供」するもの、と説明しています。
facade(façade)は英語で建物の正面・外観のことです。通りから見えるのは整った正面だけで、裏の配管や柱は見えません。プログラムでも、呼び出す側には整った入口だけを見せ、中で何個のクラスがどんな順番で動いているかを隠します。
Facade が解決する問題
Facade が無いと、呼び出す側は次のことをすべて知っている必要があります。
- どのクラスを、どんな順番で呼ぶか(在庫を確保してから決済する、など)
- 途中で失敗したときに、何を元に戻すか(決済が通らなければ在庫を戻す、など)
- それぞれのクラスの作り方(コンストラクターの引数、設定)と例外の種類
呼び出す場所が1か所なら問題になりにくいのですが、画面・バッチ・API と呼び出す場所が増えると、同じ手順のコピーが増え、1か所だけ戻し処理を忘れる、といったずれが起きます。後の節で、このずれを実際に起こしています。
身近にある Facade の例
Python の subprocess.run
中で Popen を作り、communicate() で出力を受け取り、CompletedProcess にまとめて返す。手元の Python 3.13 のソースで確かめた。
Popen を直接使う手順を1回の呼び出しに
SLF4J(Java のログ)
公式サイトは「さまざまなログのフレームワークに対する simple facade or abstraction」と説明している。名前の F も Facade。
名前に Facade が入る例
電話での注文
refactoring.guru のたとえ。電話の受付係が、注文・支払い・配送の窓口になる。客は中の部署を知らなくてよい。
現実世界のたとえ
サービス層のクラス
Web アプリの「注文サービス」のように、複数のリポジトリや外部 API を決まった順番で呼ぶクラスは、Facade の考え方そのもの。
名前に Facade と付かないことが多い
Python の公式ドキュメントも、run() で対応できる用途ならこの関数を使うことを勧め、より高度な用途では下の層の Popen を直接使える、と書いています。入口と、直接使える下の層の両方がある、Facade の典型的な形です。subprocess.run の中身は、次のコマンドで確かめました(68行のうち、要の行だけを抜き出しています)。
$ python3 -c "import subprocess, inspect; print(inspect.getsource(subprocess.run))"
with Popen(*popenargs, **kwargs) as process:
stdout, stderr = process.communicate(input, timeout=timeout)
return CompletedProcess(process.args, retcode, stdout, stderr)
デザインパターン全体の一覧と、どのパターンをよく使うかは、関連記事「デザインパターンの使いどころ|GoF23の一覧と実務でよく使う8つ」で扱っています。この記事は、そのうち Facade を1つ掘り下げます。
Facadeパターンの構造(登場する役割)
この節では、Facadeパターンに出てくる役割と、それぞれが何を知っていて何を知らないのかが分かります。
| 役割 | この記事の題材では | 知っているもの |
|---|---|---|
| Facade(窓口) | OrderFacade | サブシステムのクラスと、呼ぶ順番・失敗時の戻し方 |
| サブシステム | Inventory・Payment・Notifier | 自分の仕事だけ。Facade の存在は知らない |
| クライアント(呼び出す側) | 画面の注文処理・バッチの定期注文 | Facade の入口(placeOrder)だけ |
| 追加の Facade(必要なら) | 返品用の ReturnFacade など | 窓口が太ったときに分けた先 |
- Facade は新しい機能を足すのではなく、既にある機能を呼ぶ順番と組み合わせをまとめます。 Wikipedia(英語版)は、Facade が呼び出しの前後に処理を足すこともある、と書いています。この記事の題材では「決済が通らなければ在庫を戻す」がそれにあたります。
- クライアントがサブシステムを直接使うことは、パターンとしては禁止されていません。 細かい操作が要る場面では直接使えるようにしておき、普通の使い方だけを Facade に通す、という分け方もよく取られます。どこまで隠すかは、言語ごとの節で実際に試しています。
Facadeなしで注文処理を書く(Python)
この節では、Facade を使わずに在庫・決済・通知を呼び出し側で直接組み合わせたコードと、そのまま動かした結果が分かります。
題材は、商品の注文です。在庫を確保し、決済の与信を取り、売上を確定し、お礼のメールを送ります。決済が通らなかったときは、確保した在庫を戻します。3つのサブシステムは、Facade の有無にかかわらず同じものを使います。
"""注文処理のサブシステム(在庫・決済・通知)。どちらの版も同じものを使う。"""
import itertools
class OutOfStock(Exception): pass
class PaymentDeclined(Exception): pass
class Inventory:
def __init__(self, stock):
self.stock = dict(stock)
def reserve(self, sku, qty):
if self.stock.get(sku, 0) < qty:
raise OutOfStock(f"{sku} の在庫が足りません(残り {self.stock.get(sku, 0)})")
self.stock[sku] -= qty
print(f" [在庫] {sku} を {qty} 個確保(残り {self.stock[sku]})")
def release(self, sku, qty):
self.stock[sku] += qty
print(f" [在庫] {sku} を {qty} 個戻す(残り {self.stock[sku]})")
class Payment:
_ids = itertools.count(1)
def __init__(self, limit=50_000):
self.limit = limit
def authorize(self, customer, amount):
if amount > self.limit:
raise PaymentDeclined(f"与信枠を超えています({amount} 円)")
auth_id = f"AUTH-{next(self._ids):03d}"
print(f" [決済] {customer} の {amount} 円を与信 {auth_id}")
return auth_id
def capture(self, auth_id):
print(f" [決済] {auth_id} を売上確定")
class Notifier:
def send(self, to, subject, body):
print(f" [通知] {to} へ「{subject}」")
Facade が無いので、画面の注文処理(web_checkout.py)は、3つのクラスと2つの例外をすべて知っています。
from subsystems import Inventory, Payment, Notifier, OutOfStock, PaymentDeclined
def checkout(inventory: Inventory, payment: Payment, notifier: Notifier,
customer, sku, qty, price):
try:
inventory.reserve(sku, qty)
except OutOfStock as e:
return f"NG: {e}"
try:
auth_id = payment.authorize(customer, price * qty)
except PaymentDeclined as e:
inventory.release(sku, qty) # 確保した在庫を戻す
return f"NG: {e}"
payment.capture(auth_id)
notifier.send(customer, "ご注文ありがとうございます", f"{sku} x {qty}")
return f"OK: {auth_id}"
同じ手順を、バッチの定期注文(batch_reorder.py)でも書きました。こちらは、例外を1つの except にまとめたときに、在庫を戻す行が抜けています。コードを読んでも気づきにくい形です。
from subsystems import Inventory, Payment, Notifier, OutOfStock, PaymentDeclined
def reorder(inventory: Inventory, payment: Payment, notifier: Notifier,
customer, sku, qty, price):
try:
inventory.reserve(sku, qty)
auth_id = payment.authorize(customer, price * qty)
except (OutOfStock, PaymentDeclined) as e:
return f"NG: {e}" # 在庫を戻し忘れている
payment.capture(auth_id)
notifier.send(customer, "定期注文を受け付けました", f"{sku} x {qty}")
return f"OK: {auth_id}"
在庫10個の商品で、4つの注文を順に流しました。
$ python3 run_nofacade.py
1) 画面から注文(成功)
[在庫] A-100 を 2 個確保(残り 8)
[決済] sato の 6000 円を与信 AUTH-001
[決済] AUTH-001 を売上確定
[通知] sato へ「ご注文ありがとうございます」
=> OK: AUTH-001
2) 画面から注文(決済NG)
[在庫] A-100 を 3 個確保(残り 5)
[在庫] A-100 を 3 個戻す(残り 8)
=> NG: 与信枠を超えています(90000 円)
3) バッチの定期注文(決済NG)
[在庫] A-100 を 3 個確保(残り 5)
=> NG: 与信枠を超えています(90000 円)
4) 画面から注文(在庫不足)
=> NG: A-100 の在庫が足りません(残り 5)
最終の在庫: {'A-100': 5}
売れたのは2個なので在庫は8個のはずですが、5個になりました。 3番のバッチで決済が通らなかったときに在庫を戻していないため、3個が「確保されたまま」になり、4番では本当は買える9個の注文も「在庫が足りません」で断っています。エラーは1つも出ていません。
Facadeありに書き換えて比べる(Python)
この節では、同じ処理を Facade にまとめたコードと、Facade なしと同じ注文を流した結果の違いが分かります。
呼ぶ順番と、失敗したときの戻し処理を OrderFacade に1回だけ書きます。 結果は例外ではなく OrderResult で返し、呼び出し側が例外の種類を知らなくてよい形にしました。
from dataclasses import dataclass
from subsystems import Inventory, Payment, Notifier, OutOfStock, PaymentDeclined
@dataclass
class OrderResult:
ok: bool
detail: str
class OrderFacade:
"""在庫・決済・通知を正しい順番で呼ぶ窓口。失敗時の戻し処理もここに1回だけ書く。"""
def __init__(self, inventory: Inventory, payment: Payment, notifier: Notifier):
self._inventory, self._payment, self._notifier = inventory, payment, notifier
def place_order(self, customer, sku, qty, price, subject="ご注文ありがとうございます"):
try:
self._inventory.reserve(sku, qty)
except OutOfStock as e:
return OrderResult(False, str(e))
try:
auth_id = self._payment.authorize(customer, price * qty)
except PaymentDeclined as e:
self._inventory.release(sku, qty)
return OrderResult(False, str(e))
self._payment.capture(auth_id)
self._notifier.send(customer, subject, f"{sku} x {qty}")
return OrderResult(True, auth_id)
呼び出し側は、OrderFacade の place_order を1回呼ぶだけになります。
from order_facade import OrderFacade
def checkout(orders: OrderFacade, customer, sku, qty, price):
r = orders.place_order(customer, sku, qty, price)
return f"OK: {r.detail}" if r.ok else f"NG: {r.detail}"
from order_facade import OrderFacade
def reorder(orders: OrderFacade, customer, sku, qty, price):
r = orders.place_order(customer, sku, qty, price, subject="定期注文を受け付けました")
return f"OK: {r.detail}" if r.ok else f"NG: {r.detail}"
同じ4つの注文を流した結果です。3番のバッチでも在庫が戻り、最終の在庫は8個(10個から売れた2個を引いた数)になりました。
$ python3 run_facade.py
3) バッチの定期注文(決済NG)
[在庫] A-100 を 3 個確保(残り 5)
[在庫] A-100 を 3 個戻す(残り 8)
=> NG: 与信枠を超えています(90000 円)
4) 画面から注文(在庫不足)
=> NG: A-100 の在庫が足りません(残り 8)
最終の在庫: {'A-100': 8}
同じ手順を呼び出し側ごとに書く
見分ける問い:「この手順を書いている場所は2か所以上あるか」
Python では「隠す」は約束ごと
Python には Java の package-private のような仕組みがありません。サブシステムをパッケージの中に入れ、名前の先頭に _ を付け、__init__.py の __all__ に Facade だけを書く、という約束で境界を示します。手元で試すと、from shop import * では Facade だけが入りましたが、中のモジュールを名指しすれば import できました。
$ python3 -c 'from shop import *; print(sorted(n for n in dir() if not n.startswith("__")))'
['OrderFacade', 'OrderResult']
$ python3 -c 'from shop._subsystems import Inventory; print(Inventory)'
<class 'shop._subsystems.Inventory'>
JavaでFacadeパターンを書く(パッケージで窓口以外を隠す)
この節では、Java で Facade を書くときに、パッケージの公開範囲を使って、呼び出し側からサブシステムを見えなくする方法と、そのときのコンパイルエラーの原文が分かります。
Java では、サブシステムのクラスから public を外し(package-private)、Facade だけを public にすると、別のパッケージからはサブシステムを使えなくなります。 Facade なし版は shop パッケージのクラスをすべて public にし、Facade あり版は order パッケージで OrderFacade と OrderResult だけを public にしました。
package order;
import java.util.Map;
/** 在庫・決済・通知を正しい順番で呼ぶ窓口。order パッケージの外に見えるのはこのクラスだけ。 */
public class OrderFacade {
private final Inventory inventory;
private final Payment payment;
private final Notifier notifier;
public OrderFacade(Map<String, Integer> stock, long creditLimit) {
this(new Inventory(stock), new Payment(creditLimit), new Notifier());
}
OrderFacade(Inventory inventory, Payment payment, Notifier notifier) { // テスト用(同じパッケージから)
this.inventory = inventory; this.payment = payment; this.notifier = notifier;
}
public OrderResult placeOrder(String customer, String sku, int qty, long price, String subject) {
try {
inventory.reserve(sku, qty);
} catch (OutOfStockException e) {
return new OrderResult(false, e.getMessage());
}
String authId;
try {
authId = payment.authorize(customer, price * qty);
} catch (PaymentDeclinedException e) {
inventory.release(sku, qty);
return new OrderResult(false, e.getMessage());
}
payment.capture(authId);
notifier.send(customer, subject, sku + " x " + qty);
return new OrderResult(true, authId);
}
public int stockOf(String sku) { return inventory.left(sku); }
}
呼び出し側(app パッケージ)は、OrderFacade と OrderResult の2つだけを import します。
package app;
import order.OrderFacade;
import order.OrderResult;
public class WebCheckout {
private final OrderFacade orders;
public WebCheckout(OrderFacade orders) { this.orders = orders; }
public String checkout(String customer, String sku, int qty, long price) {
OrderResult r = orders.placeOrder(customer, sku, qty, price, "ご注文ありがとうございます");
return (r.ok() ? "OK: " : "NG: ") + r.detail();
}
}
Facade なし版の WebCheckout は、5つのクラスを import し、try-catch を2つ書いています(31行。全文は Python 版と同じ流れです)。コンパイルと実行の結果です。
$ javac -encoding UTF-8 -d out $(find src -name '*.java')
$ java -cp out app.Main
1) 成功
[在庫] A-100 を 2 個確保(残り 8)
[決済] sato の 6000 円を与信 AUTH-001
[決済] AUTH-001 を売上確定
[通知] sato へ「ご注文ありがとうございます」
=> OK: AUTH-001
2) 決済NG
[在庫] A-100 を 3 個確保(残り 5)
[在庫] A-100 を 3 個戻す(残り 8)
=> NG: 与信枠を超えています(90000 円)
最終の在庫: 8
app パッケージから、package-private にした Inventory を直接使おうとすると、コンパイルが通りません。
$ javac -encoding UTF-8 -cp facade/out -d badout bad/app/Sneaky.java
bad/app/Sneaky.java:2: error: Inventory is not public in order; cannot be accessed from outside package
import order.Inventory;
^
bad/app/Sneaky.java:6: error: Inventory is not public in order; cannot be accessed from outside package
new Inventory(java.util.Map.of("A-100", 10)).release("A-100", 99);
^
2 errors
- サブシステムを隠すと、呼び出し側はサブシステムを作れなくなります。 そこで OrderFacade に「在庫の初期値と与信枠を受け取り、中で組み立てる」public のコンストラクターを用意し、サブシステムを受け取るコンストラクターは同じパッケージのテストからだけ使えるようにしました。
- Spring などの DI コンテナを使うときは、Facade を
@Serviceにし、サブシステムをコンストラクターで受け取る形が一般的です。DI の考え方は、関連記事「Spring DIとは|@Autowiredとコンストラクタインジェクション」で扱っています。
TypeScriptでFacadeパターンを書く(モジュールの公開範囲とtsc)
この節では、TypeScript でモジュールの index.ts を Facade の入口にし、型(interface)で呼び出し側との約束を決める書き方と、tsc のエラーの原文が分かります。
TypeScript では、order/index.ts から Facade と型だけを export し、サブシステムのクラスは再エクスポートしません。 呼び出し側は OrderFacade のクラスではなく、OrderService という interface に依存させました。テストで偽物に差し替えやすくするためです。
import { Inventory, Notifier, OutOfStock, Payment, PaymentDeclined } from "./subsystems";
export type OrderResult = { ok: true; authId: string } | { ok: false; reason: string };
// 呼び出し側が知るのはこの型だけ(テストで偽物に差し替えやすい)
export interface OrderService {
placeOrder(customer: string, sku: string, qty: number, price: number, subject?: string): OrderResult;
}
export class OrderFacade implements OrderService {
constructor(
private readonly inventory: Inventory,
private readonly payment: Payment,
private readonly notifier: Notifier,
) {}
placeOrder(customer: string, sku: string, qty: number, price: number,
subject = "ご注文ありがとうございます"): OrderResult {
try {
this.inventory.reserve(sku, qty);
} catch (e) {
if (e instanceof OutOfStock) return { ok: false, reason: e.message };
throw e;
}
let authId: string;
try {
authId = this.payment.authorize(customer, price * qty);
} catch (e) {
this.inventory.release(sku, qty);
if (e instanceof PaymentDeclined) return { ok: false, reason: e.message };
throw e;
}
this.payment.capture(authId);
this.notifier.send(customer, subject, `${sku} x ${qty}`);
return { ok: true, authId };
}
}
// 組み立てもここで行い、サブシステムは外に出さない
export function createOrderFacade(stock: Record<string, number>, creditLimit: number) {
const inventory = new Inventory(stock);
return { orders: new OrderFacade(inventory, new Payment(creditLimit), new Notifier()),
stockOf: (sku: string) => inventory.left(sku) };
}
// order モジュールの公開窓口。サブシステムのクラスは再エクスポートしない
export { createOrderFacade, OrderFacade } from "./facade";
export type { OrderResult, OrderService } from "./facade";
呼び出し側は、型を1つ import するだけです。
import type { OrderService } from "../order";
export function checkout(orders: OrderService, customer: string, sku: string, qty: number, price: number): string {
const r = orders.placeOrder(customer, sku, qty, price);
return r.ok ? `OK: ${r.authId}` : `NG: ${r.reason}`;
}
tsc でコンパイルし、Facade なし版と Facade あり版を同じ注文で動かしました。
$ npx tsc -p . && node dist/run.js
== Facade なし
[在庫] A-100 を 2 個確保(残り 8)
[決済] sato の 6000 円を与信 AUTH-001
[決済] AUTH-001 を売上確定
[通知] sato へ「ご注文ありがとうございます」
=> OK: AUTH-001
[在庫] A-100 を 3 個確保(残り 5)
[在庫] A-100 を 3 個戻す(残り 8)
=> NG: 与信枠を超えています(90000 円)
== Facade あり
(同じ順番で同じログ)
=> NG: 与信枠を超えています(90000 円)
最終の在庫: 8 8
index.ts を通して Inventory を import しようとすると、tsc が止めます。
$ npx tsc --noEmit --strict --types node bad/sneaky.ts
bad/sneaky.ts(1,10): error TS2305: Module '"../src/order"' has no exported member 'Inventory'.
- TypeScript の export しない、は「index.ts を通す限り見えない」という意味です。
../src/order/subsystemsを名指しすれば import できます。チームで「order の外からは index.ts だけを import する」と決め、ESLint の import の制限などで機械的に確かめる方法もあります。 - OrderResult を
{ ok: true; authId } | { ok: false; reason }の判別可能な共用体にすると、r.okを確かめる前にr.authIdを読むコードは tsc が通しません。結果の形を Facade で決めると、呼び出し側の書き間違いも減ります。
コード量・依存の数・変更の影響を比べた結果
この節では、Facade あり・なしで、呼び出し側のコード量、呼び出し側が知るクラスの数、サブシステムを変えたときに直すファイルの数を、3言語で数えた結果が分かります。
| 数えたもの | Java | Python | TypeScript |
|---|---|---|---|
| 画面側の行数(Facadeなし) | 31行 | 15行 | 21行 |
| 画面側の行数(Facadeあり) | 11行 | 4行 | 5行 |
| Facade 本体の行数 | 32行 | 23行 | 39行 |
| 画面側が import する名前(なし→あり) | 5→2 | 5→1 | 5→1(型のみ) |
| サブシステムを直接使えるか | 使えない(コンパイルエラー) | 使える(約束ごと) | index.ts 経由では使えない |
全体の行数は Facade の分だけ増えます。 呼び出し側が1か所だけなら、Facade なしのほうが短く済みます。減るのは「呼び出し側1か所あたりの行数と知識」で、呼び出し側が2か所、3か所と増えるほど差が開きます。
通知の API を変えたときに直す場所
通知のメソッドを send(to, subject, body) から、Message を受け取る send(message) に変えました(Python)。サブシステムを変えたあと、呼び出し側を直さずに動かすと、次のエラーで止まります。
notifier.send(customer, "ご注文ありがとうございます", f"{sku} x {qty}")
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: Notifier.send() takes 2 positional arguments but 4 were given
直したあとの差分を diff で数えました(サブシステム本体の変更は両方とも同じなので除いています)。
== Facade なし(2ファイル・4行)
nofacade/web_checkout.py
- notifier.send(customer, "ご注文ありがとうございます", f"{sku} x {qty}")
+ notifier.send(Message(customer, "ご注文ありがとうございます", f"{sku} x {qty}"))
nofacade/batch_reorder.py
- notifier.send(customer, "定期注文を受け付けました", f"{sku} x {qty}")
+ notifier.send(Message(customer, "定期注文を受け付けました", f"{sku} x {qty}"))
(各ファイルの import の行も1行ずつ変更)
== Facade あり(1ファイル・2行)
facade/order_facade.py
- self._notifier.send(customer, subject, f"{sku} x {qty}")
+ self._notifier.send(Message(customer, subject, f"{sku} x {qty}"))
(import の行も1行変更)
呼び出し側が N か所あれば、Facade なしでは N ファイル、Facade ありでは常に1ファイル(Facade)を直します。 呼び出し側が画面・バッチ・API・管理画面の4か所なら、4ファイルと1ファイルの差になります。
テストのしやすさ(偽物の数が変わる)
この節では、Facade を入れると、呼び出し側のテストに要る偽物(モック)の数がどう変わるか、また Facade 自体のテストはどうなるかが分かります。
呼び出し側のテストは、偽物が3つから1つになりました。 ただし「決済が通らなければ在庫を戻す」を確かめるテストは、Facade のテストとして3つの偽物を使って書きます。複雑さは消えず、Facade のテスト1か所に集まります。
| テストの対象 | 偽物の数 | 行数(空行を除く) | 結果 |
|---|---|---|---|
| 画面側(Facadeなし) | 3(在庫・決済・通知) | 16行 | ok |
| 画面側(Facadeあり) | 1(Facade) | 11行 | ok |
| Facade 自体 | 3(在庫・決済・通知) | 15行 | ok |
| バッチ側(Facadeなし) | 3(在庫・決済・通知) | 12行 | FAILED |
Facade ありの画面側のテストです。偽物は Facade の1つだけです。
import sys, unittest; sys.path[:0] = [".", "facade"]
from unittest.mock import Mock
from order_facade import OrderResult
from web_checkout import checkout
class CheckoutTest(unittest.TestCase):
def test_失敗をNGで返す(self):
orders = Mock() # 偽物は1つ
orders.place_order.return_value = OrderResult(False, "与信NG")
self.assertEqual(checkout(orders, "sato", "A-100", 3, 30_000), "NG: 与信NG")
if __name__ == "__main__":
unittest.main(verbosity=2)
Facade なしのバッチ側に「決済NGなら在庫を戻す」のテストを書くと、前の節の戻し忘れが見つかります。
$ python3 test_batch_nofacade.py
test_決済NGなら在庫を戻す (__main__.ReorderTest.test_決済NGなら在庫を戻す) ... FAIL
Traceback (most recent call last):
File "/dev/shm/t93/py/test_batch_nofacade.py", line 11, in test_決済NGなら在庫を戻す
inventory.release.assert_called_once_with("A-100", 3)
AssertionError: Expected 'release' to be called once. Called 0 times.
FAILED (failures=1)
Facade なしでは、同じテストを呼び出し側の数だけ書く必要があります。Facade ありなら、戻し処理のテストは Facade に1本あればよく、呼び出し側は「成功と失敗をどう表示するか」だけをテストします。TypeScript でも、OrderService の偽物1つで画面側のテストが通りました。
$ node --test dist/checkout.test.js
ok 1 - Facade の偽物1つで、画面側の分岐を確かめる
# tests 1
# pass 1
# fail 0
Facade で楽になるテスト
呼び出し側(画面・バッチ・API)の分岐
偽物は Facade 1つで済む
サブシステムの例外の種類を知らなくてよい
Facade に集まるテスト
呼ぶ順番・戻し処理・例外の変換
偽物は3つ要る(減らない)
ここを厚くテストすれば、呼び出し側は薄くできる
AdapterパターンとFacadeパターンの違い(Mediator・Proxyとも比べる)
この節では、Facade と混同しやすい Adapter・Mediator・Proxy・Decorator との違いが、動かしたコードと表で分かります。
Facade は「複数をまとめて簡単にする」、Adapter は「1つの形を、こちらが期待する形に合わせる」です。 refactoring.guru は、Facade は既存のオブジェクト群に新しいインターフェースを定義し、Adapter は既存のインターフェースを使えるようにするもので、Adapter は通常1つのオブジェクトだけを包む、と説明しています。
| パターン | 目的 | 包む相手 | インターフェース |
|---|---|---|---|
| Facade | 複雑な仕組みを簡単に使う | 複数(サブシステム) | 新しく作る(簡単な入口) |
| Adapter | 形の合わないものを使えるようにする | 通常1つ | 既に決まった形に合わせる |
| Mediator | 部品どうしのやり取りをまとめる | 部品が仲介役を知る | 部品と仲介役の双方向 |
| Proxy | 本物の前に立って代わりに受ける | 1つ | 本物と同じ(入れ替えられる) |
| Decorator | 動きを後から足す | 1つ | 本物と同じ |
Adapter:外部の決済 SDK の形を合わせる
金額を文字列で受け取り、結果をコードで返す外部の決済 SDK を、こちらの PaymentGateway の形に合わせる Adapter です。包むのは1つで、呼ぶ順番をまとめてはいません。
class LegacyPayAdapter implements PaymentGateway {
constructor(private readonly client: LegacyPayClient, private readonly tokenOf: (c: string) => string) {}
authorize(customer: string, amount: number): string {
const r = this.client.charge(String(amount), this.tokenOf(customer));
if (r.code !== "00") throw new Error(`決済が通りません(code=${r.code})`);
return r.txn;
}
}
$ node dist/patterns/adapter.js
Adapter: TX9001
Adapter: 決済が通りません(code=51)
Facade と Adapter は一緒に使えます。Adapter で外部 SDK を PaymentGateway の形に合わせ、Facade はその PaymentGateway を在庫・通知と一緒に呼ぶ、という組み合わせです。
Mediator:部品どうしが仲介役にだけ話す
Mediator は、部品が「起きたこと」を仲介役に伝え、仲介役が他の部品を動かします。Facade との違いは向きです。Facade のサブシステムは Facade を知りませんが、Mediator の部品は Mediator を知っていて、話しかけます。
class Stock {
constructor(private m: Mediator, private left = 3) {}
take(qty: number) { this.left -= qty; if (this.left < 2) this.m.notify("stock", "low", { left: this.left }); }
}
class StoreMediator implements Mediator {
stock = new Stock(this); purchasing = new Purchasing(); alert = new Alert();
notify(sender: string, event: string, data: Record<string, unknown>) {
if (sender === "stock" && event === "low") { this.purchasing.order(10); this.alert.post("在庫が少なくなりました"); }
}
}
$ node dist/patterns/mediator.js
Mediator: stock から low { left: 1 }
[仕入れ] 10 個を発注
[通知] 在庫が少なくなりました
refactoring.guru は、どちらも密に結びついたクラスの協調を整理するものの、Facade は新しい機能を足さず、サブシステムの中のオブジェクトどうしは直接やり取りする、Mediator はやり取りを一か所に集め、部品は Mediator だけを知る、と違いを説明しています。
Proxy・Singleton・Abstract Factory との関係
- Proxy:本物と同じインターフェースを持ち、入れ替えられます。Facade は本物と違う、簡単なインターフェースを作ります。
- Singleton:Facade は1つあれば足りることが多いため、Singleton にされることがあります。テストで差し替えにくくなるため、DI で1つだけ作る形のほうが扱いやすくなります。
- Abstract Factory:隠したいのが「オブジェクトの作り方」だけなら、Facade の代わりに使えます(refactoring.guru)。
LaravelのFacadeはGoFのFacadeパターンと別物
この節では、Laravel の Cache::get() のような Facade が何者で、GoF の Facade パターンとどこが違うのかが、公式ドキュメントの範囲で分かります(Laravel は動かしていません)。
Laravel の Facade は、サービスコンテナにあるオブジェクトへの「静的なプロキシ(static proxies)」です。 Laravel 13.x の公式ドキュメントは、Facade が、簡潔で表現力のある書き方を提供しつつ、従来の静的メソッドよりテストしやすく柔軟だ、と説明しています。
use Illuminate\Support\Facades\Cache;
Route::get('/cache', function () {
return Cache::get('key');
});
| GoF の Facade パターン | Laravel の Facade | |
|---|---|---|
| 目的 | 複数のクラスを決まった順番で呼ぶ処理を、簡単な入口にまとめる | コンテナのオブジェクトを、静的メソッドの書き方で呼べるようにする |
| 包む相手 | 複数(サブシステム) | 1つ(コンテナに登録されたオブジェクト) |
| 仕組み | 普通のクラスとメソッド | __callStatic() で、getFacadeAccessor() が返す名前のオブジェクトをコンテナから取り出して呼ぶ |
| 近いパターン | — | Proxy(名前のとおり「プロキシ」) |
| テスト | Facade の偽物を渡す | Cache::shouldReceive('get') などで差し替える |
- 仕組み:すべての Facade は
Illuminate\Support\Facades\Facadeを継承し、getFacadeAccessor()でコンテナの登録名(Cache ならcache)を返します。Cache::get()を呼ぶと、コンテナからcacheのオブジェクトを取り出し、そのgetを呼びます。 - ヘルパー関数との違い:公式ドキュメントは、Facade とヘルパー関数(
view()・cache()など)に実用上の違いはまったく無い、と書いています。 - テスト:
Cache::shouldReceive('get')->with('key')->andReturn('value')のように差し替えられ、Cache::spy()で呼ばれたかを後から確かめることもできます。公式は、Request の Facade はモックせず、テストのget・postに値を渡すこと、Config はConfig::setを使うことを勧めています。 - 注意点:公式ドキュメントは「Facade の主な危険はクラスの scope creep(責務が気づかないうちに広がること)」だと書いています。注入しないので、1つのクラスが多くの Facade を使っても、コンストラクターが長くなるといった兆候が見えにくいためです。
Laravel で「Facade を作る」と言うとき、多くは Laravel の Facade(静的プロキシ)のことです。GoF の Facade パターンを Laravel で書くなら、サービスクラスを作り、コンストラクターでリポジトリなどを受け取る形になります。Laravel のモデル操作は、関連記事「Laravel Eloquentの使い方|リレーション・N+1対策」で扱っています。
よくある誤用:何でも入るGod Object化
この節では、Facade が太りすぎて何でも入る「God Object(神オブジェクト)」になる例と、その見つけ方・分け方が分かります。
Facade は「入口が1つ」で便利なため、関係の無い処理まで足されやすく、アプリ全体と結びついた God Object になりがちです。 refactoring.guru も短所としてこの点を挙げ、太ったら一部を別の Facade に切り出すことを勧めています。
注文・返品・クーポン・配送・会員・売上集計を1つに入れた例を作り、Python の ast でメソッドの数と、使っているサブシステムの数を数えました(クラスは要点だけを抜き出しています)。
class ShopFacade:
"""悪い例:何でも入る窓口(God Object 化)"""
def __init__(self, inventory, payment, notifier, shipping, coupon, report, user_repo): ...
def place_order(self, *a): ... # 在庫・決済・通知
def cancel_order(self, *a): ... # 決済・在庫・通知
def return_item(self, *a): ... # 配送・決済・在庫
def apply_coupon(self, *a): ... # クーポン
def monthly_sales(self, *a): ... # 売上集計
def register_user(self, *a): ... # 会員・通知
def reset_password(self, *a): ... # 会員・通知
def export_csv(self, *a): ... # 売上集計
# ほか ship・change_address・restock・send_campaign
$ python3 count_deps.py god.py
ShopFacade methods 12 deps 7 ['coupon', 'inventory', 'notifier', 'payment', 'report', 'shipping', 'user_repo']
place_order 3
cancel_order 3
return_item 3
apply_coupon 1
ship 2
change_address 2
monthly_sales 1
register_user 2
reset_password 2
restock 1
export_csv 1
send_campaign 3
12のメソッドのうち、サブシステムを1つしか使わないメソッドが4つ(apply_coupon・monthly_sales・restock・export_csv)ありました。1つを呼ぶだけなら、呼ぶ順番をまとめる Facade の仕事はしていません。テストのたびに7つの依存を用意する必要もあります。
11つしか呼ばないメソッドは外す
apply_coupon のような素通しは、呼び出し側がサブシステムを直接使えばよい。
2使い方のまとまりで分ける
注文(OrderFacade)・返品(ReturnFacade)・会員(AccountFacade)のように、呼び出し側の目的ごとに窓口を分ける。
3依存の数を目安にする
コンストラクターの引数が増え続けたら分けどき。Laravel の公式が言う scope creep と同じ兆候。
責務を分ける考え方(単一責任の原則)は、関連記事「SOLID原則をわかりやすく|早見表とJavaの直す前・後で比較」で扱っています。
そのほかの誤用
- 業務の判断を Facade に書き込む:「会員ランクが金なら送料無料」のような業務の決まりは、Facade ではなくドメインのクラスに置きます。Facade は順番と組み合わせだけを持つと、テストも薄く済みます。
- サブシステム1つを包むだけ:形を合わせるだけなら Adapter、同じ形で前に立つなら Proxy です。名前に Facade と付けると、読む人が「複数をまとめている」と誤解します。
- Facade の戻り値にサブシステムのオブジェクトをそのまま返す:呼び出し側がそれを使い始め、隠したはずの依存が戻ってきます。この記事の題材では、結果を OrderResult にまとめて返しました。
Facadeパターンの使いどころと判断表
この節では、Facade を入れるべき場面と、入れないほうがよい場面を、手元の比較の結果から判断する目安が分かります。
- 1同じ手順を2か所以上から呼ぶ → 入れる画面とバッチで戻し処理がずれた例が典型
- 2呼ぶ順番や失敗時の戻しに決まりがある → 入れる決まりを1か所に書き、1か所でテストする
- 3外部ライブラリーの変更から自分のコードを守りたい → 入れる変更は Facade だけに閉じ込める
- 4層(レイヤー)の境界に入口を置きたい → 入れる各層に Facade を置き、層の間の結びつきを減らす
- 5呼び出し側が1か所で、手順も短い → まだ入れない行数は Facade の分だけ増える。2か所目ができたときに切り出す
- 6包む相手が1つだけ → Facade ではなく Adapter か Proxy目的に合った名前にする
| 場面 | 選ぶもの | 理由 |
|---|---|---|
| 在庫・決済・通知を画面とバッチから呼ぶ | Facade | 手順と戻し処理を1か所に |
| 外部の決済 SDK の形がこちらと違う | Adapter | 1つの形を合わせるだけ |
| 外部 SDK が複数あり、使う順番も複雑 | Adapter + Facade | 形を合わせてから、まとめる |
| 部品が互いを呼び合って絡まっている | Mediator | やり取りを仲介役に集める |
| 重いオブジェクトの作成を遅らせたい・権限を確かめたい | Proxy | 本物と同じ形で前に立つ |
| 関数1つを1か所から呼ぶだけ | 何も使わない | パターンを足すと読む量が増える |
| Laravel で Cache・DB を呼ぶ | Laravel の Facade か DI | GoF の Facade とは別の話 |
Facade を入れる手順
同じ手順を書いている場所を探す
サブシステムのクラスを import しているファイルを grep し、同じ順番で呼んでいる箇所を並べる。手順のずれ(戻し忘れ)がないかもここで見る。
入口のメソッドを決める
呼び出し側が本当に必要とする引数と結果だけで、メソッドの形を決める。結果は専用の型(OrderResult など)にし、サブシステムのオブジェクトを返さない。
Facade に手順を移し、テストを書く
呼ぶ順番と戻し処理を Facade に移し、成功・在庫不足・決済NG の3通りをテストする。偽物は3つ要る。
呼び出し側を1か所ずつ置き換える
画面・バッチなどを1か所ずつ Facade の呼び出しに替え、そのたびにテストを通す。
公開範囲を絞る
Java は package-private、TypeScript は index.ts から再エクスポートしない、Python は _ と __all__。直接使ってよい場面が残るなら、それも決めておく。
インターフェースと抽象クラスのどちらで Facade の型を決めるかは、関連記事「Javaインターフェースと抽象クラスの違い|比較表と使い分け」で扱っています。「抽象化とは」の記事(公開予定)とも合わせて読むと、Facade が「呼び出し側から見た抽象化」の1つの形だと分かります。
Facadeパターンでよくあるエラー・つまずき
この節では、Facade を書いて動かすときに手元で出たエラーの原文と、その原因・直し方が分かります。
| 出た文言(原文) | 原因 | 直し方 |
|---|---|---|
error: Inventory is not public in order; cannot be accessed from outside package(javac) |
package-private にしたサブシステムを、別のパッケージから使った | Facade を通す。テストは同じパッケージに置く |
error TS2305: Module '"../src/order"' has no exported member 'Inventory'.(tsc) |
index.ts が再エクスポートしていないクラスを import した | Facade と型だけを使う |
TypeError: Notifier.send() takes 2 positional arguments but 4 were given(Python) |
サブシステムの API を変えたのに、呼び出し側を直していない | 呼び出しを Facade に集め、直す場所を1つにする |
AssertionError: Expected 'release' to be called once. Called 0 times. |
呼び出し側ごとに書いた手順の1つで、戻し処理が抜けている | 手順を Facade に移し、戻し処理を1か所でテストする |
error TS1259: Module '"node:assert/strict"' can only be default-imported using the 'esModuleInterop' flag |
テストで import assert from と書いた(esModuleInterop が無効) |
import * as assert from にするか、設定を有効にする |
Error: Cannot find module '/dev/shm/t93/ts/dist'(node --test dist/) |
Node.js 22 の node --test にフォルダーを渡した |
テストのファイル名を渡す |
日本語が [??] A-100 ? 2 ? と表示される(java) |
ロケールが未設定(POSIX)で、標準出力が ASCII になった | LANG=C.UTF-8 などを設定して実行する |
- 1Facade のメソッドは、サブシステムを2つ以上呼んでいるか1つだけなら素通し。Facade に置く理由が薄い
- 2戻り値にサブシステムのオブジェクトを返していないか返すと、隠した依存が呼び出し側に戻ってくる
- 3失敗時の戻し処理を Facade のテストで確かめているか成功・在庫不足・決済NG の3通り
- 4コンストラクターの引数が増え続けていないか増えたら目的ごとに Facade を分ける
- 5業務の決まりを Facade に書き込んでいないか決まりはドメインのクラスへ。Facade は順番と組み合わせだけ
よくある質問
Q. Facadeパターンとは、一言でいうと何ですか? A. 複雑な仕組みに、簡単な入口を1つ用意するデザインパターンです。いくつものクラスを決まった順番で呼ぶ処理を1つのクラス(Facade)にまとめ、呼び出す側には Facade だけを見せます。
Q. ファサードとはどういう意味ですか? A. 英語の facade(façade)は建物の正面・外観のことです。中の複雑な構造を見せない「顔」という意味で、パターンの名前に使われています。
Q. Facadeパターンのメリットとデメリットは? A. メリットは、呼び出し側の依存が減る(手元で5→1)、手順の変更を1か所で直せる(2→1ファイル)、呼び出し側のテストが偽物1つで済むことです。デメリットは、全体の行数が Facade の分だけ増えること、太って God Object になりやすいことです。
Q. Facade と Adapter の違いは何ですか? A. Facade は複数のクラスをまとめて、新しい簡単な入口を作ります。Adapter は通常1つの相手を包み、こちらが既に決めている形に合わせます。両方を組み合わせて使うこともできます。
Q. Facade と Mediator の違いは何ですか? A. 向きが違います。Facade のサブシステムは Facade を知らず、呼ばれるだけです。Mediator の部品は Mediator を知っていて、起きたことを伝え、Mediator が他の部品を動かします。
Q. Facade とサービス層(Service クラス)は同じですか? A. 複数のリポジトリや外部 API を決まった順番で呼ぶサービスクラスは、Facade の考え方そのものです。ただしサービス層には業務の判断も入ることが多く、Facade は本来、順番と組み合わせだけを持つ点が違います。
Q. Facade を使っても、サブシステムを直接呼んでよいですか? A. パターンとしては禁止されていません。細かい操作が要る場面だけ直接使い、普通の使い方は Facade を通す、という分け方もあります。直接使わせたくないなら、Java の package-private のように言語の仕組みで閉じます。
Q. Laravel の Facade は GoF の Facade パターンですか? A. 名前は同じですが別物です。Laravel の公式ドキュメントは、Facade をサービスコンテナのオブジェクトへの「静的なプロキシ」と説明しています。1つのオブジェクトを静的メソッドの書き方で呼べるようにする仕組みで、Proxy に近いものです。
Q. Facade は Singleton にするべきですか? A. 1つあれば足りることが多いので Singleton にされることもありますが、テストで差し替えにくくなります。DI コンテナで1つだけ作って渡す形のほうが、偽物に替えやすくなります。
Q. Facade は何個まで作ってよいですか? A. 数の決まりはありません。注文・返品・会員のように、呼び出し側の目的ごとに分けるのが目安です。1つの Facade のコンストラクターの引数が増え続けたら、分けどきです。
Q. Python で Facade を書く意味はありますか? A. あります。Python では公開範囲を言語で強制できませんが、手順と戻し処理を1か所にまとめる効果は同じです。手元の例でも、Facade にしたことでバッチの戻し忘れが無くなりました。モジュール自体を Facade にして、関数を1つだけ公開する書き方もよく使われます。
まとめ
- Facadeパターンは、複数のクラスを決まった順番で呼ぶ処理を1つの入口にまとめるパターン。サブシステムは Facade を知らない。
- 手元の注文処理では、Facade なしだとバッチの戻し忘れで在庫が3個ずれ、Facade ありではずれが0になった。
- 呼び出し側のコードは Python 15→4行・Java 31→11行・TypeScript 21→5行、知る型は5→1、通知の API 変更で直すファイルは2→1。ただし全体の行数は Facade の分だけ増える。
- 呼び出し側のテストは偽物3つ→1つ。戻し処理のテストは Facade に集まり、そこでは3つの偽物が要る。
- Adapter は1つの形を合わせる、Mediator は部品どうしのやり取りを仲介、Proxy は同じ形で前に立つ。Laravel の Facade は静的なプロキシで、GoF の Facade とは別物。太りすぎ(God Object)に気をつけ、目的ごとに分ける。
次に読むとよい記事です。クラスとオブジェクトの基本は関連記事「オブジェクト指向とは?コードでわかる基本と使いどころ」で扱っています。
参考・出典
確認日はいずれも 2026年10月29日です。
- refactoring.guru「Facade(ファサード)」:https://refactoring.guru/ja/design-patterns/facade
- Wikipedia「Facade pattern」:https://en.wikipedia.org/wiki/Facade_pattern
- Wikipedia「Facade パターン」:https://ja.wikipedia.org/wiki/Facade_パターン
- Laravel 13.x「Facades」:https://laravel.com/docs/13.x/facades
- Laravel 13.x「Mocking」:https://laravel.com/docs/13.x/mocking
- SLF4J「Simple Logging Facade for Java」:https://www.slf4j.org/
- Python ドキュメント「subprocess --- サブプロセス管理」:https://docs.python.org/ja/3.13/library/subprocess.html
- Python ドキュメント「unittest.mock --- モックオブジェクトライブラリ」:https://docs.python.org/ja/3.13/library/unittest.mock.html
- TypeScript「Modules」:https://www.typescriptlang.org/docs/handbook/2/modules.html
- Oracle「Controlling Access to Members of a Class」(The Java Tutorials):https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Facadeパターンとは|Adapter・Mediatorとの違いと3言語の実装比較」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。