In the last example, we used HandledMessageName and CanHandle to decide whether a handler could handle a request. There is one problem with that code: if a subclass decides to override CanHandle, and then decides that it no longer requires HandledMessageName, we would end up having a lingering, unused property in our system.
There are worse situations, but we are talking component design here, so why not push that system a little further toward a better design?
One way to fix this is to create a finer-grained class hierarchy, as follows:
Figure 12.4: Class diagram representing the design of the finer-grained project that implements the Chain of Responsibility and Template Method patterns
The preceding diagram looks more complicated than it is. But let’s look at our refactored code first, starting with the new MessageHandlerBase class:
namespace FinalChainOfResponsibility;
public interface IMessageHandler
{
void Handle(Message message);
}
public abstract class MessageHandlerBase : IMessageHandler
{
private readonly IMessageHandler?
_next;
public MessageHandlerBase(IMessageHandler?
next = null)
{
_next = next;
}
public void Handle(Message message)
{
if (CanHandle(message))
{
Process(message);
}
else if (HasNext())
{
_next.Handle(message);
}
}
[MemberNotNullWhen(true, nameof(_next))]
private bool HasNext()
{
return _next != null;
}
protected abstract bool CanHandle(Message message);
protected abstract void Process(Message message);
}
The MessageHandlerBase class manages the Chain of Responsibility by handling the next handler logic and by exposing two hooks (the Template Method pattern) for subclasses to extend:
- bool CanHandle(Message message)
- void Process(Message message)
This class is similar to the previous one, but the CanHandle method is now abstract, and we removed the HandledMessageName property leading to a better responsibility segregation and better hooks.Next, let’s look at the SingleMessageHandlerBase class, which replaces the logic we removed from the MessageHandlerBase class:
public abstract class SingleMessageHandlerBase : MessageHandlerBase
{
public SingleMessageHandlerBase(IMessageHandler?
next = null)
: base(next) { }
protected override bool CanHandle(Message message)
{
return message.Name == HandledMessageName;
}
protected abstract string HandledMessageName { get; }
}
The SingleMessageHandlerBase class inherits from the MessageHandlerBase class and overrides the CanHandle method. It implements the logic related to it and adds the HandledMessageName property that subclasses must define for the CanHandle method to work (a required extension point).The AlarmPausedHandler, AlarmStoppedHandler, and AlarmTriggeredHandler classes now inherit from SingleMessageHandlerBase instead of MessageHandlerBase, but nothing else has changed. Here’s the code as a reminder:
namespace FinalChainOfResponsibility;
public class AlarmPausedHandler : SingleMessageHandlerBase
{
protected override string HandledMessageName => “AlarmPaused”;
public AlarmPausedHandler(IMessageHandler?
next = null)
: base(next) { }
protected override void Process(Message message)
{
// Do something clever with the Payload
}
}
public class AlarmStoppedHandler : SingleMessageHandlerBase
{
protected override string HandledMessageName => “AlarmStopped”;
public AlarmStoppedHandler(IMessageHandler?
next = null)
: base(next) { }
protected override void Process(Message message)
{
// Do something clever with the Payload
}
}
public class AlarmTriggeredHandler : SingleMessageHandlerBase
{
protected override string HandledMessageName => “AlarmTriggered”;
public AlarmTriggeredHandler(IMessageHandler?
next = null)
: base(next) { }
protected override void Process(Message message)
{
// Do something clever with the Payload
}
}
Those subclasses of SingleMessageHandlerBase implement the HandledMessageName property, which returns the message name they can handle, and they implement the handling logic by overriding the Process method as before.Next, we look at the MultipleMessageHandlerBase class, which enables its sub-types to handle more than one message type:
public abstract class MultipleMessageHandlerBase : MessageHandlerBase
{
public MultipleMessageHandlerBase(IMessageHandler?
next = null)
: base(next) { }
protected override bool CanHandle(Message message)
{
return HandledMessagesName.Contains(message.Name);
}
protected abstract string[] HandledMessagesName { get; }
}
The MultipleMessageHandlerBase class does the same as SingleMessageHandlerBase, but it uses a string array instead of a single string, supporting multiple handler names.The DefaultHandler class has not changed. For demonstration purposes, let’s add the SomeMultiHandler class that simulates a message handler that can handle “Foo”, “Bar”, and “Baz” messages:
namespace FinalChainOfResponsibility;
public class SomeMultiHandler : MultipleMessageHandlerBase
{
public SomeMultiHandler(IMessageHandler?
next = null)
: base(next) { }
protected override string[] HandledMessagesName
=> new[] { “Foo”, “Bar”, “Baz” };
protected override void Process(Message message)
{
// Do something clever with the Payload
}
}
This class hierarchy may sound complicated, but what we did was to allow extensibility without the need to keep any unnecessary code in the process, leaving each class with a single responsibility:
- The MessageHandlerBase class handles _next.
- The SingleMessageHandlerBase class handles the CanHandle method of handlers supporting a single message.
- The MultipleMessageHandlerBase class handles the CanHandle method of handlers supporting multiple messages.
- Other classes implement their version of Process method to handle one or more messages.
And voilà! This is another example demonstrating the strength of the Template Method and Chain of Responsibility patterns working together. That last example also emphasizes the importance of the SRP by allowing greater flexibility while keeping the code reliable and maintainable.Another strength of that design is the interface at the top. Anything that does not fit the class hierarchy can be implemented directly from the interface instead of trying to adapt logic from inappropriate structures. The DefaultHandler class is a good example of that.
Tricking code into doing your bidding instead of properly designing that part of the system leads to half-baked solutions that become hard to maintain.