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;
}
}
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.
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;
}
}
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;
}
}
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;
}
...
}
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;
}
...
}
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 |
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.
This article's sample, along with others, focuses on design patterns available in my repository.
Top comments (0)