JavaScriptで学ぶデザインパターンの実装方法

JavaScriptは、フロントエンドとバックエンドの両方で広く使用されるプログラミング言語です。特に、大規模なアプリケーションを開発する際には、コードの再利用性やメンテナンス性を向上させるためにデザインパターンの知識が不可欠です。デザインパターンは、ソフトウェア設計における一般的な問題を解決するための再利用可能なソリューションです。本記事では、JavaScriptで実装可能な主要なデザインパターンについて、その概要と具体的な実装方法を順を追って解説します。これにより、実際の開発現場でデザインパターンを効果的に活用できるようになります。

目次

シングルトンパターンの概要と実装

シングルトンパターンは、クラスのインスタンスが常に一つであることを保証するデザインパターンです。これにより、インスタンスが複数生成されることを防ぎ、リソースの無駄遣いや意図しない動作を避けることができます。

シングルトンパターンの概念

シングルトンパターンは、特定のクラスが1つのインスタンスしか持たないことを保証します。例えば、設定ファイルの読み込みやログ記録など、アプリケーション全体で共通のリソースを管理する場合に役立ちます。

シングルトンパターンのJavaScriptでの実装例

JavaScriptでは、シングルトンパターンを実装するために、クラスやオブジェクトリテラルを使用することができます。以下に、JavaScriptでのシングルトンパターンの基本的な実装例を示します。

class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    this.data = [];
    Singleton.instance = this;
    return this;
  }

  addData(item) {
    this.data.push(item);
  }

  getData() {
    return this.data;
  }
}

// 使用例
const instance1 = new Singleton();
const instance2 = new Singleton();

instance1.addData('item1');
console.log(instance2.getData()); // ['item1']
console.log(instance1 === instance2); // true

この実装では、Singletonクラスのコンストラクタ内でインスタンスの存在をチェックし、すでにインスタンスが存在する場合はそのインスタンスを返します。これにより、Singletonクラスのインスタンスは常に1つだけであることが保証されます。

シングルトンパターンを使用することで、グローバルな状態管理やリソースの共有が容易になり、コードの一貫性と効率性が向上します。

ファクトリーパターンの概要と実装

ファクトリーパターンは、オブジェクトの生成を専門とするデザインパターンです。このパターンでは、インスタンス化の詳細を隠蔽し、特定の条件に基づいて適切なクラスのインスタンスを生成します。これにより、コードの柔軟性と拡張性が向上します。

ファクトリーパターンの概念

ファクトリーパターンは、オブジェクトの生成を専門のファクトリーメソッドに委ねることで、クラスのインスタンス化に関する複雑なロジックを簡素化します。例えば、複数のサブクラスが存在し、生成する具体的なクラスを実行時に決定する必要がある場合に有効です。

ファクトリーパターンのJavaScriptでの実装例

JavaScriptでは、ファクトリーパターンを使用して、条件に応じて異なるクラスのインスタンスを生成することができます。以下に、基本的なファクトリーパターンの実装例を示します。

class Dog {
  speak() {
    console.log('Woof!');
  }
}

class Cat {
  speak() {
    console.log('Meow!');
  }
}

class AnimalFactory {
  static createAnimal(type) {
    switch (type) {
      case 'dog':
        return new Dog();
      case 'cat':
        return new Cat();
      default:
        throw new Error('Unknown animal type');
    }
  }
}

// 使用例
const dog = AnimalFactory.createAnimal('dog');
dog.speak(); // Woof!

const cat = AnimalFactory.createAnimal('cat');
cat.speak(); // Meow!

この実装では、AnimalFactoryクラスのcreateAnimalメソッドが動物の種類に応じてDogまたはCatのインスタンスを生成します。このように、ファクトリーパターンを使用することで、オブジェクトの生成を効率的かつ柔軟に管理できます。

ファクトリーパターンは、アプリケーションの要件が変更されてもコードの変更を最小限に抑えることができるため、拡張性の高いシステム設計に非常に有用です。

オブザーバーパターンの概要と実装

オブザーバーパターンは、一つのオブジェクトの状態が変化したときに、それに依存する他のオブジェクトに通知を送るデザインパターンです。これにより、オブジェクト間の疎結合を保ちつつ、効率的なデータの同期を実現します。

