web-dev-qa-db-ja.com

S.O.L.I.D.、貧血ドメイン、依存性注入を回避しますか?

これはプログラミング言語にとらわれない質問かもしれませんが、.NETエコシステムを対象とした回答に興味があります。

これがシナリオです。行政のための単純なコンソールアプリケーションを開発する必要があると仮定します。アプリケーションは、車両税についてです。彼らは(のみ)以下のビジネスルールを持っています:

1.a)車両が自動車で、所有者が最後に税金を支払ったのが30日前である場合、所有者は再度支払う必要があります。

1.b)車両がバイクで、所有者が最後に税金を支払ったのが60日前である場合、所有者は再度支払う必要があります。

つまり、車を持っている場合は30日ごと、バイクを持っている場合は60日ごとに支払う必要があります。

システム内の各車両について、アプリケーションはそれらのルールをテストし、それらを満たさない車両(プレート番号と所有者情報)を印刷する必要があります。

私が欲しいのは:

2.a)S.O.L.I.D.に準拠原則(特にオープン/クローズド原則)。

私が欲しくないのは(私が思う):

2.b)貧血ドメイン。したがって、ビジネスロジックはビジネスエンティティ内にある必要があります。

私はこれから始めました:

public class Person
// You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
{
    public string Name { get; set; }

    public string Surname { get; set; }
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Person Owner { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract bool HasToPay();
}

public class Car : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class Motorbike : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public class PublicAdministration
{
    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return this.GetAllVehicles().Where(vehicle => vehicle.HasToPay());
    }

    private IEnumerable<Vehicle> GetAllVehicles()
    {
        throw new NotImplementedException();
    }
}

class Program
{
    static void Main(string[] args)
    {
        PublicAdministration administration = new PublicAdministration();
        foreach (var vehicle in administration.GetVehiclesThatHaveToPay())
        {
            Console.WriteLine("Plate number: {0}\tOwner: {1}, {2}", vehicle.PlateNumber, vehicle.Owner.Surname, vehicle.Owner.Name);
        }
    }
}

2.a:オープン/クローズの原則が保証されます。彼らが車両税から継承する自転車税が必要な場合は、HasToPayメソッドをオーバーライドすれば完了です。単純な継承で満たされたオープン/クローズの原則。

「問題」は次のとおりです。

3.a)なぜ車両は支払わなければならないかどうかを知る必要があるのですか? PublicAdministrationの問題ではありませんか?自動車税の行政規則が変更された場合、なぜ自動車が変更されなければならないのですか? 「それでは、なぜHasToPayメソッドをVehicle内に配置するのですか」と尋ねる必要があると思います。答えは、PublicAdministration内の車両タイプ(typeof)をテストしたくないためです。より良い代替案はありますか?

3.b)たぶん、納税が車両の懸念であるか、あるいは私の最初のPerson-Vehicle-Car-Motorbike-PublicAdministrationの設計がまったく間違っているか、あるいは哲学者とより優れたビジネスアナリストが必要です。解決策は次のとおりです。HasToPayメソッドを別のクラスに移動します。これをTaxPaymentと呼びます。次に、2つのTaxPayment派生クラスを作成します。 CarTaxPaymentおよびMotorbikeTaxPayment。次に、(Paymentタイプの)抽象PaymentプロパティをVehicleクラスに追加し、CarクラスとMotorbikeクラスから適切なTaxPaymentインスタンスを返します。

public abstract class TaxPayment
{
    public abstract bool HasToPay();
}

public class CarTaxPayment : TaxPayment
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class MotorbikeTaxPayment : TaxPayment
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Person Owner { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract TaxPayment Payment { get; }
}

public class Car : Vehicle
{
    private CarTaxPayment payment = new CarTaxPayment();
    public override TaxPayment Payment
    {
        get { return this.payment; }
    }
}

public class Motorbike : Vehicle
{
    private MotorbikeTaxPayment payment = new MotorbikeTaxPayment();
    public override TaxPayment Payment
    {
        get { return this.payment; }
    }
}

