私はドメイン駆動設計に非常に慣れていません。ドメインの一部に遭遇しましたが、可能な限り最善の方法でモデル化したかどうかはわかりません。
メンバーシップタイプを持つMembership
エンティティがあります。
public enum MembershipType
{
TypeA,
TypeB,
TypeC,
TypeD,
}
各Membership
は1つまたはいくつかのBusinesses
(最大2)を持つことができます。 TypeA
、TypeB
、およびTypeC
は1つしかBusiness
を持ちませんが、TypeD
は2つのビジネスを持っている必要があります。この要件のため、次のようにMembership
エンティティ内にanIList<Business>
が含まれ、ビジネスの配列を取得するAddBusinesses
というメソッドがあります。簡単に言うと、MembershipType
enum
はMembership
エンティティの動作を変更し、これを可能な限り最善の方法でモデル化したのか、これに取り組むためのより良い方法があるのかわかりません。 。
public class Membership
{
public MembershipType MembershipType {get; protected set;}
private IList<Business> _businesses;
public IReadOnlyList<Business> Businesses => _businesses;
public Membership(MembershipType membershipType)
{
MembershipType = membershipType;
}
public void AddBusinesses(param Business[] businesses)
{
if(MembershipType == MembershipType.TypeD && businesses.Length != 2)
throw new InvalidOperationException("Membership Type D require 2 businesses");
else if(businesses.Length != 1)
throw new InvalidOperationException($"MembershipType {MembershipType} require 1 business");
foreach(var business in businesses)
{
_businesses.Add(business);
}
}
}
MembershipType
はほとんど変更されず、変更されても、既存の変更ではなく、新しいMembershipType
のみになります。
どんな助けも大歓迎です。
[〜#〜]更新[〜#〜]
皆さん、私の質問に答えてくれてありがとう。私はあなたの答えから多くを学びました。この質問でドメインの一部を省略したのは私が犯した間違いだと思います(申し訳ありません)。
Business
エンティティがない場合、Membership
がないと意味がありません。つまり、最初にBusiness
を作成せずにMembership
を作成することはできません。 Businessには、Membership.MembershipType
と呼ばれるBusinessType
に依存する不変条件があります。
一部の純粋主義者は、これは本質的に悪いデザインだと主張するかもしれません。私の意見はこれについてそれほど厳密ではありません。
プログラミングにおける多くのことのように、許容できるものと許容できないもののしきい値があります。極端を探りましょう:
許可されない:
列挙型に基づいて、すべてのプロパティ/メソッドで完全に異なる動作をするエンティティ。
このような場合、基本的にいくつかのseparateエンティティを1つのエンティティにマージし、enumを使用して、使用するエンティティ動作を基本的に選択します。
許容:
プロパティ/メソッドのsmallパーセントを除いて、列挙型に関係なく同じように動作するエンティティ。
この場合、エンティティはまだ同じ役割を表しており、列挙型はエンティティ全体にわずかな影響しか与えません。
ここでの良い例は、性別に基づく人の名称です。
_public string FormalName
{
get
{
switch(this.Gender)
{
case Gender.Male:
return $"Mr {this.LastName}";
case Gender.Female:
if(this.IsMarried)
return $"Mrs {this.LastName}";
else
return $"Ms {this.LastName}";
}
}
}
_
これはあなたの質問の法案に適合します、それはそのエンティティの動作を変更する列挙型です。ただし、変更自体はエンティティの目的のほんの一部にすぎないため、リファクタリングの必要性に関して無視できます。
より抽象的な例については、一部の純粋主義者は継承/インターフェース実装/構成を主張するかもしれません。私には、この立場を間違いなく主張する同僚がいます。
しかし、私はそれが過度に設計されていることを発見したので、まず抽象化が本当に正当化されるかどうかを評価します。この場合、私はそれが正当であるとは思いません。
それであなたはどこにスペクトルに落ちますか?
列挙型の影響を受けないクラスの一部を省略しているため、評価するのは少し難しいです。
私が見る限り、列挙型はエンティティのvalidationのみを決定します。これは、2つの異なる列挙型間で完全に異なるフィールド/値を本質的に使用しない限り、許容されます。
ここで代替案のいずれかが優れているかどうかを自問してください。
コードレビュー
ただし、メソッドの目的自体については疑問に思います。
_public void AddBusinesses(param Business[] businesses)
{
if(MembershipType == MembershipType.TypeD && businesses.Length != 2)
throw new InvalidOperationException("Membership Type D require 2 businesses");
else if(businesses.Length != 1)
throw new InvalidOperationException($"MembershipType {MembershipType} require 1 business");
//...
}
_
このメソッドは、AddBusinesses()
が呼び出されると想定しているようです。ビジネスの完全なリストで提供されていること。
これは、十分なビジネスが提供されなかった場合にすぐに例外をスローするという事実に基づいて推測しています。 __businesses
_に既に1つのアイテムが含まれていて、2番目のアイテムを追加するメソッドを使用する場合、TypeD
の検証エラーは発生しないはずですが、いずれにしてもスローされます。
ただし、一般的にはAdd()
メソッドもチェーンできることを期待しています。私がこれを行うと仮定します:
_var myMembership = new Membership() { MembershipType = MembershipType.TypeD };
foreach(var business in myListOfFiveBusinesses)
{
if(business.NeedsToBeAdded)
myMembership.AddBusinesses(business);
}
_
最初のビジネスを追加すると例外がスローされるため、このコードは失敗します。あなたが私がもっとビジネスを追加するかどうか見るのを待つことはありません!
奇妙なことに、残りのコードは、このメソッドが複数回呼び出される可能性があるという考えの下で機能しているように見えます。
_ foreach(var business in businesses)
{
_businesses.Add(business);
}
_
このコードは、ビジネスを__businesses
_の既存リストに追加することに注意してください。
既存のリストが上書きされる場合は、ここで別のコードを期待します。
__businesses = businesses; //out with the old list!
_
現在のAddBusinesses()
メソッドが一貫して使用されていないようです。検証では、指定されたパラメーターoverwriteが既存のリストであると想定しています。ただし、後続のロジックは、指定されたパラメーターが既存のリストに対して追加であることを前提としています。
私の考えでは、SOLID)の5つすべてを適用できます。DDDを始める前に(これは高レベルのビューです。)SOLIDとモデルのスコープの設計の経験を積んでください。そして、それらは最善の方法ではありません。最も適切な実装を見つけてください。そして、ここが私の興味です。
public interface IBusiness
{
}
public enum MembershipType
{
TypeA,
TypeB,
TypeC,
TypeD,
}
public interface IMemberShip
{
MembershipType Type { get; }
}
public abstract class MemberShip : IMemberShip
{
public abstract MembershipType Type { get; }
}
public abstract class SingleBusinessMemberShip : MemberShip
{
protected SingleBusinessMemberShip(IBusiness business)
{
Business = business ?? throw new ArgumentNullException(nameof(business));
}
public IBusiness Business { get; }
}
public abstract class MultipleBusinessesMemberShip : MemberShip
{
protected MultipleBusinessesMemberShip(IList<IBusiness> businesses)
{
Businesses = businesses ?? throw new ArgumentNullException(nameof(businesses));
}
public IList<IBusiness> Businesses { get; }
}
メンバーシップサブクラスは、必要に応じてビジネスを必要とする必要があり、それは単一のビジネスまたは複数のビジネスに依存します
public class TypeAMemberShip1 : SingleBusinessMemberShip
{
public TypeAMemberShip1(IBusiness business) : base(business)
{
}
public override MembershipType Type => MembershipType.TypeA;
}
public class TypeAMemberShip2 : SingleBusinessMemberShip
{
public TypeAMemberShip2(IBusiness business) : base(business)
{
}
public override MembershipType Type => MembershipType.TypeA;
}
public class TypeDMemberShip1 : MultipleBusinessesMemberShip
{
public TypeDMemberShip1(IList<IBusiness> businesses) : base(businesses)
{
}
public override MembershipType Type => MembershipType.TypeD;
}
public class TypeDMemberShip2 : MultipleBusinessesMemberShip
{
public TypeDMemberShip2(IList<IBusiness> businesses) : base(businesses)
{
}
public override MembershipType Type => MembershipType.TypeD;
}
ここで必要なのは、Open/Close原理を適切に適用することです
membershipType enumはMembershipエンティティの動作を変更します
MembershipType
が適切な抽象化でカプセル化されている場合はそうではありません。 Businesses
クラスを作成します。これにより、必要なビジネスの最小数と最大数が自然にわかります。次に、Membership
は知っているだけです-ビジネスオブジェクトから通知されます-Businesses
が何であるかに関係なく、MembershipType
がいっぱいかどうか。
MembershipType
モデルに何か不足していますか?
あなたの顧客がこれらの用語が許容できるビジネスの数だけを意味することを想像するのは難しいです。それ以上のものがあるに違いありません。もしそうなら、Businesses
クラスは、コンセプトをコードで拡張可能にするための良いスタートです。
結果として、Membership
はすべてのMembershipType
ishに対して完全に一貫したAPIになります。 AddBusinesses
メソッドに見られるような平凡な詳細を公開すると、拡張性が損なわれ、優れたAPIが強制終了されます。
建設
@BobDalgleishによって提案されているように、Businesses
コンストラクタを介してカスタムMembership
を注入するファクトリを使用します。 MembershipType
を使用して、何を構築するかをファクトリーに指示します。
MembershipType
値自体がそのクラスの一部になるかどうかは、設計上の問題です。ただし、Membership
または任意のBusinesses
クライアントに、Businesses
が自分のためにすべきことを実行させるように公開することで、漏れやすい抽象化しないでください。余計なお世話だ。