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