7つの必須ソフトウェアデザインパターン

#JavaScript#Software Design Patterns

ソフトウェアデザインパターンは、異なる言語やプラットフォームで繰り返し現れる一般的なプログラミング問題に対する、実証済みの解決策です。これらのパターンは複雑な概念ではありません。経験豊富な開発者がより清潔で保守性の高いコードを書くために使用する、実証済みのアプローチなのです。

1994年、「Gang of Four(四人組)」として知られる4人の開発者が、影響力のある書籍で23の一般的に使用されるデザインパターンを文書化しました。ここでは23すべてを扱いませんが、これら7つの必須パターンを理解することで、コードの品質と問題解決能力が大幅に向上します。

全てのデザインパターンは3つの主要カテゴリに分類されます:

  • 生成パターン(Creational Patterns): オブジェクトの生成に焦点を当て、オブジェクトの生成方法に柔軟性を提供
  • 構造パターン(Structural Patterns): オブジェクト同士の関係を扱い、より大きな構造を構築するための設計図のような役割
  • 行動パターン(Behavioral Patterns): オブジェクト間のコミュニケーションと、相互作用および責任の分散を処理

生成パターン(Creational Patterns)

1. Singletonパターン

Singletonパターンは、クラスが単一のインスタンスのみを持ち、そのインスタンスへのグローバルアクセスを提供することを保証します。アプリケーションのログシステムのようなものと考えてください。アプリ全体でエラーが発生した際、一貫したフォーマットで全てを処理する中央ログガーが一つあることを望みます。

Singletonなし:

javascript
const logger1 = new Logger();
const logger2 = new Logger(); // 複数のログガーが混乱を生む

Singletonあり:

javascript
class Logger {
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }

    this.logs = [];
    Logger.instance = this;
  }

  static getInstance() {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  log(message) {
    this.logs.push(`${new Date().toISOString()}: ${message}`);
    console.log(message);
  }
}

// 使用例
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true - 同一インスタンス

使用タイミング: データベース接続プールや設定管理など、グローバルにアクセス可能な単一インスタンスが絶対に必要な場合。

利点: 単一インスタンスの保証とグローバルアクセス 欠点: テストが困難で、グローバル変数の美化版になりがち

2. Builderパターン

Builderパターンは、複数のオプションパラメータを持つ複雑なオブジェクトを作成するのに最適です。15個以上のパラメータを持つコンストラクタの代わりに、メソッドを任意の順序でチェーンでき、オプションのものをスキップできます。

Builderなし:

javascript
const request = new HTTPRequest(
  "https://api.example.com", 
  "POST", 
  headers, 
  queryParams, 
  bodyData, 
  timeout, 
  retryLogic
);

Builderあり:

javascript
class RequestBuilder {
  constructor() {
    this.config = {
      url: '',
      method: 'GET',
      headers: {},
      timeout: 5000,
      retries: 3
    };
  }

  url(url) {
    this.config.url = url;
    return this;
  }

  method(method) {
    this.config.method = method;
    return this;
  }

  header(key, value) {
    this.config.headers[key] = value;
    return this;
  }

  timeout(timeout) {
    this.config.timeout = timeout;
    return this;
  }

  retries(retries) {
    this.config.retries = retries;
    return this;
  }

  build() {
    return new HTTPRequest(this.config);
  }
}

class HTTPRequest {
  constructor(config) {
    this.config = config;
  }

  async execute() {
    // 実装はここ
    console.log('設定でリクエストを実行中:', this.config);
  }
}

// 使用例
const request = new RequestBuilder()
  .url("https://api.example.com")
  .method("POST")
  .header("Content-Type", "application/json")
  .timeout(10000)
  .retries(5)
  .build();

使用タイミング: コンストラクタのパラメータが指の数より多い場合、または段階的にオブジェクトを構築する必要がある場合。

3. Factoryパターン

Factoryパターンは、オブジェクト作成ロジックを一元化し、クライアントコードから生成の複雑さを隠します。コードベース全体にnewキーワードを散在させる代わりに、オブジェクト作成をファクトリーに委譲します。

Factoryなし:

javascript
let user;
if (type === "admin") {
  user = new AdminUser();
} else if (type === "moderator") {
  user = new ModeratorUser();
} else {
  user = new RegularUser();
}

Factoryあり:

javascript
class AdminUser {
  constructor(id, name) {
    this.id = id;
    this.name = name;
    this.role = 'admin';
    this.permissions = ['read', 'write', 'delete', 'manage'];
  }
}

class ModeratorUser {
  constructor(id, name) {
    this.id = id;
    this.name = name;
    this.role = 'moderator';
    this.permissions = ['read', 'write', 'moderate'];
  }
}

class RegularUser {
  constructor(id, name) {
    this.id = id;
    this.name = name;
    this.role = 'user';
    this.permissions = ['read'];
  }
}

class UserFactory {
  static createUser(type, id, name) {
    switch (type) {
      case 'admin':
        return new AdminUser(id, name);
      case 'moderator':
        return new ModeratorUser(id, name);
      default:
        return new RegularUser(id, name);
    }
  }
}

