【TypeScript】ストラテジーパターンの型定義 - 柔軟で型安全なアルゴリズム切り替え

【TypeScript】ストラテジーパターンの型定義 - 柔軟で型安全なアルゴリズム切り替え

2024-11-10

2024-11-10

ストラテジーパターンとTypeScriptの型安全な実装

ストラテジーパターンは、異なるアルゴリズムや動作をオブジェクトとして分離し、必要に応じて動的に切り替えることができるデザインパターンです。TypeScriptでストラテジーパターンを実装するとき、各アルゴリズムのインターフェースや型を統一することで、柔軟で型安全な設計が可能になります。この記事では、TypeScriptを使ったストラテジーパターンの実装方法とその型定義について詳しく解説します。

ストラテジーパターンの基本構造

ストラテジーパターンは、次の3つの要素で構成されています:

  1. Strategy(アルゴリズムのインターフェース): 実行されるアルゴリズムの共通インターフェースを定義します。
  2. ConcreteStrategy(具体的なアルゴリズムの実装): Strategyインターフェースを実装し、異なるアルゴリズムを提供します。
  3. Context(実行環境): Strategyのインスタンスを保持し、動作を委譲する役割を担います。 この仕組みにより、Contextは異なるStrategyに切り替えることで、動作を動的に変更できます。

TypeScriptでの基本的なストラテジーパターン実装

まずは、簡単なストラテジーパターンをTypeScriptで実装してみます。ここでは、テキストの変換を行うアルゴリズムをStrategyとして設計します。

// Strategyインターフェース
interface TextFormatter {
  format(text: string): string;
}
// ConcreteStrategy: 大文字変換
class UpperCaseFormatter implements TextFormatter {
  format(text: string): string {
    return text.toUpperCase();
  }
}
// ConcreteStrategy: 小文字変換
class LowerCaseFormatter implements TextFormatter {
  format(text: string): string {
    return text.toLowerCase();
  }
}
// Contextクラス
class TextEditor {
  private formatter: TextFormatter;
  constructor(formatter: TextFormatter) {
    this.formatter = formatter;
  }
  setFormatter(formatter: TextFormatter): void {
    this.formatter = formatter;
  }
  publishText(text: string): string {
    return this.formatter.format(text);
  }
}

この例では、TextFormatterインターフェースが異なるフォーマット方式の共通インターフェースとなり、UpperCaseFormatterLowerCaseFormatterが実際のフォーマットアルゴリズムを定義しています。TextEditorクラスがContextとしてTextFormatterを保持し、フォーマッタを切り替えることで異なるアルゴリズムを使用できます。

使用例

const editor = new TextEditor(new UpperCaseFormatter());
console.log(editor.publishText("Hello, World!")); // 出力: HELLO, WORLD!
editor.setFormatter(new LowerCaseFormatter());
console.log(editor.publishText("Hello, World!")); // 出力: hello, world!

このコードにより、TextEditorクラスはフォーマッタの切り替えが可能となり、動的にアルゴリズムを変更できるようになります。

型安全なストラテジーパターンの実装

TypeScriptでは、ジェネリクスを使用してストラテジーパターンをさらに型安全にすることが可能です。以下は、ジェネリクスを使った型安全な実装の例です。

型安全なストラテジーパターンの設計

ここでは、数値の計算に関するストラテジーパターンを実装します。異なる計算方法(加算、乗算など)をStrategyとして定義し、それぞれのメソッドに適切な型を付与します。

// Strategyインターフェース
interface CalculationStrategy<T> {
  calculate(a: T, b: T): T;
}
// ConcreteStrategy: 加算
class AdditionStrategy implements CalculationStrategy<number> {
  calculate(a: number, b: number): number {
    return a + b;
  }
}
// ConcreteStrategy: 乗算
class MultiplicationStrategy implements CalculationStrategy<number> {
  calculate(a: number, b: number): number {
    return a * b;
  }
}
// Contextクラス
class Calculator<T> {
  private strategy: CalculationStrategy<T>;
  constructor(strategy: CalculationStrategy<T>) {
    this.strategy = strategy;
  }
  setStrategy(strategy: CalculationStrategy<T>): void {
    this.strategy = strategy;
  }
  execute(a: T, b: T): T {
    return this.strategy.calculate(a, b);
  }
}