オブザーバーパターンの概念

オブザーバーパターンでは、オブザーバー(観察者)とサブジェクト(被観察者)の2つの役割があります。サブジェクトは状態の変化を管理し、オブザーバーにその変化を通知します。これにより、オブザーバーはサブジェクトの状態に応じて自動的に更新されます。

オブザーバーパターンのJavaScriptでの実装例

JavaScriptでは、オブザーバーパターンを使って、イベントリスナーやリアクティブプログラミングの基盤を構築することができます。以下に、基本的なオブザーバーパターンの実装例を示します。

class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notifyObservers(message) {
    this.observers.forEach(observer => observer.update(message));
  }
}

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

  update(message) {
    console.log(`${this.name} received message: ${message}`);
  }
}

// 使用例
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notifyObservers('Hello, observers!');
// Observer 1 received message: Hello, observers!
// Observer 2 received message: Hello, observers!

この実装では、Subjectクラスがオブザーバーを管理し、状態の変化を通知します。Observerクラスは、通知を受け取って適切なアクションを実行します。これにより、サブジェクトとオブザーバーの間に疎結合を保ちながら、効率的な通知システムを構築できます。

オブザーバーパターンは、リアルタイムデータの同期やイベント駆動型プログラミングに非常に有用で、アプリケーションの反応性と拡張性を高めます。

モジュールパターンの概要と実装

モジュールパターンは、コードを整理し、グローバルスコープの汚染を防ぐために使用されるデザインパターンです。このパターンにより、プライベートなデータやメソッドを作成し、公開されたAPIを通じてのみアクセスできるようにします。

モジュールパターンの概念

モジュールパターンは、即時関数表現(IIFE)を使用してプライベートなスコープを作り出し、その中で定義された変数や関数を外部から隠蔽します。そして、必要な部分だけを公開します。これにより、コードの可読性と保守性が向上します。

モジュールパターンのJavaScriptでの実装例

JavaScriptでのモジュールパターンの基本的な実装例を以下に示します。

const myModule = (function() {
  // プライベート変数と関数
  let privateVariable = 'I am private';
  function privateMethod() {
    console.log(privateVariable);
  }

  return {
    // パブリックメソッド
    publicMethod: function() {
      console.log('I am public');
      privateMethod();
    }
  };
})();

// 使用例
myModule.publicMethod();
// I am public
// I am private

console.log(myModule.privateVariable); // undefined
myModule.privateMethod(); // TypeError: myModule.privateMethod is not a function

この実装では、myModuleオブジェクトが即時関数によって作成され、その内部に定義された変数や関数は外部からアクセスできません。公開されたpublicMethodを通じてのみ、内部のプライベートメソッドにアクセスできます。

ES6モジュールによる実装

ES6以降、JavaScriptはネイティブにモジュールをサポートしています。これにより、よりモダンで簡潔なモジュールパターンの実装が可能です。

// myModule.js
const privateVariable = 'I am private';
function privateMethod() {
  console.log(privateVariable);
}

export function publicMethod() {
  console.log('I am public');
  privateMethod();
}
// main.js
import { publicMethod } from './myModule.js';

publicMethod();
// I am public
// I am private

この方法では、exportキーワードを使用して必要な部分だけを公開し、他の部分はモジュール内に隠蔽されます。ES6モジュールはネイティブなサポートを提供し、依存関係の管理やコードの再利用性を大幅に向上させます。

モジュールパターンを使用することで、コードベースをより整理され、保守しやすい状態に保つことができ、大規模なアプリケーション開発において特に有効です。

プロトタイプパターンの概要と実装

プロトタイプパターンは、既存のオブジェクトを元にして新しいオブジェクトを作成するデザインパターンです。このパターンを使うことで、クラスベースの継承を避け、オブジェクトの複製を効率的に行うことができます。

プロトタイプパターンの概念

プロトタイプパターンは、あるオブジェクトをプロトタイプとして、そのプロトタイプを元に新しいオブジェクトを生成する手法です。これにより、オブジェクトの作成コストを低減し、柔軟なオブジェクト設計が可能になります。JavaScriptでは、全てのオブジェクトが他のオブジェクトからプロパティを継承するため、このパターンに特に適しています。

プロトタイプパターンのJavaScriptでの実装例

