デザインパターン — 4 つの代表パターンと語彙
デザインパターンの価値は、実装のテンプレートであること以上に共通の語彙になることにあります。「ここは Strategy で」と言えば、条件分岐を差し替え可能な部品に切り出すという設計が一言で伝わります。
一方で、名前だけ覚えて中身が入れ替わっていると、会話が噛み合わなくなります。この章では代表的な 4 つを取り上げ、それが解こうとした問題に加えて混同されやすい別のパターンを並べて整理します。
この章で学ぶこと
- Strategy・Singleton・Observer・Abstract Factory がそれぞれ何を解くか
- Strategy と State、Observer とポーリングの違い
- Abstract Factory と Factory Method の関係
クラスとインターフェースを書いた経験があれば読めます。コード例は TypeScript です。
この章で扱わないこと
各パターンを TypeScript でどう書くか、そもそも現代の TypeScript でそのパターンが必要かという判定は、専用のガイドが扱います。
- TypeScript デザインパターン — GoF の 23 パターンを現代の TypeScript で読み直す全 16 章
- 判定の一覧 — 各パターンを「言語機能で代替できる / 条件付き / クラス実装を推奨」の 3 段階で判定した表
本章は語彙をそろえるところまでを担当します。採否の判断はそちらを読んでください。
Strategy — アルゴリズムを差し替え可能にする
解く問題: 同じ目的の処理に複数のやり方があり、実行時に選び分けたい。条件分岐で書くと、やり方が増えるたびに分岐が伸びていく。
やり方 (アルゴリズム) を外から渡せる形にして、呼び出し側から差し替えられるようにします。
type ShippingStrategy = (weightKg: number) => number;
const standard: ShippingStrategy = (w) => 500 + w * 100;
const express: ShippingStrategy = (w) => 1200 + w * 150;
const freeOverLimit: ShippingStrategy = (w) => (w > 10 ? 0 : 500);
function checkout(weightKg: number, calcShipping: ShippingStrategy): number {
return calcShipping(weightKg);
}
checkout(5, express); // 1950
配送料の計算方法が増えても checkout は変わりません。「何をするか」は固定で、「どうやるか」を差し替えるのが Strategy です。
State との違い
よく混同されるのが State パターンです。
| Strategy | State | |
|---|---|---|
| 差し替えるもの | アルゴリズム | オブジェクトの状態ごとの振る舞い |
| 誰が選ぶか | 外部の呼び出し側 | 自分自身 (状態遷移で切り替わる) |
| 遷移の概念 | 無い | ある |
**できる操作がいまの状態で決まり、その状態が移り変わっていくなら State です。**Strategy に遷移という概念はありません。外から渡されたやり方をそのまま使うだけです。
注文が「受付 → 発送済み → 配達完了」と移り変わり、状態ごとに可能な操作が変わる、というのが State です。配送料の計算方法を選ぶのが Strategy です。
Singleton — インスタンスを 1 つに限定する
解く問題: 設定やコネクションプールのように、システム全体で 1 つだけ存在すべきものがある。複数作られると状態が食い違う。
その型のインスタンスが 1 つしか存在しないことを保証し、取得の窓口を 1 か所にまとめます。
class AppConfig {
private static instance: AppConfig | null = null;
private constructor(readonly apiBaseUrl: string) {}
static getInstance(): AppConfig {
if (AppConfig.instance === null) {
AppConfig.instance = new AppConfig(process.env.API_BASE_URL ?? '');
}
return AppConfig.instance;
}
}
AppConfig.getInstance() === AppConfig.getInstance(); // true
コンストラクタを外から呼べなくして、取得の窓口を 1 つに絞るのが基本の形です。
Singleton には「どこからでも同じものへ手が届く」という側面もあります。これは便利さと同時に、どこからでも書き換えられる状態を作るという危うさも持ちます。テストで差し替えにくい、依存関係が見えなくなる、といった批判が向けられるのはこのためです。
なお JavaScript と TypeScript では、モジュールが 1 度だけ評価される仕組みがあるので、クラスを書かずに同じ効果を得られる場面が多くあります。その線引きは専用ガイドの Singleton の章 が扱います。
Observer — 状態の変化を購読者へ通知する
解く問題: ある対象の状態が変わったときに、複数の関心を持つ側へ知らせたい。対象が相手を直接知っていると、増減のたびに書き換えが要る。
対象 (subject) が購読者 (observer) のリストを持ち、変化が起きた時点で対象の側から通知します。
type Listener<T> = (value: T) => void;
class Store<T> {
private listeners: Listener<T>[] = [];
constructor(private value: T) {}
subscribe(listener: Listener<T>): () => void {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter((l) => l !== listener);
};
}
set(next: T): void {
this.value = next;
// 変化した「その時」に通知する
this.listeners.forEach((l) => l(next));
}
}
const cart = new Store(0);
const unsubscribe = cart.subscribe((count) => console.log(`カート: ${count} 点`));
cart.set(3); // "カート: 3 点"
unsubscribe();
ポーリングとの違い
**Observer は定期的にチェックする仕組みではありません。**ここが最も誤解されやすい点です。
| Observer (プッシュ型) | ポーリング (プル型) | |
|---|---|---|
| きっかけ | 変化が起きた瞬間 | 一定間隔のタイマー |
| 無駄な処理 | 起きない | 変化が無くても毎回走る |
| 遅延 | ほぼゼロ | 最大で間隔ぶん |
| 実装 | 購読リストと通知 | setInterval などで繰り返し確認 |
変化に気づくために一定の間隔で問い合わせ続けるのは、ポーリングのやり方です。Observer は逆で、変化した側が知らせに行くので、問い合わせ自体が不要になります。これが Observer を使う動機そのものです。ブラウザや Node.js の上では、この知らせがイベントとして積まれ、手が空いた順に処理されます (イベントループ)。
ブラウザの addEventListener、Node.js の EventEmitter、状態管理ライブラリの購読機構は、いずれもこの形です。
Abstract Factory — 関連する一群を同じ窓口で作る
解く問題: 一緒に使われるべきオブジェクトの組があり、組み合わせを間違えると壊れる。しかも具体的な型を利用側に書かせたくない。
一緒に使う部品の組を、利用側に具体的な型を書かせないまま、ひとつの窓口からまとめて作れるようにします。
interface Button { render(): string }
interface Checkbox { render(): string }
// 関連する一群をまとめて作る窓口
interface UiFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
const lightFactory: UiFactory = {
createButton: () => ({render: () => '<button class="light">'}),
createCheckbox: () => ({render: () => '<input class="light">'}),
};
const darkFactory: UiFactory = {
createButton: () => ({render: () => '<button class="dark">'}),
createCheckbox: () => ({render: () => '<input class="dark">'}),
};
// 利用側は具象を知らない。light と dark が混ざることもない
function buildForm(factory: UiFactory): string {
return factory.createButton().render() + factory.createCheckbox().render();
}
利用側は lightFactory か darkFactory かを 1 か所で決めるだけです。**明るいテーマのボタンと暗いテーマのチェックボックスが混ざる、という不整合が構造的に起きません。**これが「関連する一群」をまとめて扱う価値です。
Factory Method との違い
| Factory Method | Abstract Factory | |
|---|---|---|
| 作るもの | 1 種類のオブジェクト | 関連する複数種類の組 |
| 主な狙い | 生成の詳細を隠す | 組み合わせの一貫性を保つ |
Factory Method が「ボタンを作る 1 つのメソッド」なら、Abstract Factory は「ボタンもチェックボックスも入力欄も作れる 1 つの窓口」です。
混同されやすい組み合わせ
| 説明 | 正しいパターン |
|---|---|
| できる操作が、いまの状態によって変わる | State (Strategy ではない) |
| 一定間隔で問い合わせて変化に気づく | ポーリング (Observer ではない) |
| 1 種類のオブジェクトの生成を隠す | Factory Method (Abstract Factory ではない) |
| アルゴリズムを外から差し替える | Strategy |
| 変化した側が購読者へ知らせる | Observer |
| 関連する一群を同じ窓口で作る | Abstract Factory |
| インスタンスを 1 つに限定する | Singleton |
よくある誤解
「Strategy と State は同じもの」 — クラス図はほとんど同じですが、意図が違います。State には「次はどの状態か」という遷移があり、Strategy にはありません。Strategy で差し替えを決めるのは常に呼び出し側です。
「Observer は変化を監視し続ける仕組み」 — 名前から受ける印象と逆です。購読側は何も見張りません。変化した側が通知を送るので、購読側は待っているだけで済みます。
「Abstract Factory は 1 つのオブジェクトを作るパターン」 — 1 種類なら Factory Method です。Abstract Factory は関連する複数種類の組を扱います。
「パターンを多く使うほど良い設計」 — パターンは目的ではなく手段です。言語機能で足りるならそちらが簡潔です。判定は専用ガイドを参照してください。
確認問題
問 1. 通知機能を Slack とメールの両方に対応させます。次の 2 案のうち Abstract Factory と呼べるのはどちらで、もう一方は何ですか。
// 案 A
interface NotifierFactory {
createSender(): Sender;
}
// 案 B
interface ChannelFactory {
createSender(): Sender;
createFormatter(): Formatter;
createRetryPolicy(): RetryPolicy;
}
答え: 案 B が Abstract Factory、案 A は Factory Method
分かれ目は作るものが 1 種類か、関連する一群かです。
案 A は Sender だけを作ります。何を作るかの決定をサブクラスへ委ねる形で、これが Factory Method です。
案 B は送信・整形・再送の 3 つをまとめて作ります。Slack 用の工場からは Slack 用の 3 点が、メール用の工場からはメール用の 3 点が出てくるので、Slack の送信処理にメール用の整形が混ざる組み合わせが作れません。この「一貫性の保証」が Abstract Factory の狙いです。
案 B の工場をアプリ全体で 1 つに絞りたくなったら、そこで初めて Singleton の出番になります。Abstract Factory と Singleton は競合せず、重ねて使えます。
問 2. 「支払い方法をクレジットカード・コンビニ払い・銀行振込から選べるようにする」のはどのパターンですか。
答え: Strategy
支払い処理という同じ目的に対して複数のやり方があり、実行時に呼び出し側が選びます。遷移はありません。ユーザーが選んだ方法をそのまま使うだけです。
type PaymentMethod = (amountYen: number) => Promise<void>;
async function pay(amountYen: number, method: PaymentMethod): Promise<void> {
await method(amountYen);
}
もしこれが「注文が受付中 → 支払い済み → 発送済みと遷移し、状態ごとにできる操作が変わる」という話なら State です。選ぶのが外部か、遷移で自動的に変わるかが判断の分かれ目です。
問 3. 「1 秒ごとに API を叩いて在庫数の変化を検出する」実装を Observer パターンだと説明されました。妥当ですか。
答え: 妥当ではありません。ポーリングです。
Observer は変化が起きた側が購読者へ知らせる仕組みです。この実装は購読側が定期的に問い合わせているので、方向が逆です。
| この実装 | Observer | |
|---|---|---|
| きっかけ | タイマー | 在庫が変わった瞬間 |
| 変化が無いとき | それでも問い合わせる | 何も起きない |
| 反映の遅れ | 最大 1 秒 | ほぼゼロ |
サーバー側の変化をクライアントへ push したいなら、WebSocket や Server-Sent Events を使います。これらは通知が push されるので Observer 的な構造になります。
ポーリングが悪いわけではありません。実装が単純で、接続を維持しなくてよいという利点があります。ただしこれを Observer と呼んでしまうと、設計の議論が噛み合わなくなります。
まとめ
- Strategy はアルゴリズムを外から差し替えるパターンです。状態遷移を扱う State とは別物です
- Singleton はインスタンスを 1 つに限定し、取得の窓口を 1 か所にまとめるパターンです
- Observer は変化した側が購読者へ通知するプッシュ型です。定期的にチェックするポーリングとは逆向きです
- Abstract Factory は一緒に使う部品の組を、ひとつの窓口からまとめて作るパターンです。1 種類なら Factory Method です
- パターンは共通の語彙です。採否の判断は TypeScript デザインパターン を参照してください
次に読む
- TCP/IP — ここからネットワークの層へ降りていきます