最近いくつかのGregYoungビデオを見ていて、ドメインオブジェクトのセッターに対して否定的な態度がある理由を理解しようとしています。ドメインオブジェクトは、DDDのロジックで「重い」はずだと思いました。悪い例の良い例がオンラインにあり、それを修正する方法はありますか?どんな例や説明も良いです。これは、CQRS方式で保存されたイベントにのみ適用されますか、それともすべてのDDDに適用されますか?
7 に答える
他の理由で不変条件に関するロジャー アルシングの回答を補完するために、この回答に貢献しています。
意味情報
Roger は、プロパティ セッターはセマンティック情報を保持しないことを明確に説明しました。Post.PublishDate のようなプロパティのセッターを許可すると、投稿が公開されているかどうか、または公開日が設定されている場合にのみわからないため、混乱が生じる可能性があります。記事を公開するのにこれだけで十分かどうかはわかりません。オブジェクトの「インターフェース」は、その「意図」を明確に示していません。
ただし、「有効」のようなプロパティは、「取得」と「設定」の両方に十分な意味を持っていると思います。これはすぐに有効になるはずであり、プロパティ セッターにセマンティクスがないという理由だけで、SetActive() または Activate()/Deactivate() メソッドの必要性がわかりません。
不変条件
Roger はまた、プロパティ セッターを介して不変条件を破る可能性についても話しました。これはまったく正しいことであり、読み取り専用プロパティとして「結合された不変値」(ロジャーの例を使用する三角形の角度など) を提供するために連携して機能するプロパティを作成し、それらをすべて一緒に設定するメソッドを作成する必要があります。 1 つのステップですべての組み合わせを検証します。
プロパティ順序の初期化/依存関係の設定
これは、一緒に検証/変更する必要があるプロパティで問題が発生するため、不変条件の問題に似ています。次のコードを想像してください。
public class Period
{
DateTime start;
public DateTime Start
{
get { return start; }
set
{
if (value > end && end != default(DateTime))
throw new Exception("Start can't be later than end!");
start = value;
}
}
DateTime end;
public DateTime End
{
get { return end; }
set
{
if (value < start && start != default(DateTime))
throw new Exception("End can't be earlier than start!");
end = value;
}
}
}
これは、アクセス順序の依存関係を引き起こす「セッター」検証の単純な例です。次のコードは、この問題を示しています。
public void CanChangeStartAndEndInAnyOrder()
{
Period period = new Period(DateTime.Now, DateTime.Now);
period.Start = DateTime.Now.AddDays(1); //--> will throw exception here
period.End = DateTime.Now.AddDays(2);
// the following may throw an exception depending on the order the C# compiler
// assigns the properties.
period = new Period()
{
Start = DateTime.Now.AddDays(1),
End = DateTime.Now.AddDays(2),
};
// The order is not guaranteed by C#, so either way may throw an exception
period = new Period()
{
End = DateTime.Now.AddDays(2),
Start = DateTime.Now.AddDays(1),
};
}
期間オブジェクトの終了日を過ぎて開始日を変更することはできないため (両方の日付が default(DateTime) に設定された「空の」期間でない限り - はい、それは素晴らしい設計ではありませんが、私が何を得るかつまり...) 最初に開始日を設定しようとすると、例外がスローされます。
オブジェクト初期化子を使用すると、さらに深刻になります。C# は代入順序を保証しないため、安全な仮定を行うことはできず、コンパイラの選択に応じて、コードが例外をスローする場合とスローしない場合があります。悪い!
これは、最終的にはクラスのデザインの問題です。プロパティは両方の値を更新していることを「認識」できないため、両方の値が実際に変更されるまで検証を「オフ」にすることはできません。両方のプロパティを読み取り専用にし、両方を同時に設定する方法を提供する (オブジェクト初期化子の機能を失う) か、プロパティから検証コードを完全に削除する (おそらく IsValid のような別の読み取り専用プロパティを導入するか、検証する) 必要があります。必要なときはいつでも)。
ORM「水分補給」*
簡単に言えば、ハイドレーションとは、永続化されたデータをオブジェクトに戻すことを意味します。私にとって、これはプロパティ セッターの背後にロジックを追加する際の最大の問題です。
多く/ほとんどの ORM は、永続化された値をプロパティにマップします。プロパティ セッター内のオブジェクトの状態 (他のメンバー) を変更する検証ロジックまたはロジックがある場合、「不完全な」オブジェクト (まだ読み込まれているオブジェクト) に対して検証を試みることになります。フィールドが「水和」される順序を制御できないため、これはオブジェクトの初期化の問題に非常に似ています。
ほとんどの ORM では、永続性をプロパティではなくプライベート フィールドにマップできます。これにより、オブジェクトをハイドレートできますが、検証ロジックのほとんどがプロパティ セッター内にある場合は、ロードされたオブジェクトが有効であることを確認するために、他の場所に複製する必要がある場合があります。か否か。
多くの ORM ツールは遅延読み込み (ORM の基本的な側面です!) をサポートしているため、仮想プロパティ (またはメソッド) を使用してフィールドにマッピングすると、ORM がフィールドにマッピングされたオブジェクトを遅延読み込みできなくなります。
結論
したがって、最後に、コードの重複を避けるために、ORM が可能な限り最高のパフォーマンスを発揮できるようにし、フィールドが設定される順序に応じて予想外の例外が発生するのを防ぎます。プロパティ セッターからロジックを移動することが賢明です。
私はまだ、この「検証」ロジックがどこにあるべきかを考えています。オブジェクトの不変の側面をどこで検証しますか? 高レベルの検証をどこに置くか? ORM でフックを使用して検証 (OnSave、OnDelete など) を実行しますか? Etc. Etc. Etc. しかし、これはこの回答の範囲ではありません。
セッターはセマンティック情報を伝達しません。
例えば
blogpost.PublishDate = DateTime.Now;
投稿が公開されたことを意味しますか?それとも、公開日が設定されているだけですか?
検討:
blogpost.Publish();
これは、ブログ投稿を公開する意図を明確に示しています。
また、セッターはオブジェクトの不変条件を壊す可能性があります。たとえば、「Triangle」エンティティがある場合、不変条件は、すべての角度の合計が180度である必要があるということです。
Assert.AreEqual (t.A + t.B + t.C ,180);
セッターがあれば、不変条件を簡単に破ることができます。
t.A = 0;
t.B = 0;
t.C = 0;
つまり、すべての角度の合計が0である三角形があります...それは本当に三角形ですか?いいえと思います。
セッターをメソッドに置き換えると、不変条件を維持する必要が生じる可能性があります。
t.SetAngles(0,0,0); //should fail
このような呼び出しは、エンティティに無効な状態を引き起こすことを通知する例外をスローする必要があります。
したがって、セッターの代わりにメソッドを使用してセマンティクスと不変条件を取得します。
この背後にある理由は、エンティティ自体がその状態の変更を担当する必要があるためです。エンティティ自体の外側にプロパティを設定する必要がある理由は特にありません。
簡単な例は、名前を持つエンティティです。パブリック セッターがある場合は、アプリケーションのどこからでもエンティティの名前を変更できます。代わりにそのセッターを削除しChangeName(string name)
、エンティティのようなメソッドを配置すると、名前を変更する唯一の方法になります。名前を変更する方法は 1 つしかないため、名前を変更したときに常に実行される任意の種類のロジックを追加できます。これは、名前を何かに設定するよりもはるかに明確です。
基本的にこれは、状態を非公開に保ちながら、エンティティの動作を公開することを意味します。
元の質問には .net というタグが付けられているため、エンティティをビューに直接バインドしたいコンテキストに対して実用的なアプローチを提出します。
それは悪い習慣であり、おそらくビューモデル(MVVMのように)などを用意する必要があることは知っていますが、一部の小さなアプリでは、IMHOを過度にパターン化しないことが理にかなっています.
プロパティを使用することは、.net ですぐに使えるデータ バインディングが機能する方法です。おそらくコンテキストは、データ バインディングが両方の方法で機能することを規定しているため、セッター ロジックの一部として INotifyPropertyChanged を実装し、PropertyChanged を発生させることは理にかなっています。
クライアントが無効な値を設定すると、エンティティはたとえば、壊れたルールのコレクションなどに項目を追加できます (CSLA には数年前にその概念があり、おそらくまだそうであることは知っています)。そのコレクションは UI に表示されます。作業単位は、後で無効なオブジェクトを永続化することを拒否します。
私は、デカップリングや不変性などを大いに正当化しようとしています。いくつかのコンテキストでは、より単純なアーキテクチャが求められていると言っているだけです。
セッターは単純に値を設定します。であってはなりません"heavy" with logic
。
わかりやすい名前を持つオブジェクトのメソッドは"heavy" with logic
、ドメイン自体に類似した名前を持つ必要があります。
Eric Evansによる DDD の本とBertrand Meyer による Object-Oriented Software Construction を読むことを強くお勧めします。必要なサンプルがすべて揃っています。