JavaScriptでは、プロトタイプチェーンを利用してプロトタイプパターンを実装できます。以下に、基本的なプロトタイプパターンの実装例を示します。

const vehiclePrototype = {
  init: function(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
  },
  getDetails: function() {
    return `${this.year} ${this.make} ${this.model}`;
  }
};

function createVehicle(make, model, year) {
  const vehicle = Object.create(vehiclePrototype);
  vehicle.init(make, model, year);
  return vehicle;
}

// 使用例
const car1 = createVehicle('Toyota', 'Corolla', 2021);
const car2 = createVehicle('Honda', 'Civic', 2022);

console.log(car1.getDetails()); // 2021 Toyota Corolla
console.log(car2.getDetails()); // 2022 Honda Civic

この実装では、vehiclePrototypeオブジェクトがプロトタイプとして使用され、新しい車のオブジェクトがObject.createメソッドを使用して作成されます。各車オブジェクトはプロトタイプチェーンを通じてvehiclePrototypeのメソッドを共有します。

プロトタイプベースの継承

JavaScriptでは、プロトタイプベースの継承を使用してオブジェクトのプロパティやメソッドを共有できます。以下に、プロトタイプベースの継承の例を示します。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

function Employee(name, age, jobTitle) {
  Person.call(this, name, age);
  this.jobTitle = jobTitle;
}

Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;

Employee.prototype.work = function() {
  console.log(`${this.name} is working as a ${this.jobTitle}`);
};

// 使用例
const emp1 = new Employee('John', 30, 'Software Developer');
emp1.greet(); // Hello, my name is John
emp1.work(); // John is working as a Software Developer

この例では、PersonクラスとEmployeeクラスがあり、EmployeeクラスはPersonクラスを継承しています。Employeeクラスのインスタンスは、Personクラスのメソッドを利用できます。

プロトタイプパターンは、メモリ効率を高め、コードの再利用性を向上させるために非常に有用です。このパターンを理解し、適切に活用することで、JavaScriptでの効果的なオブジェクト指向プログラミングが可能になります。

デコレーターパターンの概要と実装

デコレーターパターンは、既存のオブジェクトに対して動的に機能を追加するためのデザインパターンです。このパターンを使用することで、元のオブジェクトのコードを変更せずに、柔軟に新しい機能を追加することができます。

デコレーターパターンの概念

デコレーターパターンは、基本的なオブジェクトに対して追加の責任や機能を持つデコレーターオブジェクトを作成し、それらを組み合わせることで機能を拡張します。これは、継承を使うよりも柔軟で、特定の機能を必要に応じて付加することができます。

デコレーターパターンのJavaScriptでの実装例

JavaScriptでは、関数やクラスにデコレータを適用することでこのパターンを実現できます。以下に、関数デコレータの基本的な実装例を示します。

function addLogging(originalFunction) {
  return function(...args) {
    console.log(`Arguments: ${args}`);
    const result = originalFunction.apply(this, args);
    console.log(`Result: ${result}`);
    return result;
  };
}

// 使用例
function add(a, b) {
  return a + b;
}

const addWithLogging = addLogging(add);
addWithLogging(2, 3);
// Arguments: 2,3
// Result: 5

この例では、addLogging関数がデコレータとして機能し、add関数に引数と結果のログを追加します。これにより、元のadd関数のコードを変更せずに、ログ機能を付加できます。

クラスデコレータの実装

ES7以降、JavaScriptはクラスデコレータをサポートしています。以下に、クラスデコレータの実装例を示します。

function readonly(target, key, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

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

  @readonly
  getName() {
    return this.name;
  }
}

// 使用例
const person = new Person('Alice');
console.log(person.getName()); // Alice

person.getName = function() {
  return 'Bob';
}; // TypeError: Cannot assign to read only property 'getName' of object '#<Person>'

この例では、readonlyデコレータがgetNameメソッドに適用され、そのメソッドを読み取り専用にします。これにより、メソッドのオーバーライドを防ぎます。

デコレーターパターンを使用することで、コードの再利用性が向上し、新しい機能を柔軟に追加することができます。特に、大規模なアプリケーション開発において、機能の拡張や修正が容易になり、メンテナンス性が向上します。

状態パターンの概要と実装

