web-dev-qa-db-ja.com

列挙型はエンティティの動作を変更していますか?

私はドメイン駆動設計に非常に慣れていません。ドメインの一部に遭遇しましたが、可能な限り最善の方法でモデル化したかどうかはわかりません。

メンバーシップタイプを持つMembershipエンティティがあります。

public enum MembershipType
{
    TypeA,
    TypeB,
    TypeC,
    TypeD,
}

Membershipは1つまたはいくつかのBusinesses(最大2)を持つことができます。 TypeATypeB、およびTypeCは1つしかBusinessを持ちませんが、TypeDは2つのビジネスを持っている必要があります。この要件のため、次のようにMembershipエンティティ内にanIList<Business>が含まれ、ビジネスの配列を取得するAddBusinessesというメソッドがあります。簡単に言うと、MembershipTypeenumMembershipエンティティの動作を変更し、これを可能な限り最善の方法でモデル化したのか、これに取り組むためのより良い方法があるのか​​わかりません。 。

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に依存する不変条件があります。

3
Amila

一部の純粋主義者は、これは本質的に悪いデザインだと主張するかもしれません。私の意見はこれについてそれほど厳密ではありません。

プログラミングにおける多くのことのように、許容できるものと許容できないもののしきい値があります。極端を探りましょう:

許可されない

列挙型に基づいて、すべてのプロパティ/メソッドで完全に異なる動作をするエンティティ。

このような場合、基本的にいくつかの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つの異なる列挙型間で完全に異なるフィールド/値を本質的に使用しない限り、許容されます。

  • 列挙AがFieldAとFieldBを必要とする場合。列挙型BにはFieldCとFieldDが必要です。その場合、それらは実際には2つの完全に別個のエンティティーです。
  • 両方の列挙型が10個のフィールドを必要とし、必要な特定の1つのフィールドのみが異なる場合、これは(IMO)許容可能な動作です。

ここで代替案のいずれかが優れているかどうかを自問してください。

  • 列挙型の違いは、個別のエンティティを正当化するのに十分な大きさですか?
  • 列挙型の1つが別の(より一般化された)列挙型の「より具体的な」バージョンですか?もしそうなら、多分あなたはお互いから派生する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が既存のリストであると想定しています。ただし、後続のロジックは、指定されたパラメーターが既存のリストに対して追加であることを前提としています。

3
Flater

私の考えでは、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;
}
3
HungDL

ここで必要なのは、Open/Close原理を適切に適用することです

membershipType enumはMembershipエンティティの動作を変更します

MembershipTypeが適切な抽象化でカプセル化されている場合はそうではありません。 Businessesクラスを作成します。これにより、必要なビジネスの最小数と最大数が自然にわかります。次に、Membershipは知っているだけです-ビジネスオブジェクトから通知されます-Businessesが何であるかに関係なく、MembershipTypeがいっぱいかどうか。


MembershipTypeモデルに何か不足していますか?

あなたの顧客がこれらの用語が許容できるビジネスの数だけを意味することを想像するのは難しいです。それ以上のものがあるに違いありません。もしそうなら、Businessesクラスは、コンセプトをコードで拡張可能にするための良いスタートです。

結果として、MembershipはすべてのMembershipTypeishに対して完全に一貫したAPIになります。 AddBusinessesメソッドに見られるような平凡な詳細を公開すると、拡張性が損なわれ、優れたAPIが強制終了されます。


建設

@BobDalgleishによって提案されているように、Businessesコンストラクタを介してカスタムMembershipを注入するファクトリを使用します。 MembershipTypeを使用して、何を構築するかをファクトリーに指示します。

MembershipType値自体がそのクラスの一部になるかどうかは、設計上の問題です。ただし、Membershipまたは任意のBusinessesクライアントに、Businessesが自分のためにすべきことを実行させるように公開することで、漏れやすい抽象化しないでください。余計なお世話だ。

1
radarbob