はじめに
GoF(Gang of Four)の Design Patterns(1994年)では23のデザインパターンが紹介されていますが、すべてを暗記する必要はありません。
この記事では、実務で特に使用頻度が高いパターンに絞って、「何を解決するのか」「どう使うのか」をコード例とともに解説します。
デザインパターンとは
デザインパターンとは、ソフトウェア設計で繰り返し現れる問題に対する再利用可能な解決策のカタログです。GoF はこれを以下の3カテゴリに分類しました。
| カテゴリ | 目的 | この記事で扱うパターン |
|---|---|---|
| 構造 | オブジェクトの組み合わせ方 | Adapter, Facade, Decorator, Composite |
| 振る舞い | オブジェクト間の責務の分担 | Strategy, Observer, Template Method, Iterator |
| 生成 | オブジェクトの作り方 | Factory Method, Singleton |
なお本記事は、原典の並び(生成・構造・振る舞い)ではなく、実務での使用頻度が高いと思われる順に構造・振る舞い・生成の順で解説します。
構造に関するパターン
Adapter(アダプター)
既存のクラスのインターフェースを、クライアントが期待するインターフェースに変換する。
「コンセントの変換プラグ」のイメージです。既存のコードを変更せずに、異なるインターフェース同士を接続します。
// 既存の外部ライブラリ(変更できない)
class OldLogger {
writeLog(msg: string) {
console.log(`[OLD] ${msg}`);
}
}
// アプリが期待するインターフェース
interface Logger {
log(message: string): void;
}
// Adapter: 既存のクラスを新しいインターフェースに適合させる
class OldLoggerAdapter implements Logger {
constructor(private oldLogger: OldLogger) {}
log(message: string) {
this.oldLogger.writeLog(message); // メソッド名の違いを吸収
}
}
// 使用側は Logger インターフェースだけを知っている
function processOrder(logger: Logger) {
logger.log("注文を処理しました");
}
const adapter = new OldLoggerAdapter(new OldLogger());
processOrder(adapter); // [OLD] 注文を処理しました
使いどころ: 外部ライブラリの統合、レガシーコードの段階的な置き換え。使用側が具象クラス(OldLogger)ではなく抽象(Logger)に依存できるようになるため、DIP(依存性逆転の原則)を実現する手段としても機能します。
Facade(ファサード)
複雑なサブシステムに対して、シンプルなインターフェースを提供する。
「ホテルのフロントデスク」のイメージです。裏側にある複数のサービスを、1つの窓口にまとめます。
// 複雑なサブシステム群
class Inventory {
check(productId: string): boolean {
console.log("在庫確認");
return true;
}
}
class Payment {
charge(amount: number): boolean {
console.log(`¥${amount} を決済`);
return true;
}
}
class Shipping {
arrange(address: string) {
console.log(`${address} に配送手配`);
}
}
class MailService {
sendConfirmation(email: string) {
console.log(`${email} に確認メール送信`);
}
}
// Facade: 複雑な処理を1つのメソッドにまとめる
class OrderFacade {
constructor(
private inventory: Inventory,
private payment: Payment,
private shipping: Shipping,
private mailService: MailService
) {}
placeOrder(productId: string, amount: number, address: string, email: string) {
if (!this.inventory.check(productId)) {
throw new Error("在庫がありません");
}
this.payment.charge(amount);
this.shipping.arrange(address);
this.mailService.sendConfirmation(email);
}
}
// 使用側はサブシステムの詳細を知らなくていい
const facade = new OrderFacade(
new Inventory(), new Payment(), new Shipping(), new MailService()
);
facade.placeOrder("P001", 3000, "東京都...", "user@example.com");
使いどころ: 複雑な API の簡略化、サブシステムの隠蔽、外部から見たインターフェースの単純化。
Decorator(デコレーター)
既存のオブジェクトに、動的に機能を追加する。
継承を使わずに機能を拡張できます。「ラッピング」のイメージです。
interface Notifier {
send(message: string): void;
}
class EmailNotifier implements Notifier {
send(message: string) {
console.log(`メール: ${message}`);
}
}
// Decorator: 元の機能に追加機能をラップ
class SlackDecorator implements Notifier {
constructor(private wrapped: Notifier) {}
send(message: string) {
this.wrapped.send(message); // 元の処理
console.log(`Slack: ${message}`); // 追加の処理
}
}
class LogDecorator implements Notifier {
constructor(private wrapped: Notifier) {}
send(message: string) {
console.log(`[LOG] 通知送信: ${message}`);
this.wrapped.send(message);
}
}
// デコレーターを重ねて機能を追加
let notifier: Notifier = new EmailNotifier();
notifier = new SlackDecorator(notifier); // + Slack通知
notifier = new LogDecorator(notifier); // + ログ出力
notifier.send("サーバーダウン");
// [LOG] 通知送信: サーバーダウン
// メール: サーバーダウン
// Slack: サーバーダウン
使いどころ: ロギング、キャッシュ、認証チェック、圧縮など、既存の処理を変更せずに機能を追加したい場合。
Composite(コンポジット)
個別のオブジェクトと、オブジェクトの集合を同一視して扱う。
「ファイルとフォルダ」のような木構造を表現します。
interface FsEntry {
getName(): string;
getSize(): number;
}
class FileNode implements FsEntry {
constructor(private name: string, private size: number) {}
getName() { return this.name; }
getSize() { return this.size; }
}
class Directory implements FsEntry {
private children: FsEntry[] = [];
constructor(private name: string) {}
add(entry: FsEntry) {
this.children.push(entry);
}
getName() { return this.name; }
getSize(): number {
// 再帰的に子要素のサイズを合計
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
}
const root = new Directory("root");
const src = new Directory("src");
src.add(new FileNode("index.ts", 100));
src.add(new FileNode("app.ts", 200));
root.add(src);
root.add(new FileNode("README.md", 50));
console.log(root.getSize()); // 350
使いどころ: ファイルシステム、UI コンポーネントツリー、組織図、メニュー構造。
振る舞いに関するパターン
Strategy(ストラテジー)
アルゴリズムをクラスとして切り出し、実行時に差し替え可能にする。
interface SortStrategy {
sort(data: number[]): number[];
}
class QuickSort implements SortStrategy {
sort(data: number[]): number[] {
console.log("クイックソートで並べ替え");
return this.quickSort(data);
}
private quickSort(data: number[]): number[] {
if (data.length <= 1) return [...data];
const [pivot, ...rest] = data;
const left = rest.filter(n => n < pivot);
const right = rest.filter(n => n >= pivot);
return [...this.quickSort(left), pivot, ...this.quickSort(right)];
}
}
class BubbleSort implements SortStrategy {
sort(data: number[]): number[] {
console.log("バブルソートで並べ替え");
const result = [...data];
for (let i = 0; i < result.length - 1; i++) {
for (let j = 0; j < result.length - 1 - i; j++) {
if (result[j] > result[j + 1]) {
[result[j], result[j + 1]] = [result[j + 1], result[j]];
}
}
}
return result;
}
}
class Sorter {
constructor(private strategy: SortStrategy) {}
setStrategy(strategy: SortStrategy) {
this.strategy = strategy;
}
sort(data: number[]): number[] {
return this.strategy.sort(data);
}
}
const sorter = new Sorter(new QuickSort());
sorter.sort([3, 1, 2]); // クイックソートで並べ替え
sorter.setStrategy(new BubbleSort());
sorter.sort([3, 1, 2]); // バブルソートで並べ替え
使いどころ: 支払い方法の切り替え、割引計算、ソートアルゴリズム、バリデーションルール。OCP(開放閉鎖の原則)の実現手法としてよく使われます。
Observer(オブザーバー)
あるオブジェクトの状態が変化したとき、依存するオブジェクトに自動的に通知する。
「YouTubeのチャンネル登録」のイメージです。
interface Observer {
update(event: string, data: any): void;
}
class EventEmitter {
private listeners: Map<string, Observer[]> = new Map();
subscribe(event: string, observer: Observer) {
const observers = this.listeners.get(event) || [];
observers.push(observer);
this.listeners.set(event, observers);
}
notify(event: string, data: any) {
const observers = this.listeners.get(event) || [];
observers.forEach(o => o.update(event, data));
}
}
class EmailAlert implements Observer {
update(event: string, data: any) {
console.log(`メール通知: ${event} - ${JSON.stringify(data)}`);
}
}
class SlackAlert implements Observer {
update(event: string, data: any) {
console.log(`Slack通知: ${event} - ${JSON.stringify(data)}`);
}
}
const emitter = new EventEmitter();
emitter.subscribe("order.placed", new EmailAlert());
emitter.subscribe("order.placed", new SlackAlert());
emitter.notify("order.placed", { orderId: "123" });
// メール通知: order.placed - {"orderId":"123"}
// Slack通知: order.placed - {"orderId":"123"}
使いどころ: イベント駆動アーキテクチャ、UI のリアクティブ更新、Webhook、メッセージングシステム。
Template Method(テンプレートメソッド)
処理の骨格を親クラスで定義し、具体的なステップをサブクラスに任せる。
abstract class DataExporter {
// テンプレートメソッド: 処理の骨格を定義
export(data: any[]) {
const filtered = this.filter(data);
const formatted = this.format(filtered);
this.output(formatted);
}
// サブクラスで実装するステップ
protected abstract filter(data: any[]): any[];
protected abstract format(data: any[]): string;
protected abstract output(content: string): void;
}
class CsvExporter extends DataExporter {
protected filter(data: any[]) {
return data.filter(d => d.active);
}
protected format(data: any[]) {
return data.map(d => `${d.name},${d.email}`).join("\n");
}
protected output(content: string) {
console.log("CSV出力:\n" + content);
}
}
class JsonExporter extends DataExporter {
protected filter(data: any[]) {
return data;
}
protected format(data: any[]) {
return JSON.stringify(data, null, 2);
}
protected output(content: string) {
console.log("JSON出力:\n" + content);
}
}
使いどころ: データのインポート/エクスポート、テストフレームワークのセットアップ/ティアダウン。継承が正当化される数少ないパターンです。
Iterator(イテレーター)
コレクションの内部構造を公開せずに、要素を順番にアクセスする方法を提供する。
多くの言語に組み込まれているため(for...of、foreach、yield など)、パターンとして意識する機会は少ないかもしれません。ただし「走査の方法」と「データの持ち方」を分離するという発想自体は、実務でも効いてきます。
まずはプロトコルを手書きで実装した例です。
class NumberRange {
constructor(private start: number, private end: number) {}
// JavaScript/TypeScript の Iterator プロトコル
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next(): IteratorResult<number> {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
}
for (const num of new NumberRange(1, 5)) {
console.log(num); // 1, 2, 3, 4, 5
}
JavaScript には2つのプロトコルがあります。[Symbol.iterator]() を実装するのが反復可能プロトコル、その戻り値が持つ next() がイテレータープロトコルです。for...of は前者で対象が反復可能かを判断し、後者の next() を done: true になるまで呼び続けます(MDN: 反復処理プロトコル)。
ジェネレータで書く
上のコードは、ジェネレータ関数(function* / yield)を使うと同じことをずっと短く書けます。current のような状態変数を自分で管理する必要がなくなり、中断と再開はランタイムが面倒を見てくれます。
class NumberRange {
constructor(private start: number, private end: number) {}
*[Symbol.iterator](): Generator<number> {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
実装すると何が使えるようになるか
Symbol.iterator を実装したオブジェクトは、for...of だけでなく言語機能全般に乗せられるようになります。ここが「自前のクラスに Iterator を実装する」最大の見返りです。
const range = new NumberRange(1, 5);
console.log([...range]); // [1, 2, 3, 4, 5] スプレッド構文
console.log(Array.from(range)); // [1, 2, 3, 4, 5] Array.from
const [first, second] = range; // 分割代入
console.log(first, second); // 1 2
console.log(Math.max(...range)); // 5
内部が配列なのか、リンクリストなのか、木構造なのかを一切公開せずに、これらすべてが使えます。内部構造を公開せずに走査手段だけを提供するという Iterator の定義が、そのまま実利になっている形です。
遅延評価と無限列
Iterator は要素を要求されたときに1つずつ作ります。したがって「全件をメモリに載せてから返す」必要がなく、終わりのない列も扱えます。
function* naturals(): Generator<number> {
let n = 1;
while (true) yield n++; // 無限に続く
}
function* take<T>(source: Iterable<T>, count: number): Generator<T> {
let i = 0;
for (const item of source) {
if (i++ >= count) return;
yield item;
}
}
console.log([...take(naturals(), 5)]); // [1, 2, 3, 4, 5]
naturals() は無限ループですが、take が5個受け取った時点で return するため止まります。配列で同じことをしようとすると先に無限ループでハングします。
実務で効くのは非同期イテレータ
同期版を自前で書く機会は確かに多くありませんが、ページネーションのある API を隠す用途では非同期イテレータ(Symbol.asyncIterator / for await...of)が非常に有効です。
type User = { id: number; name: string };
// fetchPage(page) はページ単位で取得する既存の API 呼び出し
declare function fetchPage(page: number): Promise<{ users: User[]; hasNext: boolean }>;
class UserRepository {
// ページングを内部に閉じ込め、呼び出し側には「ユーザーの並び」だけを見せる
async *[Symbol.asyncIterator](): AsyncGenerator<User> {
let page = 0;
while (true) {
const { users, hasNext } = await fetchPage(page);
yield* users;
if (!hasNext) return;
page++;
}
}
}
for await (const user of new UserRepository()) {
console.log(user.name);
}
呼び出し側から page 変数も hasNext の判定も消えます。全件をメモリに溜めることもありません。while (hasNext) ループを毎回書いている箇所があれば、置き換えの候補です。
使いどころ: ページネーション API のラップ、大きなファイルの行単位の読み込み、DB カーソル、ストリーム処理。いずれも「呼び出し側に走査の手続きを書かせない」という一点で共通しています。
生成に関するパターン
Factory Method(ファクトリーメソッド)
オブジェクトの生成をサブクラスに委ねる。 親クラスは「何を作るか」を知らないまま処理の流れを書き、生成する具体クラスの決定はサブクラスが担当します。
interface AppNotification {
send(message: string): void;
}
class EmailNotification implements AppNotification {
send(message: string) { console.log(`メール: ${message}`); }
}
class SmsNotification implements AppNotification {
send(message: string) { console.log(`SMS: ${message}`); }
}
// Creator: 処理の流れだけを持ち、生成はサブクラスに任せる
abstract class NotificationSender {
// これがファクトリメソッド。何を作るかはサブクラスが決める
protected abstract createNotification(): AppNotification;
send(message: string) {
const notification = this.createNotification();
notification.send(message);
}
}
// ConcreteCreator: 生成する具体クラスをここで確定させる
class EmailSender extends NotificationSender {
protected createNotification(): AppNotification {
return new EmailNotification();
}
}
class SmsSender extends NotificationSender {
protected createNotification(): AppNotification {
return new SmsNotification();
}
}
// 使用側は NotificationSender としてだけ扱う
const senders: NotificationSender[] = [new EmailSender(), new SmsSender()];
senders.forEach(sender => sender.send("お知らせ"));
// メール: お知らせ
// SMS: お知らせ
Simple Factory との違い(よくある混同)
実務では、次のような static メソッドと switch による生成をまとめて「ファクトリー」と呼ぶことが多いです。
// Simple Factory(単純ファクトリ)
class NotificationFactory {
static create(type: string): AppNotification {
switch (type) {
case "email": return new EmailNotification();
case "sms": return new SmsNotification();
default: throw new Error(`Unknown type: ${type}`);
}
}
}
const notification = NotificationFactory.create("email");
notification.send("お知らせ"); // メール: お知らせ
こちらは Simple Factory(単純ファクトリ)と呼ばれる書き方で、GoF の 23 パターンには含まれません。生成をサブクラスに委ねておらず、1つのクラスが分岐を抱え込んでいるためです。
とはいえ実用性は高く、種類が少なく増減も稀なら Simple Factory で十分です。種類が増えるたびに switch を編集したくなったとき(=OCP に反し始めたとき)が、Factory Method への切り替えを検討するタイミングになります。
使いどころ: 設定値に応じたオブジェクト生成、プラグインシステム、テスト用のモック生成。
Singleton(シングルトン)
クラスのインスタンスが1つだけであることを保証する。
class DatabaseConnection {
private static instance: DatabaseConnection;
private constructor() {
console.log("DB接続を確立");
}
static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
query(sql: string) {
console.log(`SQL実行: ${sql}`);
}
}
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true(同じインスタンス)
注意: Singleton はグローバル状態を作るため、テストが困難になりやすいです。現代では DI コンテナでスコープを制御する方が好まれます。使いすぎに注意してください。
パターンの選び方ガイド
| やりたいこと | パターン |
|---|---|
| 既存クラスのインターフェースを変えたい | Adapter |
| 複雑なシステムを簡単に使いたい | Facade |
| 機能を動的に追加したい | Decorator |
| 木構造を統一的に扱いたい | Composite |
| アルゴリズムを差し替え可能にしたい | Strategy |
| 状態変化を通知したい | Observer |
| 処理の骨格だけ決めたい | Template Method |
| 内部構造を隠して順に走査したい | Iterator |
| 生成するオブジェクトの型を隠したい | Factory Method |
| インスタンスを1つに制限したい | Singleton |
デザインパターンと SOLID 原則の関係
デザインパターンの多くは、SOLID原則を実現するための具体的な手法です。
| SOLID 原則 | 関連するパターン |
|---|---|
| SRP(単一責務) | Strategy(アルゴリズムを独立したクラスに切り出す) |
| OCP(開放閉鎖) | Strategy, Decorator, Observer, Factory Method(既存コードを修正せずに拡張する) |
| LSP(リスコフの置換) | Composite(個別要素と複合要素を同じ型として置き換えられる) |
| DIP(依存性逆転) | Adapter, Factory Method, Strategy, Observer(具象ではなく抽象に依存する) |
なお、この対応は「傾向」であって一対一の対応ではありません。ISP(インターフェース分離の原則)のように特定のパターンと結びつきにくい原則もありますし、Facade や Template Method のように、個別の原則というより「複雑さをどこに閉じ込めるか」という観点で効いてくるパターンもあります。
まとめ
デザインパターンは「暗記するもの」ではなく、「設計の引き出しを増やすもの」です。まずは以下のパターンから始めると、実務で効果を実感しやすいです。
- Strategy — if/switch の条件分岐を置き換える
- Adapter — 外部ライブラリを統合する
- Facade — 複雑な処理をまとめる
- Observer — イベント駆動の設計に使う
- Factory Method — オブジェクト生成を柔軟にする
パターンを適用する前に、常に「このパターンが本当に必要か?」を問いかけてください。パターンの適用自体が目的になると、かえってコードが複雑になります。
参考記事・データ
- オブジェクト指向における再利用のためのデザインパターン 改訂版(GoF、SBクリエイティブ) — 原著 Design Patterns: Elements of Reusable Object-Oriented Software(1994年)の日本語訳
- SOLID原則のまとめ
- GRASP — オブジェクトに責務を割り当てる9つの原則
- Refactoring.Guru - Design Patterns