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).
źródło
Myślę, że w takiej sytuacji, w której chcesz, aby reguły biznesowe istniały poza obiektami docelowymi, testowanie typu jest więcej niż dopuszczalne.
Oczywiście, w wielu sytuacjach powinieneś użyć patern Strategii i pozwolić obiektom same sobie poradzić, ale nie zawsze jest to pożądane.
W prawdziwym świecie urzędnik sprawdzałby typ pojazdu i na tej podstawie sprawdzał dane podatkowe. Dlaczego twoje oprogramowanie nie powinno stosować się do tej samej reguły busienss?
źródło
Masz to do tyłu. Domena anemiczna to taka, której logika znajduje się poza podmiotami gospodarczymi. Istnieje wiele powodów, dla których stosowanie dodatkowych klas do implementacji logiki jest złym pomysłem; i tylko kilka powodów, dla których powinieneś to zrobić.
Stąd powód, dla którego nazywa się to anemiczne: klasa nie ma w tym nic…
Co bym zrobił:
źródło
Nie. Jak rozumiem, cała Twoja aplikacja dotyczy podatków. Tak więc troska o to, czy pojazd powinien zostać opodatkowany, nie jest już kwestią administracji publicznej, niż czymkolwiek innym w całym programie. Nie ma powodu, aby umieszczać to nigdzie indziej niż tutaj.
Te dwa fragmenty kodu różnią się tylko stałą. Zamiast przesłonić całą funkcję HasToPay (), przesłoń an
int DaysBetweenPayments()
funkcję funkcję. Nazwij to zamiast używać stałej. Jeśli to wszystko, czym się różnią, porzuciłbym zajęcia.Sugerowałbym również, że motocykl / samochód powinien być właściwością kategorii pojazdu, a nie podklasy. To daje większą elastyczność. Na przykład ułatwia zmianę kategorii motocykla lub samochodu. Oczywiście w prawdziwym życiu raczej nie można przerobić motocykli na samochody. Ale wiesz, że pojazd zostanie źle wprowadzony i trzeba go później zmienić.
Nie całkiem. Obiekt, który celowo przekazuje swoje dane do innego obiektu jako parametr, nie stanowi dużego problemu związanego z enkapsulacją. Prawdziwą troską są inne obiekty sięgające do tego obiektu w celu wyciągnięcia danych lub manipulowania danymi wewnątrz obiektu.
źródło
Możesz być na dobrej drodze. Problem polega na tym, jak zauważyłeś, że w TaxPayment nie ma członka LastPaidTime . Właśnie tam należy, choć wcale nie musi być publicznie ujawniany:
Za każdym razem, gdy otrzymujesz płatność, tworzysz nowe wystąpienie
ITaxPayment
zgodnie z bieżącymi zasadami, które mogą mieć zupełnie inne reguły niż poprzednie.źródło
Zgadzam się z odpowiedzią @sebastiangeiger, że problem dotyczy raczej własności pojazdów niż pojazdów. Proponuję użyć jego odpowiedzi, ale z kilkoma zmianami. Chciałbym, aby
Ownership
klasa była klasą ogólną:I użyj dziedziczenia, aby określić interwał płatności dla każdego typu pojazdu. Sugeruję również użycie silnie typowanej
TimeSpan
klasy zamiast zwykłych liczb całkowitych:Teraz twój rzeczywisty kod musi zajmować się tylko jedną klasą -
Ownership<Vehicle>
.źródło
Nie chcę ci tego łamać, ale masz domenę anemiczną , czyli logikę biznesową poza obiektami. Jeśli to jest to, co zamierzasz, jest w porządku, ale powinieneś przeczytać o powodach, aby tego uniknąć.
Staraj się nie modelować domeny problemu, ale modeluj rozwiązanie. Właśnie dlatego powstają równoległe hierarchie dziedziczenia i większa złożoność niż potrzebujesz.
Coś takiego wystarczy do tego przykładu:
Jeśli chcesz podążać za SOLIDEM, to świetnie, ale jak powiedział sam wujek Bob, są to tylko zasady inżynieryjne . Nie są przeznaczone do rygorystycznego stosowania, ale stanowią wytyczne, które należy wziąć pod uwagę.
źródło
isMotorBike
metody nie jest dobrym wyborem projektu OOP. To tak, jakby zastąpić polimorfizm kodem typu (choć boolowskim).enum Vehicles
Myślę, że masz rację, że okres płatności nie jest własnością rzeczywistego samochodu / motocykla. Ale jest to atrybut klasy pojazdu.
Chociaż możesz pójść dalej drogą posiadania if / select na typie obiektu, nie jest to zbyt skalowalne. Jeśli później dodasz samochód ciężarowy i Segway i pchasz rower, autobus i samolot (może się to wydawać mało prawdopodobne, ale nigdy nie wiesz o usługach publicznych), wówczas warunek staje się większy. A jeśli chcesz zmienić zasady dotyczące ciężarówki, musisz ją znaleźć na tej liście.
Dlaczego więc nie uczynić go dosłownie atrybutem i użyć refleksji, aby go wyciągnąć? Coś takiego:
Możesz również zastosować zmienną statyczną, jeśli jest to wygodniejsze.
Wady są
że będzie nieco wolniej i zakładam, że musisz przetworzyć wiele pojazdów. Jeśli jest to problem, mógłbym przyjąć bardziej pragmatyczny pogląd i trzymać się abstrakcyjnej własności lub podejścia opartego na interfejsie. (Chociaż rozważam również buforowanie według typu).
Podczas uruchamiania będziesz musiał sprawdzić, czy każda podklasa Vehicle ma atrybut IPaymentRequiredCalculator (lub zezwala na ustawienie domyślne), ponieważ nie chcesz, aby przetwarzał 1000 samochodów, a jedynie powodował awarię, ponieważ motocykl nie ma okresu płatności.
Plusem jest to, że jeśli później spotkasz miesiąc kalendarzowy lub roczną płatność, możesz po prostu napisać nową klasę atrybutów. To jest właśnie definicja OCP.
źródło