業務ルールを"関数"で差し込む — チェック関数(DCF)と導出関数(DDF)の設計
検証と派生計算を、コードの海に埋めず「差し替え可能な関数」として登録する。定義駆動の書き込みエンジンの心臓部。
WHYルールを本体から剥がす
通販基幹のような業務システムでは、書き込み処理の周囲に膨大な業務ルールが張り付く。「この項目が変わったら送料を再計算する」「登録日時が無ければ受け付けない」「配送先を請求先からコピーする」——こうしたルールをエンジン本体の分岐として書き連ねると、コードは急速に読めなくなり、ルールを一つ足すたびに全体を壊す恐怖と戦うことになる。
定義駆動の書き込みエンジンでは、この問題を「業務ルールを2種類の小さな関数として外から差し込む」という設計で解く。エンジン本体はルールの中身を知らない。何が登録されているかだけを見て、必要なものを呼び出す。ルールは資産として独立して増えていく。
DCF / DDF2種類の関数
差し込む関数は役割で2種類に分かれる。
- DCF(データチェック関数):入力の妥当性検証や、状態遷移の可否を判定する「ゲート」。通す/通さないを返す。値は変えない。
- DDF(データ導出関数):派生値を生成する「計算機」。送料・割引の算出、住所のコピーなど。データに新しい値を書き込む。
どちらも evaluate(入力) → {結果} という統一インターフェースを持つ。エンジンから見れば両者は「呼べば結果を返す箱」でしかなく、この均質さが差し替え可能性の土台になる。DCFはゲート、DDFは生成——役割は対照的だが、外形は同じという点が重要だ。
HOWエンジンの駆動
実行の流れはシンプルだ。エンジンは「変更された項目」を起点に、それに反応するはずの関数を洗い出してタスク化する。同じ関数が複数の項目から呼ばれても重複は1つに畳み、関数間の依存関係を解決してから順に走らせる。
DDFの実行は再帰的になる点に注意したい。送料を導出した結果、割引の前提が変わり、別のDDFが新たに必要になる——こうした「派生の派生」を、対象がなくなるまで解決し続ける。DCFは値を変えないため通常は先に評価してゲートを固め、DDFで値を作っていく。
CODE2つの関数の骨格
class RegisteredAtCheck(DCF):
# DCF: 登録日時の必須チェック(ゲート)
triggers = ["registered_at"]
def evaluate(self, data):
if not data.get("registered_at"):
return {"ok": False,
"errors": ["registered_at is required"]}
return {"ok": True, "errors": []}
class ShippingFeeDerive(DDF):
# DDF: 送料と割引の導出(値の生成)
triggers = ["subtotal", "region"]
def evaluate(self, args):
base = fee_for(args["region"])
# 一定額以上は送料無料(派生の派生を誘発しうる)
fee = 0 if args["subtotal"] >= FREE_LINE else base
return {"data": {"shipping_fee": fee}}TRADE-OFF得るものと払うもの
この設計の利点は明確だ。ルールを単位でテスト・追加・差し替えでき、エンジン本体に手を入れずに業務を変えられる。関数が自己完結しているため、AIに「この1関数を丸ごと生成して」と頼みやすいのも実務上大きい。
一方で払う代償もある。関数が増えるほど実行順序と依存関係の管理が難しくなり、依存の循環や解決漏れは追跡が厄介だ。処理が多数の小関数に分散するためデバッグ時の見通しが悪く、どの関数がどの値を書いたかを追う仕組みが要る。さらにDDFが状態を書き換える以上、関数間の副作用を制御しないと、実行順によって結果が変わる不安定さを招く。小さく差し替えられる自由と、全体を俯瞰しにくくなる不透明さは表裏一体である。
業務ルールをDCF(検証ゲート)とDDF(値の導出)という統一インターフェースの小関数に切り出せば、エンジン本体を触らずにルールを増減・差し替えできる。代償は実行順序・依存・副作用の管理コスト。自由度と俯瞰性のトレードオフを、依存解決とトレースの仕組みで埋めるのが設計の勘所。