状態パターンは、オブジェクトの内部状態によってその振る舞いを変更するデザインパターンです。このパターンを使うことで、オブジェクトが状態に応じて異なる動作をすることができ、状態遷移の管理が容易になります。

状態パターンの概念

状態パターンでは、オブジェクトの状態をクラスとして定義し、それぞれの状態に応じた振る舞いを実装します。状態遷移を管理するためのコンテキストクラスがあり、そのコンテキストが現在の状態を持ち、状態の変更に応じて適切な振る舞いを呼び出します。

状態パターンのJavaScriptでの実装例

JavaScriptでの状態パターンの基本的な実装例を以下に示します。

class OrderContext {
  constructor() {
    this.state = new NewOrderState(this);
  }

  setState(state) {
    this.state = state;
  }

  processOrder() {
    this.state.processOrder();
  }

  cancelOrder() {
    this.state.cancelOrder();
  }
}

class NewOrderState {
  constructor(order) {
    this.order = order;
  }

  processOrder() {
    console.log('Processing new order...');
    this.order.setState(new ShippedOrderState(this.order));
  }

  cancelOrder() {
    console.log('Cancelling new order...');
    this.order.setState(new CancelledOrderState(this.order));
  }
}

class ShippedOrderState {
  constructor(order) {
    this.order = order;
  }

  processOrder() {
    console.log('Order already shipped.');
  }

  cancelOrder() {
    console.log('Cannot cancel, order already shipped.');
  }
}

class CancelledOrderState {
  constructor(order) {
    this.order = order;
  }

  processOrder() {
    console.log('Cannot process, order is cancelled.');
  }

  cancelOrder() {
    console.log('Order already cancelled.');
  }
}

// 使用例
const order = new OrderContext();
order.processOrder(); // Processing new order...
order.cancelOrder(); // Cannot cancel, order already shipped.
order.processOrder(); // Order already shipped.

この実装では、OrderContextクラスが現在の状態を管理し、状態に応じた振る舞いをNewOrderStateShippedOrderStateCancelledOrderStateクラスに委譲します。状態の変更は、setStateメソッドを使って行われます。

状態パターンの応用例

状態パターンは、UIコンポーネントの状態管理やワークフロー管理など、さまざまなシナリオで活用できます。以下は、トラフィックライトシステムの例です。

class TrafficLightContext {
  constructor() {
    this.state = new RedLightState(this);
  }

  setState(state) {
    this.state = state;
  }

  changeLight() {
    this.state.changeLight();
  }
}

class RedLightState {
  constructor(light) {
    this.light = light;
  }

  changeLight() {
    console.log('Changing from Red to Green.');
    this.light.setState(new GreenLightState(this.light));
  }
}

class GreenLightState {
  constructor(light) {
    this.light = light;
  }

  changeLight() {
    console.log('Changing from Green to Yellow.');
    this.light.setState(new YellowLightState(this.light));
  }
}

class YellowLightState {
  constructor(light) {
    this.light = light;
  }

  changeLight() {
    console.log('Changing from Yellow to Red.');
    this.light.setState(new RedLightState(this.light));
  }
}

// 使用例
const trafficLight = new TrafficLightContext();
trafficLight.changeLight(); // Changing from Red to Green.
trafficLight.changeLight(); // Changing from Green to Yellow.
trafficLight.changeLight(); // Changing from Yellow to Red.

この例では、TrafficLightContextクラスがトラフィックライトの状態を管理し、RedLightStateGreenLightStateYellowLightStateクラスが各状態の振る舞いを定義しています。

状態パターンを使用することで、状態遷移の管理がシンプルになり、複雑な条件分岐を避けることができます。これにより、コードの可読性と保守性が向上し、システム全体の設計がより直感的になります。

コマンドパターンの概要と実装

コマンドパターンは、操作や要求をオブジェクトとしてカプセル化し、操作の実行者と操作の受け手を分離するデザインパターンです。これにより、操作の履歴管理や操作の取り消し、再実行が容易になります。

コマンドパターンの概念

コマンドパターンでは、コマンドオブジェクトが操作や要求をカプセル化します。これにより、クライアントは具体的な操作を知らなくても、コマンドオブジェクトを使って操作を実行できます。具体的な実行は、コマンドオブジェクトの内部で行われます。

