第7回:設計パターンとモジュール分割

はじめに

みなさん、こんにちは!前回はテストとリファクタリングについて学びました。今日は「設計パターン」というプログラミングの世界で広く使われている解決策のカタログと、それをモジュール分割にどう活用するかについて学んでいきます。

設計パターンは、経験豊かな開発者たちが長年かけて発見した「良いコードの書き方」の集大成です。料理のレシピのようなもので、一からすべて考えるのではなく、先人の知恵を借りることで効率的に良い設計ができるようになります。

今日の講義では、よく使われる設計パターンとその活用法、そして注意点について学んでいきましょう。

よく使われる設計パターンとその活用法

設計パターンは大きく分けて「生成」「構造」「振る舞い」の3つのカテゴリに分類されます。今日はそれぞれのカテゴリから特に重要なパターンを見ていきましょう。

1. ファサードパターン(構造パターン)

パターンの概要

ファサードパターンは「複雑なシステムに対してシンプルなインターフェースを提供する」パターンです。ファサード(Facade)とは「建物の正面」という意味で、複雑な内部構造を隠し、シンプルな「顔」を見せるイメージです。

問題と解決策

問題:システムの一部が複雑になりすぎて、使いにくくなっている。

解決策:複雑なサブシステムの前に立つシンプルなインターフェース(ファサード)を作る。

例えば、動画変換システムを考えてみましょう:

// 複雑なサブシステム
class VideoFile {
  constructor(filename) {
    this.filename = filename;
    // ファイルを開く処理...
  }
}

class CompressionCodec {
  compress(file) {
    // 圧縮処理...
  }
}

class AudioMixer {
  mix(compressedVideo) {
    // 音声ミキシング処理...
  }
}

// ファサード
class VideoConverter {
  convertVideo(filename, format) {
    const file = new VideoFile(filename);
    const codec = new CompressionCodec();
    const compressedVideo = codec.compress(file);
    const mixer = new AudioMixer();
    const result = mixer.mix(compressedVideo);
    
    return result;
  }
}

// クライアントコード
const converter = new VideoConverter();
const mp4 = converter.convertVideo("birthday.mov", "mp4");
// クライアントは複雑なサブシステムを知らなくても良い
Code language: JavaScript (javascript)

モジュール分割への応用

ファサードパターンは、大きなモジュールの「公開インターフェース」として使えます。モジュール内部は複雑でも、外部からはシンプルに使えるようになります。

例えば、支払い処理モジュールのファサードを作ると:

// 支払いモジュールのファサード
class PaymentFacade {
  constructor() {
    this.cardProcessor = new CreditCardProcessor();
    this.paypalProcessor = new PayPalProcessor();
    this.bankTransferProcessor = new BankTransferProcessor();
    this.fraudDetector = new FraudDetectionService();
  }
  
  processPayment(amount, paymentMethod, details) {
    // 不正検出
    this.fraudDetector.checkForFraud(amount, details);
    
    // 支払い方法に応じた処理
    switch(paymentMethod) {
      case 'creditcard':
        return this.cardProcessor.charge(amount, details);
      case 'paypal':
        return this.paypalProcessor.processPayment(amount, details);
      case 'banktransfer':
        return this.bankTransferProcessor.transfer(amount, details);
      default:
        throw new Error('未対応の支払い方法です');
    }
  }
}

// 使用例
const paymentService = new PaymentFacade();
paymentService.processPayment(10000, 'creditcard', { cardNumber: '1234...' });
Code language: JavaScript (javascript)

これにより、支払い処理の複雑な内部実装を知らなくても、簡単に使えるようになります。

2. アダプターパターン(構造パターン)

パターンの概要

アダプターパターンは「互換性のないインターフェースを持つクラスを協調して動作させる」ためのパターンです。電源アダプターのように、異なる規格を変換するイメージです。

問題と解決策

問題:既存のクラスのインターフェースが、必要なインターフェースと合っていない。

解決策:アダプタークラスを作り、既存クラスのインターフェースを必要なインターフェースに変換する。

例えば、外部の決済サービスを自社システムに統合する場合:

