拡張可能な計算機:保守性と柔軟性を両立するアーキテクチャ設計
現代のフロントエンド開発において、単なる「計算機能」の実装は初歩的な課題です。しかし、ビジネス要求が複雑化し、通貨換算、単位変換、あるいは独自のビジネスロジックが追加されるにつれ、単一の関数や巨大なswitch文で構築された計算機は瞬く間に技術的負債へと変貌します。本稿では、デザインパターンを駆使し、機能追加が容易でテスト性に優れた「拡張可能な計算機」の設計手法を解説します。
設計思想:CommandパターンとStrategyパターンの融合
拡張可能な計算機を設計する上で最も重要なのは、「計算のロジック」と「計算機のエンジン(実行環境)」を完全に分離することです。これを実現するために、CommandパターンとStrategyパターンを組み合わせるアプローチを採用します。
計算機を一つの巨大なオブジェクトとして定義するのではなく、個別の演算を「操作オブジェクト」として定義し、それらを登録・実行するレジストリを用意します。これにより、新しい計算機能を追加する際に、既存の計算機コードを一行も変更することなく(Open/Closed Principleの遵守)、新しいモジュールを追加するだけで拡張が可能になります。
具体的な実装戦略
まず、すべての演算子が従うべきインターフェースをTypeScriptで定義します。これにより、型安全性が担保され、開発体験が飛躍的に向上します。
interface Operation {
execute: (a: number, b: number) => number;
}
class Calculator {
private operations: Map = new Map();
register(symbol: string, operation: Operation): void {
this.operations.set(symbol, operation);
}
calculate(symbol: string, a: number, b: number): number {
const op = this.operations.get(symbol);
if (!op) {
throw new Error(`Operation ${symbol} not supported.`);
}
return op.execute(a, b);
}
}
この実装の利点は、Calculatorクラスが具体的な計算ロジックを一切知らない点にあります。`register`メソッドを通じて外部から動的に機能を追加できるため、プラグインのようなアーキテクチャを実現可能です。
高度な拡張:演算子の動的注入とミドルウェア
実務においては、単なる計算結果だけでなく、計算の履歴(ログ)、入力値のバリデーション、あるいは計算前後のフック処理が必要になるケースが多いでしょう。ここで、計算機エンジンに「ミドルウェア」の概念を導入します。
type Middleware = (a: number, b: number, next: () => number) => number;
class AdvancedCalculator extends Calculator {
private middlewares: Middleware[] = [];
use(middleware: Middleware) {
this.middlewares.push(middleware);
}
// ミドルウェアを適用した計算処理
calculateWithMiddleware(symbol: string, a: number, b: number): number {
let index = 0;
const next = () => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index++];
return middleware(a, b, next);
}
return super.calculate(symbol, a, b);
};
return next();
}
}
このように設計することで、例えば「計算結果をコンソールに出力する」「負の数の入力を禁止する」といった横断的な関心事を、計算ロジックから分離して管理できます。
実務における設計のアドバイス
拡張可能な計算機を設計する際、以下の3つの観点を重視してください。
1. 疎結合の維持:演算ロジックをクラスとして切り出し、単体テストを容易にしてください。例えば、`AddOperation`、`SubtractOperation`といったクラスに分割することで、各演算のテストカバレッジを100%に保つことが容易になります。
2. 型定義の厳格化:TypeScriptを使用する場合、`symbol`を単なる`string`ではなく、リテラル型のユニオン型として定義することで、コンパイル時に計算機のサポート範囲を制限できます。
3. エラーハンドリングの統一:計算失敗(ゼロ除算など)が発生した際、個々の演算子が例外を投げるのではなく、結果型(Resultパターン)を返す設計にすると、呼び出し側でのエラーハンドリングが非常にクリーンになります。
関数型アプローチとの比較
オブジェクト指向的なアプローチに加え、関数型プログラミング(FP)の考え方を取り入れることも検討に値します。計算処理を純粋関数(Pure Function)として定義し、それらを関数合成(Composition)することで計算機を構築する手法です。
FPのアプローチは、計算の履歴を追跡する(Time Travel Debugging)際に非常に強力です。Reduxのような状態管理ライブラリを使用している場合、各演算をReduxのActionとして定義し、Reducerで計算結果を更新する設計にすると、Reactアプリケーションとの親和性が極めて高くなります。
拡張性の限界と現実的なトレードオフ
最後に、過剰な設計(Over-engineering)に対する警告を述べます。拡張性を追求するあまり、インターフェースやクラスが乱立し、コードベースが複雑になりすぎるのは本末転倒です。
もし計算機の要件が「四則演算のみ」で将来的な変更の可能性が極めて低いのであれば、単純なswitch文やif文で十分です。拡張可能な設計を採用するのは、あくまで「今後、新しい計算ロジックが追加される可能性が高い」または「計算過程のログやバリデーションなど、付加機能が複雑化する予兆がある」場合に限るべきです。
まとめ
拡張可能な計算機を作ることは、単なるコードの記述以上に「システムの保守性をどう定義するか」というアーキテクチャの思考訓練になります。
1. 計算ロジックをクラスや関数としてモジュール化する。
2. レジストリパターンを用いて動的な拡張を可能にする。
3. ミドルウェアやフックを用いて横断的な関心事を分離する。
4. 常にYAGNI原則(You Ain't Gonna Need It)を意識し、必要最小限の複雑性に留める。
これらの原則を守ることで、数年後も陳腐化せず、新しいチームメンバーが自信を持って機能追加できる堅牢な計算機エンジンを構築できるはずです。フロントエンドの技術スタックは日々進化しますが、こうした設計パターンは普遍的な価値を持ちます。ぜひ、あなたのプロジェクトでも実践してみてください。

コメント