はじめに
前回の「デザインパターン①」に続き、今回は残りの代表的な10パターンを1本にまとめました。デザインパターン①で「パターンとは何か・なぜ学ぶのか」を扱ったので、本記事は各パターンの解説に集中します。
このシリーズでは、先に SOLID原則 と デザインパターン①(テンプレートメソッド・シングルトン・アダプター・イテレータ・ファクトリーメソッド・ファサード)を解説しています。パターンの説明ではSOLID原則やデザインパターン①のパターンが何度も出てくるので、あわせて読むと理解が深まります。
今回扱うのは、プロトタイプ・ビルダー・アブストラクトファクトリー・ストラテジー・ステート・責任の連鎖(Chain of Responsibility)・コンポジット・デコレーター・プロキシ・ブリッジ の10個です。読みやすさのため、GoFの3分類(生成/振る舞い/構造)でグループ分けして進めます。各パターンは「概要 → 構成要素・オブジェクト指向的要素 → メリット・デメリット → 使いどころ → 実装」の順で解説します。
この記事で分かること
- 生成・振る舞い・構造の代表的な10パターンを、たとえ・図・TypeScriptコードでイメージできる
- 浅いコピーと深いコピーの違い、それがプロトタイプパターンでどうバグにつながるかが分かる
- アブストラクトファクトリーとファクトリーメソッドの違いが整理できる
- ストラテジー・ステート・責任の連鎖など「条件分岐を消す」パターンの考え方が身につく
- 各パターンがSOLID原則(特にオープンクローズド・単一責任)とどう結びつくかが分かる
生成に関するパターン
1. プロトタイプ(Prototype)
概要
プロトタイプ(prototype)は「原型・試作品」という意味です。プロトタイプパターンは、原型となるインスタンスをコピーして新しいインスタンスを生成するパターンです。親クラスにコピー用のメソッドを定義し、子クラスで自分自身のコピーを返すように実装します。GoFの分類では「生成に関するパターン」です。
このパターンを使うときに重要なのが、浅いコピー(shallow copy) と 深いコピー(deep copy) の違いです。
- 浅いコピー:値はそのままコピーされるが、オブジェクトの参照を持つプロパティは「参照」がコピーされる。つまりコピー元とコピー先が同じ実体を共有し、片方の変更がもう片方に影響する
- 深いコピー:参照ではなく、その「実体」までコピーされる。どちらを変更してももう片方に影響しない
構成要素とオブジェクト指向的要素
マネージャーがコピー対象のオブジェクトを管理し、要求されたら管理オブジェクトのコピーを返します。Prototypeはコピー用APIを定めるインターフェース/抽象クラス、ConcretePrototypeがコピー用メソッドを実装します。継承とポリモーフィズムを利用したパターンで、クライアントは Prototype のAPIだけを使うため、ConcretePrototype の差し替え・追加が容易です。
メリット・デメリット
メリットは、生成処理を隠蔽でき利用者が具体的な生成方法を意識しなくてよいこと、構築済みインスタンスのクローンで初期化コードの重複を減らせること、具体クラスとの結合が弱まりオープンクローズドの原則に沿うことです。デメリットは、浅いコピーと深いコピーを意識しないと想定外のバグを生む点です。
使いどころと実装例
クラスからのインスタンス生成が難しい場合(例:PowerPointの図形のように、ユーザー操作で作られたオブジェクト)や、インスタンス化のコストがクローンより高い場合に有効です。ここでは、TypeScriptで手軽にコピーを作るため lodash を使い、浅い/深いコピーの違いを確認します。
import _ from "lodash";
type Detail = {
comment: string[];
};
abstract class ItemPrototype {
constructor(
public name: string,
public detail: Detail = { comment: [] },
) {}
addComment(comment: string): void {
this.detail.comment.push(comment);
}
abstract createCopy(): ItemPrototype;
}
// 深いコピー
class DeepCopyItem extends ItemPrototype {
createCopy(): ItemPrototype {
return _.cloneDeep(this);
}
}
// 浅いコピー
class ShallowCopyItem extends ItemPrototype {
createCopy(): ItemPrototype {
return _.clone(this);
}
}
class ItemManager {
private items: { [key: string]: ItemPrototype } = {};
registerItem(key: string, item: ItemPrototype): void {
this.items[key] = item;
}
create(key: string): ItemPrototype {
if (key in this.items) {
return this.items[key].createCopy();
}
throw new Error("指定された記事は存在しません");
}
}
// 使い方
const mouse = new DeepCopyItem("マウス");
mouse.addComment("オリジナル");
const keyboard = new ShallowCopyItem("キーボード");
keyboard.addComment("オリジナル");
const manager = new ItemManager();
manager.registerItem("マウス", mouse);
manager.registerItem("キーボード", keyboard);
const clonedMouse = manager.create("マウス");
const clonedKeyboard = manager.create("キーボード");
clonedMouse.addComment("クローン"); // deep:コピー先だけに追加される
clonedKeyboard.addComment("クローン"); // shallow:detail を共有し、元にも影響してしまう
深いコピーのマウスは、クローンに追加したコメントがコピー先だけに反映されます。一方、浅いコピーのキーボードは detail オブジェクトを元と共有しているため、クローンへのコメント追加が元にも影響してしまいます。これが浅い/深いコピーを意識しないと起きるバグです。
最近のNode.js(17以降)やモダンブラウザには、依存ライブラリなしで深いコピーができる structuredClone() があります。ただし関数やDOM要素はコピーできないなどの制約があるため、対象データに応じて lodash の cloneDeep と使い分けてください。
2. ビルダー(Builder)
概要
ビルダー(builder)は「建築者」という意味です。ビルダーパターンは、同じ生成手順で、異なる材料や手段を使って異なるオブジェクトを生成するパターンです。家を建てる工程(壁 → 床 → 屋根 → ドア・窓)はどんな家でもだいたい同じでも、各工程の材料や手法で和風・洋風・平屋・2階建てと仕上がりが変わる、というイメージです。GoFの分類では「生成に関するパターン」です。
構成要素とオブジェクト指向的要素
- Builder:生成手段となるAPIを提供するインターフェース/抽象クラス
-
ConcreteBuilder:Builder のAPIを実装し、生成したオブジェクトを取得するメソッド(
getResult)も提供する - Director:建築者役。Builder のAPIを使って段階的にオブジェクトを組み立てる
処理の流れは、クライアントが ConcreteBuilder を生成 → Director に渡す → Director が Builder のメソッドを順に呼んで組み立て → クライアントが成果物を受け取る、という順です。継承・ポリモーフィズム・委譲を利用します。Director は自ら処理せず、渡された Builder に組み立てを委譲するのがポイントです。
メリット・デメリット
メリットは、生成過程を隠蔽でき、Director は ConcreteBuilder を知らないためオープンクローズドの原則に沿うこと、オブジェクト構築コードをビジネスロジックから分離でき単一責任の原則にも沿うことです。デメリットは、クラスが増え、小さなシステムでは過剰設計になりうる点です。大量のパラメータをコンストラクタに渡している場合、各工程のメソッドで小分けに渡せるので、可読性が上がり順番間違いのバグも減らせます。
使いどころと実装例
生成手順が同じで詳細が異なるオブジェクトを作る場合や、大量のパラメータを扱う場合に有効です。ここではコンピュータを生成する例を実装します。
class Computer {
type = "";
cpu = "";
ram = 0;
}
interface ComputerBuilder {
addCpu(cpu: string): void;
addRam(ram: number): void;
}
class DesktopBuilder implements ComputerBuilder {
private computer = new Computer();
constructor() {
this.computer.type = "Desktop";
}
addCpu(cpu: string): void {
this.computer.cpu = cpu;
}
addRam(ram: number): void {
this.computer.ram = ram;
}
getResult(): Computer {
return this.computer;
}
}
class LaptopBuilder implements ComputerBuilder {
private computer = new Computer();
constructor() {
this.computer.type = "Laptop";
}
addCpu(cpu: string): void {
this.computer.cpu = cpu;
}
addRam(ram: number): void {
this.computer.ram = ram;
}
getResult(): Computer {
return this.computer;
}
}
class Director {
constructor(private builder: ComputerBuilder) {}
// 通常スペックの生成手順
construct(): void {
this.builder.addCpu("Core i5");
this.builder.addRam(16);
}
// ハイスペックの生成手順
highSpecConstruct(): void {
this.builder.addCpu("Core i9");
this.builder.addRam(64);
}
}
// 使い方:同じ手順でも、渡す Builder によって成果物が変わる
const desktopBuilder = new DesktopBuilder();
new Director(desktopBuilder).construct();
const desktop = desktopBuilder.getResult(); // Core i5 / 16GB
const laptopBuilder = new LaptopBuilder();
new Director(laptopBuilder).highSpecConstruct();
const laptop = laptopBuilder.getResult(); // Core i9 / 64GB
Director に渡す Builder の種類を変えるだけで、同じ生成手順から異なるコンピュータを組み立てられます。
3. アブストラクトファクトリー(Abstract Factory)
概要
アブストラクトファクトリーは直訳すると「抽象的な工場」。関連したオブジェクト(部品)のセットを、具体的なクラスを指定することなく生成するためのAPIを提供するパターンです。部品の具体的な実装ではなく抽象のAPIに注目し、それだけを使って関連部品のセットを組み立てます。GoFの分類では「生成に関するパターン」です。
構成要素とオブジェクト指向的要素
抽象を扱う層と具体を扱う層に分けられます。
- AbstractFactory:抽象的な部品(AbstractProduct)を生成するAPIを定義する
- AbstractProduct:部品ごとのAPIを定義する
- ConcreteFactory:AbstractFactory を実装し、具体的な部品(ConcreteProduct)を返す
- ConcreteProduct:AbstractProduct を実装した具体的な部品
ポリモーフィズムを利用したパターンで、利用者は抽象のAPIだけを使います。どの ConcreteFactory を使うか切り替えるだけで、生成される部品群がまるごと切り替わります。
メリット・デメリット
メリットは、具体クラスをクライアントから隠せること、そして部品群の整合性を保てることです。ConcreteFactory は関連部品のセットを生成するので、「一部の部品だけ別のOS用」といった不整合を防げます。デメリットは、必要なクラス数が多くコードが複雑になりうる点です。
使いどころと実装例
関連する部品群を、種別ごとに整合性を保って切り替えたい場合に有効です。典型例がOS別のGUIです。Windowsの画面にMacのボタンが出てきたら違和感がありますよね。ここではボタンとチェックボックスをOS別に生成します。
// 抽象を扱う層
interface Button {
press(): void;
}
interface Checkbox {
switch(): void;
}
interface GuiFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
// 具体を扱う層(Windows)
class WindowsButton implements Button {
press(): void {
console.log("Windows用のボタンが押されました");
}
}
class WindowsCheckbox implements Checkbox {
switch(): void {
console.log("Windowsのチェックボックスが切り替えられました");
}
}
class WindowsGuiFactory implements GuiFactory {
createButton(): Button {
return new WindowsButton();
}
createCheckbox(): Checkbox {
return new WindowsCheckbox();
}
}
// 具体を扱う層(Mac)
class MacButton implements Button {
press(): void {
console.log("Mac用のボタンが押されました");
}
}
class MacCheckbox implements Checkbox {
switch(): void {
console.log("Macのチェックボックスが切り替えられました");
}
}
class MacGuiFactory implements GuiFactory {
createButton(): Button {
return new MacButton();
}
createCheckbox(): Checkbox {
return new MacCheckbox();
}
}
// 使い方:工場を切り替えるだけで、部品セットが整合性を保って切り替わる
function run(factory: GuiFactory): void {
const button = factory.createButton();
const checkbox = factory.createCheckbox();
button.press();
checkbox.switch();
}
run(new WindowsGuiFactory());
run(new MacGuiFactory());
ファクトリーメソッドとの違い ― 同じ「ファクトリー」でも2点で異なります。① 生成するインスタンス数:ファクトリーメソッドは1つ、アブストラクトファクトリーは複数部品のセット。② 抽象化の対象:ファクトリーメソッドはメソッドレベル、アブストラクトファクトリーはクラスレベル。後者のほうが抽象度が高く、柔軟性も高いといえます。
振る舞いに関するパターン
4. ストラテジー(Strategy)
概要
ストラテジー(strategy)は「戦略」という意味です。ストラテジーパターンは、複数のアルゴリズムを別々のクラスとして定義し、切り替えられるようにするパターンです。親クラス(インターフェース)で共通APIを定義し、子クラスで具体的なアルゴリズムを実装します。GoFの分類では「振る舞いに関するパターン」です。
構成要素とオブジェクト指向的要素
- Strategy:各アルゴリズム共通のAPIを定義するインターフェース/抽象クラス
- ConcreteStrategy:共通APIを満たすように具体的なアルゴリズムを実装する
- Context:Strategy 型のオブジェクトをフィールドに持ち、具体的な処理をそれに委譲する
継承・ポリモーフィズム・委譲を利用します。Context はコンストラクタで受け取った ConcreteStrategy に処理を委譲するだけです。
メリット・デメリット
メリットは、実行時にアルゴリズムを交換できること、アルゴリズムの詳細を利用側から分離できること、オープンクローズドの原則に沿ってアルゴリズムを追加できることです。デメリットは、アルゴリズムが少ないと過剰設計になる点です。クラス内にアルゴリズム切り替えの条件文が多い場合、それを各クラスに切り出せるのも利点です。
使いどころと実装例
問題の解法が複数あり、実行時に動的に切り替えたい場合に有効です。ここでは2つのソートアルゴリズムを切り替えます(アルゴリズム本体は別ファイルに用意した想定です)。
import { bubbleSort, insertionSort } from "./sortAlgorithm";
interface SortStrategy {
sort(list: number[]): number[];
}
class BubbleSort implements SortStrategy {
sort(list: number[]): number[] {
console.log("バブルソート");
return bubbleSort(list);
}
}
class InsertionSort implements SortStrategy {
sort(list: number[]): number[] {
console.log("挿入ソート");
return insertionSort(list);
}
}
class SortContext {
constructor(private strategy: SortStrategy) {}
sort(list: number[]): number[] {
return this.strategy.sort(list);
}
}
// 使い方:渡す Strategy を変えるだけでアルゴリズムが切り替わる
const list = [4, 1, 5, 7, 6, 3];
new SortContext(new BubbleSort()).sort(list);
new SortContext(new InsertionSort()).sort(list);
新しいアルゴリズムを追加したいときは、SortStrategy を実装するクラスを1つ足すだけで済みます。
5. ステート(State)
概要
ステート(state)は「状態」という意味です。ステートパターンは、複数の状態を別々のクラスとして定義し、状態が変わったときに振る舞いを切り替えられるようにするパターンです。構造はストラテジーとよく似ていますが、切り替えるのが「アルゴリズム」ではなく「状態」である点が異なります。GoFの分類では「振る舞いに関するパターン」です。
構成要素とオブジェクト指向的要素
- State:各状態共通のAPIを定義するインターフェース/抽象クラス
- ConcreteState:各状態ごとの振る舞いを実装する
- Context:State 型のオブジェクトを持ち、処理をそれに委譲する
継承・ポリモーフィズム・委譲を利用します。ステートパターンでは、ある状態が次の状態を知っていて構いません(nextState で次の状態のインスタンスを返す)。これにより、利用者は今どの状態かを意識せずに状態遷移できます。
メリット・デメリット
メリットは、状態ごとの実装を別クラスに分離でき単一責任の原則に沿うこと、状態選択の条件文(if/switch)をなくせて可読性が上がること、オープンクローズドの原則に沿って状態を追加できることです。デメリットは、状態が少なかったり遷移がめったに起きない場合は過剰設計になる点です。
使いどころと実装例
状態に応じて振る舞いが変わり、状態数が多い場合や、状態判定の条件分岐が多い場合に有効です。ここでは簡単な認証機能(認証済み/未認証)を実装します。
interface UserState {
isAuthenticated(): boolean;
displayPage(): void;
nextState(): UserState;
}
class AuthorizedState implements UserState {
isAuthenticated(): boolean {
return true;
}
displayPage(): void {
console.log("トップページ");
}
nextState(): UserState {
return new UnauthorizedState();
}
}
class UnauthorizedState implements UserState {
isAuthenticated(): boolean {
return false;
}
displayPage(): void {
console.log("エラーページ:認証されていません");
}
nextState(): UserState {
return new AuthorizedState();
}
}
class User {
// 初期状態は未認証
private state: UserState = new UnauthorizedState();
isAuthenticated(): boolean {
return this.state.isAuthenticated();
}
displayPage(): void {
this.state.displayPage();
}
switchState(): void {
this.state = this.state.nextState();
}
}
// 使い方
const user = new User();
console.log(user.isAuthenticated()); // false
user.displayPage(); // エラーページ:認証されていません
user.switchState();
console.log(user.isAuthenticated()); // true
user.displayPage(); // トップページ
複雑な条件分岐を書かずに、現在の状態に応じて振る舞いを切り替えられます。
6. 責任の連鎖(Chain of Responsibility)
概要
Chain of Responsibility(責任の連鎖)は、リクエストを処理するオブジェクトを鎖のようにつなぎ、処理できるオブジェクトに順に渡していくパターンです。バケツリレーのイメージで、リクエストを渡された各オブジェクト(ハンドラ)は「自分が処理すべきか」を判断し、できなければ次のハンドラに渡します。GoFの分類では「振る舞いに関するパターン」です。
構成要素とオブジェクト指向的要素
- Handler:要求を処理するAPIを定義する抽象クラス/インターフェース。内部に次のハンドラを保持する
- ConcreteHandler:担当する処理を実装する
- Client:鎖の先頭のハンドラにリクエストを送る
ポリモーフィズムを利用します。鎖を構成するオブジェクトはすべて Handler 型なので、具体クラスを意識せずに扱え、切り替えも容易です。
メリット・デメリット
メリットは、連鎖順でリクエスト処理の順番を制御できること、クライアントと処理クラスの結びつきが弱まり新しい処理クラスの追加が容易なことです。デメリットは、たらい回しによりパフォーマンスに影響が出る可能性がある点です。
使いどころと実装例
複数の処理を特定の順序で実行したい場合(各値のバリデーションなど)や、ハンドラの組み合わせ・順序を実行時に変えたい場合に有効です。ここでは入力値のバリデーション(空欄チェック → 英字チェック → 最小文字数チェック)を実装します。
abstract class ValidationHandler {
private nextHandler: ValidationHandler | null = null;
setHandler(handler: ValidationHandler): void {
this.nextHandler = handler;
}
protected abstract executeValidation(input: string): boolean;
protected abstract getErrorMessage(): void;
// このパターンの心臓部
validate(input: string): boolean {
const result = this.executeValidation(input);
if (!result) {
this.getErrorMessage();
return false; // 失敗したら中断
} else if (this.nextHandler) {
return this.nextHandler.validate(input); // 次のハンドラへ
} else {
return true; // すべて通過
}
}
}
class NotNullValidationHandler extends ValidationHandler {
protected executeValidation(input: string): boolean {
const result = !!input;
console.log(`NotNullバリデーションの結果:${result}`);
return result;
}
protected getErrorMessage(): void {
console.log("何も入力されていません");
}
}
class AlphabetValidationHandler extends ValidationHandler {
protected executeValidation(input: string): boolean {
const reg = new RegExp(/^[a-zA-Z]+$/);
const result = reg.test(input);
console.log(`Alphabetバリデーションの結果:${result}`);
return result;
}
protected getErrorMessage(): void {
console.log("英字で入力してください");
}
}
class MinLengthValidationHandler extends ValidationHandler {
protected executeValidation(input: string): boolean {
const result = input.length >= 8;
console.log(`MinLengthバリデーションの結果:${result}`);
return result;
}
protected getErrorMessage(): void {
console.log("8文字以上で入力してください");
}
}
// 使い方:NotNull → Alphabet → MinLength の順で鎖をつなぐ
const notNullHandler = new NotNullValidationHandler();
const alphabetHandler = new AlphabetValidationHandler();
const minLengthHandler = new MinLengthValidationHandler();
alphabetHandler.setHandler(minLengthHandler);
notNullHandler.setHandler(alphabetHandler);
if (notNullHandler.validate("HelloWorld")) {
console.log("すべてのバリデーションに成功");
}
setHandler でつなぐ順番を変えるだけで、バリデーションの順序や組み合わせを柔軟に変更できます。
構造に関するパターン
7. コンポジット(Composite)
概要
コンポジットパターンは、ツリー構造のデータに対して再帰的に処理を行えるようにするパターンです。ツリー構造の任意の枝や葉に共通のAPIでアクセスできるようにすることで、あらゆる要素を「同じもの」として扱えます。GoFの分類では「構造に関するパターン」です。
構成要素とオブジェクト指向的要素
構成要素は次の3つです。
| 役割 | この例のクラス | 説明 |
|---|---|---|
| Component | Entry |
クライアントからアクセスさせる共通APIを定義する抽象クラス/インターフェース |
| Leaf | File |
ツリーの末端(葉)。子を持たない |
| Composite | Directory |
ツリーの枝。子要素(Entry の配列)を保持し、子の追加・削除メソッドも持つ |
ポリモーフィズムを利用します。クライアントは共通APIだけを使うので、相手が枝(Composite)でも葉(Leaf)でも同一視して扱えます。Composite も子を Component 型で扱うため、子が枝でも葉でも同じように処理できます。
メリット・デメリット
メリットは、複雑なツリー構造を条件分岐なしで簡単に扱えること、既存の枝葉を変えずに新しい枝葉を追加できることです。デメリットは、枝と葉の機能が大きく異なると共通APIを定義できず、適用が難しくなる点です。
使いどころと実装例
ディレクトリツリー、組織階層、HTMLのDOMツリーなど、再帰的なツリー構造を扱う場合に有効です。ここではディレクトリツリーを実装します。
abstract class Entry {
constructor(private name: string) {}
getName(): string {
return this.name;
}
abstract getSize(): number;
abstract remove(): void;
}
// Leaf
class File extends Entry {
constructor(name: string, private size: number) {
super(name);
}
getSize(): number {
return this.size;
}
remove(): void {
console.log(`${this.getName()}を削除しました`);
}
}
// Composite
class Directory extends Entry {
private children: Entry[] = [];
constructor(name: string) {
super(name);
}
getSize(): number {
let size = 0;
this.children.forEach((child) => {
size += child.getSize(); // 子の同じメソッドを呼び、末端まで再帰的に合計
});
return size;
}
remove(): void {
this.children.forEach((child) => child.remove()); // 再帰的に削除
console.log(`${this.getName()}を削除しました`);
}
add(child: Entry): void {
this.children.push(child);
}
}
// クライアントは File でも Directory でも同じ Entry として扱える
function client(entry: Entry): void {
console.log(entry.getName());
console.log(entry.getSize());
entry.remove();
}
const dir1 = new Directory("デザインパターンパート②");
const dir2 = new Directory("composite");
const file1 = new File("composite1.ts", 100);
const file2 = new File("composite2.ts", 150);
dir2.add(file1);
dir2.add(file2);
dir1.add(dir2);
client(dir1); // getSize は 100 + 150 = 250、remove は末端から再帰的に実行される
getSize も remove も、子要素に対して同じメソッドを呼ぶことでツリーの末端まで再帰的に処理できます。
8. デコレーター(Decorator)
概要
デコレート(decorate)は「装飾する」という意味です。デコレーターパターンは、基本となるオブジェクトに柔軟に機能を追加するパターンです。クリスマスツリーに飾りをつけるように、オブジェクトに機能を「飾り付け」ていきます。既存オブジェクトを新しい機能で包むように見えるので、ラッパーパターンとも呼ばれます。GoFの分類では「構造に関するパターン」です。
継承でも機能追加はできますが、一部の機能だけ追加できず不要な機能まで付いてきたり、機能ごとにサブクラスを作ると大量のクラスができたりします。デコレーターはより柔軟で動的な機能追加を可能にします。
構成要素とオブジェクト指向的要素
- Component:拡張される基本機能のAPIを定義する
- ConcreteComponent:基本機能を実装する
- Decorator:Component を実装しつつ内部に Component を保持する抽象クラス。処理を委譲する
- ConcreteDecorator:基本機能に対して具体的な装飾を行う
継承とポリモーフィズムを利用します。機能を追加される側(Component)と追加する側(Decorator)が同じAPIを持つので、利用者はどちらを操作しているか意識しなくて済み、デコレーターを何重にも重ねられます。
メリット・デメリット
メリットは、継承と違い実行時の機能拡張が容易なこと、複数の機能を組み合わせられ、追加順序が意味を持つ場合に有効なことです。デメリットは、追加した機能から特定の機能だけを外すのが難しいこと、振る舞いが組み合わせ順に依存することです(順序が不要な場合はデメリットになります)。
使いどころと実装例
追加したい機能パターンが複数ある場合、順序がある場合、継承での拡張が難しい場合(Javaの final クラスなど)に有効です。ここではログ出力にタイムスタンプとログレベルを付ける例を実装します。
interface Component {
getLogMessage(message: string): string;
}
class Logger implements Component {
getLogMessage(message: string): string {
return message; // 基本機能:メッセージをそのまま返すだけ
}
}
abstract class Decorator implements Component {
constructor(protected component: Component) {}
abstract getLogMessage(message: string): string;
}
class TimestampDecorator extends Decorator {
getLogMessage(message: string): string {
const timestamp = new Date().toLocaleString("ja-JP");
return this.component.getLogMessage(`${timestamp} ${message}`);
}
}
class LogLevelDecorator extends Decorator {
constructor(component: Component, private logLevel: string) {
super(component);
}
getLogMessage(message: string): string {
return this.component.getLogMessage(`${this.logLevel} ${message}`);
}
}
// 使い方:基本機能を機能で「包んで」いく
const logger = new Logger();
const logLevel = new LogLevelDecorator(logger, "INFO");
const timestamp = new TimestampDecorator(logLevel);
console.log(logger.getLogMessage("デザインパターン")); // そのまま
console.log(logLevel.getLogMessage("デザインパターン")); // INFO 付き
console.log(timestamp.getLogMessage("デザインパターン")); // タイムスタンプ + INFO 付き
もともと文字列をそのまま返すだけだった Logger に、実行時に複数の機能を重ねて追加できました。
9. プロキシ(Proxy)
概要
プロキシ(proxy)は「代理人」という意味です。プロキシパターンは、代理となるオブジェクトを通じて、間接的に目的のオブジェクトにアクセスさせるパターンです。代理人を挟むことで、アクセス制御をしたり、リクエストの前後にログ出力などの処理を挟めます。外部ネットワークのアクセス制御やキャッシュを行う「プロキシサーバー」がまさにこの役割です。GoFの分類では「構造に関するパターン」です。
構成要素とオブジェクト指向的要素
- Subject:目的のオブジェクトが提供する機能のAPIを定義する抽象クラス/インターフェース
- RealSubject:目的の機能を提供する「本人」
- Proxy:目的のオブジェクトの「代理人」。内部に本人を保持し、必要に応じて本人の機能を呼ぶ
ポリモーフィズムと委譲を利用します。RealSubject と Proxy が同じ Subject を実装するので、クライアントは両者の違いを意識しません。Proxy は前処理などを行った後、本人に処理を委譲します。
メリット・デメリット
メリットは、アクセスが間接的になり前後に処理を挟めること、本人(例:まだ構築途中のDB)の完成を待たずにクライアント開発を進められること、オープンクローズドの原則に沿って新規プロキシを追加できることです。デメリットは、目的オブジェクトがシンプルで前後処理も不要な場合、クラスが増えて複雑になる点です。
使いどころと実装例
リクエストの前後に処理(ロギング・キャッシュ)を挟みたい場合や、アクセス制御を行いたい場合に有効です。ここではサーバーへのリクエストに、ロギングと簡易的なアクセス制御を行います。
interface Server {
handle(userId: string): void;
}
class RealServer implements Server {
handle(userId: string): void {
console.log(`${userId}の処理を実行中`);
}
}
class ProxyServer implements Server {
constructor(private server: Server) {}
private authorize(userId: string): void {
const authorizedUserIds = ["1", "2", "3"]; // 実際はDBなどから取得
if (!authorizedUserIds.includes(userId)) {
throw new Error("操作が許可されていません");
}
}
handle(userId: string): void {
this.authorize(userId); // アクセス制御
console.log("処理を開始します"); // 前処理ログ
this.server.handle(userId); // 本人に委譲
console.log("処理が終了しました"); // 後処理ログ
}
}
// 使い方:クライアントは本人ではなく代理人を呼ぶ
const proxy = new ProxyServer(new RealServer());
proxy.handle("1"); // 許可 → ログ付きで実行
// proxy.handle("5"); // 不許可 → 「操作が許可されていません」でブロック
許可リストにないユーザーIDが渡されると authorize でエラーになり、本人の処理は実行されません。代理人がアクセス制御と前後処理を担っています。
10. ブリッジ(Bridge)
概要
ブリッジ(bridge)は「橋」という意味です。ブリッジパターンは、機能を提供するクラスと実装を提供するクラスを独立させるパターンです。言い換えると「何を行うのか(目的)」と「どうやって行うのか(手段)」を分離します。委譲によって機能と実装を橋渡しするように見えることから、この名前がついています。GoFの分類では「構造に関するパターン」です。
構成要素とオブジェクト指向的要素
機能側と実装側の2つのクラス群に分かれます。
- Abstraction:基本機能を提供するクラス。内部に実装オブジェクトを持ち、処理を委譲する
- RefinedAbstraction:Abstraction を継承し、機能を拡張・追加する
- Implementor:実装を提供するインターフェース/抽象クラス(Abstraction のAPIと一致している必要はない)
- ConcreteImplementor:具体的な実装を行う
継承・ポリモーフィズム・委譲を利用します。Abstraction のフィールドを抽象(Implementor)にすることで、具体的にどの実装が使われているかを意識せず、機能側の処理を実装側に委譲できます。
メリット・デメリット
メリットは、機能と実装が分離され互いに影響せず拡張・修正できること、実行時に実装を切り替えられること、そしてクラス数の増加を抑えられることです。継承だけで機能×実装を実現すると「機能数 × 実装数」のクラスが必要ですが、ブリッジなら「機能数 + 実装数 + 2」で済みます(例:機能5×実装5なら、25クラスが12クラスに)。デメリットは、バリエーションが少ないと余計にクラスが増える点です。
使いどころと実装例
機能と実装の組み合わせが多い場合に有効です。ここでは、複数のOSからアプリを使ってメッセージを送信する例を実装します。
// 実装側(手段)
interface MessageApp {
send(): void;
}
class Line implements MessageApp {
send(): void {
console.log("LINEでメッセージ送信");
}
}
class Twitter implements MessageApp {
send(): void {
console.log("Twitterでメッセージ送信");
}
}
class Facebook implements MessageApp {
send(): void {
console.log("Facebookでメッセージ送信");
}
}
// 機能側(目的)
abstract class OS {
protected app: MessageApp | null = null;
setApp(app: MessageApp): void {
this.app = app;
}
abstract sendMessage(): void;
}
class IOS extends OS {
sendMessage(): void {
console.log("iOSでメッセージ送信");
if (this.app) {
this.app.send(); // 実装に委譲
} else {
throw new Error("アプリが指定されていません");
}
}
}
class Android extends OS {
sendMessage(): void {
console.log("Androidでメッセージ送信");
if (this.app) {
this.app.send();
} else {
throw new Error("アプリが指定されていません");
}
}
}
// 使い方:機能(OS)と実装(App)を自由に組み合わせられる
const ios = new IOS();
ios.setApp(new Line());
ios.sendMessage(); // iOS × LINE
const android = new Android();
android.setApp(new Facebook());
android.sendMessage(); // Android × Facebook
setApp で別のアプリをセットするだけで組み合わせを変えられます。PCのOSや新しいアプリを追加しても、OS側とアプリ側は互いに影響しません。
まとめ:パターン早見表とチェックリスト
パート2で扱った10パターンを振り返ります。
| パターン | GoF分類 | 主なOO要素 | ひとことで |
|---|---|---|---|
| Prototype | 生成 | 継承/ポリモーフィズム | 原型を複製して生成(浅い/深いコピーに注意) |
| Builder | 生成 | 継承/ポリモーフィズム/委譲 | 同じ手順で異なる成果物を組み立てる |
| Abstract Factory | 生成 | ポリモーフィズム | 整合性のある部品セットをまとめて生成 |
| Strategy | 振る舞い | 継承/ポリモーフィズム/委譲 | アルゴリズムを差し替え可能に |
| State | 振る舞い | 継承/ポリモーフィズム/委譲 | 状態ごとに振る舞いを切り替え |
| Chain of Responsibility | 振る舞い | ポリモーフィズム | 処理を鎖でつなぎ順に渡す |
| Composite | 構造 | ポリモーフィズム | 木構造を再帰的に同一視して扱う |
| Decorator | 構造 | 継承/ポリモーフィズム | 機能を動的に「包んで」追加 |
| Proxy | 構造 | ポリモーフィズム/委譲 | 代理人経由でアクセス制御・前後処理 |
| Bridge | 構造 | 継承/ポリモーフィズム/委譲 | 機能と実装を分離して組み合わせる |
設計で迷ったときのチェックリストとして、次の観点が役立ちます。
- インスタンス生成が難しい/コストが高い → Prototype(浅い/深いコピーを意識する)
- 生成手順は同じで詳細だけ違う、パラメータが多い → Builder
- 整合性のある部品群をまとめて切り替えたい → Abstract Factory
- 解法(アルゴリズム)を実行時に差し替えたい → Strategy
- 状態によって振る舞いが変わり、状態判定の分岐が多い → State
- 複数の処理を順に適用したい(バリデーションなど)→ Chain of Responsibility
- 木構造を枝も葉も同じように扱いたい → Composite
- 機能を動的に、組み合わせて追加したい → Decorator
- アクセス制御や前後処理を挟みたい → Proxy
- 機能と実装の組み合わせが多く、クラス爆発を防ぎたい → Bridge
- そもそも、そのパターンは本当に必要か(小規模なら過剰設計かも)を一度立ち止まって考える