ドメインモデル貧血症の処方箋
以前のプロジェクトで、ドメインモデル貧血症なプログラムに悩まされたので、学んだことを書いてみます。
ドメインモデル貧血症とは
オブジェクト指向におけるアンチパターン。
振る舞いとデータが分かれてしまっており、手続型の設計・実装になってしまう状態。
詳しくは以下のURLで。
Martin Fowler's Bliki in Japanese - ドメインモデル貧血症
自分なりの理解では、ドメインモデル=エンティティクラスと思ってます。
試しに以下のような従業員クラスでドメインモデル貧血症を考えてみたいと思います。
/** * 従業員クラス */ public class Employee { private int employeeId; private String familyName; private String firstName; private double weight; private double height; //getter setter }
ID、姓、名、体重、身長だけの簡単なエンティティクラスです。
エンティティクラス自体は仮定のものですが、
以下に示すコードは実際にSIer現場で目にしたものです。
他の現場も似たようなものだと思います。
以下のようなコードで、従業員の姓名を得ることが出来ます。
employee.getFamilyName() + employee.getFirstName();
ただ、この状態ですと、
従業員に対してメールを送信する際の宛名に姓名をつけたいとき、
何かの帳票に従業員の姓名を印字したい時など、
従業員の姓名を使いたい場面はいくつか考えられますが、
その都度、このコードを書く必要が出てきてしまいます。
複数で同じロジックが出てくると、共通化という概念が生まれます。
共通関数などと呼ばれるものです。
/** * 引数の従業員の姓名を取得する * @param employee 従業員 * @return 従業員の姓名 */ public static String getFullName(Employee employee){ return employee.getFamilyName() + employee.getFirstName(); }
共通関数は多くの場合staticメソッドで、ユーティリティクラスに実装されています。
さて、一つメソッドを呼ぶだけで目的の処理が行えるようになりました。
しかし、これではデータと振る舞いが分離されてしまっています。
本来はこうするべきです。
public class Employee { //省略 /** * フルネームを返す * @return 従業員の氏名 */ public String getFullName(){ return this.familyName + this.firstName; } }
これだと、引数もなくなり記述も簡単になると同時にデータと振る舞いが同じ個所に書かれるようになりました。
次にちょっと複雑な例として、従業員のBMIを計算する処理を考えてみましょう。
BMIが基準以上の従業員に対してジムの優待券を送付したり、
注意喚起のメールを送付したりといった業務が考えられますね。
計算式は単純で 体重 /(身長 * 身長)です。
ドメインモデル貧血症なシステムでは、以下のようなコードで計算されます。
double bmi = employee.getWeight() / ((employee.getHeight() / 100) * (employee.getHeight() / 100));
このコードの問題点は、この処理が何をしているかという業務要件をコードから読み取れないことです。
そこでこのようになります。
//従業員のBMIを計算する double bmi = employee.getWeight() / ((employee.getHeight() / 100) * (employee.getHeight() / 100));
コードが語らないので、コメントで無理矢理語らせるのです。
これもドメインモデルが貧血になっているので、
ドメインモデルがBMIを計算し返すようにするのがいいと思います。
コードは以下のようになります。
/** * 従業員のBMIを計算し返す * @return 従業員のBMI */ public double calculateBodyMassIndex(){ return this.weight / ((this.height / 100) * (this.height / 100)); }
このようにデータと振る舞いをまとめておくと、
コードから業務もなんとなく見えてきますよね。
以下のコードで、BMIが25以上の従業員を抽出できます。(Javaエンジニア養成読本読みながらラムダ式で書いてみました!)
employees.stream() .filter(e -> e.calculateBodyMassIndex() > 25) .map(e -> e.getFullName()) .forEach(System.out::println);
実際のSIの現場では、O/Rマッパーで使うためにgetter/setterだけのエンティティクラスを作り、
それらの値の処理は手続型で実装されていることが多いと思います。
上記のように、少しメソッドを纏めるだけでコードの見通しもよくなり
クラスを眺めるとシステムで行われる処理も分かるようになります。
実際私も、2014年末からのPJで実践して、効果を実感できました。