メインコンテンツへスキップ
中級28分で読める

デザインパターンの基礎知識

ソフトウェア開発における代表的なデザインパターンを、実例とコードを通じて分かりやすく解説。GoFパターンを中心に、実践的な使い方と適用場面を学びます。

システム開発デザインパターン設計プログラミングオブジェクト指向ソフトウェアアーキテクチャ

🎯 この記事で学べること

  • 1
    デザインパターンの概念と重要性を理解できます
  • 2
    生成に関するパターン(Singleton、Factory、Builder)を習得できます
  • 3
    構造に関するパターン(Adapter、Decorator、Facade)を理解できます
  • 4
    振る舞いに関するパターン(Observer、Strategy、Command)を学べます
  • 5
    パターンの適切な選択と実装方法を身につけられます

読了時間: 約5

23個の呪文が世界を変えた

1994年、シリコンバレーのある書店。

4人のプログラマーが書いた1冊の本が、ひっそりと技術書コーナーに並んだ。タイトルは「Design Patterns: Elements of Reusable Object-Oriented Software」。地味な表紙、学術的なタイトル。誰も注目していなかった。

しかし、この本は瞬く間にプログラマーたちの間で話題となった。「これを読むと、コードが見える世界が変わる」「今まで感覚でやっていたことが、理論として整理されている」「まるで魔法の呪文書だ」。

著者の4人、エリック・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ブリシディーズは、いつしか「Gang of Four(四人組)」と呼ばれるようになった。彼らが提示した23個のパターンは、ソフトウェア設計の共通言語となった。

30年後の今、世界中のプログラマーが「ここはObserverパターンで」「Factoryを使おう」という会話をしている。まるで料理人が「ここは弱火で」「隠し味にバターを」と言うように、設計の定石が共有されている。

この23個のパターンは、なぜ世界を変えたのか?それは、プログラミングを「個人の技」から「共有可能な知識」に変えたからだ。

ストリーミング大手のバグが教えてくれたこと

2008年、ある大手企業はDVDレンタルからストリーミングサービスへの大転換を図っていた。

しかし、深刻な問題に直面していた。新機能を追加するたびに、既存の機能が壊れる。あるエンジニアがレコメンドエンジンを改良すると、なぜか視聴履歴が狂う。別のエンジニアが高画質配信を実装すると、字幕が表示されなくなる。

コードを調査した結果、判明した事実は衝撃的だった。システム全体が巨大なスパゲッティコードになっていた。すべてがすべてに依存し、どこを触っても予期せぬ影響が出る。まるで絡まった釣り糸のようだった。

CTOのエイドリアン・コックロフトは決断した。「システム全体を、デザインパターンを使って再構築する」。

チームは半信半疑だった。学術的な理論が、実際のビジネスで役立つのか?しかし、結果は驚くべきものだった。

Observerパターンを使って、各コンポーネントの依存関係を整理。Strategyパターンで、配信アルゴリズムを切り替え可能に。Decoratorパターンで、動画処理のパイプラインを構築。

1年後、新機能の開発速度は3倍になった。バグは80%減少した。そして何より、新人エンジニアでもコードが理解できるようになった。「まるで地図ができたようだ」とある開発者は語った。

料理とプログラミングの共通点

優れた料理人は、レシピを暗記しているわけではない。

彼らが知っているのは「パターン」だ。煮込み料理なら「香味野菜を炒めて、肉を焼いて、液体を加えて煮込む」。炒め物なら「強火で短時間、油をまわして」。これらのパターンを組み合わせて、無限の料理を生み出す。

プログラミングも同じだ。優れたプログラマーは、すべてのコードを暗記しているわけではない。彼らが知っているのは「設計のパターン」だ。

// 料理:「出汁を取る」というパターン
// 材料は変われど、手順は同じ

// プログラミング:「唯一のインスタンスを保証する」Singletonパターン
class DatabaseConnection {
  static instance = null;
  
  constructor() {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }
    this.connection = this.connect();
    DatabaseConnection.instance = this;
  }
  
  connect() {
    // データベースへの接続処理
    return { /* 接続オブジェクト */ };
  }
}

