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#». Подробнее