スパゲッティコードからの脱却
動くコードを書くこと自体は簡単です。本当の挑戦は、プロジェクトが1万行を超え、少しの変更がまるでジェンガをしているかのように感じられるようになった時に始まります。TypeScriptを使い始めたばかりの頃、私のファイルはグローバル変数とネストされたif-elseブロックで埋め尽くされた500行のモンスターへと化していました。デザインパターンへの移行は単なるスタイルの選択ではなく、コードベースが成長する中で正気を保つための唯一の方法でした。
デザインパターンは、繰り返し発生するアーキテクチャ上の問題を解決するための、実戦で鍛えられた設計図です. TypeScriptでは、インターフェースと厳格な型付けを使用することで、保存ボタンを押す前にエラーをキャッチでき、これらのパターンはさらに強力になります。ここでは、Singleton、Factory、Strategyという3つの不可欠なパターンを見ていきましょう。
アプローチの比較:手続き型 vs パターン指向
パターンの価値を理解するには、それらが防いでくれる混乱を見るのが一番です。アーキテクチャをレベルアップさせる前に、通常どのようにロジックを処理しているかを確認しましょう。
手続き型による混乱
手続き型のセットアップでは、ロジックは直線的に流れます。データベース接続が必要な場合、それをグローバルにインスタンス化するか、12個の異なる関数のチェーンを通じて渡すことになるかもしれません。3つの異なる支払いタイプをサポートする必要がある場合、コアとなるビジネスロジックの中に巨大な switch 文が出来上がります。これは週末のサイドプロジェクトには向いていますが、複数の貢献者がいるプロジェクトでユニットテストを行ったり拡張したりするのは悪夢です。
パターン指向による解決策
パターンを使用することで、考え方がオブジェクト指向設計へとシフトします。生のステップの順序に集中するのではなく、どのコンポーネントがロジックを所有し、それらがどのように対話するかに注目します。ロジックは本来あるべき場所に隠されます。クライアントから新しい「仮想通貨」決済方法を求められた場合、単に新しいクラスを追加するだけです。既存のテスト済みコードに触れる必要はありません。この分離により、システムはモジュール化され、堅牢になります。
トレードオフ:コードを増やす価値はあるか?
パターンはすべての問題に対する魔法の解決策ではありません。単純なランディングページを、複雑なFactoryの網へとオーバーエンジニアリングしてしまう開発者を何度も見てきました。本番環境で使用する際の現実は以下の通りです。
メリット
- メンテナンスの容易化: 決済処理を壊す心配をせずに、ロギングロジックのバグを修正できます。
- 即座のコンテキスト把握: 新しい開発者が「Factory」フォルダを見たとき、すべての行を読まなくても、そのコードの意図をすぐに理解できます。
- 信頼性の高いテスト: ロジックがカプセル化されているため、焦点を絞ったユニットテストが書けます。グローバルな状態をモック化するよりも, 単一のインターフェースをモック化する方がはるかに簡単です。
コスト
- 冗長性: 最初に書くボイラープレートコードが15〜20%増える可能性があります。
- 学習曲線: チームのジュニア開発者は、なぜ単純な
new MyClass()呼び出しを使わないのかを理解するために、簡単な説明が必要になるかもしれません。
環境のセットアップ
これらのパターンを試すには、基本的なTypeScriptのセットアップだけで十分です。私は通常、整理しやすくするためにパターン名ごとにソースフォルダを分けています。
# クイックセットアップ
mkdir ts-patterns && cd ts-patterns
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init
ファイル構成は以下のようにしてみてください:
src/
├── singleton/
├── factory/
└── strategy/
└── index.ts
1. Singletonパターン:インスタンスの制御
Singletonは、アプリケーションのライフサイクル全体を通じて、クラスのインスタンスが確実に1つだけ存在するようにします。これは、複数のインスタンスがメモリを浪費したり同期の問題を引き起こしたりする可能性がある、データベースプールやグローバル設定の管理に最適です。
実装方法
class DatabaseConnection {
private static instance: DatabaseConnection;
private constructor() {
// プライベートコンストラクタにより、'new DatabaseConnection()' の呼び出しを防ぐ
console.log("一意のデータベースプールを初期化中...");
}
public static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
public query(sql: string) {
console.log(`実行中: ${sql}`);
}
}
// 使用例
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - 両方の変数が同じメモリ空間を指している
コンストラクタをロックすることで、他の開発者が誤って5つの異なるデータベース接続を開いてしまうのを防ぎます。これにより、リソース使用量を予測可能に保ち、接続リークを防ぐことができます。
2. Factoryパターン:オブジェクト生成を柔軟に
Factoryパターンは、作成されるオブジェクトの正確なクラスを指定せずにオブジェクトを作成する方法を提供します。私はこれを、異なる通知サービスやロギングレベルのような統合機能を扱う際によく使用します。
実装方法
interface Logger {
log(message: string): void;
}
class FileLogger implements Logger {
log(message: string) { console.log(`[ファイル] ${message}`); }
}
class ConsoleLogger implements Logger {
log(message: string) { console.log(`[コンソール] ${message}`); }
}
class LoggerFactory {
public static createLogger(type: 'file' | 'console'): Logger {
if (type === 'file') return new FileLogger();
return new ConsoleLogger();
}
}
// 使用例
const logger = LoggerFactory.createLogger('file');
logger.log("ユーザーがサインインしました");
このセットアップにより、生成の「方法」とロジックの「内容」が分離されます。ローカルファイルロガーからAWS S3ロガーに切り替える必要がある場合、Factory内の1行を変更するだけで済みます。アプリの残りの部分は変更不要です。
3. Strategyパターン:ロジックを動的に切り替える
Strategyパターンは、一連のアルゴリズムを定義し、それらを交換可能にします。これは、チェックアウトシステムを処理する際に私が最も気に入っているパターンです。複雑な条件分岐を使わずに、実行時にPayPal、Stripe、またはBitcoin決済を切り替えることができます。
実装方法
interface PaymentStrategy {
pay(amount: number): void;
}
class PaypalStrategy implements PaymentStrategy {
pay(amount: number) { console.log(`PayPal経由で$${amount}を処理中。`); }
}
class CreditCardStrategy implements PaymentStrategy {
pay(amount: number) { console.log(`クレジットカードで$${amount}を決済中。`); }
}
class ShoppingCart {
private strategy: PaymentStrategy;
constructor(strategy: PaymentStrategy) {
this.strategy = strategy;
}
public setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy;
}
public checkout(amount: number) {
this.strategy.pay(amount);
}
}
// 使用例
const cart = new ShoppingCart(new PaypalStrategy());
cart.checkout(49.99);
// ユーザーが気が変わり、カードの使用を希望した場合
cart.setStrategy(new CreditCardStrategy());
cart.checkout(49.99);
ShoppingCart はPayPalのAPIの内部詳細を知る必要はありません。単に pay という契約を信頼するだけです。これにより、10番目の決済方法を追加することも、2番目を追加するのと同じくらい簡単になります。
最後に:ベストプラクティス
パターンはツールであり、ルールではありません。これらをTypeScriptのワークフローに組み込む際は、次の3つのポイントを念頭に置いてください。
- オーバーエンジニアリングを避ける: 単純な10行の関数で問題が解決するなら、そのためにFactoryを作る必要はありません。複雑さが予想される場合や、高いテスト容易性が必要な場合にのみパターンを使用してください。
- インターフェースを活用する: TypeScriptの最大の強みはその型システムです。StrategyやFactoryには常に明確なインターフェースを定義し、コードが自己文書化されるようにしましょう。
- 「なぜ」を説明する: 複雑なパターンを実装した場合は、短いコメントを残してください。単純なswitch文ではなくStrategyを選んだ理由を説明することで、チームメイトがあなたのロジックを理解しやすくなります。
これら3つのパターンをマスターすれば、ソフトウェアの見方が変わるでしょう。単にスクリプトを書くのをやめ、メンテナンスが本当に楽しくなるようなシステムをエンジニアリングし始めることができます。

