ソリッド、貧血ドメインの回避、依存性注入?


33

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

これがシナリオです:行政用のシンプルなコンソールアプリケーションを開発する必要があるとします。アプリケーションは、車両税に関するものです。これらの(のみ)次のビジネスルールがあります。

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

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

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

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

私が欲しいのは:

2.a)SOLID原則(特にオープン/クローズド原則)に準拠します。

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

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:オープン/クローズド原則が保証されています。自転車税が必要な場合は、Vehicleから継承するだけで、HasToPayメソッドをオーバーライドして完了です。単純な継承によって満たされるオープン/クローズド原則。

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

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

3.b)納税は結局車両の問題かもしれません。あるいは、最初の個人-車両-車-バイク-公共管理の設計がまったく間違っているか、哲学者とより良いビジネスアナリストが必要です。解決策としては、HasToPayメソッドを別のクラスに移動し、TaxPaymentと呼ぶことができます。次に、2つのTaxPayment派生クラスを作成します。CarTaxPaymentおよびMotorbikeTaxPayment。次に、(TaxPaymentタイプの)抽象的な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のコンストラクターに渡すことはできますが、それは車両のカプセル化や責任に違反することになるでしょう。

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

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

この単純なシナリオの設計上の選択は何ですか?明らかに、私が示した例は「複雑すぎて」簡単なタスクではありませんが、同時に、SOLID原則を順守し、(破壊するように委託された)貧血領域を防ぐことも考えています。

回答:


15

私は、支払いの期日を人も車両も知るべきではないと言うでしょう。

問題領域の再検討

問題領域「車を持っている人」を見ると、車を購入するために、所有権を1人から別の人に移す何らかの契約があり、その過程で車も人も変わりません。モデルにはそれが完全に欠けています。

思考実験

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

=>所有権

誰にも所有されていない車はまだ車ですが、だれも税金を払わないでしょう。実際、あなたは車に税金を払っているのではなく、車を所有する特権のために払っています。したがって、私の提案は次のようになります。

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#コードではありませんが、アイデアが得られることを願っています(誰かがこれをクリーンアップできるかもしれません)。唯一の欠点は、おそらくCarOwnershipa Carとaの間で正しいものを作成するために工場または何かが必要になるということですPerson


私は、車両自体がそれに支払われた税金を記録し、個人がすべての車両に支払われた税金を記録すべきだと思います。車の税金が6月1日に支払われ、6月10日に販売された場合、新しい所有者は6月10日、7月1日、または7月10日に税金の責任を負うように税コードを書くことができます現在、変更できる方法の1つです)。所有権の変更によっても30日間のルールが自動車に対して一定であるが、誰にも負われていない自動車が税金を支払うことができない場合、30日以上所有されていない自動車を購入する場合、納税
...-supercat

...架空の「未所有車両税額控除」者から記録されるため、販売時点で車両が未所有の期間に関係なく、納税義務は0日または30日のいずれかになります。
supercat

4

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

確かに、多くの場合、Strategyパターンを使用してオブジェクトに物事を処理させる必要がありますが、それは必ずしも望ましいことではありません。

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


わかりました、GetVehiclesThatHaveToPayメソッドから車両のタイプを検査します。LastPaidTimeの責任者は誰ですか?LastPaidTimeは車両の問題ですか、それともPublicAdministrationの問題ですか?ビークルなら、そのビジネスロジックはビークルにあるべきではないのですか?質問3.cについて教えてください。

@DavidRobertJones-理想的には、車両ライセンスに基づいて、支払い履歴ログで最後の支払いを検索します。納税は税金の問題であり、車両の問題ではありません。
エリックFunkenbusch

5
@DavidRobertJonesあなたはあなた自身の比phorと混同していると思います。あなたが持っているのは、車両が行うすべてのことを行う必要がある実際の車両ではありません。代わりに、簡単にするためにTaxableVehicle(または同様のもの)をVehicleと名付けました。ドメイン全体が納税です。実際の車両が何をするか心配する必要はありません。また、必要に応じてメタファーを削除し、より適切なメタファーを作成します。

3

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

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

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

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

私がすること:

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

あなたが正しい。もともと質問はそのように定式化されていなかったので、一部を削除すると意味が変わりました。現在修正されています。ありがとう!

2