料理人が「今日は鶏で出汁を取ろう」と考えるように、プログラマーは「ここはSingletonパターンを使おう」と考える。材料(実装言語)は変わっても、パターン(設計思想)は変わらない。

建築家クリストファー・アレグザンダーの予言

デザインパターンの起源は、意外にもソフトウェアではなく建築にある。

1970年代、建築家クリストファー・アレグザンダーは革新的な考えを提唱した。「建築には、繰り返し現れる問題と、それに対する解決策のパターンがある」。

例えば「光の入る場所に人は集まる」というパターン。だから、リビングには大きな窓を設ける。「プライベートな空間には段階的に移行する」というパターン。だから、玄関から寝室まで、徐々にプライベート度が上がるよう配置する。

20年後、ケント・ベックとウォード・カニンガムという2人のプログラマーが、この考えをソフトウェアに応用した。「ソフトウェア設計にも、繰り返し現れる問題と解決策があるはずだ」。

そして、Gang of Fourが体系化した。建築のパターンが「住みやすい家」を作るように、ソフトウェアのパターンは「保守しやすいシステム」を作る。

デザインパターンは「レシピ」ではなく「料理の基本技法」です。材料や調味料は変わっても、基本となる調理法は普遍的です。

Singletonパターン:世界に一つだけの存在

東京の地下鉄の運行管理システムを想像してほしい。

もし複数の管理システムが同時に動いていたら、大混乱になる。A システムが「山手線を3分間隔で運行」と指示し、B システムが「5分間隔で運行」と指示したら?電車は衝突し、ダイヤは崩壊する。

これがSingletonパターンが解決する問題だ。「システム全体で、たった一つのインスタンスしか存在してはいけない」という制約を実現する。

class TrainControlSystem {
  static #instance = null;
  #trains = new Map();
  
  constructor() {
    if (TrainControlSystem.#instance) {
      throw new Error('既に運行管理システムは起動しています');
    }
    TrainControlSystem.#instance = this;
  }
  
  static getInstance() {
    if (!TrainControlSystem.#instance) {
      TrainControlSystem.#instance = new TrainControlSystem();
    }
    return TrainControlSystem.#instance;
  }
  
  scheduleTrain(lineId, interval) {
    console.log(`${lineId}線を${interval}分間隔で運行設定`);
    this.#trains.set(lineId, { interval, lastDeparture: Date.now() });
  }
  
  getNextDeparture(lineId) {
    const train = this.#trains.get(lineId);
    if (!train) return null;
    
    const nextTime = train.lastDeparture + (train.interval * 60 * 1000);
    return new Date(nextTime);
  }
}

// 使用例
const control1 = TrainControlSystem.getInstance();
const control2 = TrainControlSystem.getInstance();

console.log(control1 === control2); // true - 同じインスタンス

control1.scheduleTrain('山手線', 3);
console.log(control2.getNextDeparture('山手線')); // control1の設定が反映される

現実世界でSingletonが使われる例は多い。データベース接続プール、ログ管理システム、設定マネージャー。すべて「複数存在すると困る」ものだ。

Factoryパターン:自動車工場の生産ラインに学ぶ

日本のある大手自動車メーカーの工場。

同じ生産ラインから、カローラ、プリウス、クラウンが次々と流れてくる。注文に応じて、適切な部品が選ばれ、適切な工程で組み立てられる。工員は車種を意識せず、指示書に従って作業するだけだ。

これがFactoryパターンの本質だ。「何を作るか」と「どう作るか」を分離する。

// 抽象的な製品
class Vehicle {
  constructor(model) {
    this.model = model;
    this.features = [];
  }
  
  addFeature(feature) {
    this.features.push(feature);
  }
  
  describe() {
    return `${this.model}: ${this.features.join(', ')}`;
  }
}

// 具体的な製品
class ElectricCar extends Vehicle {
  constructor(model) {
    super(model);
    this.batteryCapacity = '75kWh';
  }
  
