AdventureWorksをモデリングしてDDDしながらドメインモデルで実装してみる(8)
今までの作業の中心はエンティティだったのでDOA(データ中心アプローチ)と同じような作業と感じを方もいるかもしれません。ビジネスデータの中心は確かにエンティティなのですが、VALUE OBJECTを上手に利用することでビジネスロジックの記述をより高いレベルにもっていけます。VALUE OBJECTはDOAではほとんど取り扱われないと思いますので、DOAと少し違った観点になります。
VALUE OBJECTを利用する
ユーザ機能「休暇を申請する」の処理シナリオでVALUE OBJECTをどのように利用するか考えます。以下のコードは先のユーザ機能から識別された「休暇申請サービス」の実装です。この処理でVALUE OBJECTにあたるのが引数のDateTimeRangeで期間を表現しています。DateTimeRangeを利用しないで開始日時と終了日時を利用してももちろん実装可能ですが、DateTimeRangeを導入することで多くのメリットが得られます。
public void RequireVacation(DateTimeRange requiring, EmployeeVactionType type) { Validate.Check(IsActive, "在職の社員のみ休暇申請ができます"); int requiringHour = (int)requiring.Intersect(this.CurrentShift.Range).TotalHours; this.VacationItems[type].Require(requiringHour); }
実際のDateTimeRangeのコードは以下のようになっています。VALUE OBJECTの必須条件ではありませんがDateTimeRangeはイミュータブル(不変)にしてあります。イミュータブルにすることでオブジェクトの変更シナリオを気にする必要がなくなり、利用方法が単純化できるメリットがあります。
public class DateTimeRange { public DateTimeRange(DateTime start, DateTime end) { Validate.Check(start <= end, "開始日時よりも後に終了日時を指定してください"); this.Start = start; this.End = end; } public DateTime Start { get; private set;} public DateTime End { get; private set; } ...
VALUE OBJECTをリッチ化する
エンティティをリッチ化するのと同じようにVALUE OBJECTもリッチ化することが可能です。VALUE OBJECT化する典型的なオブジェクトの1つとして単位付き数量や今回の期間のような複合データで計算するタイプの汎用クラスがあります。このようなタイプのクラスは演算が複雑になる傾向がありますので、演算可能な仕組みをクラスに実装することでリッチ化することが有効です。逆にこのようなことが行われていない場合、複雑な演算がいくつものビジネスロジック上にちらばることになります。今回はDateTimeRangeに交差している時間を算出するメソッドを定義しています。なお、TimeRange クラスは日時を除く時分秒で期間をあらわすクラスです。もちろん、TimeRange クラスもVALUE OBJECTです。
public class DateTimeRange { ... public TimeSpan Intersect(TimeRange innerRange) { DateTime day = Start; TimeSpan ret = new TimeSpan(); while (day <= End) { DateTime sDate = (day.Date == Start.Date ? Start : new DateTime() ); DateTime eDate = (day.Date == End.Date ? End : new DateTime()); ret += new TimeRange(sDate, eDate).Intersect(innerRange).Span; day = day.AddDays(1); } return ret; } }
このようにDateTimeRangeクラスがリッチになっているため、「休暇申請サービス」の実装がシンプルになり読みやすくなっています。申請期間の稼動時間を算出する処理が1行で記述できています。
//申請期間(requiring)と現勤務シフト(CurrentShift)の稼働時間の交差時間(Intersect)を求める int requiringHour = (int)requiring.Intersect(this.CurrentShift.Range).TotalHours;