BT

.NETでドメイン駆動開発~ValueObject後編~

| 作者 上坂 貴志 - (株)ネクストスケープ フォローする 0 人のフォロワー 投稿日 2014年3月25日. 推定読書時間: 13 分 |

 さて、前編ではValueObjectの特徴はstring型の特徴と良く似ており、string型の内部挙動を理解することでValueObjectを理解しようという説明を行った。後編では実際にコードを記載してみたいと思う。
その前にValueObjectとは何か、をおさらいしておこう。

ValueObjectとは、

  1. 不変である。保持した値は決して変更されない。変更のアクションを外側から依頼された時は自身のプロパティ値のコピー+変更値で新しいオブジェクトを生成して返却する。
  2. オブジェクトを識別する一意となる値を保持していない。例えば、データベースの自動採番列のようなものは保持していない。
  3. 全プロパティ値が同じなら、同じオブジェクトである。例えオブジェクトのアドレスが異なっていても同じオブジェクト、として認識される。
  4. Entityのプロパティに使用する。

前編でご紹介した私なりの解釈である。一般的に説明されているものとは異なることにご注意いただきたい。これをさらにわかりやすく説明すると、

ValueObjectとは、

  1. string型と同じような不変性質を持つ。
  2. Entityのプロパティに使用する属性用のclassのこと。

であった。詳しくは前編を参照願いたい。
さて、ではValueObjectとして実装する例として「期間」を取り上げてみよう。データとして「期間」を取り扱う場合、通常開始と終了があり、場合によっては時刻単位まで考慮することもある。ここでは時刻は考慮せず、日付単位で開始日、終了日が設定されている「期間」をValueObjectとして実装してみるとしよう。クラス名はDateRangeとした。

 1: public class DateRange 
2: { 3:     public DateRange(DateTime startDate, DateTime endDate)        // コンストラクタでのみデータを受け取る 4:     {
5:         this._startDate = startDate;
6:         this._endDate = endDate;
7:     }
8: 9:     private readonly DateTime _startDate;       // readonly のフィールドに格納することで不変性を担保 10:     private readonly DateTime _endDate;
11:
12:     public DateTime StartDate { get { return this._startDate; } } // 外部公開する場合は取得のみを許可(setterは実装しない)
13:     public DateTime EndDate { get { return this._endDate; } }
14:
15:     public static bool operator ==(DateRange x, DateRange y)
16:     {
17:         return x.StartDate.Date == y.StartDate.Date && x.EndDate.Date == y.EndDate.Date;    //開始日と終了日の日付部分が一致する場合のみ、trueを返却
18:     }
19:
20:     public static bool operator !=(DateRange x, DateRange y)
21:     {
22:         return x.StartDate.Date != y.StartDate.Date || x.EndDate.Date != y.EndDate.Date;    //開始日と終了日の日付部分のいずれが不一致の場合、trueを返却
23:     }
24:
25:    public override bool Equals(object obj)      // 「==」演算子と同じ結果を返却するために Equalsメソッドをオーバーライド
26:    {
27:         if (null == obj || this.GetType() != obj.GetType())
28:         {
29:             return false;
30:         }
31:
32:         return this == obj as DateRange;
33:     }
34:
35:     public override int GetHashCode()
36:     {
37:         return this.StartDate.GetHashCode() ^ this.EndDate.GetHashCode();
38:     }
39: }

