In this article, we will talk about a behavioral design pattern, the Template Method Pattern. We are going to see how this pattern helps deal with certain design problems and how to implement it in C#.

To download the source code for this article, you can visit our GitHub repository.

If you want to read more about design patterns in C#, you can inspect our C# Design Patterns page.

Let’s start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

What is The Template Method Design Pattern?

The Template Method is a design paradigm that emphasizes the reuse of an algorithm structure while letting subclasses redefine certain parts of the algorithm. As the name implies, this pattern tells us to design our concrete classes based on a “template method”. 

In C#, such a template method is nothing but a top-level public routine inherited from a base class:

Template Method Design Pattern

In the illustration, operation() is the template method that is defined in an abstract base class and each concrete descendant inherits it naturally. This routine outlines the combined algorithm and carries out a sequence of sub-routines. We can classify these sub-routines into three categories –

Private/protected methods represent the static parts of the algorithm. These routines define the logic blocks that should not vary in descendants.

Abstract methods represent the dynamic steps of the algorithm which vary in descendant classes and hence must be implemented there.

Virtual methods (aka Hooks) allow the base class to provide a default implementation as well as let the sub-classes revise the logic if needed.

Overall, by following the template pattern, we can derive a revised algorithm simply by refining the select sub-routines of the base algorithm. The key takeaways of this pattern are maximum reuse of logic blocks and minimum effort to adapt a variation.

What Problem Does The Template Method Pattern Solve?

Like other design patterns, the Template Method pattern helps us deal with certain design problems.

As an example, let’s think of a reporting service that sends XML output to a recipient email address:

public class ProductXmlReporting
{
    public void Send()
    {
        var products = GetProducts();

        var output = Transform(products);

        var recipient = GetRecipient();

        SendEmail(output, recipient);
    }

    private Product[] GetProducts() 
        => [.. ProductService.GetProducts().OrderBy(e => e.Name)];

    private string Transform(Product[] products) 
        => "XML output";

    private string GetRecipient()
        => "default recipient";

    private void SendEmail(string output, string recipient) 
        => Console.WriteLine($"Sent {output} to {recipient}");
}

Our reporting feature works in a few steps – reads products from data service, transforms to XML output, collects recipient information, and finally sends an email to the recipient with the generated output.

We also have an equivalent reporting service that sends output in JSON format:

public class ProductJsonReporting
{
    public void Send()
    {
        var products = GetProducts();

        var output = Transform(products);

        var recipient = GetRecipient();

        SendEmail(output, recipient);
    }

    private Product[] GetProducts()
        => [.. ProductService.GetProducts().OrderBy(e => e.Name)];

    private string Transform(Product[] products) 
        => "JSON output";

    private string GetRecipient()
        => "default recipient";

    private void SendEmail(string output, string recipient)
        => Console.WriteLine($"Sent {output} to {recipient}");
}

Since we are dealing with the same set of records and a similar logical sequence of procedures, this service nearly resembles the previous one. The only variation is their internal mechanism inside the “transform” step. Hence by keeping them as isolated classes, we are violating the DRY (Don’t Repeat Yourself) principle and losing the single point of control.

It makes more sense to create a base reporting class with the Send() method and selectively defer the varying parts to subclasses. This is where the template method pattern comes into play.

Implement Template Method Pattern in C#

So, let’s fix our reporting classes using the Template Method pattern.

Let’s add the base abstract class and copy all routines from the XML reporting class:

public abstract class ProductReportingBase
{
    public void Send()
    {
        var products = GetProducts();

        var output = Transform(products);

        var recipient = GetRecipient();

        SendEmail(output, recipient);
    }

    private Product[] GetProducts()
        => [.. ProductService.GetProducts().OrderBy(e => e.Name)];

    protected abstract string Transform(Product[] products);

    private string GetRecipient()
        => "default recipient";

    private void SendEmail(string output, string recipient)
        => Console.WriteLine($"Sent {output} to {recipient}");
}

Since the sub-routines of GetProducts, GetRecipient, and SendEmail have the same logic in both reporting services, we leave them private. However, we enforce the explicit implementation of Transform() in descendants by marking it as a protected abstract.

Let’s clean up our reporting subclasses and derive them from the base class.

The ProductXmlReporting:

public class ProductXmlReporting : ProductReportingBase
{
    protected override string Transform(Product[] products) 
        => "XML output";
}

And the ProductJsonReporting:

public class ProductJsonReporting : ProductReportingBase
{
    protected override string Transform(Product[] products) 
        => "JSON output";
}

As expected, both reporting classes hold only the transformation logic and do not need to bother about other steps. Pretty neat and straightforward.

Using Default Implementation

If we want to implement a PDF reporting service, we can jump-start just by implementing the Transform method with PDF generation logic.

However, let’s imagine the PDF service has a slightly different requirement. It needs to send the report to a different recipient. That means the logic of GetRecipient()now has a variation but we don’t want to change the logic for existing reports.

We can solve this situation by marking the sub-routine as protected virtual:

public abstract class ProductReportingBase
{
    // omitted for brevity

    protected virtual string GetRecipient()
        => "default recipient";

    // omitted for brevity
}

With this change, existing descendants can keep using the default recipient implementation. At the same time, the new PDF service can plug in its version of GetRecipient:

public class ProductPdfReporting : ProductReportingBase
{
    protected override string Transform(Product[] products) 
        => "PDF output";

    protected override string GetRecipient()
        => "pdf recipient";
}

This is how template pattern helps us maintain DRY code and embrace new variations cleanly and effortlessly.

Caveats of Template Method Pattern

Template Method is one of the basic design practices that we can follow in our everyday programming. However, it has some trade-offs that we should be aware of.

The fact that the base class owns the control point may also make it less adaptable and more error-prone. Also, maintaining the right sequence of steps to cover all possible scenarios might be challenging when there are too many steps involved. 

Another potential source of side effects may arise when we employ hooks. Such algorithms often expect the subclasses to call the base method before/after the override is done. Failing to call the base method at the right point results in an unexpected behavior which is quite a common mistake in such scenarios.

Since the template pattern works on inheritance-based flow, it may eventually suffer from a deep hierarchy tree. A composition-based pattern like the Bridge Pattern may provide a better solution.

Conclusion

In this article, we have learned how to use the Template Method design pattern in a C# application. We have demonstrated how this pattern can help us to design reusable algorithms. We have also discussed some factors we should consider while using such patterns.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!