2011-11-25 17 views
22
public class Person 
{ 
    public IList<String> SpecialBirthPlaces; 
    public static readonly DateTime ImportantDate; 
    public String BirthPlace {get;set;} 

    public DateTime BirthDate 
    { 
     set 
     { 
      if (BirthPlace!=null && 
       value < ImportantDate && 
       SpecialBirthPlaces.Contains(BirthPlace)) 
      { 
       BirthPlace = DataBase.GetBirthPlaceFor(BirthPlace, value); 
      } 
     } 
    } 
} 

これは、ドメインモデルに単純なルールをカプセル化する試みです。私がキャプチャしようとしているルールは、何らかの理由で人の生年月日を更新したとき(元のユーザー入力に間違いがあったなど)、人の生年月日を確認し、それが私たちのデータベースに特別な出生地として記載されている場合は、データベースに保存されます。DDD(ドメインドリブンデザイン)、エンティティの状態変更の処理方法、大量のデータを処理するビジネスルールのカプセル化方法

しかし、私はそれを実装する2つの問題を持っている:

  1. この規則は、ドメインエンティティの状態(プロパティ)を変更し、私は、ユーザーインターフェイスでこの変更を反映する必要があります。私のドメインモデルはPOCOです。私はこのロジックをViewModelに入れることができますが、これはUIロジックではないので間違っています。それは私がキャプチャする必要がある重要なドメインルールです。

  2. 私のSpecialBirthPlaceのリストはかなり大きく、私はデータベースから顧客を得るたびにそれを設定したくありません。また、ルールが満たされている場合、出生地の代替品を取得する必要があります。私が言ったように、特別な出生地とその代替品のリストは非常に大きく、DBに保存されています。

DDDスタイルで必要なロジックを実装する方法は?

+0

私は本当にその質問を理解していません。大きなアイテムのリスト(SpecialBirthPlaces)をキャッシュする方法や、ドメインモデルへの変更を永続化する方法を尋ねていますか? DDDの観点からは、Bounded Contextは人の誕生日と場所の周りのルールの集合であり、Person集約ルートは日付と場所を変更できるメソッドを持つ必要があります。生年月日と出生地の変更により、データストアの更新につながるイベントが発生します。私は自分自身でDDDを初めて使っているので、おそらく完全にオフになっています。 – Jason

+2

出身地が1991年以前で、後に「サンクトペテルブルク」がある場合は、「レニングラード」を出生地として表示しようとしていましたか? –

答えて

0

IMOパフォーマンスに最も優れたアプローチは、db(SaveChanges()呼び出し)の変更をコミットするときに、dbおよびmarkエンティティのプロパティ変更イベントをストアドプロシージャを呼び出して呼び出すことです。その場合はObjectContext.ExecuteFunctionがあなたの友人です。

出生地検索と更新に関するすべてのロジックをそのsprocに入れてください。トランザクションにsprocが含まれていることを確認してください。更新が失敗した場合、変更がロールバックされます。

編集: 申し訳ありませんが、DDD関連の回答はありません。

+3

申し訳ありませんが、ポールがあなたの答えは、実際には私のケースとthats非常におかしく100%のincorectです - )まずはDDDとは何の関係もありません。 2.私はオブジェクトデータベースを使用していますが、私のエンティティはPOCOオブジェクトであり、INotifyPropertyChangedを実装していません。ObjectContexts(EF)を使用しません。propety変更イベントを呼び出すと、これまでに聞いたしかし、ポール、あなたは本当に私を怒らせました、私はあなたに答えを読んで前に、私は非常に落ち込んでいた、ありがとう。 –

+0

うれしい私はあなたを元気づけました!私は実際にonPropertyChangedEventハンドラからsprocを引き出すことを提案していませんでした - それは実際は恐ろしいでしょう。ポイントは実際にdbレベルでルックアップを行うことでした(bdはほとんど変更されないため、リストをメモリに保存する必要はありません)。 – Paul