// 自社システムが想定しているインターフェース
class PaymentProcessor {
  pay(amount) {
    // 支払い処理...
  }
}

// 外部の決済サービス(変更できない)
class ExternalPaymentService {
  makePayment(dollars, cents) {
    // 外部サービスの支払い処理...
  }
}

// アダプター
class ExternalPaymentAdapter extends PaymentProcessor {
  constructor(externalService) {
    super();
    this.externalService = externalService;
  }
  
  pay(amount) {
    // 円をドルとセントに変換(例として)
    const dollars = Math.floor(amount / 100);
    const cents = amount % 100;
    
    // 外部サービスのインターフェースで呼び出し
    return this.externalService.makePayment(dollars, cents);
  }
}

// 使用例
const externalService = new ExternalPaymentService();
const adapter = new ExternalPaymentAdapter(externalService);

// 自社システムのインターフェースで呼び出せる
adapter.pay(12345); // 123.45ドルとして処理される
Code language: JavaScript (javascript)

モジュール分割への応用

アダプターパターンは、以下のような場面でモジュール分割に役立ちます:

  1. レガシーコードと新しいコードの統合
  2. サードパーティライブラリの統合
  3. テスト中にモックオブジェクトを使用する場合

例えば、データベースアクセス層をリファクタリングする場合:

// 古いデータアクセスインターフェース
class OldUserRepository {
  getUserById(id) { /* ... */ }
  getAllUsers() { /* ... */ }
}

// 新しいインターフェース
class UserRepository {
  findById(id) { /* ... */ }
  findAll() { /* ... */ }
}

// アダプター(新しいインターフェースを維持しながら古いコードを利用)
class LegacyUserRepositoryAdapter extends UserRepository {
  constructor(oldRepository) {
    super();
    this.oldRepository = oldRepository;
  }
  
  findById(id) {
    return this.oldRepository.getUserById(id);
  }
  
  findAll() {
    return this.oldRepository.getAllUsers();
  }
}
Code language: JavaScript (javascript)

このようにすれば、古いコードを一度に書き換えなくても、新しいインターフェースで統一的に扱えるようになります。

3. ストラテジーパターン(振る舞いパターン)

パターンの概要

ストラテジーパターンは「同じ問題を解決するための異なるアルゴリズムをカプセル化し、実行時に切り替え可能にする」パターンです。戦略(Strategy)を状況に応じて変えるイメージです。

問題と解決策

問題:同じ処理を行うが、状況によって異なるアルゴリズムが必要。

解決策:各アルゴリズムを別々のクラスにカプセル化し、それらを交換可能にする。

例えば、支払い方法を選択できるシステムの例:

// ストラテジーインターフェース
class PaymentStrategy {
  pay(amount) {
    throw new Error("サブクラスで実装する必要があります");
  }
}

// 具体的なストラテジー
class CreditCardStrategy extends PaymentStrategy {
  constructor(cardNumber, name, cvv, expiryDate) {
    super();
    this.cardNumber = cardNumber;
    this.name = name;
    this.cvv = cvv;
    this.expiryDate = expiryDate;
  }
  
  pay(amount) {
    console.log(`${amount}円をクレジットカードで支払いました: ${this.cardNumber}`);
  }
}

class PayPalStrategy extends PaymentStrategy {
  constructor(email, password) {
    super();
    this.email = email;
    this.password = password;
  }
  
  pay(amount) {
    console.log(`${amount}円をPayPalで支払いました: ${this.email}`);
  }
}

// コンテキスト
class ShoppingCart {
  constructor() {
    this.items = [];
    this.paymentStrategy = null;
  }
  
  addItem(item) {
    this.items.push(item);
  }
  
  calculateTotal() {
    return this.items.reduce((total, item) => total + item.price, 0);
  }
  
  setPaymentStrategy(strategy) {
    this.paymentStrategy = strategy;
  }
  
  checkout() {
    const amount = this.calculateTotal();
    if (!this.paymentStrategy) {
      throw new Error("支払い方法を選択してください");
    }
    this.paymentStrategy.pay(amount);
  }
}

