決定表関数パターン
1. 目的
複数の条件変数から構成される判定ロジックを簡潔に管理する
- 料金計算や給与計算など、複数の条件変数の組み合わせによって結果が決定される判定ロジックはコードが煩雑になりがち
- また、判定ロジックとビジネスロジックや入出力ロジックが混在することで保守性が低下してしまう
- これらの問題を解決したい
2. 課題
if文がネストしたコードはメンテナンスが困難
- 例えば、↓のような3つの因子の組み合わせから結果を返却するプログラムを考えてみる
# 3つの因子の組み合わせ毎に異なる値を返す関数
def example(value1, value2, value3):
if value1 == 'xxx':
if value2 == 'xxx':
if value3 == 'xxx':
# 数行から数十行のビジネスロジック
return "Result08"
else:
# 数行から数十行のビジネスロジック
return "Result04"
else:
if value3 == 'xxx':
# 数行から数十行のビジネスロジック
return "Result06"
else:
# 数行から数十行のビジネスロジック
return "Result02"
else:
if value2 == 'xxx':
if value3 == 'xxx':
# 数行から数十行のビジネスロジック
return "Result07"
else:
# 数行から数十行のビジネスロジック
return "Result03"
else:
if value3 == 'xxx':
# 数行から数十行のビジネスロジック
return "Result5"
else:
# 数行から数十行のビジネスロジック
return "Result01"
if __name__=='__main__':
print(example("---", "---", "---")) # => Result01
print(example("xxx", "---", "---")) # => Result02
print(example("---", "xxx", "---")) # => Result03
print(example("xxx", "xxx", "---")) # => Result04
print(example("---", "---", "xxx")) # => Result05
print(example("xxx", "---", "xxx")) # => Result06
print(example("---", "xxx", "xxx")) # => Result07
print(example("xxx", "xxx", "xxx")) # => Result08
- この例では理解を容易にするためビジネスロジック部分のコードを省略している
- これだけだと判定と結果の返却だけなのでそこまで理解が困難というわけではないが、それでもif文が3重にネストしていることで読みづらさが否めない
- また、このような判定ロジックの中でビジネスロジックや入出力ロジックが実行されていると、判定ロジックとビジネスロジックが混ざり合ってしまい、本質的な判定ロジックが分かりにくくなってしまう
- 判定路ロジックとビジネスロジックや入出力ロジックが混在していると、副作用の発生を考慮する必要があり、気軽に実行して試すことも難しくなる上、実際の動作イメージも分かりづらい
- 更に言えば、現実のコードは例のように判定処理が網羅されているとは限らない為、どのような組み合わせが正常ケース/エラーケースになるのか分かりづらい
- このようなコードはメンテナンスの難易度が高く、バグを生みやすいコードであると言える
3. 解決策
判定処理は判定処理のみを行う関数に分離する
- 先述したプログラムの問題を修正したコードを以下に示す
# 因子1を判定する関数
def factor1(value):
return value == 'xxx'
# 因子2を判定する関数
def factor2(value):
return value == 'xxx'
# 因子3を判定する関数
def factor3(value):
return value == 'xxx'
def find_matrix(value1, value2, value3):
result1 = factor1(value1)
result2 = factor2(value2)
result3 = factor3(value3)
if all([not result1, not result2, not result3]):
return "Pattern01"
elif all([result1, not result2, not result3]):
return "Pattern02"
elif all([not result1, result2, not result3]):
return "Pattern03"
elif all([result1, result2, not result3]):
return "Pattern04"
elif all([not result1, not result2, result3]):
return "Pattern05"
elif all([result1, not result2, result3]):
return "Pattern06"
elif all([not result1, result2, result3]):
return "Pattern07"
elif all([result1, result2, result3]):
return "Pattern08"
else:
raise ValueError()
# ↓こういう書き方もあり
# if all([not result1, not result2, not result3]): return "Pattern01"
# if all([result1, not result2, not result3]): return "Pattern02"
# if all([not result1, result2, not result3]): return "Pattern03"
# if all([result1, result2, not result3]): return "Pattern04"
# if all([not result1, not result2, result3]): return "Pattern05"
# if all([result1, not result2, result3]): return "Pattern06"
# if all([not result1, result2, result3]): return "Pattern07"
# if all([result1, result2, result3]): return "Pattern08"
# raise ValueError()
if __name__=='__main__':
print(find_matrix("---", "---", "---")) # => Pattern01
print(find_matrix("xxx", "---", "---")) # => Pattern02
print(find_matrix("---", "xxx", "---")) # => Pattern03
print(find_matrix("xxx", "xxx", "---")) # => Pattern04
print(find_matrix("---", "---", "xxx")) # => Pattern05
print(find_matrix("xxx", "---", "xxx")) # => Pattern06
print(find_matrix("---", "xxx", "xxx")) # => Pattern07
print(find_matrix("xxx", "xxx", "xxx")) # => Pattern08
- 組み合わせを判定するロジックと因子の判定を行うロジックを分離した(
find_matrix
とfactor1
, factor2
, factor3
)
-
find_matrix
関数ではif文をネストせず、all
関数(Pythonの場合。他の言語ではevery
関数だったりする)で各因子の判定結果をまとめて判定することで、判定条件と戻り値を1:1で対応させる
- 判定条件と戻り値が1:1で対応することで行数は増えたが、条件と結果の対応は分かりやすくなりメンテナンスも容易になった
- 判定条件と戻り値が1:1である為、イレギュラーな条件にも対応しやすいコードになった
- また、ディシジョンテーブルと組み合わせることでディシジョンテーブルと直接対応した判定ロジックを作ることができる
判断ロジック内でビジネスロジックや入出力ロジックを実行しない
- 判断ロジックは値やフラグを返却することに特化し、ビジネスロジックや入出力ロジックの実行は呼び出し元が行うようにする
- 判断ロジックは判断ロジックの実行と結果の返却にのみ責任を負うようにし、ビジネスロジックや入出力ロジックと切り離すことで保守性を確保する
- サンプルでは説明を簡単にする為、戻り値を文字列で返却しているが、実際に実装する場合はEnmuや定数を返却した方がよいだろう
- もしくは、ビジネスロジックオブジェクトや関数オブジェクトを返すことでStrategyパターン風にしてもよい
def main():
# (1) 判断ロジック呼び出し
result = find_matrix(param1, param2, param3)
# (2) 判定結果に応じたビジネスロジック実行
if result == "Pattern01":
# Pattern1 のビジネスロジック実行
...
if result == "Pattern02":
# Pattern2 のビジネスロジック実行
...
...
4. メリット
if文のネストを避けることができる
- if文をネストするのではなく、AND条件でまとめることで判断条件と決定を1:1で管理することになる
- 判断条件と判定結果を1:1で管理することで、if文のネストを避けながら複雑な判定条件を管理することができる
判断ロジックをビジネスロジックから分離できる
- 判断ロジックは判断結果のみを提供し、ビジネスロジックや入出力から独立させることでビジネスロジックの影響を受けなくなる為、判断ロジックの保守性が高く保たれる
- 判断ロジックの独立性が高く保たれることで、将来的な移植性も確保される
- 判断ロジックはドメインのコアとなる知識となる為、極力ビジネスロジックや入出力ロジックから切り離されていることが望ましい
ディシジョンテーブルをそのままプログラムに落とし込める
- ディシジョンテーブルは要するにbool値の集合なので、ディシジョンテーブルで表現している判断ロジックをそのままプログラムとして実装できる
- 通常のif文でもディシジョンテーブルを再現することは可能だが、可読性やディシジョンテーブルとの対応の点でAND条件でまとめる形の方が優れている
- 判断ロジックのみを提供することでディシジョンテーブルと直接対応したプログラムが実装できる為、実装の漏れを抑えることができる
5. デメリット
因子が増えるとコードが長くなりがち
- 因子が3つの場合は8パターンだが、4つになる16、5つになると32...と倍増していくので、判定条件と求める結果に対して本質的な因子を絞り込む設計能力が求められる
6. 注意
関連文書
参考資料
Appendix-1 クラスによる実装
-
決定表関数パターン
と名乗ってはいるが、クラスとして実装しても当然構わない
- DBや外部APIにアクセスする必要がある場合はそれらの接続処理をモックとして差し込めるようにクラスとして実装した方がよいかもしれない
class DecisionTable:
def factor1(self, factor):
return factor == 'xxx'
def factor2(self, factor):
return factor == 'xxx'
def factor3(self, factor):
return factor == 'xxx'
def find_matrix(self, value1, value2, value3):
result1 = self.factor1(value1)
result2 = self.factor2(value2)
result3 = self.factor3(value3)
if all([not result1, not result2, not result3]):
return "Pattern01"
elif all([result1, not result2, not result3]):
return "Pattern02"
elif all([not result1, result2, not result3]):
return "Pattern03"
elif all([result1, result2, not result3]):
return "Pattern04"
elif all([not result1, not result2, result3]):
return "Pattern05"
elif all([result1, not result2, result3]):
return "Pattern06"
elif all([not result1, result2, result3]):
return "Pattern07"
elif all([result1, result2, result3]):
return "Pattern08"
else:
raise ValueError()
if __name__=='__main__':
d = DecisionTable()
print(d.find_matrix("---", "---", "---")) # => Pattern01
print(d.find_matrix("xxx", "---", "---")) # => Pattern02
print(d.find_matrix("---", "xxx", "---")) # => Pattern03
print(d.find_matrix("xxx", "xxx", "---")) # => Pattern04
print(d.find_matrix("---", "---", "xxx")) # => Pattern05
print(d.find_matrix("xxx", "---", "xxx")) # => Pattern06
print(d.find_matrix("---", "xxx", "xxx")) # => Pattern07
print(d.find_matrix("xxx", "xxx", "xxx")) # => Pattern08
Appendix-2 Go言語実装例
import (
"errors"
)
func factor1(factor string) bool {
return factor == "xxx"
}
func factor2(factor string) bool {
return factor == "xxx"
}
func factor3(factor string) bool {
return factor == "xxx"
}
func every(value1 bool, value2 bool, value3 bool) bool {
return value1 && value2 && value3
}
func FindMatrix(value1 string, value2 string, value3 string) (string, error) {
result1 := factor1(value1)
result2 := factor2(value2)
result3 := factor3(value3)
switch {
case every(!result1, !result2, !result3):
return "Pattern01", nil
case every(result1, !result2, !result3):
return "Pattern02", nil
case every(!result1, result2, !result3):
return "Pattern03", nil
case every(result1, result2, !result3):
return "Pattern04", nil
case every(!result1, !result2, result3):
return "Pattern05", nil
case every(result1, !result2, result3):
return "Pattern06", nil
case every(!result1, result2, result3):
return "Pattern07", nil
case every(result1, result2, result3):
return "Pattern08", nil
default:
return nil, errors.New("Error!")
}
}
Appendix-3 テーブル駆動プログラミングを取り入れたバージョン
- テーブル駆動プログラミング/テーブル駆動方式を取り入れたバージョン
- こちらの方がシンプルで可読性も高い
_table = [
{"factor1": True, "factor2": True, "factor3": True, "result": "Pattern01"},
{"factor1": True, "factor2": False, "factor3": False, "result": "Pattern02"},
{"factor1": False, "factor2": True, "factor3": False, "result": "Pattern03"},
{"factor1": True, "factor2": True, "factor3": False, "result": "Pattern04"},
{"factor1": False, "factor2": False, "factor3": True, "result": "Pattern05"},
{"factor1": True, "factor2": False, "factor3": True, "result": "Pattern06"},
{"factor1": False, "factor2": True, "factor3": True, "result": "Pattern07"},
{"factor1": False, "factor2": False, "factor3": False, "result": "Pattern08"},
]
class Condition:
def __init__(self, value1, value2, value3):
self.value1 = value1
self.value2 = value2
self.value3 = value3
def factor1(self):
return self.value1 == "xxx"
def factor2(self):
return self.value2 == "xxx"
def factor3(self):
return self.value3 == "xxx"
class RuleSet:
def __init__(self):
self.ruleset = _table
def find(self, cond):
for r in self.ruleset:
match = all([
cond.factor1() == r["factor1"],
cond.factor2() == r["factor2"],
cond.factor3() == r["factor3"],
])
if match:
return r["result"]
raise ValueError()
if __name__=='__main__':
rs = RuleSet()
c = Condition("xxx", "xxx", "xxx")
result = rs.find(c)
print(result) # => Pattern01