  charge() {
    console.log(`${this.model}を充電中...`);
  }
}

class HybridCar extends Vehicle {
  constructor(model) {
    super(model);
    this.fuelType = 'ガソリン + 電気';
  }
  
  switchMode() {
    console.log('EVモードに切り替え');
  }
}

class GasolineCar extends Vehicle {
  constructor(model) {
    super(model);
    this.engineSize = '2000cc';
  }
  
  refuel() {
    console.log('給油中...');
  }
}

// Factoryクラス
class AutoFactory {
  static createVehicle(type, model) {
    let vehicle;
    
    // 車種に応じて適切なクラスをインスタンス化
    switch(type) {
      case 'electric':
        vehicle = new ElectricCar(model);
        vehicle.addFeature('自動運転レベル2');
        vehicle.addFeature('回生ブレーキ');
        break;
        
      case 'hybrid':
        vehicle = new HybridCar(model);
        vehicle.addFeature('燃費30km/L');
        vehicle.addFeature('静音走行');
        break;
        
      case 'gasoline':
        vehicle = new GasolineCar(model);
        vehicle.addFeature('ターボエンジン');
        vehicle.addFeature('6速MT');
        break;
        
      default:
        throw new Error(`Unknown vehicle type: ${type}`);
    }
    
    // 共通の装備
    vehicle.addFeature('安全システム');
    vehicle.addFeature('コネクテッドサービス');
    
    return vehicle;
  }
}

// 注文処理
const order1 = AutoFactory.createVehicle('electric', 'EV-X');
const order2 = AutoFactory.createVehicle('hybrid', 'ハイブリッドカー');
const order3 = AutoFactory.createVehicle('gasoline', 'スポーツカー');

console.log(order1.describe());
console.log(order2.describe());
console.log(order3.describe());

// それぞれの車種固有のメソッドも使える
order1.charge();
order2.switchMode();
order3.refuel();

Factoryパターンの美しさは、新しい車種を追加する時に現れる。既存のコードを変更せず、新しいクラスとFactoryの一行を追加するだけ。まるで自動車メーカーが新車種を開発する時、工場全体を作り直す必要がないように。

Observerパターン:SNS革命の裏側

2006年、あるSNSの最初のプロトタイプが動き始めた。

しかし、すぐに問題が発生した。ユーザーAがツイートすると、フォロワーBのタイムラインに反映させる必要がある。単純に考えれば、AがツイートするたびにBのデータベースを更新すればいい。

しかし、Aのフォロワーが100万人いたら?ツイートのたびに100万回のデータベース更新?システムは即座にダウンする。

エンジニアのブレイン・クックは、新聞配達を思い出した。新聞社は購読者リストを持っている。朝刊ができたら、リストの全員に配達する。購読をやめた人はリストから外す。

これがObserverパターンだ。

// SNSの簡略版実装
class User {
  constructor(username) {
    this.username = username;
    this.followers = new Set();
    this.timeline = [];
  }
  
  // フォロワーを追加(Subscribe)
  addFollower(follower) {
    this.followers.add(follower);
    console.log(`@${follower.username} が @${this.username} をフォローしました`);
  }
  
  // フォロワーを削除(Unsubscribe)
  removeFollower(follower) {
    this.followers.delete(follower);
    console.log(`@${follower.username} が @${this.username} のフォローを解除しました`);
  }
  
  // ツイートする(Notify)
  tweet(message) {
    const tweet = {
      author: this.username,
      message: message,
      timestamp: new Date(),
      likes: 0,
      retweets: 0
    };
    
    console.log(`\n@${this.username}: ${message}`);
    
    // 全フォロワーに通知
    this.followers.forEach(follower => {
      follower.receiveTweet(tweet);
    });
  }
  
  // ツイートを受信(Update)
  receiveTweet(tweet) {
    this.timeline.push(tweet);
    console.log(`  → @${this.username} のタイムラインに追加されました`);
  }
  
