コーディング · デザインパターン
デザインパターン入門:GoFの23パターンをまとめて解説
結論
デザインパターンとは、ソフトウェア設計で繰り返し現れる問題に対する、名前の付いた再利用可能な解決策を、具体的なコードより一段上の抽象度で記述したものです。代表的なカタログは、Gamma、Helm、Johnson、Vlissides の4人(Gang of Four、GoF)による1994年の書籍『Design Patterns: Elements of Reusable Object-Oriented Software』で、生成5つ、構造7つ、振る舞い11の計23パターンが3つのグループに整理されています。パターンはチームの共通語彙になりますが、特定の問題を解くための道具であり、それ自体が目的ではありません。
デザインパターンとは
デザインパターンは、ライブラリでも、コピーして使うコードでもありません。繰り返し現れる問題、解決策の核心、そして採用したときのトレードオフを記述したものです。実際のコードは、使う言語やプログラムに合わせて自分で書きます。
GoFの書籍では、各パターンを次の4つの基本要素で説明しています。
- 名前:「Decoratorで包めばよさそうです」のように、設計について会話するための一言か二言の名称です。
- 問題:そのパターンをいつ適用するか、どのような状況で必要になるかを示します。
- 解決策:関わるクラスやオブジェクト、それぞれの責務、協調のしかたを示します。
- 結果:得られる利点と支払うコストです。たとえば、柔軟性が高まる代わりにクラスが増えます。
この考え方は建築に由来します。Christopher Alexander の著書『A Pattern Language』(1977年)は建物や街づくりで繰り返し使われる解決策を記述しており、GoFの著者たちは同じ発想をオブジェクト指向ソフトウェアに応用しました。
Gang of Four(GoF)の書籍
『Design Patterns: Elements of Reusable Object-Oriented Software』は1994年に Addison-Wesley から出版されました。サンプルコードはC++とSmalltalkですが、パターン自体はJava、C#、TypeScript、Python、Kotlinなど、あらゆるオブジェクト指向言語に応用できます。
カタログに加えて、この書籍は多くのパターンに共通する次の2つの一般原則を主張しています。
- 実装ではなくインターフェースに対してプログラミングする。コードは具体的なクラスではなく、オブジェクトが何をできるかに依存すべきです。
- クラスの継承よりオブジェクトの合成を優先する。オブジェクトを組み合わせて振る舞いを作るほうが、深いクラス階層を作るよりも、たいてい柔軟です。
また、この書籍は各パターンを適用範囲で分類しています。クラスパターンは継承を使い、コンパイル時に固定されます(Factory Method、クラス版Adapter、Interpreter、Template Method)。オブジェクトパターンは合成を使い、実行時に変更できます(それ以外のすべて)。
カテゴリ別・GoF全23パターン
生成パターンはオブジェクトをどう作るか、構造パターンはクラスやオブジェクトをどう組み合わせて大きな構造にするか、振る舞いパターンはオブジェクト同士がどう通信して責務を分担するかを扱います。
| パターン | カテゴリ | 一言でいうと |
|---|---|---|
| Abstract Factory | 生成 | 具体的なクラス名を指定せずに、関連するオブジェクトのファミリーを作る。 |
| Builder | 生成 | 複雑なオブジェクトを段階的に組み立て、構築手順と表現を分離する。 |
| Factory Method | 生成 | どのクラスをインスタンス化するかをサブクラスに任せる。 |
| Prototype | 生成 | 既存のインスタンスをコピーして新しいオブジェクトを作る。 |
| Singleton | 生成 | クラスのインスタンスを1つに限定し、そこへのグローバルなアクセス点を提供する。 |
| Adapter | 構造 | あるインターフェースを、クライアントが期待する別のインターフェースに変換する。 |
| Bridge | 構造 | 抽象と実装を分離し、両方を独立して変更できるようにする。 |
| Composite | 構造 | 個々のオブジェクトとオブジェクトの木構造を同じように扱う。 |
| Decorator | 構造 | オブジェクトを包むことで、動的に責務を追加する。 |
| Facade | 構造 | 複雑なサブシステムに対して、単純な窓口を1つ提供する。 |
| Flyweight | 構造 | 共通の状態を共有して、多数の小さなオブジェクトを効率よく扱う。 |
| Proxy | 構造 | 別のオブジェクトへのアクセスを制御する代理を置く。 |
| Chain of Responsibility | 振る舞い | 処理できるハンドラが見つかるまで、要求をハンドラの連鎖に沿って渡す。 |
| Command | 振る舞い | 要求をオブジェクトにして、キューへの追加、ログ記録、取り消しができるようにする。 |
| Interpreter | 振る舞い | 単純な言語の文法をクラスで表し、その言語の文を評価する。 |
| Iterator | 振る舞い | 内部構造を公開せずに、コレクションの要素を順に取り出す。 |
| Mediator | 振る舞い | オブジェクト間の通信を一か所に集め、互いを直接参照しないようにする。 |
| Memento | 振る舞い | カプセル化を壊さずに、オブジェクトの状態を保存して復元する。 |
| Observer | 振る舞い | オブジェクトが変化したとき、依存するすべてのオブジェクトに自動で通知する。 |
| State | 振る舞い | 内部状態が変わると、オブジェクトの振る舞いも変わるようにする。 |
| Strategy | 振る舞い | アルゴリズムの集まりを1つのインターフェースの背後で交換可能にする。 |
| Template Method | 振る舞い | アルゴリズムの骨組みを定め、一部のステップをサブクラスに任せる。 |
| Visitor | 振る舞い | クラス階層を変更せずに、新しい操作を追加する。 |
実践例:TypeScriptで見るStrategy
パターンが働く小さな例を見てみましょう。チェックアウト処理で、送料を複数の方法で計算する必要があります。増え続ける if/else の連鎖にする代わりに、各ルールを1つのインターフェースの背後にある交換可能なオブジェクトにします。
interface ShippingStrategy {
cost(weightKg: number): number;
}
class FlatRate implements ShippingStrategy {
cost(): number { return 5; }
}
class PerKilogram implements ShippingStrategy {
cost(weightKg: number): number { return 2 * weightKg; }
}
class Checkout {
constructor(private shipping: ShippingStrategy) {}
setShipping(s: ShippingStrategy): void { this.shipping = s; }
total(itemsPrice: number, weightKg: number): number {
return itemsPrice + this.shipping.cost(weightKg);
}
}
const checkout = new Checkout(new FlatRate());
console.log(checkout.total(40, 3)); // 45
checkout.setShipping(new PerKilogram());
console.log(checkout.total(40, 3)); // 46TypeScriptでは関数も値なので、同じ設計をより少ないコードで書けます。これはよくあるテーマで、第一級関数を持つ言語では、多くのパターンが単純になったり、不要になったりします。
type Shipping = (weightKg: number) => number;
const flatRate: Shipping = () => 5;
const perKilogram: Shipping = (w) => 2 * w;
function total(itemsPrice: number, weightKg: number, shipping: Shipping): number {
return itemsPrice + shipping(weightKg);
}
console.log(total(40, 3, flatRate)); // 45
console.log(total(40, 3, perKilogram)); // 46パターンの選び方
パターンからではなく、問題から始めてください。パターンが増やすクラスに見合うのは、コードの現実の痛みを取り除くときだけです。次の問いが、よく使われる候補を示してくれます。
| こんな問題があるなら | 検討するパターン |
|---|---|
| 生成するクラスの具体名にコードが依存してはいけない | Factory Method、Abstract Factory |
| コンストラクタにオプション引数が多い | Builder |
| 既存のクラスを別のインターフェースに合わせたい | Adapter |
| 複雑なライブラリに単純な入口がほしい | Facade |
| サブクラスを増やさずにオブジェクトへ機能を追加したい | Decorator |
| メニュー、ファイル、UI部品のような木構造を扱う | Composite、Visitor、Iterator |
| 1つのオブジェクトが変化したとき、多くの部品が反応する必要がある | Observer |
| モードや状態によって分岐だらけの振る舞いになっている | State |
| 交換可能なアルゴリズムやルールが必要 | Strategy |
| 操作を取り消す、キューに入れる、記録する必要がある | Command、Memento |
- まず、動く最もシンプルなコードを書きます。
- 同じ種類の変更で何度も困るようになったら(たとえば、決済手段を追加するたびに5つのファイルを修正する場合)、その問題に名前を付けます。
- その問題に対処するパターンを調べ、結果(利点とコスト)を読みます。
- 痛みを取り除ける最小限の形でパターンを適用し、そこで止めます。
批判と限界
デザインパターンは有用ですが、誤用されることも多くあります。よくある批判を知っておきましょう。
- 過剰な適用:「念のため」にファクトリ、インターフェース、マネージャを足すと、単純なコードが読みにくくなります。これはパターン熱とも呼ばれます。
- 言語機能の不足の補い:1996年に Peter Norvig は、23のGoFパターンのうち16個が、LispやDylanのような動的言語では目立たなくなるか、より単純になることを示しました。第一級関数はStrategyやCommandの代わりになり、組み込みのイテレータはIteratorの代わりになります。
- 特にSingletonは、グローバル状態として使うとアンチパターンと見なされることが多く、依存関係を隠し、テストを難しくします。
- カタログは1994年のものです。その後の実践では、依存性の注入(DI)、リポジトリ、Model-View-Controllerの各種変形など、書籍に載っていない有名なパターンも加わりました。
この書籍の変わらない価値は、共通語彙と、トレードオフを考える習慣にあります。「これはAdapterです」と言うだけで、二言で多くのことが他の開発者に伝わります。
関連する原則
パターンは、オブジェクト指向設計のSOLID原則とあわせて説明されることがよくあります。たとえば、StrategyとDecoratorは開放閉鎖の原則を守る直接的な方法で、Abstract Factoryは依存性逆転の原則を適用する一般的な方法です。原則を学ぶと、パターンがなぜその形をしているのかが理解しやすくなります。
よくある質問
デザインパターンはいくつありますか?
GoFの書籍には23のパターンが載っており、内訳は生成5つ、構造7つ、振る舞い11です。その後も、並行処理、エンタープライズアプリケーション、ユーザーインターフェースなど向けに、多くのパターンが提案されています。
最初に学ぶべきデザインパターンはどれですか?
実際のコードで最もよく出会うものから始めましょう。Strategy、Observer、Factory Method、Decorator、Adapter、Facadeです。そのあとで、Builder、Composite、Commandを学ぶとよいでしょう。
デザインパターンは今でも通用しますか?
はい。共通語彙として、また繰り返し現れる問題への解決策として有効です。一部のパターンは言語やフレームワークに組み込まれており、完全なクラス構造を書かなくても使えます。
デザインパターンはオブジェクト指向言語だけのものですか?
GoFパターンはオブジェクト指向言語向けに書かれましたが、根底にある考え方はどこにでも現れます。関数型言語では、その多くが単純な高階関数になります。
デザインパターンとアルゴリズムの違いは何ですか?
アルゴリズムは、ソートのように計算上の問題を解く厳密な手順の列です。デザインパターンは、コードと責務をどう整理するかを示すもので、手順どおりに従うのではなく、プログラムごとに調整して使います。