支払いが必要かどうかを車両が知る必要があるのはなぜですか?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はサブクラスではなく車両のカテゴリプロパティにする必要があることもお勧めします。これにより、柔軟性が高まります。たとえば、バイクや車のカテゴリを簡単に変更できます。もちろん、実際の生活で自転車が車に変わることはまずありません。しかし、車両が間違って入力されてしまい、後で変更する必要があることを知っています。

もちろん、この値をTaxPaymentのコンストラクターに渡すことはできますが、それは車両のカプセル化や責任に違反することになるでしょう。

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


1

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

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に、現在のポリシーに従っての新しいインスタンスを作成します。これには、以前のポリシーとはまったく異なるルールを設定できます。


問題は、支払い期限は所有者が最後に支払った時間に依存することです(異なる車両、異なる期限)。ITaxPaymentは、計算を行うために車両(または所有者)が最後に支払ったのはどれであるかをどのように知るのですか?

1

@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);
    }
}

0

私はあなたにそれを破ることを嫌いますが、あなたは貧血のドメイン、すなわちオブジェクトの外のビジネスロジックを持っています。これが目的の場合は問題ありませんが、回避する理由について読む必要があります。

問題領域をモデル化せずに、ソリューションをモデル化してください。そのため、最終的に並列継承階層が必要になり、必要以上に複雑になります。

この例に必要なのは次のようなものです:

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をフォローしたい場合はすばらしいですが、ボブおじさんが言ったように、それらは単なるエンジニアリングの原則です。それらは厳密に適用されることを意図していませんが、考慮すべきガイドラインです。


4
あなたのソリューションは特にスケーラブルではないと思います。追加する車両の種類ごとに新しいブール値を追加する必要があります。私の意見ではそれは悪い選択です。
エリックファンケンブッシュ

@Garrett Hall、私の「ドメイン」からPersonクラスを削除したことが気に入った。私はそもそもそのクラスを持っていなかったので、ごめんなさい。しかし、isMotorBikeは意味がありません。どちらが実装になりますか?

1
@DavidRobertJones:私はMystereに同意しisMotorBikeます。メソッドを追加することはOOP設計の良い選択ではありません。これは、ポリモーフィズムを型コード(ブール型ではあるが)に置き換えるようなものです。
グルー

@MystereMan、簡単にブール値をドロップして使用することができますenum Vehicles
レーダーボブ

+1。問題領域をモデル化せずに、ソリューションをモデル化してください簡潔な。記憶に残る。そして、原則のコメント。厳密な文字通りの解釈の試みが多すぎる。ナンセンス。
レーダーボブ

0

支払い期間は実際の車/バイクの所有物ではないことは正しいと思います。しかし、それは車両のクラスの属性です。

オブジェクトのタイプで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;
    }
}

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

欠点は

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

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

利点は、後でカレンダー月または年払いに出くわした場合、新しい属性クラスを書くことができるということです。これがまさにOCPの定義です。


これの主な問題は、車両の支払い間の日数を変更する場合、再構築および再展開する必要があり、支払い頻度がエンティティのプロパティ(エンジンサイズなど)によって異なる場合、そのためのエレガントな修正を取得することは困難です。
エドジェームズ

@EdWoodcock:最初の問題はほとんどのソリューションに当てはまると思いますが、本当に心配することはないでしょう。後でそのレベルの柔軟性が必要な場合は、DBアプリを提供します。2番目の点については、私はまったく同意しません。その時点で、オプションの引数としてVehicleをHasToPay()に追加し、必要のない場合は無視します。
pdr

私はそれがあなたがアプリのために持っている長期計画に加えて、クライアントの特定の要件に依存していると思います。 -駆動、常にそうであるとは限りません)。ただし、2番目のポイントでは、重要な修飾子はエレガントです。属性のメソッドに追加のパラメーターを追加し、その属性が関連付けられているクラスのインスタンスを取得することは、特にエレガントなソリューションとは言えません。ただし、これもクライアントの要件によって異なります。
エドジェームズ

@EdWoodcock:実際、私はこれについて考えていただけで、lastPaid引数はすでに Vehicleのプロパティである必要があります。あなたのコメントを念頭に置いて、後でではなく今すぐその議論を置き換える価値があるかもしれません-編集します。
pdr
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.