+1

@Paul - DB側でルックアップを行うアイデアはもちろん、それは本当に単なる「contains」クエリなので妥当ですが、推奨するアプローチはドメイン駆動型アプローチに反するため、 DBのようなさまざまなレイヤーではなく、オブジェクト自体のすべての決定ロジックにより、非常に明確で保守可能なソフトウェアを作成することができます。 – codekaizen

4

この問題をカプセル化したのは、変更トラッキングで、作業単位パターンです。私は自分のDDDリポジトリを作業単位に関連付けました。リポジトリのいずれかから取得したエンティティの集合に対して、作業単位を照会して変更の対象を確認することができます。

大規模なコレクションについては、読み取り専用セットのようです。これを処理する1つの方法は、これがローカルにアクセスされた場合にこれをローカルにプリロードしてキャッシュすることです。リポジトリはメモリ内バージョンに対してクエリを実行できます。私はNHibernateを使用しており、このケースを扱うのは簡単です。 の方法がRAMに保存するには大きすぎる場合(100s以上のMBなど)、データベースに対してSpecialBirthPlaces.Contains(BirthPlace)クエリが実行されるように、おそらく特別なケースのリポジトリクエリが必要です(ストアドプロシージャ、ハ!)。文字列の大規模なコレクションではなく、エンティティのリポジトリとしてSpecialBirthPlacesを表現したいと思うでしょう。これは、 "クエリ"パターンを使ってオブジェクト全体をロードする必要がなくなります。この長い物語の後

は、ここでいくつかの例です:それはパラメータでわかりますので、あなたは修正の誕生日に渡す方法を持つ

public class BirthPlace 
{ 
    public String Name { get; set; } 
} 

public class SpecialBirthPlace : BirthPlace 
{ 
} 

public class Person 
{ 
    public static readonly DateTime ImportantDate; 
    public BirthPlace BirthPlace { get; set; } 

    public DateTime BirthDate 
    { 
     get; private set; 
    } 

    public void CorrectBirthDate(IRepository<SpecialBirthPlace> specialBirthPlaces, DateTime date) 
    { 
     if (BirthPlace != null && date < ImportantDate && specialBirthPlaces.Contains(BirthPlace)) 
     { 
      BirthPlace = specialBirthPlaces.GetForDate(date); 
     } 
    } 
} 

は、実際に出産を修正するために必要とされるものより優れたデザインですdate:SpecialBirthPlaceエンティティのリポジトリ(つまりコレクション)と正しい日付。この明示的な契約は、ドメインが何をしているのかを明確にし、エンティティの状態でコレクション全体を隠すというエンティティ契約を読むだけで、ビジネスニーズを明確にします。

エンティティにBirthPlaceを作成したので、ドメインモデルを少しフラットにする最適化がもう1つあるかもしれません。我々は実際にBirthPlaceを専門にする必要はありませんが、特別であるかどうかを示す必要があります。オブジェクトにプロパティを追加することができます(ドメインオブジェクトのプロパティを使いこなす人もいますが、特にLINQを使用するとクエリが簡単になるため)。その後、我々は完全にContainsクエリを取り除くことができます。

public class BirthPlace 
{ 
    public BirthPlace(String name, Boolean isSpecial = false) 
    { 
     Name = name; 
     IsSpecial = isSpecial 
    } 

    public String Name { get; private set; } 
    public Boolean IsSpecial { get; private set; } 
} 

public class Person 
{ 
    public static readonly DateTime ImportantDate; 
    public BirthPlace BirthPlace { get; set; } 

    public DateTime BirthDate 
    { 
     get; private set; 
    } 

    public void CorrectBirthDate(IRepository<BirthPlace> birthPlaces, DateTime date) 
    { 
     if (BirthPlace != null && date < ImportantDate && BirthPlace.IsSpecial) 
     { 
      BirthPlace = birthPlaces.GetForDate(date); 
     } 
    } 
} 
+0

