Как и зачем использовать Template Method в C#

от автора

Template Method (он же «Шаблонный метод») — это паттерн проектирования, который определяет скелет алгоритма в методе, оставляя определенные шаги подклассам. Проще говоря, есть базовый алгоритм, но мы можно менять детали, переопределяя части этого алгоритма в наследниках.

Классический пример — процесс заказа товара в интернет-магазине. Независимо от того, какой у вас магазин, шаги примерно одинаковые: проверка наличия товара, оплата, упаковка и доставка. Но в зависимости от специфики магазина, эти шаги могут отличаться в деталях.

Template Method позволяет создать базовую структуру этих шагов и менять конкретные реализации без изменения самой структуры. В этой статье мы рассмотрим как реализовать этот паттерн на C#.

Основная структура паттерна

Для начала — нарисуем на пальцах. Вот есть базовый класс OrderProcess, и он содержит метод ProcessOrder(). В этом методе прописаны основные шаги — шаги шаблона. Эти шаги могут быть представлены как методы, которые подклассы могут переопределять, изменяя поведение.

public abstract class OrderProcess {     // Шаблонный метод, определяющий основной алгоритм.     public void ProcessOrder()     {         SelectProduct();         MakePayment();         if (CustomerWantsReceipt()) // Перехватчик хука — необязательный шаг         {             GenerateReceipt();         }         Package();         Deliver();     }      // Шаги, которые могут быть переопределены в подклассах.     protected abstract void SelectProduct();     protected abstract void MakePayment();     protected abstract void Package();     protected abstract void Deliver();      // "Хук" — метод с базовой реализацией, который можно переопределить.     protected virtual bool CustomerWantsReceipt()      {         return true; // По умолчанию считаем, что клиент хочет чек     }      // Этот метод остается фиксированным — он не изменяется.     private void GenerateReceipt()     {         Console.WriteLine("Чек сгенерирован.");     } }

Теперь создадим две реализации процесса заказа — OnlineOrder и StoreOrder. OnlineOrder будет представлять покупку в онлайн-магазине, а StoreOrder — обычный заказ в розничном магазине.

Пример кода для OnlineOrder:

public class OnlineOrder : OrderProcess {     protected override void SelectProduct()     {         Console.WriteLine("Выбран товар в интернет-магазине.");     }      protected override void MakePayment()     {         Console.WriteLine("Оплата произведена онлайн.");     }      protected override void Package()     {         Console.WriteLine("Товар упакован для доставки.");     }      protected override void Deliver()     {         Console.WriteLine("Товар отправлен почтой.");     }      protected override bool CustomerWantsReceipt()     {         return false; // Онлайн-заказчик, предположим, не хочет чека     } }

Пример кода для StoreOrder:

public class StoreOrder : OrderProcess {     protected override void SelectProduct()     {         Console.WriteLine("Выбран товар в магазине.");     }      protected override void MakePayment()     {         Console.WriteLine("Оплата произведена на кассе.");     }      protected override void Package()     {         Console.WriteLine("Товар упакован в пакет.");     }      protected override void Deliver()     {         Console.WriteLine("Товар выдан покупателю.");     } }

Здесь мы сделали вот что:

  • Шаблонный метод ProcessOrder — фиксирует общую структуру алгоритма.

  • Абстрактные методы SelectProduct, MakePayment, Package, Deliver — определяют шаги, которые должны быть реализованы в подклассах.

