導入
実務におけるアプリケーション開発では、DB接続や外部API呼び出しといった「失敗しうる操作」をどう扱うかがコードの堅牢性を左右します。しかし、多くの実装ではビジネスロジック内に直接副作用が埋め込まれ、エラー時の振る舞いをテストすることが困難になっています。今回解説する「Handleパターン」は、依存性逆転原則(DIP)を応用し、エラー処理とロジックを分離することで、テスト容易性と保守性を飛躍的に高める手法です。
基礎知識
Handleパターンとは、副作用(エラーが発生する可能性がある操作)を抽象的なレコードやインターフェースとして定義し、実行時にその実装を差し替える手法です。関数型プログラミングの文脈では、依存関係を「関数」として定義し、ロジックに渡すことで実現します。これにより、本番環境では実際のDB操作を行い、テスト環境では「意図的にエラーを発生させる関数」を注入するといった柔軟な制御が可能になります。
実装/解決策
具体的なアプローチは以下の通りです。
1. エラーを伴う操作を「引数として受け取る関数型(Handle)」として定義する。
2. ロジック本体は、具体的な実装の詳細を知らず、与えられたHandleを使用する。
3. 実行時に必要な実装(本番用またはモック)を注入する。
この手法により、ロジック自体は純粋性を保ちつつ、外部との境界を明確に分離できます。
サンプルプログラム
以下は、TypeScriptを用いたHandleパターンの実装例です。
// 1. エラーを起こしうる操作を関数型として定義(Handle)
type SaveUserHandle = (user: { name: string }) => Promise
// 2. 依存を受け取るビジネスロジック
// 外部操作の実装ではなく、定義された型のみに依存する
async function registerUser(
saveUser: SaveUserHandle,
user: { name: string }
): Promise
try {
await saveUser(user);
return “ユーザー登録成功”;
} catch (error) {
// エラーハンドリングのロジックをここに集約
return “ユーザー登録失敗: 外部サービスの利用不可”;
}
}
// 3. 利用例
async function main() {
// 本番用実装
const realSaveUser: SaveUserHandle = async (u) => console.log(`DBに保存: ${u.name}`);
// テスト用モック(必ず失敗する)
const failSaveUser: SaveUserHandle = async () => { throw new Error(“DB Error”); };
console.log(await registerUser(realSaveUser, { name: “田中” }));
console.log(await registerUser(failSaveUser, { name: “佐藤” }));
}
main();
応用・注意点
Handleパターンを採用する際の注意点は、「抽象化の粒度」です。あまりに細かい関数を大量に定義すると、コードの可読性が低下します。関連する操作(例: ユーザー検索と保存)は、一つのレコードにまとめて「Service Handle」として定義するのが現場での定石です。また、エラーハンドリングをロジック側で握りすぎると再利用性が落ちるため、エラーをResult型(成功か失敗かを表す代数的データ型)で返す設計にすると、より関数型らしい堅牢なエラー制御が可能になります。テスト時は、このHandleを差し替えるだけで、通常では発生しにくいネットワークタイムアウトやDBのデッドロック状態を簡単に再現できることを活用してください。

コメント