SOLID, anämische Domänen meiden, Abhängigkeitsinjektion?

33

Obwohl dies eine programmiersprachenunabhängige Frage sein könnte, bin ich an Antworten interessiert, die auf das .NET-Ökosystem abzielen.

Dies ist das Szenario: Angenommen, wir müssen eine einfache Konsolenanwendung für die öffentliche Verwaltung entwickeln. Die Anwendung ist über Kfz-Steuer. Sie haben (nur) die folgenden Geschäftsregeln:

1.a) Wenn es sich bei dem Fahrzeug um ein Auto handelt und der Besitzer die Steuer das letzte Mal vor 30 Tagen gezahlt hat, muss der Besitzer erneut zahlen.

1.b) Wenn es sich bei dem Fahrzeug um ein Motorrad handelt und der Besitzer die Steuer das letzte Mal vor 60 Tagen gezahlt hat, muss der Besitzer erneut zahlen.

Mit anderen Worten, wenn Sie ein Auto haben, müssen Sie alle 30 Tage bezahlen, oder wenn Sie ein Motorrad haben, müssen Sie alle 60 Tage bezahlen.

Für jedes Fahrzeug im System sollte die Anwendung die Regeln testen und die Fahrzeuge (Kennzeichen und Besitzerinformationen) ausdrucken, die diese nicht erfüllen.

Was ich will ist:

2.a) Den SOLID-Grundsätzen (insbesondere dem Open / Closed-Prinzip) entsprechen.

Was ich nicht will ist (denke ich):

2.b) Eine anämische Domäne, daher sollte Geschäftslogik innerhalb von Geschäftseinheiten liegen.

Ich habe damit angefangen:

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: Das Open / Closed-Prinzip ist garantiert; Wenn sie eine Fahrradsteuer wollen, die Sie gerade von Vehicle geerbt haben, überschreiben Sie die HasToPay-Methode und Sie sind fertig. Offenes / geschlossenes Prinzip durch einfache Vererbung erfüllt.

Die "Probleme" sind:

3.a) Warum muss ein Fahrzeug wissen, ob es bezahlen muss? Ist das nicht ein Problem der öffentlichen Verwaltung? Wenn sich die Vorschriften der öffentlichen Verwaltung für die Kfz-Steuer ändern, warum muss sich das Auto ändern? Ich denke, Sie sollten fragen: "Warum haben Sie dann eine HasToPay-Methode in Vehicle eingefügt?", Und die Antwort lautet: Weil ich in PublicAdministration nicht auf den Fahrzeugtyp (typeof) testen möchte. Sehen Sie eine bessere Alternative?

3.b) Vielleicht ist die Steuerzahlung doch ein Problem des Fahrzeugs, oder vielleicht ist mein ursprüngliches Design der Personen-, Fahrzeug-, Motorrad- und öffentlichen Verwaltung einfach falsch, oder ich brauche einen Philosophen und einen besseren Wirtschaftsanalytiker. Eine Lösung könnte sein: Verschieben Sie die HasToPay-Methode in eine andere Klasse, wir können es TaxPayment nennen. Dann erstellen wir zwei von TaxPayment abgeleitete Klassen. CarTaxPayment und MotorbikeTaxPayment. Dann fügen wir der Fahrzeugklasse eine abstrakte Payment-Eigenschaft (vom Typ TaxPayment) hinzu und geben die richtige TaxPayment-Instanz aus den Auto- und Motorradklassen zurück:

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

Und so rufen wir den alten Prozess auf:

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

Dieser Code kann jetzt jedoch nicht kompiliert werden, da kein LastPaidTime-Mitglied in CarTaxPayment / MotorbikeTaxPayment / TaxPayment enthalten ist. Ich sehe CarTaxPayment und MotorbikeTaxPayment eher als "Algorithmusimplementierungen" als als Geschäftseinheiten. Irgendwie brauchen diese "Algorithmen" jetzt den LastPaidTime-Wert. Sicher, wir können den Wert an den Konstrukteur von TaxPayment weitergeben, aber das würde die Verkapselung von Fahrzeugen oder Verantwortlichkeiten verletzen, oder wie auch immer diese OOP-Evangelisten es nennen, nicht wahr?

3.c) Angenommen, wir haben bereits einen Entity Framework ObjectContext (der unsere Domänenobjekte verwendet: Person, Fahrzeug, Auto, Motorrad, Öffentliche Verwaltung). Wie würden Sie vorgehen, um eine ObjectContext-Referenz von der PublicAdministration.GetAllVehicles-Methode abzurufen und damit die Funktionalität zu implementieren?

