Хоча це може бути агностичним питанням мови програмування, мені цікаві відповіді, спрямовані на екосистему .NET.
Такий сценарій: припустимо, нам потрібно розробити просту консольну програму для публічної адміністрації. У заяві йдеться про податок на транспортні засоби. Вони (лише) мають такі ділові правила:
1.а) Якщо транспортний засіб є автомобілем і останній раз, коли його власник сплачував податок, було 30 днів тому, тоді власник повинен сплатити ще раз.
1.b) Якщо транспортний засіб є мотоциклом і останній раз його власник сплачував податок було 60 днів тому, тоді власник повинен сплатити ще раз.
Іншими словами, якщо у вас є машина, ви повинні платити кожні 30 днів або якщо у вас є мотоцикл, ви повинні платити кожні 60 днів.
Для кожного транспортного засобу в системі додаток повинен перевірити ці правила та надрукувати ті транспортні засоби (номер таблички та інформацію про власника), які їх не задовольняють.
Я хочу:
2.a) Відповідати принципам SOLID (особливо принципу відкритого / закритого).
Що я не хочу (я думаю):
2.b) Анемічна область, тому ділова логіка повинна діяти всередині суб'єктів господарювання.
Я почав з цього:
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: принцип відкритості / закритості гарантується; якщо вони хочуть податок на велосипед, який ви просто успадкуєте від Транспортного засобу, тоді ви перекриєте метод HasToPay і все закінчено. Відкритий / закритий принцип задовольняється простим успадкуванням.
"Проблеми":
3.a) Чому транспортний засіб повинен знати, якщо він повинен платити? Чи не стосується PublicAdministration? Якщо правила державного управління щодо зміни податку на автомобіль змінюються, чому автомобіль повинен змінюватися? Я думаю, ви повинні запитати: "тоді чому ви помістите метод HasToPay всередину транспортного засобу?", І відповідь: тому що я не хочу перевіряти тип транспортного засобу (typeof) всередині PublicAdministration. Ви бачите кращу альтернативу?
3.b) Можливо, сплата податку викликає занепокоєння транспортним засобом, або, можливо, мій початковий проект "Особистість-транспортний засіб-автомобіль-мотоцикл-громадське управління" просто невірний, або мені потрібен філософ і кращий бізнес-аналітик. Рішенням може бути: перенести метод HasToPay до іншого класу, ми можемо назвати це TaxPayment. Потім ми створюємо два похідні класи TaxPayment; CarTaxPayment та MotorbikeTaxPayment. Потім ми додаємо абстрактне майно платежу (типу TaxPayment) до класу Vehicle і повертаємо потрібний екземпляр TaxPayment з класів Автомобіль та Мотоцикл:
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; }
}
}
І ми викликаємо старий процес таким чином:
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
}
Але тепер цей код не буде компілюватися, оскільки в CarTaxPayment / MotorbikeTaxPayment / TaxPayment немає жодного учасника LastPaidTime. Я починаю бачити CarTaxPayment та MotorbikeTaxPayment більше, ніж "впровадження алгоритму", а не суб'єкти господарювання. Якимось чином цим "алгоритмам" потрібно значення LastPaidTime. Звичайно, ми можемо передати це значення конструктору TaxPayment, але це порушило б інкапсуляцію транспортних засобів, або відповідальність, або як би це не називали євангелісти ООП, чи не так?
3.c) Припустимо, у нас вже є Entity Framework ObjectContext (який використовує наші об’єкти домену; Person, Vehicle, Car, Motorbike, PublicAdministration). Як би ви зробили, щоб отримати посилання на ObjectContext від методу PublicAdministration.GetAllVehicles і, отже, реалізувати функціональність?
3.d) Якщо нам також потрібна та сама посилання на ObjectContext для CarTaxPayment.HasToPay, але інша для MotorbikeTaxPayment.HasToPay, яка відповідає за "введення" або як би ви передали ці посилання на класи платежів? У цьому випадку уникнення анемічного домену (і, отже, жодних об’єктів обслуговування) не приведе нас до катастрофи?
Які ваші варіанти дизайну для цього простого сценарію? Зрозуміло, що приклади, які я показав, "занадто складні" для легкого завдання, але в той же час ідея полягає в дотриманні принципів SOLID і запобігання анемічним доменам (з яких було доручено знищити).