そして、このように古いプロセスを呼び出します。

    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
    }

しかし、CarTaxPayment/MotorbikeTaxPayment/TaxPayment内にLastPaidTimeメンバーがないため、このコードはコンパイルされません。 CarTaxPaymentとMotorbikeTaxPaymentは、ビジネスエンティティではなく、「アルゴリズムの実装」のように見え始めています。どういうわけか、これらの「アルゴリズム」にはLastPaidTime値が必要です。確かに、この値をTaxPaymentのコンストラクターに渡すことはできますが、それは車両のカプセル化、または責任、またはOOP伝道者がそれを呼び出すもの)に違反しますね。

3.c)Entity Framework ObjectContext(ドメインオブジェクトを使用するPerson、Vehicle、Car、Motorbike、PublicAdministration)がすでにあるとします。 PublicAdministration.GetAllVehiclesメソッドからObjectContext参照を取得して、機能を実装するにはどうすればよいですか?

3.d)CarTaxPayment.HasToPayに対して同じObjectContext参照が必要であるが、MotorbikeTaxPayment.HasToPayに対して別の参照が必要な場合、「注入」を担当する人、またはそれらの参照を支払いクラスにどのように渡すのでしょうか。この場合、貧血のドメイン(つまり、サービスオブジェクトがない)を回避して、私たちを災害に導きませんか?

この単純なシナリオの設計上の選択肢は何ですか?明らかに、ここで示した例は簡単なタスクでは「複雑すぎる」が、同時にS.O.L.I.D.原則と貧血領域(それらは破壊するために委託されている)を防ぎます。

33

私は、人も車両も支払期日であるかどうかを知るべきではないと言います。

問題のあるドメインを再検討する

問題のあるドメイン「車を持っている人」を見ると、車を購入するために、ある人から別の人に所有権を譲渡する何らかの契約があり、車も人もそのプロセスで変更されません。あなたのモデルはそれを完全に欠いています。

思考実験

夫婦がいると仮定して、男は車を所有し、税金を払っています。今や男は亡くなり、妻は車を相続して税金を払う。それでも、あなたの皿は同じままです(少なくとも私の国ではそうです)。まだ同じ車です!ドメインでそれをモデル化できるはずです。

=>所有権

誰も所有していない車も車ですが、誰も税金を払いません。実際、あなたは車に税金を支払うのではなく、車を所有する特権に対して税金を支払います。だから私の命題は:

public class Person
{
    public string Name { get; set; }

    public string Surname { get; set; }

    public List<Ownership> getOwnerships();

    public List<Vehicle> getVehiclesOwned();
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Ownership currentOwnership { get; set; }

}

public class Car : Vehicle {}

public class Motorbike : Vehicle {}

public abstract class Ownership
{
    public Ownership(Vehicle vehicle, Owner owner);

    public Vehicle Vehicle { get;}

    public Owner CurrentOwner { get; set; }

    public abstract bool HasToPay();

    public DateTime LastPaidTime { get; set; }

   //Could model things like payment history or owner history here as well
}

public class CarOwnership : Ownership
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class MotorbikeOwnership : Ownership
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public class PublicAdministration
{
    public IEnumerable<Ownership> GetVehiclesThatHaveToPay()
    {
        return this.GetAllOwnerships().Where(HasToPay());
    }

}

これは完璧とはほど遠く、おそらくリモートで修正するC#コードすらありませんが、あなたがそのアイデアを理解してくれることを願っています(誰かがこれをクリーンアップできます)。唯一の欠点は、CarOwnershipCarの間に正しいPersonを作成するためのファクトリまたは何かがおそらく必要になることです。

15
sebastiangeiger

このような状況では、ビジネスルールをターゲットオブジェクトの外部に存在させたい場合、型のテストは許容範囲を超えています。

確かに、多くの場合、戦略パターンを使用してオブジェクトに処理を任せる必要がありますが、それが常に望ましいとは限りません。