これは私が考えていたものに近いですが、私はBirthdateを設定するだけではうまくいきませんが、BirthPlaceがまだ設定されていない(nullの場合)ようにはできません。 –

+0

コンストラクタでこれを行います。それがDDDの唯一の選択肢です。あなたのビジネスルールは、あなたが "それを設定する"ことはできないので、ドメインを壊すような技術的なイディオムを強制しないように指示します。 – codekaizen

+0

私はまだBirthPlaceを更新する必要がない場合、BirthDateを設定できないということについて少し不快ですが、あなたのコードのこの変更についてどう思いますか? http://pastebin.com/aF7v0a4z、確かにthrowinfの例外は行く方法ではありませんが、私は現時点では何も考えられません。そのため、モデルの消費者は、まずCorrectBirthDateの呼び出しが必要かどうかをチェックする必要があります。 –

3

私は文が異なる2について説明し、「それは私がキャプチャする必要がある重要なドメインのルールだ」「私は、ユーザーインターフェイスでこの変更を反映する必要がある」と考えます問題。明らかに、最初のものを解決する必要があります。第二のものがしていることは明らかではない。

ドメインモデルの他の部分がここでの変更点を知る必要がある場合は、ドメインイベント(たとえば、Udi Dahan's implementation)を調べることをお勧めします。また、BirthDateが設定されたときにBirthPlaceプロパティを設定する場合は、この操作を使用して、潜在的に長い操作である場合は非同期に設定することもできます。

それ以外の場合は、UIの問題を見てみましょう。まず、私のドメインモデルでは、各エンティティをインタフェースとして抽象化します。あなたがしない場合は、少なくともいくつかのプロパティを作成する必要がありますvirtual。また、IoC /ファクトリ/リポジトリなどのエンティティを生成/返す抽象レイヤを使用しています。私はこの層をドメインモデルそのものの範囲外にあると考えています。

ここで、ドメインエンティティのプロパティの変更をUIに通知するメカニズムが必要ですが、もちろんドメインモデル自体は閉鎖的なシステムです。新しいメンバーや動作を導入することは望ましくありません外部の関心事の必要性。

INotifyPropertyChangedを実装する実装で問題のエンティティを飾るとどうでしょうか?私たちはリポジトリでこれを行うことができました。これはドメインの範囲外であるため、ドメインモデル自体を変更することはなく、ドメインモデル外のシステムが必要とする機能を持つエンティティをラップするコンポジションのみを使用します。再び言い換えると、BirthPlaceの再計算は、ドメインモデルの関心事であり、UI通知ロジックはドメインモデルのの外にあるのままです。

それはこのようになります:

public class NotifyPerson : IPerson, INotifyPropertyChanged 
{ 
    readonly IPerson _inner; 

    public NotifyPerson(IPerson inner) // repository puts the "true" domain entity here 
    { 
     _inner = inner; 
    } 

    public DateTime BirthDate 
    { 
     set 
     { 
      if(value == _inner.BirthDate) 
       return; 

      var previousBirthPlace = BirthPlace; 
      _inner.BirthDate = value; 
      Notify("BirthDate"); 

      if(BirthPlace != previousBirthPlace) 
       Notify("BirthPlace"); 
     } 
    } 

    void Notify(string property) 
    { 
     var handler = PropertyChanged; 
     if(handler != null) handler(this, new PropertyChangedEventArgs(property)); 
    } 
} 

をインターフェースを使用していない場合は、代わりに単に_innerbaseにメンバーを呼び出し、Personから継承しBirthDateプロパティをオーバーライドします。

+0

あなたが書いたものは、ViewModel、wraping Modelのように見えます。しかし、BirthPlaceがBirthDateを設定した後に変更されているかどうかを確認するという考えがありました。私は、これがBirthDateの変更がBirthPlaceの変更をもたらす可能性のある重要な情報を隠していると思います。そして、ドメインイベントのような実体上の明白な出来事のいくつかは、この事実を開発者にとってより顕著にするでしょう。 –

+0

@AlexBurtsev ViewModelは特定のビューに適合します。一方、このオブジェクトは、アプリケーション全体でエンティティ*として使用されます。それがもたらす利点は、ドメインモデルを元の状態に保つことだけです。その行動はここに隠されていない。それはモデルに隠されているので、ドメインイベントは良い代替ソリューションだと私は思っています。しかし、あなたはまだUIを更新するという問題があります。ここに示したようなことをしたり、ドメインモデルの外からドメインイベントを購読したりする必要があります。 – Jay

+0

ドメイン外からのドメインイベントを購読するのはどうですか?それは悪い習慣ですか? –

1

次に、実装例を示します。この実装は、いくつかの層で構成されています。ドメイン層、サービス層、およびプレゼンテーション層です。このサービスレイヤの目的は、プレゼンテーションレイヤーやWebサービスなど、他のレイヤーにドメインレイヤーの機能を公開することです。そのために、そのメソッドはドメインレイヤーで処理できる特定のコマンドに対応しています。特に私たちは誕生日を変更する命令を持っています。さらに、この実装ではUdi Dahanのバージョンdomain event frameworkを使用しています。これは、誕生日の変更に関連するビジネスロジックからドメインエンティティを切り離すために行われます。これはメリットと欠点の両方とみなすことができます。欠点は、ビジネスロジック全体が複数のクラスにまたがっていることです。利点は、ドメインイベントの処理方法に柔軟性を得ることです。さらに、この方法は、補助機能を実行するBirthDateChangedEventに加入者を追加できるので、より拡張性があります。さらに、Udiの実装の背後にある推論に貢献したもう1つのメリットは、Personエンティティが、ドメインエンティティの範囲外であるように見えるリポジトリを意識する必要がなくなったことです。全体として、この実装ではかなりのインフラストラクチャが必要ですが、ドメインに大きく投資することを考えているのであれば、最初のトラブルに見合う価値があります。また、この実装では、ASP.NET MVCベースのプレゼンテーションレイヤが想定されていました。ステートフルUIでは、プレゼンテーションロジックを変更する必要があり、ViewModelは変更通知を提供する必要があります。

/// <summary> 
/// This is your main entity, while it may seem anemic, it is only because 
/// it is simplistic. 
/// </summary> 
class Person 
{ 
    public string Id { get; set; } 
    public string BirthPlace { get; set; } 

    DateTime birthDate; 

    public DateTime BirthDate 
    { 
     get { return this.birthDate; } 
     set 
     { 
      if (this.birthDate != value) 
      { 
       this.birthDate = value; 
       DomainEvents.Raise(new BirthDateChangedEvent(this.Id)); 
      } 
     } 
    } 
} 

/// <summary> 
/// Udi Dahan's implementation. 
/// </summary> 
static class DomainEvents 
{ 
    public static void Raise<TEvent>(TEvent e) where TEvent : IDomainEvent 
    { 
    } 
} 

interface IDomainEvent { } 

/// <summary> 
/// This is the interesting domain event which interested parties subscribe to 
/// and handle in special ways. 
/// </summary> 
class BirthDateChangedEvent : IDomainEvent 
{ 
    public BirthDateChangedEvent(string personId) 
    { 
     this.PersonId = personId; 
    } 

    public string PersonId { get; private set; } 
} 

/// <summary> 
/// This can be associated to a Unit of Work. 
/// </summary> 
interface IPersonRepository 
{ 
    Person Get(string id); 
    void Save(Person person); 
} 

/// <summary> 
/// This can implement caching for performance. 
/// </summary> 
interface IBirthPlaceRepository 
{ 
    bool IsSpecial(string brithPlace); 
    string GetBirthPlaceFor(string birthPlace, DateTime birthDate); 
} 

interface IUnitOfWork : IDisposable 
{ 
    void Commit(); 
} 

static class UnitOfWork 
{ 
    public static IUnitOfWork Start() 
    { 
     return null; 
    } 
} 

class ChangeBirthDateCommand 
{ 
    public string PersonId { get; set; } 
    public DateTime BirthDate { get; set; } 
} 

/// <summary> 
/// This is the application layer service which exposes the functionality of the domain 
/// to the presentation layer. 
/// </summary> 
class PersonService 
{ 
    readonly IPersonRepository personDb; 

    public void ChangeBirthDate(ChangeBirthDateCommand command) 
    { 
     // The service is a good place to initiate transactions, security checks, etc. 
     using (var uow = UnitOfWork.Start()) 
     { 
      var person = this.personDb.Get(command.PersonId); 
      if (person == null) 
       throw new Exception(); 

      person.BirthDate = command.BirthDate; 

      // or business logic can be handled here instead of having a handler. 

      uow.Commit(); 
     } 
    } 
} 

/// <summary> 
/// This view model is part of the presentation layer. 
/// </summary> 
class PersonViewModel 
{ 
    public PersonViewModel() { } 

    public PersonViewModel(Person person) 
    { 
     this.BirthPlace = person.BirthPlace; 
     this.BirthDate = person.BirthDate; 
    } 

    public string BirthPlace { get; set; } 
    public DateTime BirthDate { get; set; } 
} 

/// <summary> 
/// This is part of the presentation layer. 
/// </summary> 
class PersonController 
{ 
    readonly PersonService personService; 
    readonly IPersonRepository personDb; 

    public void Show(string personId) 
    { 
     var person = this.personDb.Get(personId); 
     var viewModel = new PersonViewModel(person); 
     // UI framework code here. 
    } 

    public void HandleChangeBirthDate(string personId, DateTime birthDate) 
    { 
     this.personService.ChangeBirthDate(new ChangeBirthDateCommand { PersonId = personId, BirthDate = birthDate }); 
     Show(personId); 
    } 
} 

interface IHandle<TEvent> where TEvent : IDomainEvent 
{ 
    void Handle(TEvent e); 
} 

/// <summary> 
/// This handler contains the business logic associated with changing birthdates. This logic may change 
/// and may depend on other factors. 
/// </summary> 
class BirthDateChangedBirthPlaceHandler : IHandle<BirthDateChangedEvent> 
{ 
    readonly IPersonRepository personDb; 
    readonly IBirthPlaceRepository birthPlaceDb; 
    readonly DateTime importantDate; 

    public void Handle(BirthDateChangedEvent e) 
    { 
     var person = this.personDb.Get(e.PersonId); 
     if (person == null) 
      throw new Exception(); 

     if (person.BirthPlace != null && person.BirthDate < this.importantDate) 
     { 
      if (this.birthPlaceDb.IsSpecial(person.BirthPlace)) 
      { 
       person.BirthPlace = this.birthPlaceDb.GetBirthPlaceFor(person.BirthPlace, person.BirthDate); 
       this.personDb.Save(person); 
      } 
     } 
    } 
} 
+5

私はあなたの実装が好きです、それは良いコードですが、それはDDDではありません。ここであなたがやっていることは、ドメインモデル自体を使用せずに、DDDのインフラストラクチャと技術面を借りることです。そして私は人々がそれをたくさんするのを見る。ドメインロジックはサービス内に散在し、ドメインエンティティはDTOだけです。実際のシステムではトランザクション、外部サービス、ドメインロジックを処理する必要があるため、これは私の意見では正当なパターンであり、DDDの例で示したように単純ではありません。 –

+4

DomainEvents.RaiseはServiceLocator.Resolveの単なる名です。().GetBirthPlaceFor(...) –

関連する問題