  // いいねする
  like(tweet) {
    tweet.likes++;
    console.log(`@${this.username} が「${tweet.message}」をいいねしました ❤️ ${tweet.likes}`);
  }
}

// 実例:SNS上での情報伝播
const techCEO = new User('techCEO');
const spaceAgency = new User('SpaceAgency');
const rocketCompany = new User('RocketCompany');
const techEnthusiast = new User('TechLover2024');

// フォロー関係の構築
techCEO.addFollower(spaceAgency);
techCEO.addFollower(rocketCompany);
techCEO.addFollower(techEnthusiast);

spaceAgency.addFollower(techEnthusiast);

// ツイートの伝播
techCEO.tweet('Going to Mars is not just possible, it\'s inevitable');

// 宇宙機関もツイート
spaceAgency.tweet('Launch successful! Next stop: the Moon 🚀');

// フォローを解除してみる
techCEO.removeFollower(rocketCompany);

// 解除後のツイート(RocketCompanyには届かない)
techCEO.tweet('Electric vehicle production starting next month');

このパターンの威力は、疎結合にある。発信者は誰がフォローしているか意識せずツイートできる。フォロワーは好きな時にフォロー/アンフォローできる。まさに現実のSNSと同じだ。

Decoratorパターン:スターバックスの注文システム

「トールのキャラメルマキアート、豆乳に変更、ホイップ多め、キャラメルソース追加で」

スターバックスのバリスタは、この複雑な注文を難なく処理する。なぜか?すべての注文が「基本のコーヒー + オプション」という構造になっているからだ。

これがDecoratorパターンの実例だ。基本となるオブジェクトに、機能を動的に追加していく。

// 基本となるコーヒー
class Coffee {
  constructor() {
    this.description = 'コーヒー';
    this.cost = 350;
  }
  
  getDescription() {
    return this.description;
  }
  
  getCost() {
    return this.cost;
  }
}

// デコレーターの基底クラス
class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  
  getDescription() {
    return this.coffee.getDescription();
  }
  
  getCost() {
    return this.coffee.getCost();
  }
}

// 具体的なデコレーター
class Milk extends CoffeeDecorator {
  constructor(coffee, type = '通常') {
    super(coffee);
    this.milkType = type;
  }
  
  getDescription() {
    return `${this.coffee.getDescription()} + ${this.milkType}ミルク`;
  }
  
  getCost() {
    const extraCost = this.milkType === '豆乳' ? 60 : 0;
    return this.coffee.getCost() + 50 + extraCost;
  }
}

class WhipCream extends CoffeeDecorator {
  constructor(coffee, amount = '通常') {
    super(coffee);
    this.amount = amount;
  }
  
  getDescription() {
    return `${this.coffee.getDescription()} + ホイップ${this.amount}`;
  }
  
  getCost() {
    const extraCost = this.amount === '多め' ? 30 : 0;
    return this.coffee.getCost() + 50 + extraCost;
  }
}

class CaramelSyrup extends CoffeeDecorator {
  constructor(coffee, extra = false) {
    super(coffee);
    this.extra = extra;
  }
  
  getDescription() {
    const sauce = this.extra ? ' + キャラメルソース追加' : '';
    return `${this.coffee.getDescription()} + キャラメルシロップ${sauce}`;
  }
  
  getCost() {
    const extraCost = this.extra ? 50 : 0;
    return this.coffee.getCost() + 60 + extraCost;
  }
}

// サイズ変更デコレーター
class SizeOption extends CoffeeDecorator {
  constructor(coffee, size) {
    super(coffee);
    this.size = size;
    this.sizeFactors = {
      'ショート': 0.9,
      'トール': 1.0,
      'グランデ': 1.3,
      'ベンティ': 1.5
    };
  }
  
  getDescription() {
    return `${this.size} ${this.coffee.getDescription()}`;
  }
  
  getCost() {
    return Math.floor(this.coffee.getCost() * this.sizeFactors[this.size]);
  }
}

// 実際の注文
console.log('=== スターバックス注文システム ===\n');