現実の世界では、店員は車両の種類を調べ、それに基づいて税データを検索します。ソフトウェアが同じビジネスルールに従わないのはなぜですか?

4

私が欲しくないのは(私が思う):

2.b)貧困ドメイン。これは、ビジネスエンティティ内のビジネスロジックを意味します。

これは逆です。貧血ドメインとは、ロジックがoutsideビジネスエンティティであるドメインです。追加のクラスを使用してロジックを実装することが悪い考えである理由はたくさんあります。そして、なぜあなたがそれをするべきなのかについてのカップルだけです。

したがって、貧血と呼ばれる理由:クラスには何も含まれていません。

私がすること:

  1. 抽象Vehicleクラスをインターフェースに変更します。
  2. 車両が実装する必要があるITaxPaymentインターフェイスを追加します。 HasToPayをここに置いておいてください。
  3. MotorbikeTaxPayment、CarTaxPayment、TaxPaymentクラスを削除します。それらは不必要な複雑化/乱雑です。
  4. 各車両タイプ(車、自転車、キャンピングカー)は、最低限の車両を実装する必要があり、課税される場合は、ITaxPaymentも実装する必要があります。このようにして、課税されていない車両と課税されている車両を区別できます。この状況が存在すると仮定します。
  5. 各車両タイプは、クラス自体の境界内で、インターフェースで定義されたメソッドを実装する必要があります。
  6. また、ITaxPaymentインターフェイスに最後の支払い日を追加します。
3
NotMe

なぜ車両は支払わなければならないかどうかを知る必要があるのですか? PublicAdministrationの問題ではありませんか?

いいえ。私が理解しているように、あなたのアプリ全体は課税についてです。したがって、車両に課税する必要があるかどうかについての懸念は、PublicAdministrationの問題ではなく、プログラム全体の他の問題です。ここ以外に置く理由はありません。

_public class Car : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class Motorbike : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}
_

これらの2つのコードスニペットは、定数のみが異なります。 HasToPay()関数全体をオーバーライドする代わりに、int DaysBetweenPayments()関数をオーバーライドします。定数を使用する代わりにそれを呼び出します。これが実際に彼らが異なるすべてであるならば、私はクラスを落とすでしょう。

Motorbike/Carは、サブクラスではなく、vehicleのカテゴリプロパティであることもお勧めします。これにより、柔軟性が向上します。たとえば、バイクや車のカテゴリを簡単に変更できます。もちろん、自転車が実際の生活の中で自動車に変わることはまずありません。しかし、あなたは車両が間違って入力され、後で交換する必要があることを知っています。

確かに、この値をTaxPaymentのコンストラクターに渡すことはできますが、それは車両のカプセル化、または責任、またはOOP伝道者がそれを呼び出すもの)に違反しますね。

あんまり。パラメータとして意図的にデータを別のオブジェクトに渡すオブジェクトは、カプセル化の大きな問題ではありません。本当の懸念は、データを引き出したり、オブジェクト内のデータを操作したりするために、このオブジェクトの内部に到達する他のオブジェクトです。

2
Winston Ewert

あなたは正しい軌道に乗っているかもしれません。問題は、お気づきのように、LastPaidTimeメンバーがTaxPayment内にないです。それはまさにそれが属している場所ですが、公開する必要はまったくありません。

public interface ITaxPayment
{
    // Your base TaxPayment class will know the 
    // next due date. But you can easily create
    // one which uses (for example) fixed dates
    bool IsPaymentDue();
}

public interface IVehicle
{
    string Plate { get; }
    Person Owner { get; }
    ITaxPayment LastTaxPayment { get; }
} 

支払いを受け取るたびに、現在のポリシーに従ってITaxPaymentの新しいインスタンスを作成します。これには、以前のポリシーとは完全に異なるルールを設定できます。

1
Groo

私は 回答@sebastiangeiger に同意します。問題は実際には車両ではなく車両の所有権にあります。私は彼の答えを使用することをお勧めしますが、いくつかの変更があります。 Ownershipクラスをジェネリッククラスにします。