コマンドパターンのJavaScriptでの実装例

JavaScriptでのコマンドパターンの基本的な実装例を以下に示します。

class Command {
  execute() {}
  undo() {}
}

class Light {
  constructor() {
    this.isOn = false;
  }

  turnOn() {
    this.isOn = true;
    console.log('The light is on');
  }

  turnOff() {
    this.isOn = false;
    console.log('The light is off');
  }
}

class LightOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOn();
  }

  undo() {
    this.light.turnOff();
  }
}

class LightOffCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOff();
  }

  undo() {
    this.light.turnOn();
  }
}

class RemoteControl {
  constructor() {
    this.history = [];
  }

  executeCommand(command) {
    command.execute();
    this.history.push(command);
  }

  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
    }
  }
}

// 使用例
const light = new Light();
const lightOn = new LightOnCommand(light);
const lightOff = new LightOffCommand(light);
const remote = new RemoteControl();

remote.executeCommand(lightOn); // The light is on
remote.executeCommand(lightOff); // The light is off
remote.undo(); // The light is on
remote.undo(); // The light is off

この実装では、LightOnCommandLightOffCommandCommandクラスを継承し、具体的な操作を実装しています。RemoteControlクラスがコマンドを実行し、操作の履歴を管理しています。

コマンドパターンの応用例

コマンドパターンは、GUIアプリケーションのボタン操作やゲームのアクション、バッチ処理のスケジューリングなど、さまざまなシナリオで活用できます。以下は、テキストエディタの操作を管理する例です。

class TextEditor {
  constructor() {
    this.text = '';
  }

  write(text) {
    this.text += text;
    console.log(`Current text: ${this.text}`);
  }

  undo() {
    this.text = this.text.slice(0, -1);
    console.log(`Current text after undo: ${this.text}`);
  }
}

class WriteCommand extends Command {
  constructor(editor, text) {
    super();
    this.editor = editor;
    this.text = text;
  }

  execute() {
    this.editor.write(this.text);
  }

  undo() {
    this.editor.undo();
  }
}

// 使用例
const editor = new TextEditor();
const writeCommand = new WriteCommand(editor, 'Hello');
const remote = new RemoteControl();

remote.executeCommand(writeCommand); // Current text: Hello
remote.undo(); // Current text after undo: Hell

この例では、TextEditorクラスがテキスト操作を管理し、WriteCommandクラスがその操作をカプセル化しています。RemoteControlクラスがコマンドの実行と取り消しを管理しています。

コマンドパターンを使用することで、操作の抽象化と管理が容易になり、コードの柔軟性と拡張性が向上します。特に、複雑な操作のシナリオでその真価を発揮します。

応用例:Todoアプリの設計と実装

ここでは、前述のデザインパターンを活用して、シンプルなTodoアプリを設計・実装します。このアプリは、タスクの追加、削除、完了ステータスの変更、履歴管理をサポートします。

設計概要

このTodoアプリでは、以下のデザインパターンを使用します。

  • シングルトンパターン:アプリケーションの唯一のインスタンスを管理します。
  • ファクトリーパターン:新しいタスクオブジェクトを生成します。
  • オブザーバーパターン:タスクの状態変更を監視してUIを更新します。
  • モジュールパターン:コードの整理とスコープ管理を行います。
  • プロトタイプパターン:タスクオブジェクトの基本的なプロパティとメソッドを定義します。
  • デコレーターパターン:タスクに追加機能を動的に追加します。
  • コマンドパターン:タスクの操作をカプセル化し、履歴管理と取り消しをサポートします。

シングルトンパターン:アプリケーションの唯一のインスタンス

class TodoApp {
  constructor() {
    if (TodoApp.instance) {
      return TodoApp.instance;
    }
    this.tasks = [];
    this.observers = [];
    TodoApp.instance = this;
    return this;
  }

  addTask(task) {
    this.tasks.push(task);
    this.notifyObservers();
  }

  removeTask(taskId) {
    this.tasks = this.tasks.filter(task => task.id !== taskId);
    this.notifyObservers();
  }