3.d) Wenn wir für CarTaxPayment.HasToPay dieselbe ObjectContext-Referenz benötigen, für MotorbikeTaxPayment.HasToPay jedoch eine andere, wer ist für das "Injizieren" zuständig, oder wie würden Sie diese Referenzen an die Zahlungsklassen weitergeben? Führen Sie uns in diesem Fall nicht zu einer Katastrophe, wenn Sie eine anämische Domäne (und damit keine Dienstobjekte) vermeiden?

Was sind Ihre Entwurfswahlen für dieses einfache Szenario? Die Beispiele, die ich gezeigt habe, sind eindeutig "zu komplex" für eine einfache Aufgabe, aber gleichzeitig besteht die Idee darin, die SOLID-Prinzipien einzuhalten und anämische Domänen zu verhindern (von denen die Zerstörung in Auftrag gegeben wurde).

David Robert Jones
quelle

Antworten:

15

Ich würde sagen, dass weder eine Person noch ein Fahrzeug wissen sollten, ob eine Zahlung fällig ist.

Überprüfung der Problemdomäne

Wenn Sie sich die Problemdomäne "Leute, die Autos haben" ansehen: Um ein Auto zu kaufen, haben Sie einen Vertrag, der das Eigentum von einer Person auf die andere überträgt. Dabei ändern sich weder das Auto noch die Person. Ihrem Modell fehlt das völlig.

Gedankenexperiment

Vorausgesetzt, es gibt ein verheiratetes Paar, der Mann besitzt das Auto und zahlt die Steuern. Jetzt stirbt der Mann, seine Frau erbt das Auto und zahlt die Steuern. Ihre Teller bleiben jedoch gleich (zumindest in meinem Land). Es ist immer noch das gleiche Auto! Sie sollten dies in Ihrer Domain modellieren können.

=> Eigentum

Ein Auto, das niemandem gehört, ist immer noch ein Auto, aber niemand zahlt Steuern dafür. Tatsächlich zahlen Sie keine Steuern für das Auto, sondern für das Privileg, ein Auto zu besitzen . Mein Vorschlag wäre also:

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

}

Dies ist alles andere als perfekt und wahrscheinlich nicht einmal im Remote-Zugriff korrekter C # -Code, aber ich hoffe, Sie haben die Idee (und jemand könnte dies bereinigen). Der einzige Nachteil ist, dass Sie wahrscheinlich eine Fabrik oder etwas anderes benötigen, um das richtige Verhältnis CarOwnershipzwischen a Carund a herzustellenPerson

sebastiangeiger
quelle
Ich denke, das Fahrzeug selbst sollte die dafür gezahlten Steuern aufzeichnen, und Einzelpersonen sollten die für alle ihre Fahrzeuge gezahlten Steuern aufzeichnen. Ein Steuerkennzeichen könnte so geschrieben werden, dass, wenn die Steuer für ein Auto am 1. Juni bezahlt und am 10. Juni verkauft wird, der neue Eigentümer möglicherweise am 10. Juni, 1. Juli oder 10. Juli (und selbst dann, wenn dies der Fall ist) für die Steuer haftet derzeit könnte sich eine Möglichkeit ändern). Wenn die 30-Tage-Regel für das Auto auch durch Besitzerwechsel konstant ist, Autos, die von niemandem geschuldet werden, jedoch keine Steuern zahlen können, würde ich vorschlagen, dass beim Kauf eines Autos, das 30 oder mehr Tage lang nicht besessen war, a Steuerzahlung ...
Supercat
... von einer fiktiven Person erfasst werden, die eine Steuergutschrift für ein nicht besessenes Fahrzeug ausgestellt hat, so dass die Steuerschuld zum Zeitpunkt des Verkaufs entweder null oder dreißig Tage beträgt, unabhängig davon, wie lange das Fahrzeug nicht besessen war.
Supercat
4

Ich denke, dass in einer solchen Situation, in der die Geschäftsregeln außerhalb der Zielobjekte existieren sollen, das Testen auf Typ mehr als akzeptabel ist.

Sicher, in vielen Situationen sollten Sie ein Strategie-Muster verwenden und die Objekte die Dinge selbst handhaben lassen, aber das ist nicht immer wünschenswert.

