How and why to use Template Method in C#

Template Method is a design pattern that defines the skeleton of an algorithm in a method, leaving certain steps to subclasses. Simply put, there is a basic algorithm, but we can change the details by redefining parts of this algorithm in its descendants.

A classic example is the process of ordering goods in an online store. No matter what kind of store you have, the steps are roughly the same: checking product availability, payment, packaging and delivery. But depending on the specifics of the store, these steps may differ in detail.

The Template Method allows you to create a basic structure for these steps and change specific implementations without changing the structure itself. In this article we will look at how to implement this pattern in C#.

Basic pattern structure

To begin with, let's draw on our fingers. Here is the base class OrderProcessand it contains a method ProcessOrder(). This method outlines the basic steps − template steps. These steps can be represented as methods that subclasses can override to change behavior.

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("Чек сгенерирован.");
    }
}

Now let's create two implementations of the order process − OnlineOrder And StoreOrder. OnlineOrder will represent a purchase in an online store, and StoreOrder — a regular order in a retail store.

Example code for 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; // Онлайн-заказчик, предположим, не хочет чека
    }
}

Example code for 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("Товар выдан покупателю.");
    }
}

Here's what we did:

  • Template method ProcessOrder — fixes the general structure of the algorithm.

  • Abstract methods SelectProduct, MakePayment, Package, Deliver — define the steps that must be implemented in subclasses.

  • Method CustomerWantsReceipt – a “hook” that allows subclasses to modify the algorithm without redefining it entirely.

This approach avoids duplication and increases flexibility if steps suddenly need to be changed by adding new features in subclasses. For example, you can add a new subclass GiftOrder with non-standard gift packaging.

Example with a gift order:

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; // Допустим, клиент всё-таки хочет чек
    }
}

Now let's run all three implementations. Let's just create objects and call 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();
    }
}

Result:

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

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

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

Integration of Template Method with other patterns

Useful to know how Template Method can coexist harmoniously with other patterns

Template Method and Dependency Injection

When we combine the Template Method with DI, we get a flexible and testable architecture where dependencies can be easily replaced without changing the underlying algorithm.

Let's say there is a system that processes orders, and we need to log every step of the process. Instead of hard-coupling the logger with the base class, we'll implement it via the constructor:

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("Чек сгенерирован.");
    }
}

Now let's create a specific implementation of the order using a logger:

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;
    }
}

Usage:

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

Result:

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

Template Method testing

Pattern testing Template Method may seem complicated due to the dependency on inheritance, but with the right approach it is quite a doable task. Let's look at how we can test our OrderProcess and its subclasses.

Using frameworks for creating mock objects, such as how Moqyou can check method calls and behavior of subclasses.

Sample test using Moq and 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);
    }
}

In this example we create a mock object OnlineOrderwhich allows you to track method calls. We check that all required methods are called once, and the method GenerateReceipt not called because CustomerWantsReceipt() returns false.

It is also useful to test specific algorithm steps in subclasses to ensure they perform as expected. Example:

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());
        // Дополнительные проверки могут включать использование моков для отслеживания вызовов
    }
}

In production, you can separate logic to make testing easier by injecting dependencies or using events instead of direct method calls.

Potential pitfalls

Like any pattern, Template Method has its limitations and can cause problems if used incorrectly.

  • Deep inheritance hierarchy: overuse Template Method can lead to the creation of a complex class hierarchy.

  • Strong connectedness: Subclasses rely heavily on the base class, which can make them difficult to modify or reuse in other contexts.


Brief conclusions

  • Template Method helps define the general algorithm, leaving the details to the subclasses.

  • It's great for scenarios with repeating overall logic and varying steps.

  • It is important to avoid overuse of inheritance and be aware of possible pitfalls such as deep class hierarchies.

  • Combination with other patterns such as Dependency Injection or Decoratorin some cases increases the flexibility of the system.

See you again!

In conclusion, I recommend paying attention to the open lessons of the “C# Developer. Professional” course:

October 28: “Data Serializer Using Reflection and Generics.” More details
November 12: “Behavioral Design Patterns in C#.” More details

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *