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).
quelle
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?
quelle
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:
quelle
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.
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.
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.
quelle
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:
Jedes Mal, wenn Sie eine Zahlung erhalten, erstellen Sie eine neue Instanz einer
ITaxPayment
gemäß den aktuellen Richtlinien, für die völlig andere Regeln gelten können als für die vorherige.quelle
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
Ownership
Klasse zu einer generischen Klasse machen:Verwenden Sie die Vererbung, um das Zahlungsintervall für jeden Fahrzeugtyp anzugeben. Ich schlage auch vor, die stark typisierte
TimeSpan
Klasse anstelle einfacher Ganzzahlen zu verwenden:Jetzt muss sich Ihr aktueller Code nur noch mit einer Klasse befassen
Ownership<Vehicle>
.quelle
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:
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.
quelle
isMotorBike
Methode ist keine gute Wahl für das OOP-Design. Es ist wie das Ersetzen des Polymorphismus durch einen Typcode (wenn auch einen booleschen).enum Vehicles
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:
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.
quelle