// 使用例
const cart = new ShoppingCart();
cart.addItem({ name: "ノートPC", price: 80000 });
cart.addItem({ name: "マウス", price: 3000 });

// クレジットカードで支払い
cart.setPaymentStrategy(new CreditCardStrategy("1234-5678-9012-3456", "山田太郎", "123", "12/25"));
cart.checkout();

// PayPalで支払い
cart.setPaymentStrategy(new PayPalStrategy("taro@example.com", "password"));
cart.checkout();
Code language: JavaScript (javascript)

モジュール分割への応用

ストラテジーパターンは、機能の一部を差し替え可能なモジュールとして分離するのに適しています。例えば:

  1. 様々な割引計算アルゴリズム
  2. 異なる認証方法
  3. 様々なソートアルゴリズム

例えば、予約システムでの料金計算ストラテジー:

// 料金計算ストラテジー
class PricingStrategy {
  calculatePrice(booking) {
    throw new Error("サブクラスで実装する必要があります");
  }
}

// 平日料金
class WeekdayPricing extends PricingStrategy {
  calculatePrice(booking) {
    // 平日料金計算ロジック
  }
}

// 週末料金
class WeekendPricing extends PricingStrategy {
  calculatePrice(booking) {
    // 週末料金計算ロジック
  }
}

// 特別イベント料金
class EventPricing extends PricingStrategy {
  calculatePrice(booking) {
    // 特別イベント料金計算ロジック
  }
}

// 予約サービス
class BookingService {
  constructor(pricingStrategyFactory) {
    this.pricingStrategyFactory = pricingStrategyFactory;
  }
  
  createBooking(date, customer, service) {
    const booking = { date, customer, service };
    
    // 日付に基づいて適切な料金計算ストラテジーを取得
    const pricingStrategy = this.pricingStrategyFactory.getPricingStrategy(date);
    
    // 料金計算
    booking.price = pricingStrategy.calculatePrice(booking);
    
    return booking;
  }
}
Code language: JavaScript (javascript)

このようにすると、料金計算のロジックをモジュール化でき、新しい料金体系を追加する際も他のコードを変更せずに済みます。

4. オブザーバーパターン(振る舞いパターン)

パターンの概要

オブザーバーパターンは「あるオブジェクトの状態が変化したときに、それに依存するオブジェクトに通知する」ためのパターンです。新聞の購読のように、変化があったときに購読者に通知するイメージです。

問題と解決策

問題:あるオブジェクトの変更を、他の複数のオブジェクトに通知する必要がある。

解決策:サブジェクト(観察対象)とオブザーバー(観察者)に分離し、通知の仕組みを構築する。

例えば、ユーザーの行動を追跡するシステムの例:

// サブジェクト(観察対象)
class UserActivity {
  constructor() {
    this.observers = [];
  }
  
  subscribe(observer) {
    this.observers.push(observer);
  }
  
  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  
  notify(activity) {
    this.observers.forEach(observer => observer.update(activity));
  }
  
  trackActivity(user, action) {
    const activity = { user, action, timestamp: new Date() };
    this.notify(activity);
  }
}

// オブザーバー(観察者)インターフェース
class ActivityObserver {
  update(activity) {
    throw new Error("サブクラスで実装する必要があります");
  }
}

// 具体的なオブザーバー
class Logger extends ActivityObserver {
  update(activity) {
    console.log(`[LOG] ${activity.user}${activity.action}${activity.timestamp} に実行しました`);
  }
}

class AnalyticsTracker extends ActivityObserver {
  update(activity) {
    console.log(`[ANALYTICS] アクション "${activity.action}" を記録しました`);
    // 分析サービスにデータを送信...
  }
}

class SecurityMonitor extends ActivityObserver {
  update(activity) {
    if (activity.action === "login" || activity.action === "access_sensitive_data") {
      console.log(`[SECURITY] ${activity.user}${activity.action} アクションを監視中`);
    }
  }
}

// 使用例
const userActivity = new UserActivity();

userActivity.subscribe(new Logger());
userActivity.subscribe(new AnalyticsTracker());
userActivity.subscribe(new SecurityMonitor());