In der realen Welt würde ein Angestellter den Fahrzeugtyp und die darauf basierenden Steuerdaten nachschlagen. Warum sollte Ihre Software nicht der gleichen Geschäftsregel folgen?

Erik Funkenbusch
quelle
Ok, sagen Sie, Sie haben mich überzeugt und anhand der GetVehiclesThatHaveToPay-Methode überprüfen wir den Fahrzeugtyp. Wer sollte für die LastPaidTime verantwortlich sein? Ist LastPaidTime ein Fahrzeugunternehmen oder eine öffentliche Verwaltung? Denn wenn es sich um ein Fahrzeug handelt, sollte sich diese Geschäftslogik dann nicht im Fahrzeug befinden? Was können Sie mir zu Frage 3.c sagen?
@DavidRobertJones - Idealerweise würde ich die letzte Zahlung im Zahlungsprotokoll basierend auf dem Fahrzeugschein nachschlagen. Eine Steuerzahlung ist ein Steuerproblem, kein Fahrzeugproblem.
Erik Funkenbusch
5
@DavidRobertJones Ich glaube, Sie verwechseln sich mit Ihrer eigenen Metapher. Was Sie haben, ist kein tatsächliches Fahrzeug, das all die Dinge tun muss, die ein Fahrzeug tut. Stattdessen haben Sie der Einfachheit halber ein TaxableVehicle (oder ähnliches) ein Fahrzeug genannt. Ihre gesamte Domain ist Steuerzahlungen. Machen Sie sich keine Sorgen darüber, was die tatsächlichen Fahrzeuge tun. Und, falls nötig, lassen Sie die Metapher fallen und finden Sie eine passendere.
3

Was ich nicht will ist (denke ich):

2.b) Eine anämische Domäne, dh Geschäftslogik innerhalb von Geschäftseinheiten.

Sie haben dies rückwärts. Eine anämische Domäne ist eine Domäne, deren Logik außerhalb der Geschäftseinheiten liegt. Es gibt viele Gründe, warum die Verwendung zusätzlicher Klassen zur Implementierung der Logik eine schlechte Idee ist. und nur ein paar, warum du es tun solltest.

Daher der Grund, warum es anämisch heißt: Die Klasse hat nichts drin.

Was ich tun würde:

  1. Ändern Sie Ihre abstrakte Fahrzeugklasse in eine Schnittstelle.
  2. Fügen Sie eine ITaxPayment-Schnittstelle hinzu, die die Fahrzeuge implementieren müssen. Lassen Sie Ihr HasToPay hier hängen.
  3. Entfernen Sie die Klassen MotorbikeTaxPayment, CarTaxPayment, TaxPayment. Sie sind unnötige Komplikationen / Unordnung.
  4. Jeder Fahrzeugtyp (Auto, Fahrrad, Wohnmobil) sollte mindestens ein Fahrzeug implementieren und, wenn sie besteuert werden, auch ITaxPayment implementieren. Auf diese Weise können Sie zwischen Fahrzeugen, die nicht besteuert werden, und solchen unterscheiden, die ... Vorausgesetzt, diese Situation besteht überhaupt.
  5. Jeder Fahrzeugtyp sollte innerhalb der Grenzen der Klasse selbst die in Ihren Schnittstellen definierten Methoden implementieren.
  6. Fügen Sie außerdem das letzte Zahlungsdatum zu Ihrer ITaxPayment-Schnittstelle hinzu.
Nicht ich
quelle
Du hast recht. Ursprünglich war die Frage nicht so formuliert, ich habe einen Teil gelöscht und die Bedeutung geändert. Es ist jetzt behoben. Vielen Dank!
2

Warum muss ein Fahrzeug wissen, ob es bezahlen muss? Ist das nicht ein Problem der öffentlichen Verwaltung?

Soweit ich weiß, dreht sich in Ihrer gesamten App alles um Steuern. Die Besorgnis darüber, ob ein Fahrzeug besteuert werden soll, ist also nicht mehr ein Anliegen der öffentlichen Verwaltung als irgendetwas anderes im gesamten Programm. Es gibt keinen Grund, es irgendwo anders als hier zu platzieren.

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

Diese beiden Codeausschnitte unterscheiden sich nur durch die Konstante. Anstatt die gesamte HasToPay () - Funktion zu überschreiben, überschreiben Sie eine int DaysBetweenPayments()Funktion. Nennen Sie das, anstatt die Konstante zu verwenden. Wenn das alles ist, worüber sie sich tatsächlich unterscheiden, würde ich die Klassen fallen lassen.

Ich würde auch vorschlagen, dass Motorrad / Auto eher eine Kategorie des Fahrzeugs als Unterklassen sein sollte. Das gibt Ihnen mehr Flexibilität. So ist es beispielsweise einfach, die Kategorie eines Motorrads oder Autos zu ändern. Natürlich ist es unwahrscheinlich, dass Fahrräder im wirklichen Leben in Autos verwandelt werden. Aber Sie wissen, dass ein Fahrzeug falsch eingegeben wird und später gewechselt werden muss.

Sicher, wir können den Wert an den Konstrukteur von TaxPayment weitergeben, aber das würde die Verkapselung von Fahrzeugen oder Verantwortlichkeiten verletzen, oder wie auch immer diese OOP-Evangelisten es nennen, nicht wahr?

Nicht wirklich. Ein Objekt, das seine Daten absichtlich als Parameter an ein anderes Objekt übergibt, ist kein großes Kapselungsproblem. Die eigentliche Sorge ist, dass andere Objekte in dieses Objekt greifen, um Daten herauszuholen oder die Daten im Objekt zu manipulieren.

Winston Ewert
quelle
1

Sie könnten auf dem richtigen Weg sein. Das Problem ist, wie Sie bemerkt haben, dass TaxPayment kein LastPaidTime-Mitglied enthält . Genau da gehört es hin, obwohl es überhaupt nicht öffentlich zugänglich gemacht werden muss:

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

Jedes Mal, wenn Sie eine Zahlung erhalten, erstellen Sie eine neue Instanz einer ITaxPaymentgemäß den aktuellen Richtlinien, für die völlig andere Regeln gelten können als für die vorherige.

Groo
quelle
Die fällige Zahlung hängt von der letzten Zahlung des Eigentümers ab (unterschiedliche Fahrzeuge, unterschiedliche Fälligkeitstermine). Woher weiß ITaxPayment, wann ein Fahrzeug (oder Eigentümer) das letzte Mal bezahlt hat, um die Berechnung durchzuführen?
1

Ich stimme der Antwort von @sebastiangeiger zu, dass es sich bei dem Problem eigentlich eher um Fahrzeugeigentum als um Fahrzeuge handelt. Ich werde vorschlagen, seine Antwort zu verwenden, aber mit ein paar Änderungen. Ich würde die OwnershipKlasse zu einer generischen Klasse machen:

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

Verwenden Sie die Vererbung, um das Zahlungsintervall für jeden Fahrzeugtyp anzugeben. Ich schlage auch vor, die stark typisierte TimeSpanKlasse anstelle einfacher Ganzzahlen zu verwenden:

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

Jetzt muss sich Ihr aktueller Code nur noch mit einer Klasse befassen 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);
    }
}
user74130
quelle
0

Ich hasse es, es Ihnen zu brechen, aber Sie haben eine anämische Domäne , dh Geschäftslogik außerhalb von Objekten. Wenn dies das ist, was Sie wollen, ist das in Ordnung, aber Sie sollten über Gründe nachlesen, um einen zu vermeiden.

Versuchen Sie nicht, die Problemdomäne zu modellieren, sondern modellieren Sie die Lösung. Das ist der Grund, warum Sie am Ende parallele Vererbungshierarchien und mehr Komplexität haben, als Sie benötigen.

So etwas ist alles, was Sie für dieses Beispiel brauchen:

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

Wenn Sie SOLID folgen wollen, ist das großartig, aber wie Onkel Bob selbst sagte, handelt es sich nur um technische Prinzipien . Sie sollen nicht streng angewendet werden, sondern sind Richtlinien, die berücksichtigt werden sollten.

Garrett Hall
quelle
4
Ich denke, Ihre Lösung ist nicht besonders skalierbar. Sie müssten für jeden Fahrzeugtyp, den Sie hinzufügen, einen neuen Booleschen Wert hinzufügen. Das ist meiner Meinung nach eine schlechte Wahl.
Erik Funkenbusch
@ Garrett Hall, ich fand es gut, dass Sie die Person-Klasse aus meiner "Domain" entfernt haben. Ich würde diese Klasse gar nicht erst haben, also tut mir das leid. Aber isMotorBike macht keinen Sinn. Welches wäre die Umsetzung?
1
@DavidRobertJones: Ich stimme Mystere zu, das Hinzufügen einer isMotorBikeMethode ist keine gute Wahl für das OOP-Design. Es ist wie das Ersetzen des Polymorphismus durch einen Typcode (wenn auch einen booleschen).
Groo
@MystereMan, man könnte leicht die Bools fallen lassen und einenenum Vehicles
radarbob 24.12.13
+1. Versuchen Sie nicht, die Problemdomäne zu modellieren, sondern modellieren Sie die Lösung . Pithy . Denkwürdigerweise gesagt. UND die Prinzipien kommentieren. Ich sehe zu viele Versuche einer strengen wörtlichen Interpretation ; Unsinn.
Radarbob
0

Ich denke, Sie haben Recht, dass die Zahlungsfrist nicht Eigentum des tatsächlichen Autos / Motorrads ist. Aber es ist ein Attribut der Fahrzeugklasse.

Es ist zwar möglich, ein if / select für den Objekttyp festzulegen, dies ist jedoch nicht sehr skalierbar. Wenn Sie später Lastwagen und Segway hinzufügen und Fahrrad, Bus und Flugzeug schieben (dies scheint unwahrscheinlich, aber Sie wissen es nie bei öffentlichen Diensten), wird der Zustand größer. Und wenn Sie die Regeln für einen LKW ändern möchten, müssen Sie sie in dieser Liste finden.

Warum also nicht buchstäblich ein Attribut daraus machen und es mit Reflektion herausziehen? Etwas wie das:

[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;
    }
}

Sie können auch eine statische Variable verwenden, wenn dies bequemer ist.

Die Nachteile sind

  • dass es etwas langsamer sein wird, und ich gehe davon aus, dass Sie viele Fahrzeuge verarbeiten müssen. Wenn das ein Problem ist, könnte ich eine pragmatischere Ansicht vertreten und bei der abstrakten Eigenschaft oder dem Ansatz der Schnittstellen bleiben. (Obwohl ich in Betracht ziehen würde, auch nach Typ zu cachen.)

  • Sie müssen beim Start überprüfen, ob jede Fahrzeugunterklasse über ein IPaymentRequiredCalculator-Attribut verfügt (oder ein Standardattribut zulässt), da Sie nicht möchten, dass 1000 Autos verarbeitet werden, sondern nur abstürzen, weil das Motorrad keine PaymentPeriod hat.

Der Vorteil ist, dass Sie nur eine neue Attributklasse schreiben können, wenn Sie später auf einen Kalendermonat oder eine jährliche Zahlung stoßen. Dies ist die eigentliche Definition von OCP.

pdr
quelle
Das Hauptproblem dabei ist, dass, wenn Sie die Anzahl der Tage zwischen den Zahlungen für ein Fahrzeug ändern möchten, diese neu erstellt und bereitgestellt werden müssen, und dass die Zahlungshäufigkeit je nach Eigenschaft des Unternehmens (z. B. Motorgröße) variiert Es ist schwierig, eine elegante Lösung dafür zu finden.
Ed James
@EdWoodcock: Ich denke, das erste Problem trifft auf die meisten Lösungen zu und würde mich wirklich nicht beunruhigen. Wenn sie später diese Flexibilität wünschen, würde ich ihnen eine DB-App geben. Im zweiten Punkt stimme ich überhaupt nicht zu. An diesem Punkt fügen Sie einfach ein Fahrzeug als optionales Argument zu HasToPay () hinzu und ignorieren es dort, wo Sie es nicht benötigen.
pdr
Ich denke, es hängt von den langfristigen Plänen ab, die Sie für die App haben, sowie von den besonderen Anforderungen des Kunden, ob es sich lohnt, eine Datenbank einzustecken, wenn noch keine vorhanden ist (ich gehe eher davon aus, dass so ziemlich alle Software DB ist -getrieben, was meines Wissens nicht immer der Fall ist). Beim zweiten Punkt ist das wichtige Qualifikationsspiel jedoch elegant . Ich würde nicht sagen, dass das Hinzufügen eines zusätzlichen Parameters zu einer Methode für ein Attribut, das dann eine Instanz der Klasse übernimmt, der das Attribut zugeordnet ist, eine besonders elegante Lösung ist. Aber es hängt auch hier von Ihren Kundenanforderungen ab.
Ed James
@EdWoodcock: Eigentlich habe ich gerade darüber nachgedacht und das Argument lastPaid muss bereits Eigentum von Vehicle sein. In Anbetracht Ihres Kommentars ist es möglicherweise sinnvoll, dieses Argument jetzt zu ersetzen, anstatt es später zu bearbeiten.
pdr