Какой шаблон дизайна использовать
Допустим, у меня есть продукты класса, где у меня есть запас и я могу заплатить двумя способами: Paypal или наличными. Я могу продать только 2 продукта за наличный расчет. Это может измениться в будущем, поэтому я не хочу менять весь код с помощью ifs и других, поэтому я подумал об использовании шаблона стратегии.
Я только изучаю шаблоны проектирования. Я больше всего интересуюсь дизайном UML, прежде чем приступить к написанию кода.
Итак, это хороший шаблон для моего случая? Я мог бы добавить больше проверок в класс Cash или в класс Paypal.
Изменить: я добавляю дополнительную информацию.
Это просто пример. У меня есть только одна проверка, например, максимальная сумма, которую я могу продать, составляет 100 долларов, если это наличные, или 10000 долларов, если это Paypal. Но, скажем, завтра они попросят меня добавить еще одно подтверждение, что я не могу продавать наличные после 9 вечера. Я не знаю, как поместить это в дизайн, не использовать шаблон стратегии.
Edit2:
Позвольте мне привести еще один пример, который мог бы прояснить.
Вы можете забронировать билеты двумя способами: Paypal или Cash. Если вы платите наличными, я хочу разрешить только 2 билета, но если вы используете Paypal, вы можете купить любую сумму, какую захотите.
Итак, у меня есть класс под названием Reservation, в котором есть 2 детей: Paypal Cash
У меня есть целое число, которое называется numberOfTickets при бронировании. На наличные у меня есть целое число скидок На PayPal у меня есть адрес электронной почты учетной записи.
Теперь я хочу добавить некоторые правила, во-первых, ограничить до 2 билетов, если это наличные. Завтра у меня может быть 10 лимитов для Paypal.
Так является ли Стратегия лучшей?
3 ответа
На данный момент ваше решение кажется приемлемым, однако я бы предпочел, чтобы вы разработали какую-то политику правил, чтобы ваше бронирование на самом деле не заботилось о том, как оно оплачивается, а скорее, чтобы правила определялись вариантом использования (вы заметите, что это решение на самом деле также технически основано на шаблоне стратегии).
Например, предположим, у вас есть театральный класс, для которого вы бронируете билеты. Следующий метод в этом Театральном классе:
public PaymentResult MakeReservation(IPaymentPolicy paymentPolicy, int itemsToBuy)
{
var result = paymentPolicy.Verify(itemsToBuy);
if(result.HasFailingRules)
{
return result;
}
// Do your booking here.
}
Здесь объект театра отвечает за одно решение - разрешено ли бронирование с учетом предоставленных мне правил? Если да, то сделайте заказ, в противном случае сообщите об ошибках.
Затем вызывающая сторона контролирует правила в зависимости от варианта использования. Например:
public void MakePaypalReservation(int itemsToBuy)
{
var rulesPolicy = new PaymentRulesPolicy(
new MaxItemsRule(10),
new MaxAmountRule(10000)
);
var theatre = this.repo.Load("Theatre A"); // Load by id
var paymentResult = theatre.MakeReservation(rulesPolicy, itemsToBuy);
// Here you can use the result for logging or return to the GUI or proceed with the next step if no errors are present.
}
public void MakeCashReservation(int itemsToBuy)
{
var rulesPolicy = new PaymentRulesPolicy(
new MaxItemsRule(2),
new MaxAmountRule(100),
new TimeOfDayRule(8, 20) //Can only buy between 8 in the morning at 8 at night as an example.
);
var theatre = this.repo.Load("Theatre A"); // Load by id
var paymentResult = theatre.MakeReservation(rulesPolicy, itemsToBuy);
// Here you can use the result for logging or return to the GUI or proceed with the next step if no errors are present.
}
Давайте предположим, что у PaymentRulesPolicy есть конструктор с такой подписью:
public PaymentRulesPolicy(params IRule[] rules);
У вас есть метод для каждого варианта использования. Если вы можете оплатить другим способом, например, ваучером, вы можете создать новую политику с некоторыми новыми правилами.
Вам, конечно, также необходимо предоставить объекту театра всю информацию, необходимую для бронирования. Метод Verify() политики правил, скорее всего, примет все эти фрагменты информации и передаст минимально необходимую информацию отдельным правилам.
Вот пример того, как может выглядеть политика правил:
public class PaymentRulesPolicy
{
private readonly IRule[] rules;
public PaymentRulesPolicy(params IRule[] rules)
{
this.rules = rules;
}
public PaymentResult Verify(int numItemsToBuy, DateTime bookingDate)
{
var result = new PaymentResult();
foreach(var rule in this.rules)
{
result.Append(rule.Verify(numItemsToBuy, bookingDate);
}
return result;
}
}
Это уже плохой интерфейс, так как все правила требуют всю информацию, независимо от того, что он проверяет. Если это выходит из-под контроля, вы можете улучшить его, передав информацию о бронировании при первом создании политики:
var rulesPolicy = new PaymentRulesPolicy(
new MaxItemsRule(2, itemsToBuy),
new MaxAmountRule(100, itemsToBuy, costPerItem),
new TimeOfDayRule(8, 20, currentDateTime)
);
В конце концов, выгода этого шаблона заключается в том, что все ваши бизнес-решения заключены в один класс, что делает его чрезвычайно простым в обслуживании. Надеемся, что просто рассмотрение конструкции этих политик даст вам хороший обзор того, что они будут применять. Затем вы можете составить эти правила в большую политику.
Другим преимуществом этого подхода также будет модульное тестирование. Вы можете очень легко проверить правила в изоляции. Можно даже создать фабрику для правил и проверить, что фабрика создает правильную политику с правильными правилами для каждого варианта использования и т. Д.
Помните, что это только одно из многих возможных решений, и это конкретное решение может быть излишним для вашего приложения или, возможно, оно не соответствует шаблонам, с которыми вы и ваша команда знакомы. Экспериментируя с решением по наследованию, вы можете обнаружить, что его достаточно или даже проще понять, учитывая привычки и опыт вашей команды.
Сначала рассмотрим природу правила. Является ли общим правилом выдачи наличных, что вы можете принимать только наличные за пункт 1, но не за пункт 2? В моем магазине я возьму деньги на что угодно. Это правило, что магазины могут принимать только 100 долларов наличными? Опять же, не в моем магазине, где я возьму любую сумму денег.
Теперь подумайте об этом из графического интерфейса - после 21:00 вы, возможно, даже не захотите отображать кнопку "наличные", поэтому кассир знает, что наличные не разрешены. Вы еще даже не создали объект Cash - нет способа выполнить стратегию в объекте Cash. Это большая подсказка, что объект наличных денег не должен содержать стратегию, о которой вы говорите.
Таким образом, ограничения здесь, похоже, не являются свойствами наличных денег, они кажутся бизнес-правилами. Эти правила должны быть применены еще до того, как вы поймете, какой тендер они оплачивают.
Это могут быть правила для вашего объекта транзакции. Ваш бизнес не позволяет одной транзакции содержать пункт 2 плюс денежный тендер. Это кажется мне правдоподобным.
Или, может быть, это бизнес-правила, которые применяются за пределами вашего домена для объектов, транзакций и тендеров, и они должны существовать в каком-то механизме правил, который проверяет позиции и тендеры по мере их добавления в транзакцию.
Есть много способов рассмотреть проблему. Вместо того, чтобы спрашивать о шаблонах проектирования, изучите ваши объекты с точки зрения принципов разработки SOLID. Если вам кажется, что вы несете неуместную ответственность за объект, вам, вероятно, нужно найти другое место для логики.
Ваш выбор использования шаблона стратегии правильный. Чтобы решить проблему расширения, вы можете использовать шаблон Decorator, как мой пример кода на Java ниже. Чтобы облегчить это, я использовал только тип int для всех входных данных, таких как количество, время и количество билетов.
Реализация
public abstract class Payment {
protected int amount;
protected int time;
protected int numTickets;
public Payment (int amount, int numTickets) {
this.amount = amount;
this.numTickets = numTickets;
}
public Payment (int amount, int time, int numTickets) {
this.amount = amount;
this.time = time;
this.numTickets = numTickets;
}
public abstract void doPayment();
}
public class CashPayment extends Payment {
public CashPayment(int amount, int time, int numTickets) {
super(amount, time, numTickets);
}
@Override
public void doPayment() {
System.out.println("Make the payment in Cash");
}
}
public abstract class Verificator {
protected Payment payment;
protected int maxAmount;
protected int maxTime;
protected int maxNumTickets;
public abstract void verify();
public abstract void verifyUpperBound(int amount, int max);
public abstract void verifyTime(int time, int max);
public abstract void verifyNumberTickets(int numTicket, int max);
public Verificator(Payment payment, int maxAmount, int maxNumTickets) {
this.payment = payment;
this.maxAmount = maxAmount;
this.maxNumTickets = maxNumTickets;
}
public Verificator(Payment payment, int maxAmount, int maxTime, int
maxNumTickets) {
this.payment = payment;
this.maxAmount = maxAmount;
this.maxTime = maxTime;
this.maxNumTickets = maxNumTickets;
}
}
public class CashPaymentVerificator extends Verificator {
public CashPaymentVerificator(Payment payment, int maxAmount, int maxTime, int maxNumTickets) {
super(payment, maxAmount, maxTime, maxNumTickets);
}
@Override
public void verify() {
verifyUpperBound(this.payment.getAmount(), this.maxAmount);
verifyTime(this.payment.getTime(), this.maxTime);
verifyNumberTickets(this.payment.getNumTickets(), this.maxNumTickets);
}
@Override
public void verifyUpperBound(int amount, int max) {
if (amount > max)
throw new IllegalStateException("Can not pay cash over $"+max);
}
@Override
public void verifyTime(int time, int max) {
if (time > max)
throw new IllegalStateException("Can not pay cash after "+max+" PM");
}
@Override
public void verifyNumberTickets(int numTicket, int max) {
if (numTicket > max)
throw new IllegalStateException("Can not reserve more than "+max+"
tickets by cash");
}
}
public class Context {
private Payment payment;
public Context(Payment payment) {
this.payment = payment;
}
public void doPayment() {
this.payment.doPayment();
}
}
public class StrategyMain {
public static void main(String[] args) {
Payment payment = new CashPayment(99, 8, 1);
Verificator verificator = new CashPaymentVerificator(payment, 100, 9,2);
verificator.verify();
Context context = new Context(payment);
context.doPayment();
payment = new PaypalPayment(1000, 11);
verificator = new PaypalPaymentVerificator(payment, 10000, 10);
verificator.verify();
context = new Context(payment);
context.doPayment();
}
}