whack-a-mole
いくつか例示された仕様が通るように、またはテストが通るようにコードを行きあたりばったりで書き加えていくこと。
これをもぐらたたき(whack-a-mole)アンチパターンと呼ぶことにする。
極端な例として、このジョークツイートであるIsEvenの実装を参照。
アンチパターンの問題点
コードを読んだだけでは元の要求が把握できない。
エッジケースの漏れに気づきにくい。
仕様追加・変更時にこれまでの条件分岐と干渉していないかに十分注意する必要がある。
Example Based Testingでは検出しにくい。
Exampleベースのユニットテストが通るように、プロダクションコードを書けてしまう。
アンチパターンに陥るケース
全容を把握せずにコードを書く
例えば、この伊勢丹写真館の料金算出ロジックを書くとする。
code:java
if (撮影目的 == 記念撮影) {
if (撮影人数 == 1) {
if (ポーズ数 == 1) {
return 11000;
} else if (ポーズ数 == 2) {
return 16500;
} else if (ポーズ数 == 3) {
return 22000;
} else if (ポーズ数 == 4) {
return 27500;
} else if (ポーズ数 == 5) {
return 33000;
} else if (ポーズ数 == 6) {
return 38500;
} else {
throw new IllegalArgumentException("記念撮影の1人のポーズ数は6までです");
}
} else if (撮影人数 == 2) {
if (ポーズ数 == 1) {
return 15400;
} else if (ポーズ数 == 2) {
return 23100;
} else {
throw new IllegalArgumentException("記念撮影の2人のポーズ数は2までです");
}
} else {
throw new IllegalArgumentException("記念撮影の人数は2人までです")
}
}
料金の計算ルールを分析せずに、料金表をそのまま条件分岐で表すとこのようなコードになることだろう。これは変更に弱いと同時に、現場では柔軟に対応していたであろう撮影人数3人のケースやポーズ数が7の場合などの料金計算を、料金表には載ってないので例外扱いしてしまっているところに問題がある。
料金表と似たケースとして、デシジョンテーブルからそのままコードに落とすというアンチパターンも存在する。問題の元は同じである。
デシジョンテーブルの場合、条件の組み合わせから出力がなぜそれが妥当なのかを想起できなければ、ただの意味のわからない符号の組み合わせになってしまうので、より質が悪い。実装する人が、デシジョンテーブルの仕様の誤りを検出できそうにもないものになってしまったなら、簡略化したり、条件を型で表したりした方が良い。
仕様が頻繁に追加・変更される
法令によって頻繁に計算ロジックに修正が入るものは、最初に仕様の全体像を把握しづらい。
例えば協会けんぽの高額療養費の計算をシステム化することを考えよう。
基本的な考え方は、「年齢・所得により自己負担額が増減する」を実現したいわけだが、ほぼ毎年度、所得の区分見直しが入る。初期の設計段階では、将来のルール変更を予測するのは困難である。
だが、予測困難だからといって、表の通り条件分岐を書くと以下のようになり、すぐにメンテナンス困難になるのは想像できるだろう。
code:java
if (年齢 < 70) {
if (報酬月額 >= 810000) {
if (is多数該当()) {
return 140100;
} else {
return 252600 + (総医療費 - 842000) * 0.01;
}
} else if (報酬月額 >= 530000 && 報酬月額 < 810000) {
…
} else if (年齢 >= 70 && 年齢 < 75) {
if (ERA_FORMATTER.parse("平成29年8月1日").isBefore(診療年月)) {
if (標準月額報酬 >= 280000 && 高齢受給者証負担割合 == 30) {
if (外来) {
return 44400;
} else if (世帯外来 || 入院) {
if (is多数該当()) {
return 44400;
} else {
return 80100 + (総医療費 - 267000) * 0.01;
}
}
} else if (非課税者 || 所得なし) {
…
}
}
テストの意義
「テストはより特化的に、コードはより汎化的に」がTDDのサイクルであるが、Uncle Bobの指摘ではwhack-a-moleなコードでは汎化できていないため、TDDが回らないとしている。
whack-a-moleなコードにいくらテストを書いても、効果は薄く文字通りもぐら叩きになってしまうので、まずコード側を修正すべきである。
アンチパターンに陥らないために
ビジネスルールを抽象化する
例えば先の伊勢丹写真館の料金計算の場合、以下のようにルールを表現できる。
撮影シーンとは、その写真を何の目的・記念で撮るかである。
ポーズとは「立ち」「座り」など、大きく撮影構図が変わる。
カットは同一ポーズでも何枚か撮ることがあり、その一つ一つを指す。
料金は、撮影料金 + プリント料金 + 台紙・アルバム料金 + データ料金+早仕上げ料金 からなる。
撮影料金は基本料金 + (基本料金/2 * (ポーズ数-1))で決まる。
基本料金は撮影シーン毎に異なる。
撮影シーンごとに撮影人数が一定数を越えると、異なる基本料金が設定される。
…
したがって、撮影料金計算は以下の通りのロジックで表せる。
code:java
long 撮影料金計算(撮影シーン, 撮影人数, ポーズ数) {
// 例: 撮影シーン=記念写真のときは、3000円+(2000円×撮影人数)が基本料金
long 基本料金 = 撮影シーン.基本料金(撮影人数);
long 税抜き撮影料金 = 基本料金 + (基本料金/2 * (ポーズ数 - 1));
return 税抜き撮影料金 * 消費税;
}
抽象化のツール
デシジョンテーブル + 簡略化
既存のビジネスルールだけが判明していて、そのMECE度合いも分からない場合はまずデシジョンテーブルに書き出すと良い。これでルールの抜け・漏れを検出できる。ただし、このままではビジネスルールが符号化された状態で、その意味を読み取ることができないので、簡略化をおこなう。
そうすると元のビジネスルールが、浮き出てくることがある。
条件値を同値分割(グルーピング)する
簡略化すれば浮き出てくることではあるが、同値とみなせるものを考えずにデシジョンテーブルを書くと、サイズが大きくなる要因にもなるし、仕様理解も難しくなる。
table:ETC休日割
走行時間帯 車種 走行エリア 割引率
平日 普通車 都市部 0
平日 軽自動車 都市部 0
平日 二輪 都市部 0
平日 その他 都市部 0
平日 普通車 地方部 0
平日 軽自動車 地方部 0
平日 二輪 地方部 0
平日 その他 地方部 0
休日 普通車 都市部 0
休日 軽自動車 都市部 0
休日 二輪 都市部 0
休日 その他 都市部 0
休日 普通車 地方部 30
休日 軽自動車 地方部 30
休日 二輪 地方部 30
休日 その他 地方部 0
この例の場合、普通車、軽自動車、二輪でグルーピングし名前をつける。
条件を型で表す
実装の詳細、RDBのテーブルのカラムでデシジョンテーブルを書くと、条件が多くなってしまうことがある。
例えば、メールを送信できる条件が以下のとおりとする。
ユーザのメールアドレスがアクティベートされている(アクティベートフラグがtrue)
会員ステータスが「休会」および「退会」でない
このとき、アクティベートフラグと会員ステータスをデシジョンテーブルの条件にするのではなく、アクティベーション済みユーザ(アクティベートフラグ=true and 「休会」「退会」でない)か、未アクティベートユーザかの何れかである型を作れば、その型で条件を構成できる。
条件の組み合わせが、他の箇所でも現れるようならばなおさら、その組み合わせを表す型を作っておいた方がよい。
code:User.java
sealed interface User permits ActivatedUser, UnActivatedUser {
String emailAddress();
}