Implementing a Command Handler Pipeline: A Guide for Clean Software Architecture
In software development, adhering to the Single Responsibility Principle (SRP) is crucial for creating maintainable and scalable code. One effective design pattern that helps achieve SRP is the chain of command pattern. I find this pattern straightforward, intuitive, and incredibly useful when paired with the concept of a "pipeline" and the traditional factory pattern.
Building the Pipeline Structure
The pipeline acts as a container for a set of commands, which allows for the grouping of commands for different purposes, such as Order Processing and Email Notification.
Example Use Case
Consider a domain where scenarios like Order Processing, Payment Capture, or User Registration are common:
var context = new Context();
context.Request.Type = "NewOrder";
Command Handlers
Each command in our architecture is tasked with a specific part of an operation, encapsulating the business logic necessary for each step:
var orderProcessingHandler = new OrderProcessingHandler();
var paymentCaptureHandler = new PaymentCaptureHandler();
var emailNotificationHandler = new EmailNotificationHandler();
var errorHandler = new ErrorHandler();
Pipelines
Pipelines enable a flexible combination of various commands. Command handlers can be reused across different pipelines, enhancing modularity and reusability:
paymentCaptureHandler.Next(ctx => ctx.Response.Success ? emailNotificationHandler : errorHandler);
var orderProcessingPipeline = new CommandHandlerPipeline(() =>
{
orderProcessingHandler.Next(ctx => paymentCaptureHandler);
return orderProcessingHandler;
});
The Factory Pattern
A factory is then used to instantiate specific command handler pipelines based on the request type, ensuring that the correct pipeline is used for each operation:
var factory = new HandlerPipelineFactory(new Dictionary<PipelineCommandHandlerType, ICommandHandlerPipeline>()
{
[PipelineCommandHandlerType.NewOrder] = orderProcessingPipeline,
});
// Execute the pipeline for a new order.
factory.Get(PipelineCommandHandlerType.NewOrder).RunAsync(context);
Core Components
To implement this architecture, here are the essential classes and interfaces you will need:
public class HandlerPipelineFactory
{
private Dictionary<PipelineCommandHandlerType, ICommandHandlerPipeline> _pipelines;
public HandlerPipelineFactory(Dictionary<PipelineCommandHandlerType, ICommandHandlerPipeline> pipelines)
{
_pipelines = pipelines;
}
public ICommandHandlerPipeline Get(PipelineCommandHandlerType name)
{
return _pipelines.GetValueOrDefault(name);
}
}
public enum PipelineCommandHandlerType
{
NewOrder
}
public interface ICommandHandlerPipeline
{
Task RunAsync(Context context);
}
public class CommandHandlerPipeline : ICommandHandlerPipeline
{
private Func<CommandHandler> _rootHandler;
public CommandHandlerPipeline(Func<CommandHandler> rootHandler)
{
_rootHandler = rootHandler;
}
public async Task RunAsync(Context context)
{
await _rootHandler().HandleRequestAsync(context);
}
}
public abstract class CommandHandler
{
private Func<Context, CommandHandler> _nextHandler;
public void Next(Func<Context, CommandHandler> nextHandler)
{
_nextHandler = nextHandler;
}
public abstract Task HandleRequestAsync(Context context);
protected async Task PassToNextHandlerAsync(Context context)
{
var nextHandler = _nextHandler?.Invoke(context);
if (nextHandler != null)
{
await nextHandler.HandleRequestAsync(context);
}
}
}
public class Request
{
public string Type { get; set; }
}
public class Response
{
public string Type { get; set; }
public bool Success { get; set; }
}
public class Context
{
public Request Request { get; } = new Request();
public Response Response { get; } = new Response();
}
This structured approach not only enhances code maintainability but also ensures that each component of our system is responsible for a single functionality, aligning perfectly with the Single Responsibility Principle.
Good reading. Well documented. Thanks for publishing Arif Y. .