// 使用例
const admin = UserFactory.createUser('admin', '1', 'John');
const user = UserFactory.createUser('user', '2', 'Jane');

使用タイミング: 類似オブジェクトの作成でnewキーワードがコードベース全体に散在している場合。

構造パターン(Structural Patterns)

4. Facadeパターン

Facadeパターンは、複雑なサブシステムに対して簡素化されたインターフェースを提供します。ウェブサイトの「今すぐ購入」ボタンをクリックするようなものです。その簡単な操作の背後には、決済処理、在庫確認、配送計算、詐欺検出があります。

Facadeなし:

javascript
const paymentProcessor = new PaymentProcessor();
const inventorySystem = new InventorySystem();
const fraudChecker = new FraudChecker();

if (fraudChecker.isValid(order) && 
    inventorySystem.isAvailable(order.item) && 
    paymentProcessor.processPayment(order.payment)) {
  // 注文処理ロジック
}

Facadeあり:

javascript
class PaymentProcessor {
  processPayment(paymentInfo) {
    console.log('決済処理中...');
    return true; // 成功をシミュレート
  }
}

class InventorySystem {
  isAvailable(item) {
    console.log('在庫確認中...');
    return true; // 在庫ありをシミュレート
  }
}

class FraudChecker {
  isValid(order) {
    console.log('詐欺チェック中...');
    return true; // 有効な注文をシミュレート
  }
}

class OrderFacade {
  constructor() {
    this.paymentProcessor = new PaymentProcessor();
    this.inventorySystem = new InventorySystem();
    this.fraudChecker = new FraudChecker();
  }

  placeOrder(order) {
    console.log('注文を処理中...');

    if (!this.fraudChecker.isValid(order)) {
      throw new Error('注文が詐欺チェックに失敗しました');
    }

    if (!this.inventorySystem.isAvailable(order.item)) {
      throw new Error('商品が在庫切れです');
    }

    if (!this.paymentProcessor.processPayment(order.payment)) {
      throw new Error('決済に失敗しました');
    }

    console.log('注文が正常に処理されました!');
    return true;
  }
}

// 使用例
const orderFacade = new OrderFacade();
const order = {
  item: 'laptop',
  payment: { amount: 1000, method: 'credit' }
};
orderFacade.placeOrder(order);

使用タイミング: 簡素化が必要な複雑なサブシステムがある場合。本質的にはカプセル化の洗練された表現です。

5. Adapterパターン

Adapterパターンは、互換性のないインターフェースが連携できるようにします。ラップトップをテレビに接続するためのUSB-HDMIアダプターのように、このパターンは異なるAPIやライブラリ間の橋渡しをします。

Adapterなし:

javascript
const thirdPartyWeatherAPI = new ThirdPartyWeatherAPI();
const tempC = thirdPartyWeatherAPI.getTempC();
const tempF = (tempC * 9/5) + 32; // 変換ロジックがあちこちに

Adapterあり:

javascript
// 摂氏とkm/hでデータを返すサードパーティAPI
class ThirdPartyWeatherAPI {
  getTempC() {
    return 25; // 摂氏
  }

  getSpeedKmh() {
    return 50; // km/h
  }
}

// 私たちのアプリは華氏とmphを期待
class WeatherAdapter {
  constructor(thirdPartyAPI) {
    this.thirdPartyAPI = thirdPartyAPI;
  }

  getTempF() {
    const tempC = this.thirdPartyAPI.getTempC();
    return (tempC * 9/5) + 32;
  }

  getSpeedMph() {
    const speedKmh = this.thirdPartyAPI.getSpeedKmh();
    return speedKmh * 0.621371;
  }
}

// 使用例
const thirdPartyAPI = new ThirdPartyWeatherAPI();
const weather = new WeatherAdapter(thirdPartyAPI);

if (weather.getTempF() > 75) {
  console.log("暑いです!");
}

if (weather.getSpeedMph() > 10) {
  console.log("風が強いです!");
}

使用タイミング: アプリケーションが期待するインターフェースと一致しないサードパーティライブラリやAPIを統合する場合。

行動パターン(Behavioral Patterns)

6. Strategyパターン

Strategyパターンは、アルゴリズムのファミリーを定義し、それぞれをカプセル化し、相互に交換可能にします。通勤について考えてみてください。同じ目標(職場に行く)を達成するために、異なる戦略(運転、自転車、バス)があります。

Strategyなし:

javascript
class Commuter {
  goToWork(transport) {
    if (transport === 'car') {
      console.log('車を始動、ガソリンチェック、交通渋滞を回避、駐車場を見つける');
    } else if (transport === 'bus') {
      console.log('バス停まで歩く、バスを待つ、運賃を支払う');
    } else if (transport === 'bike') {
      console.log('自転車を取る、タイヤの空気圧チェック、職場までペダルを漕ぐ');
    }
  }
}

Strategyあり:

javascript
// Strategy インターフェース(JavaScriptでは暗黙的)
class TransportStrategy {
  travel() {
    throw new Error('travel()メソッドを実装する必要があります');
  }
}

