DEV Community

Cover image for Design Patterns #11: Controlled Extensibility in C#
Serhii Korol
Serhii Korol

Posted on

Design Patterns #11: Controlled Extensibility in C#

Hello. Today, I want to discuss a topic you're probably familiar with but might not consider very important: the Template Method pattern. We'll explore how it functions, when to apply it, and what problems it can solve.

Beginners often make this mistake by ignoring this pattern, which can impact readability, extendability, and performance. Let's examine a common scenario where it can be applied.

Scenario

Imagine you run an e-shop with various payment methods — the simplest way is to add a payment service for each method.

public class PayPalService
{
    public async Task ProcessPaymentAsync(decimal amount)
    {
        await ValidatePaymentAsync();
        await AuthorizePaymentAsync(amount);
        await ExecutePaymentAsync(amount);
        await SendReceiptAsync();
        await Task.CompletedTask;
    }

    private Task ValidatePaymentAsync()
    {
        Console.WriteLine("✅ Checking PayPal-account...");
        return Task.CompletedTask;
    }

    private Task AuthorizePaymentAsync(decimal amount)
    {
        Console.WriteLine($"🔐 Authorizing PayPal to {amount:C}...");
        return Task.CompletedTask;
    }

    private Task ExecutePaymentAsync(decimal amount)
    {
        Console.WriteLine($"💰 Executing PayPal payment to {amount:C}...");
        return Task.CompletedTask;
    }

    private Task SendReceiptAsync()
    {
        Console.WriteLine("📧 Sending receipt via email...");
        return Task.CompletedTask;
    }
}

public class StripeService
{
    public async Task ProcessPaymentAsync(decimal amount)
    {
        await ValidatePaymentAsync();
        await AuthorizePaymentAsync(amount);
        await ExecutePaymentAsync(amount);
        await SendReceiptAsync();
        await Task.CompletedTask;
    }

    private Task ValidatePaymentAsync()
    {
        Console.WriteLine("✅ Checking Stripe-account...");
        return Task.CompletedTask;
    }

    private Task AuthorizePaymentAsync(decimal amount)
    {
        Console.WriteLine($"🔐 Authorizing Stripe to {amount:C}...");
        return Task.CompletedTask;
    }

    private Task ExecutePaymentAsync(decimal amount)
    {
        Console.WriteLine($"💰 Executing Stripe payment to {amount:C}...");
        return Task.CompletedTask;
    }

    private Task SendReceiptAsync()
    {
        Console.WriteLine("📧 Sending receipt via email...");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

Payment methods generally share similar functions and perform identical tasks. However, they are often separated to accommodate future extensions. This can result in redundant code, even though the code functions correctly. At first glance, this approach appears advantageous because it allows service expansion from a single location.

Template method

The Template Method pattern solves the problem of redundant code by following the DRY principle.

scheme

The Template method is an abstract class consisting of three parts:

  • a public or template method, which is a non-abstract method that encapsulates all protected methods in the class;
  • abstract methods, which are meant to be overridden by derived classes;
  • virtual methods or hooks, which may have a default implementation or not.
public abstract class PaymentProcessor
{
    public async Task ProcessPaymentAsync(decimal amount)
    {
        await ValidatePaymentAsync();
        await AuthorizePaymentAsync(amount);
        await ExecutePaymentAsync(amount);
        await SendReceiptAsync();
    }

    protected abstract Task ValidatePaymentAsync();
    protected abstract Task AuthorizePaymentAsync(decimal amount);
    protected abstract Task ExecutePaymentAsync(decimal amount);

    protected virtual Task SendReceiptAsync()
    {
        Console.WriteLine("📧 Sending receipt via email...");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

Payments typically include common features like public methods and sending receipts. We can extract these methods into an abstract class to prevent code duplication.

public class PayPalPaymentProcessor : PaymentProcessor
{
    protected override Task ValidatePaymentAsync()
    {
        Console.WriteLine("✅ Checking PayPal-account...");
        return Task.CompletedTask;
    }

    protected override Task AuthorizePaymentAsync(decimal amount)
    {
        Console.WriteLine($"🔐 Authorizing PayPal to {amount:C}...");
        return Task.CompletedTask;
    }

    protected override Task ExecutePaymentAsync(decimal amount)
    {
        Console.WriteLine($"💰 Executing PayPal payment to {amount:C}...");
        return Task.CompletedTask;
    }
}

public class StripePaymentProcessor : PaymentProcessor
{
    protected override Task ValidatePaymentAsync()
    {
        Console.WriteLine("✅ Checking Stripe-account...");
        return Task.CompletedTask;
    }

    protected override Task AuthorizePaymentAsync(decimal amount)
    {
        Console.WriteLine($"🔐 Authorizing Stripe to {amount:C}...");
        return Task.CompletedTask;
    }

    protected override Task ExecutePaymentAsync(decimal amount)
    {
        Console.WriteLine($"💰 Executing Stripe payment to {amount:C}...");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

Derived classes override abstract methods solely for their specific purpose. This approach addresses the problem of duplicated code.

However, consider a situation where you need to extend a service. In the previous approach, this can be done easily. Here's an update to the base class:

public abstract class PaymentProcessor
{
    public async Task ProcessPaymentAsync(decimal amount)
    {
        await ValidatePaymentAsync();
        await AuthorizePaymentAsync(amount);
        await ExecutePaymentAsync(amount);


        await CashbackPaymentAsync(amount);

        await SendReceiptAsync();
    }

    ...

    protected virtual Task CashbackPaymentAsync(decimal amount)
    {
        return Task.CompletedTask;
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

You should add an additional hook. Because it is only required for one service, we included an empty virtual method for you to override as needed, ensuring other services remain unaffected.

public class StripePaymentProcessor : PaymentProcessor
{
    ...
    protected override Task CashbackPaymentAsync(decimal amount)
    {
        var cashback = Math.Round(amount * 0.05m, 2);
        Console.WriteLine($"🎁 Cashback is credited {cashback:C} to your Stripe-account!");
        return Task.CompletedTask;
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Behavior you can adopt in the targeted service.

Performance

What about performance? The results will impress you. Despite having one additional class and an extended service, we still deliver the best performance.

| Method                | Mean     | Error      | StdDev   |
|---------------------- |---------:|-----------:|---------:|
| TemplatePaymentMethod | 263.6 us | 1,562.5 us | 85.65 us |
| NaivePaymentMethod    | 375.4 us | 1,119.0 us | 61.34 us |
Enter fullscreen mode Exit fullscreen mode

Conclusion

Don't underestimate the significance of this pattern. The Template Method pattern addresses key challenges:

  • Readability: Each service includes only its specific code, improving clarity.
  • Extendability: It offers flexibility to add or modify multiple services.
  • Avoiding Repetition: Supports the DRY principle by preventing duplicate code.
  • Performance: Ensures optimal performance.

If you found this article helpful, please consider supporting me.
Buy Me A Beer

This article's sample, along with others, focuses on design patterns available in my repository.

Top comments (0)