実装をテストするコードを用意した。

 1: static void Main(string[] args)
 2: {
 3: var range201403_1 = new DateRange(new DateTime(2014, 3, 1), new DateTime(2014, 3, 31));
4: var range201403_2 = new DateRange(new DateTime(2014, 3, 1), new DateTime(2014, 3, 31));    // range201403_1と同じ期間
5: var range197103 = new DateRange(new DateTime(1971, 3, 1), new DateTime(1971, 3, 31));      // 全く異なる期間
6:  
7: Console.WriteLine("range201403_1 == range201403_2 : {0}", range201403_1 == range201403_2);  // 同じ期間なのでTrueと表示される
8: Console.WriteLine("range201403_1 == range197103 : {0}", range201403_1 == range197103);      // 異なる期間なのでFalseと表示される
9: Console.WriteLine("range201403_1.Equals(range201403_2) : {0}", range201403_1.Equals(range201403_2));  // 同じ期間なのでTrueと表示される
10: Console.WriteLine("range201403_1.Equals(artName3) : {0}", range201403_1.Equals(range197103));         // 異なる期間なのでFalseと表示される 11: }

実行結果は次の通りだ。



別のオブジェクトであっても期間が一致すれば同一のオブジェクトである、と認識されていることがわかる。

改めて実装を見ていただきたい。コンストラクタで初期値を受け取る以外、クラスの外側から値をセットできるところはないことがおわかりいただけるだろう。コンストラクタで受け取った値を格納しているフィールドはreadonlyであり、コンストラクタ以外で値の変更をすることができない。また、12,13行目で開始日と終了日を外部公開しているが、getterのみでありsetterを実装していない。このようにして値の不変性を担保している。
次に、15行目から23行目の等価演算子のオーバーロードをご覧いただきたい。ここで開始日と終了日の日付部分のみを対象に判定を行うことで、ValueObjectの重要な機能の一つである、「全てのプロパティ値が同じであれば、同一オブジェクトとみなす」を実現している。25行目から33行目のEqualsメソッドのオーバーライドは、等価演算子のオーバーロードと同一の結果を返却するために実装が必要だ。これを行わないと等価演算子とEqualsメソッドが異なる結果を返す、という奇妙な結果となってしまう。
さて、このDateRangeクラスはValueObjectであるから、Entityのプロパティとして使用される。例えばContract(契約)クラスというEntityでは次のように使用される。

1: public class Contract 
2: { 3:     public int Id { get; set; }                // Entityを一意に認識するための値 4:     public string Name { get; set; }           // 契約名 5:     public DateRange LifeTime { get; set; }    // 有効期間   ・   ・   ・

Entityについては本稿で説明していないが、一意性(アイデンティティ、識別性)を持つオブジェクトである。例えプロパティ値がすべて同じであっても、一意性を担保する値が異なると別のオブジェクトであるとみなす。このContractクラスではIdプロパティが一意性を表している。

 さて、ここで有効期間LifeTimeを延長する、という要件の実装について考えてみよう。この場合、DateRangeクラスに期間延長用のメソッドを用意することになる。注意が必要なのは、不変性を厳守することだ。ここでは、月数を引数に受け取って有効期間を延長するExtendsByMonthsメソッドを用意してみる。

1: public class DateRange
2: {
  ・ 
  ・
   public DateRange ExtendsByMonths(int months)
   {
        return new DateRange(this.StartDate, this.EndDate.AddMonths(months));      // 終了日を伸ばした新しいDateRangeオブジェクトを返却 
   }
  ・ 
  ・ 

内部に保持している開始日・終了日の値を変更せず、新しいDateRangeオブジェクトを生成して返却することで不変性が保たれていることがわかる。
ExtendsByMonthsメソッドの使用例は次のようになる。

 1: static void Main(string[] args)
 2: {
 3:     // 契約オブジェクトを生成 
 4:     var contract = new Contract()
 5:     {
 6:         Id = 1,
 7:         Name = "契約0001",
 8:         LifeTime = new DateRange(new DateTime(2014, 3, 1), new DateTime(2014, 3, 31))
 9:     };
10: 
11:     // 初期値確認 
12:     Console.WriteLine("契約名 : {0}", contract.Name);
13:     Console.WriteLine("有効期間開始日 : {0}", contract.LifeTime.StartDate);
14:     Console.WriteLine("有効期間終了日 : {0}", contract.LifeTime.EndDate);
15:  
16:     // 有効期間を12ヶ月延長 
17:     contract.LifeTime = contract.LifeTime.ExtendsByMonths(12);
18:     contract.Name = contract.Name + "名称変更";
19: 
20:     // 変更結果を確認 
21:     Console.WriteLine("契約名 : {0}", contract.Name);
22:     Console.WriteLine("有効期間開始日 : {0}", contract.LifeTime.StartDate);
23:     Console.WriteLine("有効期間終了日 : {0}", contract.LifeTime.EndDate);  // 12ヶ月後の値である2015/03/31となる 
24: }


実行結果は次のとおりとなる。



17行目が有効期間を延長している箇所である。

17:     contract.LifeTime = contract.LifeTime.ExtendsByMonths(12);

LifeTimeプロパティ(DateRangeオブジェクト)のExtendsByMonthメソッドの戻り値をLifeTimeプロパティに格納し直しているが、これはExntendsByMonthメソッドがDateRangeオブジェクト自身の値を書き換えることはせず、「StartDateのコピー+有効期間延長後の日付」を使用して新しいDateRangeオブジェクトを生成し、返却することで不変性を担保しているからである。

しかしこのValueObjectをわざわざ作成するメリットとは一体何だろうか。どうして不変であることにこだわるのだろう。
ValueObjectを不変に実装することで得られるメリットは、知らないうちに値が変更されることが絶対にない、ということである。
次のようなコードを考えてみてほしい。

 1: static void Main(string[] args)
 2: {
 3: var lifeTime = new DateRange(new DateTime(2014, 3, 1), new DateTime(2014, 3, 31));
 4:
 5: var contract001 = new Contract() { Id = 1, Name = "契約001", LifeTime = lifeTime };
 6: var contract002 = new Contract() { Id = 1, Name = "契約002", LifeTime = lifeTime };   // contract001と同じDateRangeオブジェクトを使用 
 7: 
 8: // 有効期間を12ヶ月延長 
 9: contract001.LifeTime.EndDate = new DateTime(2015, 3, 31);
10: 
11: // contract002のLifeTime.EndDate はどうなる? 
   ・ 
   ・ 

もしDateRangeクラスが不変性を担保していない場合、contract002オブジェクトのLifeTime.EndDateは当然12ヶ月後の日付となってしまう。しかしこれはcontract002にとって望ましいことではない。
このように、オブジェクトを共有すると問題がある場合にValueObjectを使うのである。前編で説明したstirng型のことを思い出してほしい。とある文字列を変更したら、変更したつもりの無いオブジェクトの文字列まで変更されてしまってびっくりした、なんて経験は無いはずだ。

いかがだっただろうか。ValueObjectはDDDの基本中の基本である。だが、ここで疑問が生じるのではないだろうか。ValueObjectを使用するのはいいが、Entityのプロパティにいつ、どうやってValueObjectをセットするのだろうか。次回はこの問題を解決するために必要なRepositoryパターンについて解説したいと思う。

この記事に星をつける

おすすめ度
スタイル

こんにちは

コメントするには InfoQアカウントの登録 または が必要です。InfoQ に登録するとさまざまなことができます。

アカウント登録をしてInfoQをお楽しみください。

あなたの意見をお聞かせください。

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする
コミュニティコメント

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする

ディスカッション

InfoQにログインし新機能を利用する


パスワードを忘れた方はこちらへ

Follow

お気に入りのトピックや著者をフォローする

業界やサイト内で一番重要な見出しを閲覧する

Like

より多いシグナル、より少ないノイズ

お気に入りのトピックと著者を選択して自分のフィードを作る

Notifications

最新情報をすぐ手に入れるようにしよう

通知設定をして、お気に入りコンテンツを見逃さないようにしよう!

BT