// シンプルなコーヒー
const coffee1 = new Coffee();
console.log(`${coffee1.getDescription()}: ¥${coffee1.getCost()}`);

// カフェラテ(トールサイズ)
const coffee2 = new SizeOption(
  new Milk(new Coffee()),
  'トール'
);
console.log(`${coffee2.getDescription()}: ¥${coffee2.getCost()}`);

// 複雑な注文:トールのキャラメルマキアート、豆乳、ホイップ多め、キャラメルソース追加
const coffee3 = new SizeOption(
  new CaramelSyrup(
    new WhipCream(
      new Milk(new Coffee(), '豆乳'),
      '多め'
    ),
    true // キャラメルソース追加
  ),
  'トール'
);
console.log(`${coffee3.getDescription()}: ¥${coffee3.getCost()}`);

// さらに複雑な注文も可能
const coffee4 = new SizeOption(
  new CaramelSyrup(
    new WhipCream(
      new CaramelSyrup(  // ダブルキャラメル
        new Milk(new Coffee(), '豆乳'),
        false
      ),
      '多め'
    ),
    true
  ),
  'ベンティ'
);
console.log(`${coffee4.getDescription()}: ¥${coffee4.getCost()}`);

Decoratorパターンの美しさは、無限の組み合わせを可能にすることだ。新しいオプション(例:ヘーゼルナッツシロップ)を追加しても、既存のコードに影響しない。まるでレゴブロックのように、組み合わせは自由自在だ。

Strategyパターン:ライドシェアアプリの価格アルゴリズム

金曜日の夜11時、渋谷。雨が降り始めた。

ライドシェアアプリを開くと、普段の3倍の料金が表示される。「サージプライシング」だ。需要と供給のバランスに応じて、価格が動的に変わる。

しかし、価格計算のアルゴリズムは状況によって異なる。通常時、ピーク時、深夜、悪天候、イベント開催時。それぞれ違う計算式を使う。

これをif文の嵐で実装したら、保守不能なコードになる。そこでStrategyパターンの出番だ。

// 価格計算戦略のインターフェース
class PricingStrategy {
  calculateFare(distance, duration) {
    throw new Error('calculateFareを実装してください');
  }
  
  getDescription() {
    throw new Error('getDescriptionを実装してください');
  }
}

// 通常料金
class StandardPricing extends PricingStrategy {
  calculateFare(distance, duration) {
    const baseFare = 410;  // 初乗り料金
    const perKm = 80;      // km単価
    const perMinute = 50;  // 分単価
    
    return baseFare + (distance * perKm) + (duration * perMinute);
  }
  
  getDescription() {
    return '通常料金';
  }
}

// サージプライシング(需要過多時)
class SurgePricing extends PricingStrategy {
  constructor(surgeMultiplier) {
    super();
    this.surgeMultiplier = surgeMultiplier;
  }
  
  calculateFare(distance, duration) {
    const standardPricing = new StandardPricing();
    const baseFare = standardPricing.calculateFare(distance, duration);
    return Math.floor(baseFare * this.surgeMultiplier);
  }
  
  getDescription() {
    return `サージ料金 (${this.surgeMultiplier}倍)`;
  }
}

// 深夜料金
class NightPricing extends PricingStrategy {
  calculateFare(distance, duration) {
    const baseFare = 500;
    const perKm = 100;
    const perMinute = 60;
    const nightSurcharge = 500; // 深夜割増
    
    return baseFare + (distance * perKm) + (duration * perMinute) + nightSurcharge;
  }
  
  getDescription() {
    return '深夜料金 (22:00-5:00)';
  }
}

// 悪天候料金
class BadWeatherPricing extends PricingStrategy {
  constructor(weatherCondition) {
    super();
    this.weatherCondition = weatherCondition;
  }
  
  calculateFare(distance, duration) {
    const standardPricing = new StandardPricing();
    const baseFare = standardPricing.calculateFare(distance, duration);
    
    // 天候による割増率
    const multipliers = {
      '小雨': 1.2,
      '大雨': 1.5,
      '台風': 2.0,
      '雪': 1.8
    };
    
    const multiplier = multipliers[this.weatherCondition] || 1.0;
    return Math.floor(baseFare * multiplier);
  }
  