userActivity.trackActivity("user123", "login");
userActivity.trackActivity("user123", "view_page");
userActivity.trackActivity("user123", "access_sensitive_data");
Code language: JavaScript (javascript)

モジュール分割への応用

オブザーバーパターンは、異なるモジュール間の疎結合な通信に役立ちます:

  1. UIコンポーネント間の通信
  2. ドメインイベントの処理
  3. 複数のサブシステム間の連携

例えば、注文システムでのイベント処理:

// イベント発行者(サブジェクト)
class OrderService {
  constructor() {
    this.observers = [];
  }
  
  subscribe(observer) {
    this.observers.push(observer);
  }
  
  placeOrder(order) {
    // 注文処理...
    
    // 注文完了イベントを通知
    this.notifyOrderPlaced(order);
    
    return order;
  }
  
  notifyOrderPlaced(order) {
    this.observers.forEach(observer => observer.onOrderPlaced(order));
  }
}

// イベント購読者(オブザーバー)
class InventoryService {
  onOrderPlaced(order) {
    console.log("在庫を更新します");
    // 在庫の減少処理...
  }
}

class NotificationService {
  onOrderPlaced(order) {
    console.log("お客様に確認メールを送信します");
    // メール送信処理...
  }
}

class AnalyticsService {
  onOrderPlaced(order) {
    console.log("注文データを分析システムに送信します");
    // 分析データ送信処理...
  }
}

// 使用例
const orderService = new OrderService();

orderService.subscribe(new InventoryService());
orderService.subscribe(new NotificationService());
orderService.subscribe(new AnalyticsService());

const order = { id: "ORD-12345", items: [/*...*/], customer: {/*...*/} };
orderService.placeOrder(order);
Code language: JavaScript (javascript)

このようにすると、注文処理と、その結果として必要な様々な処理を疎結合に保ちながら連携させることができます。

パターンの過剰適用を避ける方法

設計パターンは強力なツールですが、使いすぎると逆に複雑になることがあります。以下のガイドラインを参考にしてください:

1. YAGNI原則(You Aren’t Gonna Need It)

「それは必要になるまで作らない」という原則です。将来の拡張性のためにパターンを適用するのではなく、実際に必要になったときに適用しましょう。

例えば、「将来異なる支払い方法が必要になるかもしれない」と考えて、最初からストラテジーパターンを適用するのではなく、実際に複数の支払い方法が必要になったときに導入するのがベターです。

2. KISS原則(Keep It Simple, Stupid)

「シンプルにしておく」という原則です。複雑なパターンよりも、シンプルな解決策の方が理解しやすく、バグも少なくなります。

例えば、2〜3行のシンプルなif文で解決できる問題に、ステートパターンを適用するのは過剰かもしれません。

3. パターンの目的を理解する

パターンの形だけを真似るのではなく、そのパターンが解決しようとしている問題を理解することが大切です。問題がなければ、パターンを適用する必要はありません。

4. コードの読みやすさを優先する

どれだけ洗練されたパターンを使っても、他の開発者が理解できなければ意味がありません。特に、チームメンバーがパターンに慣れていない場合は、適用する前に教育や説明が必要かもしれません。

5. パターンの利用を記録する

設計パターンを適用する場合は、コメントやドキュメントで明記しておくと良いでしょう。そうすることで、他の開発者も意図を理解しやすくなります。

実習:適切なデザインパターンを選んでリファクタリングする

では、実習として既存のコードを設計パターンを使ってリファクタリングしてみましょう。

問題

以下のような通知システムがあります:

class NotificationSystem {
  constructor() {
    this.users = [];
  }
  
  addUser(user) {
    this.users.push(user);
  }
  
  sendNotification(message, channel) {
    for (const user of this.users) {
      if (channel === 'email') {
        console.log(`メール送信: ${user.email} 宛に "${message}" を送信しました`);
      } else if (channel === 'sms') {
        console.log(`SMS送信: ${user.phone} 宛に "${message}" を送信しました`);
      } else if (channel === 'push') {
        console.log(`プッシュ通知: デバイス ${user.deviceId} に "${message}" を送信しました`);
      } else {
        console.log(`未対応の通知チャンネル: ${channel}`);
      }
    }
  }
}