  toggleTaskCompletion(taskId) {
    const task = this.tasks.find(task => task.id === taskId);
    if (task) {
      task.completed = !task.completed;
      this.notifyObservers();
    }
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  notifyObservers() {
    this.observers.forEach(observer => observer.update());
  }
}

const app = new TodoApp();

ファクトリーパターン:タスクオブジェクトの生成

class TaskFactory {
  static createTask(description) {
    return {
      id: Date.now().toString(),
      description,
      completed: false
    };
  }
}

// 使用例
const task1 = TaskFactory.createTask('Learn JavaScript');
app.addTask(task1);

オブザーバーパターン:UIの更新

class TaskListView {
  constructor(app) {
    this.app = app;
    this.app.addObserver(this);
  }

  update() {
    this.render();
  }

  render() {
    const taskList = document.getElementById('taskList');
    taskList.innerHTML = '';
    this.app.tasks.forEach(task => {
      const taskItem = document.createElement('li');
      taskItem.textContent = task.description;
      if (task.completed) {
        taskItem.style.textDecoration = 'line-through';
      }
      taskList.appendChild(taskItem);
    });
  }
}

const taskListView = new TaskListView(app);

モジュールパターン:タスク管理機能

const TaskModule = (function() {
  function addTask(description) {
    const task = TaskFactory.createTask(description);
    app.addTask(task);
  }

  function removeTask(taskId) {
    app.removeTask(taskId);
  }

  function toggleTaskCompletion(taskId) {
    app.toggleTaskCompletion(taskId);
  }

  return {
    addTask,
    removeTask,
    toggleTaskCompletion
  };
})();

// 使用例
TaskModule.addTask('Read a book');
TaskModule.toggleTaskCompletion(task1.id);

プロトタイプパターン:タスクオブジェクト

const taskPrototype = {
  init(id, description) {
    this.id = id;
    this.description = description;
    this.completed = false;
  },
  toggleCompletion() {
    this.completed = !this.completed;
  }
};

// 使用例
const newTask = Object.create(taskPrototype);
newTask.init(Date.now().toString(), 'Go for a run');
app.addTask(newTask);

デコレーターパターン:タスクへの追加機能

function withPriority(task, priority) {
  task.priority = priority;
  task.showPriority = function() {
    console.log(`Priority: ${this.priority}`);
  };
  return task;
}

// 使用例
const prioritizedTask = withPriority(newTask, 'High');
prioritizedTask.showPriority(); // Priority: High

コマンドパターン:タスク操作のカプセル化

class Command {
  execute() {}
  undo() {}
}

class AddTaskCommand extends Command {
  constructor(task) {
    super();
    this.task = task;
  }

  execute() {
    app.addTask(this.task);
  }

  undo() {
    app.removeTask(this.task.id);
  }
}

class RemoveTaskCommand extends Command {
  constructor(taskId) {
    super();
    this.taskId = taskId;
  }

  execute() {
    app.removeTask(this.taskId);
  }

  undo() {
    const task = TaskFactory.createTask(this.taskId);
    app.addTask(task);
  }
}

class CommandManager {
  constructor() {
    this.history = [];
  }

  executeCommand(command) {
    command.execute();
    this.history.push(command);
  }

  undo() {
    const command = this.history.pop();
    if (command) {
      command.undo();
    }
  }
}

// 使用例
const commandManager = new CommandManager();
const task2 = TaskFactory.createTask('Write a blog post');
const addTaskCommand = new AddTaskCommand(task2);

commandManager.executeCommand(addTaskCommand); // Task added
commandManager.undo(); // Task removed

この設計により、各デザインパターンを効果的に活用して、柔軟で拡張性の高いTodoアプリを構築できます。それぞれのパターンが持つ利点を最大限に活用し、システムの保守性と可読性を高めることができます。

演習問題と解答例

ここでは、前述のデザインパターンを理解し、実装力を向上させるための演習問題を提供します。それぞれの問題に対する解答例も示しますので、確認しながら取り組んでください。

演習問題 1: シングルトンパターンの拡張

問題: TodoAppクラスに、新しいメソッド getTaskById を追加してください。このメソッドは、指定されたIDのタスクを返します。

解答例:

class TodoApp {
  constructor() {
    if (TodoApp.instance) {
      return TodoApp.instance;
    }
    this.tasks = [];
    this.observers = [];
    TodoApp.instance = this;
    return this;
  }