  • Метод CustomerWantsReceipt — «хук», который позволяет подклассам модифицировать алгоритм, не переопределяя его целиком.

Этот подход позволяет избежать дублирования и повысить гибкость, если вдруг потребуется изменить шаги, добавив новые особенности в подклассах. Например, можно добавить новый подкласс GiftOrder с нестандартной упаковкой подарков.

Пример с подарочным заказом:

public class GiftOrder : OrderProcess {     protected override void SelectProduct()     {         Console.WriteLine("Выбран товар для подарка.");     }      protected override void MakePayment()     {         Console.WriteLine("Оплата подарка произведена.");     }      protected override void Package()     {         Console.WriteLine("Товар упакован как подарок.");     }      protected override void Deliver()     {         Console.WriteLine("Подарок доставлен курьером.");     }      // Переопределяем хук — клиент может выбрать подарочную упаковку.     protected override bool CustomerWantsReceipt()     {         return true; // Допустим, клиент всё-таки хочет чек     } }

Теперь запустим все три реализации. Просто создадим объекты и вызовем ProcessOrder().

class Program {     static void Main()     {         OrderProcess onlineOrder = new OnlineOrder();         onlineOrder.ProcessOrder();          Console.WriteLine();          OrderProcess storeOrder = new StoreOrder();         storeOrder.ProcessOrder();          Console.WriteLine();          OrderProcess giftOrder = new GiftOrder();         giftOrder.ProcessOrder();     } }

Результат:

Выбран товар в интернет-магазине. Оплата произведена онлайн. Товар упакован для доставки. Товар отправлен почтой.  Выбран товар в магазине. Оплата произведена на кассе. Товар упакован в пакет. Чек сгенерирован. Товар выдан покупателю.  Выбран товар для подарка. Оплата подарка произведена. Товар упакован как подарок. Чек сгенерирован. Подарок доставлен курьером.

Интеграция Template Method с другими паттернами

Полезно знать как Template Method может гармонично сосуществовать с другими паттернами

Template Method и Dependency Injection

Когда мы комбинируем Template Method с DI, мы получаем гибкую и тестируемую архитектуру, где зависимости могут легко заменяться без изменения базового алгоритма.

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

public interface ILogger {     void Log(string message); }  public class ConsoleLogger : ILogger {     public void Log(string message)     {         Console.WriteLine($"[ConsoleLogger] {message}");     } }  public abstract class OrderProcess {     private readonly ILogger _logger;      protected OrderProcess(ILogger logger)     {         _logger = logger;     }      public void ProcessOrder()     {         _logger.Log("Начало обработки заказа.");         SelectProduct();         MakePayment();         if (CustomerWantsReceipt())         {             GenerateReceipt();         }         Package();         Deliver();         _logger.Log("Заказ обработан.");     }      protected abstract void SelectProduct();     protected abstract void MakePayment();     protected abstract void Package();     protected abstract void Deliver();      protected virtual bool CustomerWantsReceipt()     {         return true;     }      private void GenerateReceipt()     {         Console.WriteLine("Чек сгенерирован.");     } }

Теперь создадим конкретную реализацию заказа с использованием логгера:

public class OnlineOrder : OrderProcess {     public OnlineOrder(ILogger logger) : base(logger) { }      protected override void SelectProduct()     {         Console.WriteLine("Выбран товар в интернет-магазине.");     }      protected override void MakePayment()     {         Console.WriteLine("Оплата произведена онлайн.");     }      protected override void Package()     {         Console.WriteLine("Товар упакован для доставки.");     }      protected override void Deliver()     {         Console.WriteLine("Товар отправлен почтой.");     }      protected override bool CustomerWantsReceipt()     {         return false;     } }

Использование:

class Program {     static void Main()     {         ILogger logger = new ConsoleLogger();         OrderProcess onlineOrder = new OnlineOrder(logger);         onlineOrder.ProcessOrder();     } } 

Результат:

[ConsoleLogger] Начало обработки заказа. Выбран товар в интернет-магазине. Оплата произведена онлайн. Товар упакован для доставки. Товар отправлен почтой. [ConsoleLogger] Заказ обработан.

Тестирование Template Method

Тестирование паттерна Template Method может показаться сложным из-за зависимости от наследования, но с правильным подходом это вполне выполнимая задача. Рассмотрим, как можно протестировать наш OrderProcess и его подклассы.

С помощью фреймворков для создания мок-объектов, например как как Moq, можно проверять вызовы методов и поведение подклассов.

Пример теста с использованием Moq и xUnit:

using Moq; using Xunit;  public class OnlineOrderTests {     [Fact]     public void ProcessOrder_ShouldExecuteStepsCorrectly()     {         // Arrange         var loggerMock = new Mock();         var onlineOrderMock = new Mock(loggerMock.Object)          {              CallBase = true          };          // Act         onlineOrderMock.Object.ProcessOrder();          // Assert         onlineOrderMock.Verify(o => o.SelectProduct(), Times.Once);         onlineOrderMock.Verify(o => o.MakePayment(), Times.Once);         onlineOrderMock.Verify(o => o.GenerateReceipt(), Times.Never); // Поскольку CustomerWantsReceipt() возвращает false         onlineOrderMock.Verify(o => o.Package(), Times.Once);         onlineOrderMock.Verify(o => o.Deliver(), Times.Once);     } }

В этом примере создаем мок-объект OnlineOrder, который позволяет отслеживать вызовы методов. Мы проверяем, что все необходимые методы вызываются один раз, а метод GenerateReceipt не вызывается, поскольку CustomerWantsReceipt() возвращает false.

Также полезно тестировать конкретные шаги алгоритма в подклассах, чтобы убедиться, что они выполняют ожидаемые действия. Примерчик:

public class GiftOrderTests {     [Fact]     public void ProcessOrder_ShouldGenerateReceipt()     {         // Arrange         var loggerMock = new Mock();         var giftOrder = new GiftOrder(loggerMock.Object);          // Act         giftOrder.ProcessOrder();          // Assert         Assert.True(giftOrder.CustomerWantsReceipt());         // Дополнительные проверки могут включать использование моков для отслеживания вызовов     } } 

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

Потенциальные подводные камни

Как и любой паттерн, Template Method имеет свои ограничения и может привести к проблемам, если использовать его неправильно.

  • Глубокая иерархия наследования: чрезмерное использование Template Method может привести к созданию сложной иерархии классов.

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


Краткие выводы

  • Template Method помогает определить общий алгоритм, оставляя детали подклассам.

  • Он отлично подходит для сценариев с повторяющейся общей логикой и изменяющимися шагами.

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

  • Комбинирование с другими паттернами, такими как Dependency Injection или Decorator, в каких то случаях повышает гибкость системы.

До новых встреч!

В заключение порекомендую обратить внимание на открытые уроки курса «C# Developer. Professional»:

28 октября: «Сериализатор данных с использованием Reflection и Generics». Подробнее
12 ноября: «Поведенческие шаблоны проектирования в C#». Подробнее


ссылка на оригинал статьи https://habr.com/ru/articles/852996/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *