Хотя это может быть независимый от языка программирования вопрос, мне интересны ответы, нацеленные на экосистему .NET.
Это сценарий: предположим, нам нужно разработать простое консольное приложение для публичного администрирования. Приложение о транспортном налоге. У них (только) есть следующие бизнес-правила:
1.a) Если транспортное средство является автомобилем и последний раз его владелец оплачивал налог 30 дней назад, то владелец должен заплатить снова.
1.b) Если транспортное средство является мотоциклом, и последний раз, когда его владелец платил налог, был 60 дней назад, то владелец должен заплатить снова.
Другими словами, если у вас есть машина, которую вы должны платить каждые 30 дней, или если у вас есть мотоцикл, вы должны платить каждые 60 дней.
Для каждого транспортного средства в системе приложение должно проверить эти правила и распечатать те транспортные средства (номерной знак и информация о владельце), которые не удовлетворяют им.
Что я хочу это:
2.a) Соответствовать принципам SOLID (особенно принципу Open / Closed).
Чего я не хочу, так это (я думаю):
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: принцип Open / closed гарантирован; если они хотят налог на велосипед, который вы просто наследуете от Vehicle, тогда вы переопределяете метод HasToPay, и все готово. Открытый / закрытый принцип удовлетворяется посредством простого наследования.
«Проблемы»:
3.а) Почему транспортное средство должно знать, должно ли оно платить? Не является ли проблема общественной администрации? Если правила государственной администрации об изменении налога на автомобиль, почему автомобиль должен измениться? Я думаю, что вы должны спросить: «Тогда почему вы поместили метод HasToPay в Vehicle?», И ответ таков: потому что я не хочу проверять тип транспортного средства (typeof) в PublicAdministration. Вы видите лучшую альтернативу?
3.b) Может быть, в конце концов, уплата налогов - это проблема транспортного средства, или, может быть, мой первоначальный дизайн Person-Vehicle-Car-Car-Motorbike-PublicAdministration просто ошибочен, или мне нужен философ и лучший бизнес-аналитик. Решение может быть следующим: переместить метод HasToPay в другой класс, мы можем назвать его TaxPayment. Затем мы создаем два производных класса TaxPayment; CarTaxPayment и MotorbikeTaxPayment. Затем мы добавляем абстрактное свойство Payment (типа TaxPayment) в класс Vehicle и возвращаем правильный экземпляр TaxPayment из классов Car и 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; }
}
}
И мы вызываем старый процесс следующим образом:
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 и предотвращать анемичные домены (из которых было поручено уничтожить).