// 使用例
const notifier = new NotificationSystem();
notifier.addUser({ name: '山田太郎', email: 'taro@example.com', phone: '090-1234-5678', deviceId: 'device-123' });
notifier.addUser({ name: '佐藤花子', email: 'hanako@example.com', phone: '080-8765-4321', deviceId: 'device-456' });

notifier.sendNotification('システムメンテナンスのお知らせ', 'email');
notifier.sendNotification('緊急アラート', 'sms');
Code language: JavaScript (javascript)

この実装には以下の問題があります:

  1. 通知チャンネルが増えるたびに、sendNotificationメソッドを修正する必要がある
  2. 各チャンネルの通知ロジックが混在している
  3. 特定のユーザーだけに通知する機能がない

解決策:ストラテジーパターンとオブザーバーパターンの適用

このコードをリファクタリングするために、通知チャンネルの処理にストラテジーパターンを、ユーザー管理と通知配信にオブザーバーパターンを適用しましょう。

// 通知ストラテジー(Strategy Pattern)
class NotificationStrategy {
  send(user, message) {
    throw new Error("サブクラスで実装する必要があります");
  }
}

class EmailNotification extends NotificationStrategy {
  send(user, message) {
    if (user.email) {
      console.log(`メール送信: ${user.email} 宛に "${message}" を送信しました`);
      return true;
    }
    return false;
  }
}

class SMSNotification extends NotificationStrategy {
  send(user, message) {
    if (user.phone) {
      console.log(`SMS送信: ${user.phone} 宛に "${message}" を送信しました`);
      return true;
    }
    return false;
  }
}

class PushNotification extends NotificationStrategy {
  send(user, message) {
    if (user.deviceId) {
      console.log(`プッシュ通知: デバイス ${user.deviceId} に "${message}" を送信しました`);
      return true;
    }
    return false;
  }
}

// 通知サービス(Observer Pattern)
class NotificationService {
  constructor() {
    this.strategies = new Map();
    this.observers = new Set();
  }
  
  registerStrategy(channel, strategy) {
    this.strategies.set(channel, strategy);
  }
  
  subscribe(user) {
    this.observers.add(user);
  }
  
  unsubscribe(user) {
    this.observers.delete(user);
  }
  
  notify(message, channel, targetUsers = null) {
    const strategy = this.strategies.get(channel);
    
    if (!strategy) {
      console.log(`未対応の通知チャンネル: ${channel}`);
      return;
    }
    
    const users = targetUsers || this.observers;
    
    for (const user of users) {
      strategy.send(user, message);
    }
  }
}

// 使用例
const notificationService = new NotificationService();

// 通知ストラテジーの登録
notificationService.registerStrategy('email', new EmailNotification());
notificationService.registerStrategy('sms', new SMSNotification());
notificationService.registerStrategy('push', new PushNotification());

// ユーザーの登録
const user1 = { name: '山田太郎', email: 'taro@example.com', phone: '090-1234-5678', deviceId: 'device-123' };
const user2 = { name: '佐藤花子', email: 'hanako@example.com', phone: '080-8765-4321', deviceId: 'device-456' };

notificationService.subscribe(user1);
notificationService.subscribe(user2);

// 全員に通知
notificationService.notify('システムメンテナンスのお知らせ', 'email');

// 特定のユーザーだけに通知
notificationService.notify('緊急アラート', 'sms', [user1]);

// 新しい通知チャンネルの追加も簡単
class SlackNotification extends NotificationStrategy {
  send(user, message) {
    if (user.slackId) {
      console.log(`Slack通知: ${user.slackId} に "${message}" を送信しました`);
      return true;
    }
    return false;
  }
}

notificationService.registerStrategy('slack', new SlackNotification());

// Slack IDを持つユーザーを追加
const user3 = { name: '鈴木一郎', email: 'ichiro@example.com', slackId: 'U12345' };
notificationService.subscribe(user3);

