【TypeScript】ストラテジーパターンの型定義 - 柔軟で型安全なアルゴリズム切り替え
2024-11-10
2024-11-10
ストラテジーパターンとTypeScript
の型安全な実装
ストラテジーパターンは、異なるアルゴリズムや動作をオブジェクトとして分離し、必要に応じて動的に切り替えることができるデザインパターンです。TypeScript
でストラテジーパターンを実装するとき、各アルゴリズムのインターフェースや型を統一することで、柔軟で型安全な設計が可能になります。この記事では、TypeScript
を使ったストラテジーパターンの実装方法とその型定義について詳しく解説します。
ストラテジーパターンの基本構造
ストラテジーパターンは、次の3つの要素で構成されています:
- Strategy(アルゴリズムのインターフェース): 実行されるアルゴリズムの共通インターフェースを定義します。
- ConcreteStrategy(具体的なアルゴリズムの実装): Strategyインターフェースを実装し、異なるアルゴリズムを提供します。
- 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
インターフェースが異なるフォーマット方式の共通インターフェースとなり、UpperCaseFormatter
とLowerCaseFormatter
が実際のフォーマットアルゴリズムを定義しています。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>
を使用しているため、計算のデータ型を指定することが可能です。具体的なAdditionStrategy
やMultiplicationStrategy
はそれぞれ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
インターフェースを基にJsonOutputStrategy
とXmlOutputStrategy
を実装し、異なる出力形式に対応しています。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でストラテジーパターンを用いる利点
- 型安全なアルゴリズムの切り替え
インターフェースとジェネリクスを活用することで、誤ったアルゴリズムやデータ型の利用を防ぐことができます。 - コードの再利用性と柔軟性
アルゴリズムをクラスとして分離しているため、他のプロジェクトやシーンで再利用が容易です。 - 拡張性の向上
新しいアルゴリズムを追加する際に、既存のコードを変更することなくContextで切り替えるだけで利用できるため、拡張が容易です。
まとめ
TypeScript
でストラテジーパターンを実装することで、アルゴリズムを動的に切り替えながらも型安全を確保できます。インターフェースやジェネリクスを使用することで、誤ったデータ型の設定や使用が防止され、保守性と拡張性が向上します。様々な場面での動作切り替えが可能なストラテジーパターンを活用し、柔軟で堅牢なコードベースを構築しましょう。