この例では、CalculationStrategyインターフェースがジェネリック型<T>を使用しているため、計算のデータ型を指定することが可能です。具体的なAdditionStrategyMultiplicationStrategyはそれぞれnumber型で実装されており、Calculatorクラスは任意の型Tに対して計算が可能です。

使用例

const calculator = new Calculator(new AdditionStrategy());
console.log(calculator.execute(5, 3)); // 出力: 8
calculator.setStrategy(new MultiplicationStrategy());
console.log(calculator.execute(5, 3)); // 出力: 15

ジェネリクスを活用することで、異なる型にも適応可能な型安全なストラテジーパターンが実現され、柔軟性と安全性が向上しています。

型安全なストラテジーパターンの応用

異なる計算方法だけでなく、複数の異なるデータ構造に対応したパターン設計も可能です。例えば、JSON形式とXML形式のデータ出力などに対しても、ストラテジーパターンでデータの変換方法を柔軟に切り替えることができます。

データ出力に対応する例

// Strategyインターフェース
interface DataOutputStrategy {
  output(data: Record<string, any>): string;
}
// ConcreteStrategy: JSON出力
class JsonOutputStrategy implements DataOutputStrategy {
  output(data: Record<string, any>): string {
    return JSON.stringify(data);
  }
}
// ConcreteStrategy: XML出力
class XmlOutputStrategy implements DataOutputStrategy {
  output(data: Record<string, any>): string {
    const xml = Object.entries(data)
      .map(([key, value]) => `<${key}>${value}</${key}>`)
      .join("");
    return `<data>${xml}</data>`;
  }
}
// Contextクラス
class DataExporter {
  private strategy: DataOutputStrategy;
  constructor(strategy: DataOutputStrategy) {
    this.strategy = strategy;
  }
  setStrategy(strategy: DataOutputStrategy): void {
    this.strategy = strategy;
  }
  export(data: Record<string, any>): string {
    return this.strategy.output(data);
  }
}

このコードでは、DataOutputStrategyインターフェースを基にJsonOutputStrategyXmlOutputStrategyを実装し、異なる出力形式に対応しています。DataExporterクラスがContextとして機能し、動的に出力形式を変更可能です。

使用例

const exporter = new DataExporter(new JsonOutputStrategy());
console.log(exporter.export({ name: "Alice", age: 30 })); // 出力: {"name":"Alice","age":30}
exporter.setStrategy(new XmlOutputStrategy());
console.log(exporter.export({ name: "Alice", age: 30 })); // 出力: <data><name>Alice</name><age>30</age></data>

ここでDataExporterの出力形式を動的に切り替えることができ、型安全かつ柔軟に対応できるようになっています。

TypeScriptでストラテジーパターンを用いる利点

  1. 型安全なアルゴリズムの切り替え
    インターフェースとジェネリクスを活用することで、誤ったアルゴリズムやデータ型の利用を防ぐことができます。
  2. コードの再利用性と柔軟性
    アルゴリズムをクラスとして分離しているため、他のプロジェクトやシーンで再利用が容易です。
  3. 拡張性の向上
    新しいアルゴリズムを追加する際に、既存のコードを変更することなくContextで切り替えるだけで利用できるため、拡張が容易です。

まとめ

TypeScriptでストラテジーパターンを実装することで、アルゴリズムを動的に切り替えながらも型安全を確保できます。インターフェースやジェネリクスを使用することで、誤ったデータ型の設定や使用が防止され、保守性と拡張性が向上します。様々な場面での動作切り替えが可能なストラテジーパターンを活用し、柔軟で堅牢なコードベースを構築しましょう。

Recommend