  getDescription() {
    return `悪天候料金 (${this.weatherCondition})`;
  }
}

// ライドシェアコンテキスト
class RideShare {
  constructor() {
    this.pricingStrategy = new StandardPricing();
    this.currentLocation = null;
    this.currentTime = null;
    this.weather = '晴れ';
  }
  
  setPricingStrategy(strategy) {
    this.pricingStrategy = strategy;
  }
  
  setConditions(location, time, weather) {
    this.currentLocation = location;
    this.currentTime = time;
    this.weather = weather;
    
    // 条件に基づいて自動的に料金戦略を選択
    if (time >= 22 || time < 5) {
      this.setPricingStrategy(new NightPricing());
    } else if (weather !== '晴れ' && weather !== '曇り') {
      this.setPricingStrategy(new BadWeatherPricing(weather));
    } else if (location === '渋谷' && (time === 21 || time === 22)) {
      this.setPricingStrategy(new SurgePricing(2.5));
    } else {
      this.setPricingStrategy(new StandardPricing());
    }
  }
  
  estimateFare(from, to, distance, duration) {
    const fare = this.pricingStrategy.calculateFare(distance, duration);
    
    console.log('\n=== ライドシェア料金見積もり ===');
    console.log(`出発地: ${from}`);
    console.log(`目的地: ${to}`);
    console.log(`距離: ${distance}km`);
    console.log(`予想時間: ${duration}分`);
    console.log(`料金タイプ: ${this.pricingStrategy.getDescription()}`);
    console.log(`料金: ¥${fare}`);
    
    return fare;
  }
}

// シミュレーション
const rideShare = new RideShare();

// ケース1: 平日昼間
rideShare.setConditions('新宿', 14, '晴れ');
rideShare.estimateFare('新宿駅', '東京駅', 8.5, 25);

// ケース2: 金曜夜の渋谷
rideShare.setConditions('渋谷', 22, '晴れ');
rideShare.estimateFare('渋谷駅', '六本木', 3.2, 15);

// ケース3: 雨の日
rideShare.setConditions('品川', 18, '大雨');
rideShare.estimateFare('品川駅', '羽田空港', 15, 30);

// ケース4: 深夜
rideShare.setConditions('銀座', 2, '晴れ');
rideShare.estimateFare('銀座', '世田谷', 10, 28);

Strategyパターンの威力は、新しい料金体系の追加が簡単なことだ。例えば「高齢者割引」や「学生割引」を追加したければ、新しいStrategyクラスを作るだけ。既存のコードには一切触れない。

Commandパターン:Photoshopの「取り消し」魔法

Adobe Photoshopの開発者たちは、1990年代初頭、革命的な機能を実装しようとしていた。

「Ctrl+Z」で直前の操作を取り消す。今では当たり前の機能だが、当時は画期的だった。しかし、実装は困難を極めた。ブラシで描く、色を変える、レイヤーを追加する。無数の操作をどうやって「巻き戻す」のか?

トーマス・ノールは閃いた。「操作そのものをオブジェクトにすればいい」。

これがCommandパターンだ。

// エディタの実装
class PhotoEditor {
  constructor() {
    this.canvas = {
      pixels: [],
      layers: [],
      currentColor: '#000000',
      currentTool: 'brush'
    };
    this.history = [];
    this.currentIndex = -1;
  }
  
  // 現在の状態のスナップショット
  getState() {
    return JSON.parse(JSON.stringify(this.canvas));
  }
  
  // 状態の復元
  setState(state) {
    this.canvas = JSON.parse(JSON.stringify(state));
  }
  
  executeCommand(command) {
    // 現在位置より後の履歴を削除
    this.history = this.history.slice(0, this.currentIndex + 1);
    
    // コマンドを実行
    command.execute();
    
    // 履歴に追加
    this.history.push(command);
    this.currentIndex++;
    
    console.log(`実行: ${command.getName()}`);
    this.showStatus();
  }
  
  undo() {
    if (this.currentIndex < 0) {
      console.log('取り消す操作がありません');
      return;
    }
    
    const command = this.history[this.currentIndex];
    command.undo();
    this.currentIndex--;
    
    console.log(`取り消し: ${command.getName()}`);
    this.showStatus();
  }
  
  redo() {
    if (this.currentIndex >= this.history.length - 1) {
      console.log('やり直す操作がありません');
      return;
    }
    
    this.currentIndex++;
    const command = this.history[this.currentIndex];
    command.execute();
    
    console.log(`やり直し: ${command.getName()}`);
    this.showStatus();
  }
  
  showStatus() {
    console.log(`履歴: ${this.currentIndex + 1}/${this.history.length}`);
    console.log(`レイヤー数: ${this.canvas.layers.length}`);
    console.log('---');
  }
}

// コマンドの基底クラス
class Command {
  constructor(editor) {
    this.editor = editor;
    this.previousState = null;
  }
  
  execute() {
    // サブクラスで実装
  }
  
  undo() {
    if (this.previousState) {
      this.editor.setState(this.previousState);
    }
  }
  
  getName() {
    return this.constructor.name;
  }
}

// 具体的なコマンド
class AddLayerCommand extends Command {
  constructor(editor, layerName) {
    super(editor);
    this.layerName = layerName;
  }
  
  execute() {
    this.previousState = this.editor.getState();
    this.editor.canvas.layers.push({
      name: this.layerName,
      visible: true,
      opacity: 100,
      blendMode: 'normal'
    });
  }
  
  getName() {
    return `レイヤー追加: ${this.layerName}`;
  }
}

class ChangeColorCommand extends Command {
  constructor(editor, newColor) {
    super(editor);
    this.newColor = newColor;
  }
  
  execute() {
    this.previousState = this.editor.getState();
    this.oldColor = this.editor.canvas.currentColor;
    this.editor.canvas.currentColor = this.newColor;
  }
  
  getName() {
    return `色変更: ${this.newColor}`;
  }
}

class DrawCommand extends Command {
  constructor(editor, x, y, size) {
    super(editor);
    this.x = x;
    this.y = y;
    this.size = size;
  }
  
  execute() {
    this.previousState = this.editor.getState();
    this.editor.canvas.pixels.push({
      x: this.x,
      y: this.y,
      size: this.size,
      color: this.editor.canvas.currentColor
    });
  }
  
  getName() {
    return `描画: (${this.x}, ${this.y})`;
  }
}

// マクロコマンド(複数のコマンドをまとめて実行)
class MacroCommand extends Command {
  constructor(editor, commands, name) {
    super(editor);
    this.commands = commands;
    this.name = name;
  }
  
  execute() {
    this.previousState = this.editor.getState();
    this.commands.forEach(cmd => cmd.execute());
  }
  
  undo() {
    // 逆順で取り消し
    for (let i = this.commands.length - 1; i >= 0; i--) {
      this.commands[i].undo();
    }
  }
  
  getName() {
    return this.name;
  }
}

// 実際の使用例
console.log('=== Photoshop風エディタ ===\n');

const editor = new PhotoEditor();

// 基本的な操作
editor.executeCommand(new AddLayerCommand(editor, '背景'));
editor.executeCommand(new ChangeColorCommand(editor, '#FF0000'));
editor.executeCommand(new DrawCommand(editor, 100, 100, 5));
editor.executeCommand(new AddLayerCommand(editor, 'テキスト'));

// 取り消し
console.log('\n--- Ctrl+Z (取り消し) ---');
editor.undo();
editor.undo();

// やり直し
console.log('\n--- Ctrl+Y (やり直し) ---');
editor.redo();

// マクロ操作
console.log('\n--- アクション実行 ---');
const watermarkMacro = new MacroCommand(
  editor,
  [
    new AddLayerCommand(editor, '透かし'),
    new ChangeColorCommand(editor, '#0000FF'),
    new DrawCommand(editor, 200, 200, 10),
    new DrawCommand(editor, 210, 200, 10),
    new DrawCommand(editor, 220, 200, 10)
  ],
  '透かし追加アクション'
);