public class Vehicle {}

public abstract class Ownership<T>
{
    public T Item { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract TimeSpan PaymentInterval { get; }

    public virtual bool HasToPay
    {
        get { return (DateTime.Today - this.LastPaidTime) >= this.PaymentInterval; }
    }
}

また、継承を使用して、各車両タイプの支払い間隔を指定します。単純な整数の代わりに、強く型付けされたTimeSpanクラスを使用することもお勧めします。

public class CarOwnership : Ownership<Vehicle>
{
    public override TimeSpan PaymentInterval
    {
        get { return new TimeSpan(30, 0, 0, 0); }
    }
}

public class MotorbikeOwnership : Ownership<Vehicle>
{
    public override TimeSpan PaymentInterval
    {
        get { return new TimeSpan(60, 0, 0, 0); }
    }
}

これで、実際のコードは1つのクラス(Ownership<Vehicle>

public class PublicAdministration
{
    List<Ownership<Vehicle>> VehicleOwnerships { get; set; }

    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return VehicleOwnerships.Where(x => x.HasToPay).Select(x => x.Item);
    }
}
1
user74130

私はあなたにそれを壊すのは嫌いですが、あなたは貧血ドメイン、つまりオブジェクトの外のビジネスロジックを持っています。これがあなたがしようとしていることなら問題ありませんが、それを避ける理由について読むべきです。

問題ドメインをモデル化するのではなく、ソリューションをモデル化してください。そのため、並列継承階層が必要になり、必要以上に複雑になります。

このようなものはその例に必要なすべてです:

public class Vehicle
{
    public Boolean isMotorBike()
    public string PlateNumber { get; set; }
    public String Owner { get; set; }
    public DateTime LastPaidTime { get; set; }
}

public class TaxPaymentCalculator
{
    public List<Vehicle> GetVehiclesThatHaveToPay(List<Vehicle> all)
}

SOLIDそれは素晴らしいですが、ボブおじさんが言ったように、彼らはエンジニアリング原則です。これらは厳密に適用することを意図したものではなく、考慮すべきガイドラインです。

0
Garrett Hall

支払い期間は実際の自動車/バイクの特性ではないというのはあなたの言う通りだと思います。しかし、それは車両のクラスの属性です。

Type of objectでif/selectを使用することもできますが、これはあまりスケーラブルではありません。後でトラックとセグウェイおよびプッシュバイクとバスと飛行機を追加すると(可能性は低いかもしれませんが、公共サービスではわかりません)、状態が大きくなります。トラックのルールを変更したい場合は、そのリストから見つける必要があります。

では、文字どおりの属性を作り、リフレクションを使用してそれを引き出してみませんか?このようなもの:

[DaysBetweenPayments(30)]
public class Car : Vehicle
{
    // actual properties of the physical vehicle
}

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class DaysBetweenPaymentsAttribute : Attribute, IPaymentRequiredCalculator
{
    private int _days;

    public DaysBetweenPaymentAttribute(int days)
    {
        _days = days;
    }

    public bool HasToPay(Vehicle vehicle)
    {
        return vehicle.LastPaid.AddDays(_days) <= DateTime.Now;
    }
}

より快適であれば、静的変数を使用することもできます。

欠点は

  • 少し遅くなると思います。たくさんの車両を処理する必要があると思います。それが問題である場合、私はより実用的な見方をして、抽象的なプロパティまたはインターフェース化されたアプローチに固執するかもしれません。 (ただし、タイプ別のキャッシュも検討します。)

  • 起動時に、各VehicleサブクラスにIPaymentRequiredCalculator属性があること(またはデフォルトを許可すること)を検証する必要があります。これは、MotorbikeにPaymentPeriodがないためクラッシュするだけで、1000台の車を処理したくないためです。

良い点は、後で月単位または年単位の支払いに遭遇した場合に、新しい属性クラスを作成できることです。これがまさにOCPの定義です。

0
pdr