Chociaż może to być pytanie agnostyczne w języku programowania, interesują mnie odpowiedzi dotyczące ekosystemu .NET.
Oto scenariusz: załóżmy, że musimy opracować prostą aplikację konsolową dla administracji publicznej. Aplikacja dotyczy podatku od pojazdów. Mają (tylko) następujące reguły biznesowe:
1.a) Jeśli pojazd jest samochodem, a ostatni podatek zapłacił jego właściciel 30 dni temu, wówczas właściciel musi zapłacić ponownie.
1.b) Jeśli pojazd jest motocyklem, a ostatni podatek zapłacony przez właściciela był 60 dni temu, wówczas właściciel musi zapłacić ponownie.
Innymi słowy, jeśli masz samochód, musisz płacić co 30 dni lub jeśli masz motocykl, musisz płacić co 60 dni.
W przypadku każdego pojazdu w systemie aplikacja powinna przetestować te reguły i wydrukować te pojazdy (numer rejestracyjny i dane właściciela), które ich nie spełniają.
Chcę to:
2.a) Zgodne z zasadami SOLID (szczególnie zasada otwarta / zamknięta).
To, czego nie chcę, to (tak myślę):
2.b) Domena anemiczna, dlatego logika biznesowa powinna wchodzić do jednostek biznesowych.
Zacząłem od tego:
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: Zagwarantowana jest zasada otwarcia / zamknięcia; jeśli chcą podatku rowerowego, który po prostu dziedziczysz od Vehicle, wówczas zastępujesz metodę HasToPay i gotowe. Zasada otwarta / zamknięta spełniona poprzez proste dziedziczenie.
„Problemy” to:
3.a) Dlaczego pojazd musi wiedzieć, czy musi zapłacić? Czy nie dotyczy to administracji publicznej? Jeśli przepisy administracji publicznej dotyczące zmiany podatku samochodowego, dlaczego samochód musi się zmienić? Myślę, że powinieneś zapytać: „dlaczego więc umieściłeś metodę HasToPay w pojeździe?”, A odpowiedź brzmi: ponieważ nie chcę testować typu pojazdu (typof) w administracji publicznej. Czy widzisz lepszą alternatywę?
3.b) Może zapłata podatku jest w końcu problemem dla pojazdu, a może mój początkowy projekt Person-Vehicle-Car-Motor-Motor-PublicAdministration jest po prostu błędny, lub potrzebuję filozofa i lepszego analityka biznesowego. Rozwiązaniem może być: przenieś metodę HasToPay do innej klasy, którą możemy nazwać TaxPayment. Następnie tworzymy dwie klasy pochodne TaxPayment; CarTaxPayment i MotorbikeTaxPayment. Następnie dodajemy abstrakcyjną właściwość Payment (typu TaxPayment) do klasy Vehicle i zwracamy odpowiednią instancję TaxPayment z klas Car i Motorbike:
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; }
}
}
W ten sposób wywołujemy stary proces:
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
}
Ale teraz ten kod nie będzie się kompilował, ponieważ w CarTaxPayment / MotorbikeTaxPayment / TaxPayment nie ma członka LastPaidTime. Zaczynam widzieć CarTaxPayment i MotorbikeTaxPayment bardziej jak „implementacje algorytmów” niż podmioty gospodarcze. Jakoś teraz te „algorytmy” potrzebują wartości LastPaidTime. Jasne, możemy przekazać wartość konstruktorowi TaxPayment, ale to naruszyłoby hermetyzację pojazdu lub obowiązki, czy jakkolwiek nazywają to ewangeliści OOP, prawda?
3.c) Załóżmy, że mamy już obiekt Entity Framework ObjectContext (który korzysta z naszych obiektów domeny; Osoba, Pojazd, Samochód, Motocykl, Administracja publiczna). Jak byś zrobił, aby uzyskać odwołanie ObjectContext z metody PublicAdministration.GetAllVehicles, a tym samym wdrożyć funkcjonalność?
3.d) Jeśli potrzebujemy również tego samego odwołania ObjectContext dla CarTaxPayment.HasToPay, ale innego dla MotorbikeTaxPayment.HasToPay, kto jest odpowiedzialny za „wstrzykiwanie” lub w jaki sposób przekazałbyś te odniesienia do klas płatności? W takim przypadku, unikając anemicznej domeny (a tym samym żadnych obiektów usługowych), nie doprowadzasz nas do katastrofy?
Jakie są opcje projektowania dla tego prostego scenariusza? Najwyraźniej przykłady, które pokazałem, są „zbyt skomplikowane” dla łatwego zadania, ale jednocześnie chodzi o przestrzeganie zasad SOLID i zapobieganie domenom anemicznym (których zniszczenie zlecono).