副作用を境界へ集めると業務判断を純粋な関数にできる

データベースから利用者を取得し、期限切れかを判定し、メールを送る処理を一つの関数に書くと、業務判断と外部への作用が混ざる。 判定をテストするだけでも、データベース、現在時刻、メール送信を差し替えなければならない。

Functional Core, Imperative Shell は、値に対する判断を関数型コアへ置き、入出力を命令型シェルへ置く。 関数型コアは、利用者一覧と基準日時を受け取って通知対象者やメール内容を値として返す。 命令型シェルは、利用者一覧と現在時刻を取得し、コアの返したメールを実際に送る。

const users = db.getUsers();
const cutoff = new Date();
const messages = generateExpiryEmails(getExpiredUsers(users, cutoff));
email.bulkSend(messages);

現在時刻をコアの中で取得せず、基準日時として渡すことに意味がある。 同じ入力には同じ結果が返るため、期限ちょうどの利用者、無料体験中の利用者、期限前の利用者といった境界条件を、外部サービスなしで検査できる。 メール送信もコアでは実行せず、宛先と本文の組として表せば、判断と実行を別々に検査できる。

この分離は副作用をなくすものではない。 外部から値を得る処理と外部を変更する処理を境界へ集め、副作用の間にある判断を値から値への変換として切り出す。 その結果、業務判断は再利用しやすくなり、副作用を伴う結合試験の範囲も小さくなる。

ただし、判断の途中で追加の外部データが必要になる場合、シェルが先に全データを集めるだけでは不要な取得や複雑な進行制御が生じる。 その場合は、必要なデータを中断状態として返すDecision パターンのように、関数型コアと命令型シェルの往復を明示する方法がある。

https://testing.googleblog.com/2025/10/simplify-your-code-functional-core.html

#functional-programming #software-design #testing #side-effect