notificationService.notify('新機能のお知らせ', 'slack');
Code language: JavaScript (javascript)

リファクタリング後の改善点

  1. 拡張性の向上:新しい通知チャンネルを追加する場合、新しいストラテジークラスを追加するだけで済む
  2. 単一責任の原則:各通知方法が独自のクラスに分離された
  3. 柔軟性の向上:特定のユーザーだけに通知する機能を追加
  4. テストしやすさ:各ストラテジーを個別にテストできる

このようにパターンを適用することで、コードの構造が改善され、将来の変更にも対応しやすくなりました。

まとめ

今日の講義では、設計パターンとモジュール分割について学びました:

  1. ファサードパターン:複雑なサブシステムにシンプルなインターフェースを提供
  2. アダプターパターン:互換性のないインターフェースを変換
  3. ストラテジーパターン:アルゴリズムを交換可能にカプセル化
  4. オブザーバーパターン:オブジェクト間の1対多の依存関係を定義

また、パターンの過剰適用を避けるためのガイドラインとして:

  1. YAGNI原則(必要になるまで作らない)
  2. KISS原則(シンプルにする)
  3. パターンの目的を理解する
  4. コードの読みやすさを優先する
  5. パターンの利用を記録する

設計パターンは便利なツールですが、万能薬ではありません。問題に合わせて適切なパターンを選び、必要に応じて適用することが大切です。

次回は「ウェブアプリケーションの構成技法」について学びます。今日学んだ設計パターンの知識も、ウェブアプリケーションの設計に活かしていきましょう。

質問タイム

質問1:石田さんからの質問

質問:「設計パターンを学ぶのは重要だと思いましたが、実際のプロジェクトでは、どのタイミングでパターンを適用するべきですか?設計段階から考えるべきか、それとも問題が発生してから適用するのが良いのでしょうか?」

回答: とても良い質問です、石田さん。設計パターン適用のタイミングについては、バランスが重要です。

基本的には、以下の指針が役立つでしょう:

  1. 明らかな問題がすでに見えている場合:例えば、「この機能は今後何度も拡張されそうだ」と分かっている場合は、初期設計の段階からストラテジーパターンなどを検討するとよいでしょう。
  2. 過去の経験から問題が予測できる場合:同じような問題に過去に直面したことがあれば、先回りして適切なパターンを適用するのは賢明です。
  3. リファクタリングの段階:コードが成長し、パターンの必要性が明確になってきた段階で適用するのが最も一般的です。これは「設計の発展」と呼ばれるアプローチです。

個人的には、「プレマチュア・パターン化(早すぎるパターン適用)」は避けるべきだと思います。シンプルな解決策から始めて、コードの成長に合わせてパターンを導入していくのが良いケースが多いです。

Martin Fowlerは「臭いを感じたらリファクタリングせよ」と言っていますが、これは設計パターンにも当てはまります。コードの問題点(長すぎるメソッド、条件分岐の増加、重複コードなど)が見えてきたときが、パターン導入の良いタイミングです。

要は、先を見すぎず、かといって手遅れにならないよう、コードの発展に応じて適切なタイミングでパターンを検討するのがベストだと思います。

質問2:西山さんからの質問

質問:「オブザーバーパターンとPub/Subパターンの違いがよくわかりません。また、モダンなJavaScriptフレームワーク(ReactやVueなど)では、これらのパターンはどのように実装されているのでしょうか?」

回答: 鋭い質問ですね、西山さん。オブザーバーパターンとPub/Subパターンは似ていますが、重要な違いがあります。

オブザーバーパターン

  • サブジェクト(通知者)はオブザーバー(購読者)を直接知っている
  • 通常、サブジェクトがオブザーバーのメソッド(update()など)を直接呼び出す
  • 1対多の関係

Pub/Subパターン(発行/購読パターン):

  • 発行者と購読者の間に「メッセージブローカー」や「イベントチャネル」が存在する
  • 発行者と購読者は互いを直接知らない(疎結合)
  • イベントの種類によって通知をフィルタリングできる
  • 多対多の関係も可能