class CarStrategy extends TransportStrategy {
  travel() {
    console.log('車を始動、ガソリンチェック、交通渋滞を回避、駐車場を見つける');
  }
}

class BusStrategy extends TransportStrategy {
  travel() {
    console.log('バス停まで歩く、バスを待つ、運賃を支払う');
  }
}

class BikeStrategy extends TransportStrategy {
  travel() {
    console.log('自転車を取る、タイヤの空気圧チェック、職場までペダルを漕ぐ');
  }
}

class WalkStrategy extends TransportStrategy {
  travel() {
    console.log('履き心地の良い靴を履いて、職場まで歩く');
  }
}

class Commuter {
  constructor() {
    this.strategy = null;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  goToWork() {
    if (!this.strategy) {
      throw new Error('交通手段の戦略が設定されていません');
    }
    this.strategy.travel();
  }
}

// 使用例
const commuter = new Commuter();

commuter.setStrategy(new CarStrategy());
commuter.goToWork();

commuter.setStrategy(new BikeStrategy());
commuter.goToWork();

commuter.setStrategy(new WalkStrategy());
commuter.goToWork();

使用タイミング: 同じことを行う異なる方法がある場合。オープンクローズ原則に従い、既存のコードを変更することなく新しい戦略を追加できます。

7. Observerパターン

Observerパターンは、他のオブジェクトで発生するイベントにオブジェクトがサブスクライブできるようにします。YouTubeの通知のようなものです。チャンネルを登録してベルを押すと、新しい動画がアップロードされるたびに通知が届きます。

Observerなし:

javascript
class VideoChannel {
  constructor() {
    this.subscribers = [];
  }

  uploadVideo(title) {
    // 各サブスクライバーに手動で通知
    this.subscribers.forEach(user => {
      user.notify(`新しい動画: ${title}`);
    });
  }
}

Observerあり:

javascript
class VideoChannel {
  constructor() {
    this.subscribers = [];
  }

  subscribe(subscriber) {
    this.subscribers.push(subscriber);
  }

  unsubscribe(subscriber) {
    this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
  }

  uploadVideo(title) {
    console.log(`動画をアップロード中: ${title}`);
    this.notifySubscribers(`新しい動画: ${title}`);
  }

  notifySubscribers(message) {
    this.subscribers.forEach(subscriber => {
      subscriber.notify(message);
    });
  }
}

class User {
  constructor(name) {
    this.name = name;
  }

  notify(message) {
    console.log(`${this.name}が通知を受信: ${message}`);
  }
}

// 使用例
const channel = new VideoChannel();
const user1 = new User('田中');
const user2 = new User('佐藤');

channel.subscribe(user1);
channel.subscribe(user2);

channel.uploadVideo('デザインパターンチュートリアル');
// 出力:
// 動画をアップロード中: デザインパターンチュートリアル
// 田中が通知を受信: 新しい動画: デザインパターンチュートリアル
// 佐藤が通知を受信: 新しい動画: デザインパターンチュートリアル

channel.unsubscribe(user1);
channel.uploadVideo('上級JavaScript');
// 出力:
// 動画をアップロード中: 上級JavaScript
// 佐藤が通知を受信: 新しい動画: 上級JavaScript

EventEmitterを使用した代替案(Node.js):

javascript
const EventEmitter = require('events');

class VideoChannel extends EventEmitter {
  uploadVideo(title) {
    console.log(`動画をアップロード中: ${title}`);
    this.emit('videoUploaded', title);
  }
}

// 使用例
const channel = new VideoChannel();

channel.on('videoUploaded', (title) => {
  console.log(`田中が通知を受信: 新しい動画: ${title}`);
});

channel.on('videoUploaded', (title) => {
  console.log(`佐藤が通知を受信: 新しい動画: ${title}`);
});

channel.uploadVideo('デザインパターンチュートリアル');

使用タイミング: 「これが起こったら、あれをする」機能を実装する必要がある場合。イベント駆動システム、監視、状態変更通知に最適です。

結論

これら7つのデザインパターンは、すべての開発者のツールキットにおける基本的なツールです。優れたツールと同様に、成功の鍵は各パターンをいつ使用するかを知ることです:

  • Singleton:グローバルにアクセス可能な単一インスタンスが必要な場合
  • Builder:多くのオプションパラメータを持つ複雑なオブジェクト構築の場合
  • Factory:オブジェクト作成ロジックを一元化する場合
  • Facade:複雑なサブシステムを簡素化する場合
  • Adapter:互換性のないインターフェースを連携させる場合
  • Strategy:同じことを行う異なる方法がある場合
  • Observer:イベント駆動プログラミングと通知の場合

これらのパターンは、ソフトウェア開発で繰り返し現れる一般的な問題を解決するために存在することを忘れないでください。これらをマスターすれば、将来の自分とチームメイトに感謝されるような、より清潔で保守性が高く、柔軟なコードを書くことができるようになります。

鍵は実践です。既存のコードでこれらのパターンを特定し、実証済みのソリューションを使用してリファクタリングする機会を探してください。その結果、コードはより読みやすく、柔軟で、保守しやすくなるでしょう。

おすすめ記事