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/
Добавить комментарий