editor.executeCommand(watermarkMacro);

// マクロも取り消し可能
console.log('\n--- マクロを取り消し ---');
editor.undo();

Commandパターンの美しさは、「操作」と「実行」を分離することだ。操作をオブジェクトとして扱うことで、保存、再生、取り消し、やり直しが可能になる。まるで時間を巻き戻すタイムマシンのように。

パターンの落とし穴:過剰な抽象化

2010年、あるスタートアップでの出来事。

CTOのマイケルは、Gang of Fourの本に感銘を受けた。「我々のシステムをすべてデザインパターンで構築しよう!」。チームは熱狂的に取り組んだ。

3ヶ月後、システムは完成した。AbstractFactoryFactoryProvider、SingletonStrategyObserverProxy、DecoratorAdapterBridgeFacade...。23個すべてのパターンを使い、さらに独自のパターンも追加した。

しかし、誰もコードを理解できなくなった。簡単な機能追加に1週間かかる。バグ修正は迷路を彷徨うようだった。新人エンジニアは3日で辞めた。「哲学書を読んでいるようだ」という言葉を残して。

6ヶ月後、システムは破棄された。ゼロから作り直すことになった。

デザインパターンは薬のようなものです。適切な量なら病を治しますが、過剰摂取は毒になります。シンプルさを保ちながら、必要な場所で適切に使うことが重要です。

現代におけるデザインパターンの進化

2024年、デザインパターンは新しい形で生きている。

ReactのHooksは、Commandパターンの進化形だ。状態の変更を関数として扱う。ReduxはFluxパターンという新しい設計思想を生み出した。マイクロサービスは、パターンをネットワークレベルに拡張した。

// Reactの状態管理:Observerパターンの現代版
import { useState, useEffect } from 'react';

// カスタムフック:状態管理パターンの抽象化
function useWebSocket(url) {
  const [messages, setMessages] = useState([]);
  const [status, setStatus] = useState('disconnected');
  
  useEffect(() => {
    const ws = new WebSocket(url);
    
    ws.onopen = () => setStatus('connected');
    ws.onmessage = (event) => {
      setMessages(prev => [...prev, JSON.parse(event.data)]);
    };
    ws.onclose = () => setStatus('disconnected');
    
    return () => ws.close();
  }, [url]);
  
  return { messages, status };
}

// Reduxスタイル:Commandパターンの進化
const actions = {
  increment: () => ({ type: 'INCREMENT' }),
  decrement: () => ({ type: 'DECREMENT' }),
  reset: () => ({ type: 'RESET' })
};

const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT': return state + 1;
    case 'DECREMENT': return state - 1;
    case 'RESET': return 0;
    default: return state;
  }
};

AIの時代、デザインパターンの重要性はさらに増している。ChatGPTやCopilotがコードを生成する時、デザインパターンを理解していれば、より良い設計を引き出せる。「ここはObserverパターンで実装して」という指示一つで、整理されたコードが生成される。

未来を設計する力

デザインパターンは、プログラミングの共通言語だ。

建築家が「この部屋には採光窓を」と言えば、職人は理解する。料理人が「野菜は下茹でして」と言えば、見習いは理解する。プログラマーが「ここはFactoryパターンで」と言えば、チームは理解する。

30年前、Gang of Fourが提示した23個のパターンは、今も生きている。形を変え、進化しながら。そして新しいパターンも生まれている。

パターンを学ぶことは、先人の知恵を学ぶこと。車輪の再発明をせず、巨人の肩に乗ること。そして、より良いソフトウェアを、より速く、より確実に作ること。

コードは詩だ。デザインパターンは、その詩の韻律だ。韻律を知れば、美しい詩が書ける。

あなたも、この23個の魔法を使いこなせるようになる。そして、いつか新しいパターンを見つけるかもしれない。それが、ソフトウェア設計の未来を作る。