シンプルな例で言えば:

  • オブザーバー:新聞社(サブジェクト)が購読者(オブザーバー)に直接新聞を届ける
  • Pub/Sub:新聞社(発行者)が配達会社(ブローカー)に新聞を渡し、購読者(購読者)は配達会社から受け取る

モダンなJavaScriptフレームワークでの実装

  1. React
    • Reactの状態管理は基本的にはオブザーバーパターンに近い
    • ステート(サブジェクト)が変化すると、関連するコンポーネント(オブザーバー)が再レンダリングされる
    • ReduxやContextAPIはより明示的なPub/Subの要素を持つ
  2. Vue.js
    • リアクティブシステムは洗練されたオブザーバーパターン
    • データ(サブジェクト)が変わると、依存するコンポーネント(オブザーバー)を自動的に更新
    • VuexもPub/Subの要素を取り入れている
  3. Angular
    • RxJSという強力なリアクティブプログラミングライブラリを使用
    • ObservableとSubscriptionの仕組みはPub/Subパターンを高度に実装したもの

モダンフレームワークの多くは、単純なパターンそのものより、それらを発展させた「リアクティブプログラミング」の概念を取り入れています。これは状態変化を連鎖的に伝播させる仕組みで、オブザーバーパターンの洗練された形と言えるでしょう。

実務では、これらのフレームワークが提供する仕組みを活用することで、直接パターンを実装する必要性は減っていますが、その背景にある概念を理解することは依然として重要です。

質問3:中島さんからの質問

質問:「設計パターンは理解できましたが、実際のコードではパターンを組み合わせることも多いと思います。パターンを組み合わせる際の注意点や、特に相性の良いパターンの組み合わせがあれば教えてください。」

回答: 素晴らしい質問です、中島さん。実際のアプリケーションでは、複数のパターンを組み合わせることがとても一般的です。

パターンを組み合わせる際の注意点

  1. 責任の明確化:各パターンの責任範囲を明確にし、重複や競合を避ける
  2. 複雑性の管理:パターンの組み合わせで複雑になりすぎないよう注意する
  3. 一貫した命名規則:複数のパターンを使うとき、命名に一貫性を持たせる
  4. ドキュメント化:どのパターンをなぜ組み合わせたかを記録しておく

相性の良いパターン組み合わせ

  1. ファクトリー + ストラテジー
    • ファクトリーパターンでストラテジーオブジェクトを生成
    • 例:支払い方法ファクトリーが状況に応じた支払いストラテジーを生成
    class PaymentStrategyFactory { createStrategy(paymentType) { switch(paymentType) { case 'credit': return new CreditCardStrategy(); case 'paypal': return new PayPalStrategy(); // ... } } }
  2. コンポジット + ビジター
    • 階層構造(コンポジット)を持つオブジェクトに対して、異なる操作(ビジター)を適用
    • 例:UIコンポーネントツリーに対して、レンダリング、検証などの処理を適用
  3. オブザーバー + メディエーター
    • メディエーターがオブザーバーパターンの通知の仲介役になる
    • 例:チャットシステムでメディエーターが複数のユーザー間のメッセージ交換を調整
  4. デコレーター + ストラテジー
    • 基本機能にデコレーターで機能を追加し、アルゴリズムをストラテジーで切り替え
    • 例:ログ出力機能(デコレーター)を持つ、様々な暗号化アルゴリズム(ストラテジー)
  5. オブザーバー + シングルトン
    • 今日の実習でも見たように、イベント管理システムはシングルトンで実装され、オブザーバーパターンで通知する組み合わせが一般的

実務では、これらの組み合わせは自然に発生することが多いです。大切なのは、パターンを目的として使うのではなく、問題解決の手段として適切に使うことです。パターンの組み合わせも同様で、問題を解決するために必要な場合に、意識的に組み合わせるべきです。

エンタープライズアプリケーションでは、このような複数パターンの組み合わせが「アーキテクチャパターン」として体系化されていることもあります。MVCやMVVMなども、複数の基本パターンの組み合わせと考えることができます。