Реализация пользовательского шаблона посетителя

Я пытаюсь реализовать своего рода шаблон посетителя. Большинство примеров в Интернете показывают класс посетителя с методом «посетить» и несколькими перегрузками этого метода. В этом случае я назвал свой метод "посещения" CalculateFee (это семантический вопрос) с его перегрузками. Пока все в порядке, но теперь мне нужно снова реализовать посетителя, чтобы выполнить другой метод «CalculateExtraCharge», поэтому я добавил еще один метод с именем CalculateExtraCharge с его перегрузками. Но теперь у меня есть два вопроса

1) Это неправильная реализация шаблона?

2) Должен ли я всегда называть свой метод «посещением»?

Вот обзор моего кода, я опустил некоторые его части, чтобы сосредоточиться только на том, что важно для моего вопроса.

    public class CreditCard : IPaymentMethod
    {
        public decimal Amount { get; set; }

        public decimal GetFee(IPaymentCalculationsVisitor visitor)
        {
            return visitor.CalculateFee(this);
        }

        public decimal GetExtraCharge(IPaymentCalculationsVisitor visitor)
        {
            return visitor.CalculateExtraCharge(this);
        }

    }

    public class Check : IPaymentMethod
    {
        public decimal Amount { get; set; }

        public decimal GetFee(IPaymentCalculationsVisitor visitor)
        {
            return visitor.CalculateFee(this);
        }

        public decimal GetExtraCharge(IPaymentCalculationsVisitor visitor)
        {
            return visitor.CalculateExtraCharge(this);
        }
    }

    public interface IPaymentCalculationsVisitor
    {
        decimal CalculateFee(CreditCard creditCard);
        decimal CalculateFee(Check check);

        decimal CalculateExtraCharge(CreditCard creditCard);
        decimal CalculateExtraCharge(Check check);
    }

    public class PaymentCalculationsVisitor: IPaymentCalculationsVisitor
    {

        public decimal CalculateFee(CreditCard creditCard)
        {
            return creditCard.Amount * 0.15m;

        }

        public decimal CalculateFee(Check check)
        {
            return check.Amount * 0.10m;
        }

        public decimal CalculateExtraCharge(CreditCard creditCard)
        {
            return 15;
        }

        public decimal CalculateExtraCharge(Check check)
        {
            return 10;
        }

    }

    public class PaymentProcessor
    {

        public void ProcessPayment()
        {
            var paymentMethods = new List<IPaymentMethod>()
            {
                new CreditCard(),
                new Check()
            };

            var calculationsVisitor = new PaymentCalculationsVisitor();

            foreach (var paymentMethod in paymentMethods)
            {

                //First i need to get the fee
                var fee = paymentMethod.GetFee(calculationsVisitor);

                //Then i do do some other stuff, validations, other calculations etc

                //Finally i get the extra charge
                var extraCharge = paymentMethod.GetExtraCharge(calculationsVisitor);
            }

        }

    }

person FredE    schedule 19.02.2019    source источник
comment
Пожалуйста, смотрите отредактированный абзац в моем ответе. Хотя добавление второго метода посещения в иерархию IPaymentMethod будет работать, особенность шаблона заключается в том, чтобы разрешить логику новых посетителей без необходимости изменять посещенные типы. Именно поэтому метод однократного посещения является наиболее распространенным. Нет ничего плохого в двух методах посещения, но шаблон разработан таким образом, чтобы не добавлять дополнительные методы. Вместо этого вы создаете новые классы посетителей и передаете их существующему методу посещения.   -  person jaco0646    schedule 20.02.2019


Ответы (2)


1) Это неправильная реализация шаблона?

Нет, это все еще шаблон посетителя GoF. Возможность IPaymentCalculationsVisitor обращаться к двум различным методам не меняет природу паттерна. Однако, поскольку он объединяет логику для двух разных посещений, вы можете подумать о принципах SOLID.

  • Являются ли оба метода частью единой ответственности, т. е. будут ли они оба изменяться по одним и тем же причинам, или один метод потенциально может измениться независимо от другого?
  • Будут ли потенциальные клиенты всегда зависеть от обоих методов? Разделение интерфейсов разделит CalculateFee и CalculateExtraCharge, чтобы клиенты могли использовать одно без другого.

Обратите внимание, что шаблон посетителя позволяет добавлять новое поведение в иерархию типов IPaymentMethod без необходимости изменять дочерние классы, такие как CreditCard и Check. Разделив интерфейс посетителя на FeeVisitor и ExtraChargeVisitor, оба можно передать в один метод посещения.

2) Должен ли я всегда называть свой метод «посещением»?

Нет, в любом шаблоне проектирования вы можете называть свои методы как угодно. Очень важно, чтобы код имел значение для вас и для его области. Для общего словарного запаса с другими разработчиками вы можете рассмотреть возможность добавления документации с использованием опубликованной терминологии шаблона, если вы считаете, что это прояснит назначение кода.

person jaco0646    schedule 19.02.2019
comment
Если CreditCard предоставляет все необходимые данные (через свойства или методы), то почему мне нужно использовать шаблон посетителя, вместо этого PaymentProcessor просто вызовите var fee = calculationsVisitor.CalculateFee(creditCard) (удалить префикс ... Посетитель). Код был бы проще, и CreditCard не нужно было бы зависеть от IPaymentCalculationsVisitor. - person Fabio; 20.02.2019
comment
Посетитель позволяет единообразно обрабатывать коллекцию IPaymentMethod. Если вы знаете конкретный тип объекта, вы можете обращаться с ним соответствующим образом. Если вы знаете только его интерфейс, посетитель может применять поведение, зависящее от типа, без приведения или ветвления. - person jaco0646; 20.02.2019
comment
В случае OP посетитель зависит от конкретного типа CreditCard, Check. Кредитная карта только передает себя IPaymentCalculations, эту работу может выполнить PaymentProcessor, где все зависимости уже существуют. - person Fabio; 20.02.2019
comment
Интерфейс посетителя всегда зависит от конкретных типов посещаемой им иерархии. Это часть двойной отправки, которую он обеспечивает. Sourcemaking – отличный источник определений шаблонов и примеров. - person jaco0646; 20.02.2019
comment
Если вы на секунду забудете о шаблонах и подумаете о коде, в котором вы передаете IPaymentCalculationsVisitor в CredtCard, только для того, чтобы CreditCard передал себя в IPaymentCalculationsVisitor. Зачем вам нужно вводить дополнительный уровень косвенности, когда вместо этого вы просто выполняете var fee = paymentCalculations.calculateFeeFor(creditCard); в классе PaymentProcessor? - person Fabio; 21.02.2019
comment
Хотя этот ответ написан по книге, ответ @Fabio имеет больше смысла в объектно-ориентированном мире (или, я бы сказал, ориентирован на сообщения). - person Basin; 22.02.2019

2) Должен ли я всегда называть свой метод «посещением»?

Нет, назовите метод более специфичным для домена способом.

1) Это неправильная реализация шаблона?

Глядя на вашу реализацию, я нашел ее немного другой.

public class CreditCard : IPaymentMethod
{
    public decimal Amount { get; set; }

    public decimal GetFee(IPaymentCalculationsVisitor visitor)
    {
        return visitor.CalculateFee(this);
    }

    public decimal GetExtraCharge(IPaymentCalculationsVisitor visitor)
    {
        return visitor.CalculateExtraCharge(this);
    }
}

Одним из методов объектно-ориентированного программирования является инкапсуляция, при которой объект хранит свои данные (не раскрывая их внешнему миру).
С помощью паттерна "Посетитель" мы можем предоставить дополнительную функциональность объекту, не раскрывая его данные снаружи.

Поскольку внутренние данные не отображаются снаружи объекта, посетителю необходимо «посетить объект внутри», где объект сможет предоставить посетителю требуемые значения, не раскрывая эти значения снаружи (не делая эти значения общедоступными).

Для случая вопроса мы можем передать калькулятор (посетитель) в класс CreditCard, где калькулятор будет принимать только необходимые данные в качестве аргументов (обратите внимание, только необходимые значения, а не весь объект).

public class CreditCard : IPaymentMethod
{
    // Following OOP principles and keep data private
    private decimal _amount;

    public CreditCard(decimal amount) => _amount;

    public decimal GetFee(IPaymentCalculationsVisitor visitor)
    {
        return visitor.CalculateFee(_amount); // provide only required data
    }

    public decimal GetExtraCharge(IPaymentCalculationsVisitor visitor)
    {
        return visitor.CalculateExtraCharge(_amount); // provide only required data
    }
}

При таком подходе класс калькулятора (посетителя) не будет зависеть от классов, которые он может посещать. На самом деле он может посетить любой класс, который может предоставить необходимую информацию.

В вашем конкретном случае, когда CreditCard предоставляет данные (имеющие общедоступное свойство Amount) - вы можете удалить избыточный шаг и передать объект кредитной карты прямо в расчеты

 public void ProcessPayment()
 {
     var paymentMethods = new List<IPaymentMethod>()
     {
            new CreditCard(),
            new Check()
     };

     var calculations = new PaymentCalculationsVisitor();

     foreach (var paymentMethod in paymentMethods)
     {
        //First i need to get the fee
        var fee = calculations.GetFee(paymentMethod);

        //Then i do do some other stuff, validations, other calculations etc

        //Finally i get the extra charge
        var extraCharge = calculations.GetExtraCharge(paymentMethod);
    }
}
person Fabio    schedule 19.02.2019