  addTask(task) {
    this.tasks.push(task);
    this.notifyObservers();
  }

  removeTask(taskId) {
    this.tasks = this.tasks.filter(task => task.id !== taskId);
    this.notifyObservers();
  }

  toggleTaskCompletion(taskId) {
    const task = this.tasks.find(task => task.id === taskId);
    if (task) {
      task.completed = !task.completed;
      this.notifyObservers();
    }
  }

  getTaskById(taskId) {
    return this.tasks.find(task => task.id === taskId);
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  notifyObservers() {
    this.observers.forEach(observer => observer.update());
  }
}

const app = new TodoApp();

演習問題 2: ファクトリーパターンの拡張

問題: TaskFactoryに、優先度付きタスクを作成する createPriorityTask メソッドを追加してください。このメソッドは、優先度を持つタスクを返します。

解答例:

class TaskFactory {
  static createTask(description) {
    return {
      id: Date.now().toString(),
      description,
      completed: false
    };
  }

  static createPriorityTask(description, priority) {
    return {
      id: Date.now().toString(),
      description,
      completed: false,
      priority
    };
  }
}

// 使用例
const priorityTask = TaskFactory.createPriorityTask('Finish project', 'High');
app.addTask(priorityTask);

演習問題 3: オブザーバーパターンの応用

問題: 新しいオブザーバー TaskCountView を作成し、タスクの数を表示するようにしてください。

解答例:

class TaskCountView {
  constructor(app) {
    this.app = app;
    this.app.addObserver(this);
  }

  update() {
    this.render();
  }

  render() {
    const taskCountElement = document.getElementById('taskCount');
    taskCountElement.textContent = `Task Count: ${this.app.tasks.length}`;
  }
}

const taskCountView = new TaskCountView(app);

演習問題 4: コマンドパターンの応用

問題: タスクの説明を更新する UpdateTaskCommand クラスを作成してください。

解答例:

class UpdateTaskCommand extends Command {
  constructor(taskId, newDescription) {
    super();
    this.taskId = taskId;
    this.newDescription = newDescription;
    this.previousDescription = null;
  }

  execute() {
    const task = app.getTaskById(this.taskId);
    if (task) {
      this.previousDescription = task.description;
      task.description = this.newDescription;
      app.notifyObservers();
    }
  }

  undo() {
    const task = app.getTaskById(this.taskId);
    if (task) {
      task.description = this.previousDescription;
      app.notifyObservers();
    }
  }
}

// 使用例
const updateTaskCommand = new UpdateTaskCommand(priorityTask.id, 'Complete project with tests');
commandManager.executeCommand(updateTaskCommand); // Task description updated
commandManager.undo(); // Task description reverted

演習問題 5: デコレーターパターンの応用

問題: タスクに期限を追加するデコレータ withDeadline を作成してください。

解答例:

function withDeadline(task, deadline) {
  task.deadline = deadline;
  task.showDeadline = function() {
    console.log(`Deadline: ${this.deadline}`);
  };
  return task;
}

// 使用例
const taskWithDeadline = withDeadline(priorityTask, '2024-08-31');
taskWithDeadline.showDeadline(); // Deadline: 2024-08-31

これらの演習問題を通じて、各デザインパターンの理解と実装スキルを深めることができます。各パターンの特徴と使用方法を把握し、実際のプロジェクトで応用できるように練習してください。

まとめ

本記事では、JavaScriptで実装可能な主要なデザインパターンについて、具体的な例と共に詳しく解説しました。シングルトンパターン、ファクトリーパターン、オブザーバーパターン、モジュールパターン、プロトタイプパターン、デコレーターパターン、コマンドパターンのそれぞれが、どのような問題を解決し、どのように実装されるのかを理解していただけたと思います。

これらのデザインパターンを活用することで、コードの再利用性、可読性、保守性を向上させることができます。また、実際のアプリケーションの設計・実装において、適切なパターンを選択し適用することで、より堅牢で柔軟なシステムを構築することが可能です。

演習問題を通じて実装力を高め、各パターンの具体的な使用方法をマスターすることで、日常の開発に役立ててください。デザインパターンを理解し、効果的に活用することで、より効率的で拡張性の高いコードを書くことができるようになります。

コメント

コメントする

目次