Хотя это может быть независимый от языка программирования вопрос, мне интересны ответы, нацеленные на экосистему .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 и предотвращать анемичные домены (из которых было поручено уничтожить).
источник
Я думаю, что в такой ситуации, когда вы хотите, чтобы бизнес-правила существовали вне целевых объектов, тестирование на тип более чем приемлемо.
Конечно, во многих ситуациях вы должны использовать паттерн Стратегии и позволить объектам самим обрабатывать вещи, но это не всегда желательно.
В реальном мире, клерк будет смотреть на тип транспортного средства и искать свои налоговые данные на основе этого. Почему ваше программное обеспечение не должно следовать тому же правилу busienss?
источник
У вас есть это задом наперед. Анемичная область - это область, логика которой находится за пределами бизнес-объектов. Есть много причин, почему использование дополнительных классов для реализации логики - плохая идея; и только пара, почему вы должны это сделать.
Отсюда и причина анемии: в классе ничего нет ..
Что бы я сделал:
источник
Нет. Как я понимаю, все ваше приложение о налогах. Таким образом, забота о том, следует ли облагать налогом транспортное средство, больше не заботит PublicAdministration, чем что-либо еще во всей программе. Там нет причин, чтобы положить его в другом месте, кроме здесь.
Эти два фрагмента кода отличаются только константой. Вместо переопределения всей функции HasToPay () переопределяем
int DaysBetweenPayments()
функцию. Назовите это вместо использования константы. Если это все, чем они на самом деле отличаются, я бы бросил занятия.Я также предложил бы, чтобы мотоцикл / автомобиль был свойством категории транспортного средства, а не подклассами. Это дает вам больше гибкости. Например, это позволяет легко изменить категорию мотоцикла или автомобиля. Конечно, велосипеды вряд ли превратятся в автомобили в реальной жизни. Но вы знаете, что автомобиль будет введен неправильно, и его нужно будет заменить позже.
На самом деле, нет. Объект, который намеренно передает свои данные другому объекту в качестве параметра, не является большой проблемой инкапсуляции. Реальное беспокойство вызывают другие объекты, достигающие внутри этого объекта для извлечения данных или манипулирования данными внутри объекта.
источник
Вы можете быть на правильном пути. Проблема, как вы заметили, в том, что внутри TaxPayment нет члена LastPaidTime . Это именно то, где он принадлежит, хотя это вообще не нужно публично раскрывать:
Каждый раз, когда вы получаете платеж, вы создаете новый экземпляр в
ITaxPayment
соответствии с текущими политиками, который может иметь совершенно другие правила, чем предыдущий.источник
Я согласен с ответом @sebastiangeiger о том, что проблема на самом деле заключается в владельцах транспортных средств, а не в транспортных средствах. Я собираюсь предложить использовать его ответ, но с некоторыми изменениями. Я бы сделал
Ownership
класс универсальным классом:И используйте наследование, чтобы указать интервал оплаты для каждого типа транспортного средства. Я также предлагаю использовать строго типизированный
TimeSpan
класс вместо простых целых чисел:Теперь ваш реальный код должен иметь дело только с одним классом
Ownership<Vehicle>
.источник
Я ненавижу нарушать это для вас, но у вас есть анемичный домен , то есть бизнес-логика вне объектов. Если это то, к чему вы идете, это нормально, но вы должны прочитать о причинах, чтобы избежать этого.
Старайтесь не моделировать проблемную область, а моделируйте решение. Вот почему вы получаете параллельные иерархии наследования и более сложную, чем вам нужно.
Что-то вроде этого - все, что вам нужно для этого примера:
Если вы хотите следовать SOLID, это здорово, но, как сказал сам дядя Боб, это всего лишь инженерные принципы . Они не предназначены для строгого применения, но являются руководящими принципами, которые следует учитывать.
источник
isMotorBike
метода не является хорошим выбором ООП дизайна. Это похоже на замену полиморфизма кодом типа (хотя и логическим).enum Vehicles
Я думаю, что вы правы, что период оплаты не является собственностью реального автомобиля / мотоцикла. Но это атрибут класса автомобиля.
Несмотря на то, что вы можете пойти по пути наличия / выбора для объекта типа, это не очень масштабируемо. Если позже вы добавите грузовик и сегвей, а также толкнете велосипед, автобус и самолет (это может показаться маловероятным, но вы никогда не узнаете об этом с общественными службами), тогда состояние станет больше. И если вы хотите изменить правила для грузовика, вы должны найти его в этом списке.
Так почему бы не сделать это, в буквальном смысле, атрибутом и использовать отражение, чтобы вытащить его? Что-то вроде этого:
Вы также можете использовать статическую переменную, если это более удобно.
Недостатки
что это будет немного медленнее, и я предполагаю, что вам придется обрабатывать много машин. Если это проблема, то я мог бы взять более прагматичный взгляд и придерживаться абстрактного свойства или интерфейсного подхода. (Хотя я бы тоже рассмотрел кеширование по типу.)
При запуске вам нужно будет проверить, что у каждого подкласса Vehicle есть атрибут IPaymentRequiredCalculator (или разрешить значение по умолчанию), потому что вы не хотите, чтобы он обрабатывал 1000 автомобилей, а только аварийно, потому что у мотоцикла нет PaymentPeriod.
Положительным моментом является то, что если позже вы встретите календарный месяц или годовой платеж, вы можете просто написать новый класс атрибутов. Это само определение OCP.
источник