まとめ — 章を立てなかったパターンと、判定の一覧
独立した章を設けなかったパターンを短く扱い、連載全体の判定を 1 枚の表にまとめます。
章を立てなかった 8 つのパターン
いずれも GoF に収録されているパターンです1。TypeScript の観点で議論の幅が狭いか、他の章と話が重なるか、既存記事が別の文脈で扱っているため、独立した章は設けませんでした。**この取捨も、以下の各短評の「判定」も、本ガイドの編集判断です。**独立章の判定と違って、覆る条件までは書いていません。
Prototype
既存のオブジェクトを複製して新しいオブジェクトを作るパターンです。深い複製の標準 API があります。
The
structuredClone()method of theWindowinterface creates a deep clone of a value using the structured clone algorithm.2
引用はブラウザの Window を対象にしたページからのものですが、Node.js でも v17.0.0 から同じ名前のグローバル関数が使えます3。
素のデータを複製するだけなら、これで足ります。ただしクラスのインスタンスには使えません。仕様には次の制限が明記されています。
The prototype chain is not walked or duplicated.4
Property descriptors, setters, getters, and similar metadata-like features are not duplicated.4
この制限は型に現れないので、注意が要ります。
class SavedArticle {
constructor(readonly url: string) {}
get host(): string {
return new URL(this.url).host;
}
}
const original = new SavedArticle('https://example.com/a');
const copy = structuredClone(original);
// 型は SavedArticle だが、実行時は素のオブジェクト。
// copy instanceof SavedArticle は false、copy.host は undefined になる
const stillTyped: string = copy.host;
structuredClone の型は入力の型をそのまま返すので、コンパイラは何も警告しません。クラスのインスタンスを複製したいなら、自分で clone() を書くことになります。判定: 素のデータなら標準 API で足り、クラスのインスタンスなら自前の複製が要ります。
Facade
大きな部品群に簡潔な入り口を与えるパターンです。TypeScript では、1 つのモジュールなら export しないものが外から触れないので、公開する範囲がそのまま入り口になります。複数のモジュールにまたがる場合は、公開したいものだけを再エクスポートするモジュールを 1 つ置く形になります。**ただしこれは入り口をまとめるだけで、内部のモジュールを直接 import する経路までは塞げません。**塞ぐには lint のルールなど、言語の外の仕組みが要ります。判定: 入り口をまとめるだけなら、Facade クラスを立てずにモジュール境界で足ります。
Command
操作を引数ごとオブジェクトにまとめ、持ち回れるようにするパターンです。呼ぶだけならクロージャで足ります。取り消しやキューへの投入も、実行する関数と取り消す関数を組にしたオブジェクトを並べれば書けます。素のデータに落とす必要が出るのは、保存や転送のように直列化の境界を越えるときです。そのときは判別可能ユニオンで操作の種類を並べ、それを解釈する関数を別に置く形になります。判定: 呼ぶだけなら関数、直列化するなら判別可能ユニオンです。 後者の構造は Visitor と同じになります。
別の観点から扱っている既存記事があります。 ユースケース層 は PHP と DDD の文脈で Command を名前を挙げて扱い、入力を CreateOrderCommand のようなオブジェクトにまとめる利点 (型安全・バリデーション・変更のしやすさ) を挙げています。あちらが対象にしているのは層をまたぐ入力の受け渡しで、本節の「呼ぶだけなら関数」は同一プロセス内で操作を持ち回る場合の話です。前提が違うので、推奨が食い違っているわけではありません。
Bridge
抽象と実装を分けて、それぞれ独立に変えられるようにするパターンです。TypeScript では 2 つの軸を型引数や合成で表せますが、その書き方は Strategy や Adapter で扱った内容と重なります。判定: TypeScript 固有の論点が薄いため、章を設けませんでした。
Flyweight
大量の似たオブジェクトを共有してコストを下げるパターンです。効果があるかどうかは、対象の量と実行環境の挙動で決まります。判定: 適用の可否は計測で決まる話なので、言語機能の選択を扱う本ガイドの射程外です。
Mediator
多対多の連絡を 1 か所に集約するパターンです。集約役をイベントの発行と購読で書けば、Observer で扱った道具がそのまま使えます。判定: 集約するという意図は残りますが、実装の道具は Observer と同じです。
Memento
状態を保存しておき、カプセル化を破らずに後で戻せるようにするパターンです。素のデータなら structuredClone で複製できますが、Prototype と同じ制限がかかります。保存対象をクラスではなく素のオブジェクトで持つと、この制限は最初から起きません。判定: カプセル化を守るという意図は残りますが、復元の道具は Prototype と同じところに行き着きます。
Interpreter
専用の記法を定義して解釈するパターンです。構文木の表現と評価そのものは Visitor で扱いました。文法の定義や構文解析まで踏み込むと言語処理系の設計になり、設計パターンの話から離れます。判定: 評価の部分は Visitor で扱い済み、解析の部分は本連載の範囲外です。
判定の一覧
各章の判定をまとめます。判定は事実ではなく本ガイドの立場です。根拠と、その判定が覆る条件は各章に書いてあります。
| パターン | 判定 | 既定として選ぶ形 |
|---|---|---|
| Singleton | 条件付き | モジュールのトップレベル。テストで独立させたい・差し替えたいなら引数で受け取る |
| Factory Method | 言語機能で代替できる | 関数と判別可能ユニオン |
| Abstract Factory | 条件付き | 系列が実行時に決まるときだけ、まとまりを返す関数 |
| Builder | 条件付き | オブジェクトリテラル。順序制約か中間状態があるとき段階的な型 |
| Adapter | 条件付き | 名前の不一致は対処なし、形の不一致は変換関数 |
| Decorator | 条件付き | 対象がメソッド 1 つなら高階関数 |
| Proxy | 条件付き | 項目が多いか形が実行時に決まるなら組み込み Proxy |
| Composite | 条件付き | 操作が増えるならユニオン型、種類が増えるならクラス |
| Strategy | 条件付き | 操作 1 つなら関数、束ねるならオブジェクト |
| Observer | 条件付き | EventTarget と AbortSignal |
| State | 条件付き | 判別可能ユニオンと、状態を絞った引数の型 |
| Template Method | 条件付き | 穴が独立ならフックの注入 |
| Iterator | 言語機能で代替できる | ジェネレーターと Symbol.iterator |
| Visitor | 言語機能で代替できる | 判別可能ユニオンと網羅チェック |
| Chain of Responsibility | 言語機能で代替できる | 関数の配列と合成 |
| Repository (GoF 外) | 条件付き | 集約ごとの名前付きインターフェース |
「言語機能で代替できる」と判定したのは 4 つだけです。残りはすべて条件付きになりました。一方で、どのパターンも「クラス実装を推奨」には至りませんでした。素の関数だけでは足りなくなる条件は各章に書きましたが、いずれも次のような限定が付きます。
- 持ち回る状態がある
- 複数の操作を束ねる必要がある
- 増えるのが操作ではなく種類のほう
- すでにクラス階層があり、そこにメソッドを足すだけで済む
- ノード固有の状態に不変条件があり、素のオブジェクトでは守れない
- 出来事の種類ごとに引数の型を厳密に分けたい
パターンが古びたのではなく、同じ目的を果たす形が増えて、選択の問題になったというのが本連載の見立てです。
パターンを使うかどうかを決める手順
各章の判断を通して、次の順序が使えることがわかりました。
- そのパターンが解こうとした問題が、いま自分のコードにあるか確かめる。 GoF が想定した状況と自分の状況は違うことがあります。問題がないなら、そこで終わりです。
- その問題を、言語機能が既に解いていないか確かめる。 反復規約、判別可能ユニオン、第一級の関数、モジュールの境界、
EventTargetが該当します。 - 言語機能で書いた場合に何を失うか見る。 型検査が届かない範囲、素通しの記述量、変更したときの影響範囲です。
- 判定を書き留め、覆る条件も一緒に書く。 前提が変われば判定も変わります。条件を書いておかないと、後から読んだ人が理由のわからない規約として受け取ります。
この手順で繰り返し出てくるのは、**「クラスか関数か」ではなく「束ねる必要があるか」「持ち回る状態があるか」「増えるのは種類か操作か」**という問いです。GoF のパターンの多くは、これらをクラスの組み合わせで表現した形として整理されています。
既存記事との関係
本連載は設計の選択肢を扱いました。なぜその選択が良いのかという原則は、本サイトの他の記事が扱っています。
- SOLID 原則 — 開放閉鎖の原則や依存関係逆転の原則は、本連載のいくつかのパターンの背景にあります。
- クラスとインターフェース — 継承より合成を優先する考え方を扱っています。
- 条件分岐 — 判別可能ユニオンと網羅チェックの基礎です。本連載の複数の章がこれを土台にしています。
- クラスと継承 — 抽象クラス、
implements、そして言語機能としての Decorators を扱っています。 - ユースケース層 — 上の Command の節で触れたとおり、PHP と DDD の文脈で Command を扱っています。