This post is part of the series SOLID Wash Tunnel.
Definition
"Chain of Responsibility is a behavioral design pattern that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain."
- Refactoring Guru
The Chain of Responsibility (CoR) pattern is very useful in situations where a program is expected to process different kinds of requests in various ways, but the exact types of requests and their sequences are unknown beforehand.
CoR simplifies the inter-connections between objects. Instead of senders maintaining references to all candidate receivers, each sender keeps a single reference to the head of the chain, and each receiver keeps a single reference to its immediate successor in the chain.
The number and type of handlers isn't known beforehand, which makes it possible to be configured dynamically, and allow an unlimited number of handlers to be linked.
The CoR pattern enables capabilities like:
- Controling the order of request handling.
- Decouple classes that invoke operations, from classes that perform them.
- Introducing new handlers into the application, does not breaking the existing code.
But it also comes with some drawbacks like:
- Some requests may end up unhandled - In this case we need to implement a 'catch' which may throw an exception, report an error, or provide default handling (if possible).
- It can be harder to debug.
Implementation
IWashStep
IWashStep
is analogous to the Handler
in the UML diagram. It represents the entry point which the client uses to start applying wash steps upon the vehicle.
The reason why the abstract WashStep
class implements the method Visit
and the property CleaningFactor
, is because IWashStep
is an IWashAction
, and that in turn defines them. We will go more into detail about that part when we elaborate the Visitor pattern.
The method NextStep
accepts a wash step and sets the field nextStep
(which is an IWashStep
). It also returns the same step, and can not be overriden.
In the other hand Act
can be overriden because it is defined as virtual
and accepts a vehicle object, which will be acted by the right wash step (a.k.a the wash step will be applied upon the vehicle), and a callback delegate with two parameters, one being the current wash action/step and a boolean indicator if the action has been applied or not (for a reason only the handler knows about).
The base implementation looks for the next available step, if it is available than it will delegate to it. GetDescription
and Price
let each wash step describe itself.
public interface IWashStep : IWashAction
{
Money Price { get; }
void Act(IVehicle vehicle, Action<IWashStep, bool> callback);
IWashStep NextStep(IWashStep washStep);
string GetDescription();
}
public abstract class WashStep : IWashStep
{
public abstract int CleaningFactor { get; }
public abstract Money Price { get; }
private IWashStep nextStep;
public IWashStep NextStep(IWashStep washStep)
{
nextStep = washStep;
return nextStep;
}
public abstract string GetDescription();
public void Visit(IVehicle vehicle)
{
vehicle.Dirtiness =- CleaningFactor;
}
public virtual void Act(
IVehicle vehicle,
Action<IWashStep, bool> callback)
{
if (nextStep != null)
{
nextStep.Act(vehicle, callback);
}
}
}
A collection of wash steps exist. They are analogous to the ConcreteHandler
's in the UML diagram.
ChasisAndWheelWashing
ChasisAndWheelWashing
is a wash step with a description "Chasis & wheels washing", CleaningFactor = 3
, Price = 1.5 USD
(currency is adjustable as we shall see on an other article).
It overrides Act
method and calls the IVehicle.Accept
method which accepts an IWashAction
(remember IWashStep
is an IWashAction
).
It proceeds to invoke the callback delegate by supplying itself, and indicating a successful operation as the step has been applied upon the vehicle.
At the end it calls into the base.Act
method to pass the vehicle down the chain for further processing.
public class ChasisAndWheelWashing : WashStep
{
public override int CleaningFactor => 3;
public override Money Price => Money.Create(1.5m);
public override void Act(
IVehicle vehicle,
Action<IWashStep, bool> callback)
{
vehicle.Accept(this);
callback.Invoke(this, true);
base.Act(vehicle, callback);
}
public override string GetDescription()
{
return "Chasis & wheels washing";
}
}
Waxing
Waxing
is also a wash step with a Description
, CleaningFactor
, and Price
. But unlike the former, it checks on the vehicle's PaintFinishType
. If it is anything but Matte
, than it can safely apply itself upon the vehicle. In the other hand if the vehicle has a Matte
finish, than waxing will be skipped by means of providing false
in the callback delegate.
Waxing a vehicle with a matte finish is not recommended, because it fills the imperfections of matte paint that are needed to achive the non-reflective properties that it exhibits.
The invocation of IVehicle.Accept
method will not be conducted, and at the end it calls into the base.Act
method to pass the vehicle down the chain for further processing.
public class Waxing : WashStep
{
public override int CleaningFactor => 2;
public override Money Price => Money.Create(2.2m);
public override void Act(
IVehicle vehicle,
Action<IWashStep, bool> callback)
{
if (vehicle.FinishType != PaintFinishType.Matte)
{
vehicle.Accept(this);
callback.Invoke(this, true);
}
else
{
callback.Invoke(this, false);
}
base.Act(vehicle, callback);
}
public override string GetDescription()
{
return "Waxing";
}
}
FreeState
FreeState
is analogous to the Client
in the UML diagram. It represents one of the states the wash tunnel can be in. We'll go more into detail when we elaborate the state pattern.
The Handle
method of FreeState
class accepts a vehicle and the selected wash program as parameteres. It retrieves all the wash steps that are part of the wash program, loops through them, while progressively setting the next step, of the current step being iterated until the whole chain is built. At the very end it calls the Act
method on the first wash step in the array.
Remember calling Act
on any concrete wash step, will give the step (which in turn is a wash action) the opportunity to be applied upon the vehicle, while continuing to call base.Act
method, which checks if the current wash step, has a next step defined or not. If it does than it will continue to call into that.
The last step in the chain won't have a next step available, so the link gets broken in a controlled way.
Note 1: Some code has been omitted for the sake of keeping it focused for this article's purpose.
Note 2: An IndexOutOfRangeException
can not happen since a wash program can never be build without at least a single wash step.
public class FreeState : IWashTunnelState
{
public void Handle(IVehicle vehicle, IWashProgram program)
{
IWashStep[] washSteps = program.GetWashSteps().ToArray();
for (int i = 0; i < washSteps.Length - 1; i++)
{
washSteps[i].NextStep(washSteps[i + 1]);
}
washSteps[0].Act(vehicle, (action, status) => ... );
}
}
One very interesting feature of CoR is the ability to control the order of request handling. In the above example we could have reversed the for
loop, so at the end the steps would have been applied in reversed order (though in this context that wouldn't make sense). We could have also decided to apply every other step, or different kind of combinations all depending on the context.
Differences between similar patterns
A lot of the time when people talk about CoR, they see it as a single handler, handling the request but that is not the case! Each handler gets the chance to handle or pass a request, but this doesn't mean this specific handler has to break the chain. No, it can handle the request and still pass it down, all depending on your business logic.
As we saw above the Waxing
step decides wether to apply itself upon the vehicle based on its PaintFinishType
, but the business logic requires it to pass control to a potential next handler which could act upon the vehicle.
The Mediator Pattern
Contrary to the Mediator pattern, where the mediator knows which receiver is going to handle an incoming request. The CoR pattern passes a request sequentially along a dynamic chain of potential receivers until one or many of them handles it.
The Decorator Pattern
The Decorator always performs the work, and then always passes the request to its parent object (which may be another decorator). With CoR, you might (or not) do the work, and then might (or not) pass the request down the chain.
The decorator is used to add additional responsibilities to an object like IVehicle
in our case, but that would simply be wrong!!! By appling wash steps upon our vehicle, we are not changing how a vehicle behaves, we are just making it cleaner π.
Execution results
If we run the program for two cars where one of them has a Metallic finish, and the other one has a Matte finish, we see that the Waxing
step has not been applied to the Matte car. Consequently this step does not figure in the invoice, but does show up as a SMS notification with the 'SKIPPED' postfix.
static void Main(string[] args)
{
Run(new DirtyMetallicCar());
Run(new DirtyMatteCar());
}
static void Run(IVehicle vehicle)
{
IContainer container = new Container()
.AddWashTunnel()
.AddSmsNotifications("(917) 208-4154");
var panel = container.GetService<IUserPanel>();
var builder = container.GetService<ICustomWashProgramBuilder>();
var customProgram = builder
.Add(WashStepType.HighPressureWashing)
.Add(WashStepType.ChasisAndWheelWashing)
.Add(WashStepType.Shampooing)
.Add(WashStepType.HighPressureWashing)
.Add(WashStepType.Waxing)
.Build();
panel.SelectCustomizedProgram(customProgram)
.AsCompany("Ledjon SoftTech", Currency.USD)
.Start(vehicle, PrintInvoice());
}
static Action<string> PrintInvoice() => (content) =>
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine("\nInvoice Report");
Console.WriteLine("*************************");
Console.WriteLine(content);
Console.WriteLine("\n\n\n");
};
Metallic car results
[SmsClient] [(917) 208-4154] [APPLIED]: High water pressure washing
[SmsClient] [(917) 208-4154] [APPLIED]: Chasis & wheels washing
[SmsClient] [(917) 208-4154] [APPLIED]: Shampooing
[SmsClient] [(917) 208-4154] [APPLIED]: High water pressure washing
[SmsClient] [(917) 208-4154] [APPLIED]: Waxing
Invoice Report
*************************
Recepient: Ledjon SoftTech
Program type: Custom
-----------------------------
* High water pressure washing - 0.3$
* Chasis & wheels washing - 1.5$
* Shampooing - 0.8$
* High water pressure washing - 0.3$
* Waxing - 2.2$
-----------------------------
Total price: 4.08$
Applied discount: 20%
Matte car results
[SmsClient] [(917) 208-4154] [APPLIED]: High water pressure washing
[SmsClient] [(917) 208-4154] [APPLIED]: Chasis & wheels washing
[SmsClient] [(917) 208-4154] [APPLIED]: Shampooing
[SmsClient] [(917) 208-4154] [APPLIED]: High water pressure washing
[SmsClient] [(917) 208-4154] [SKIPPED]: Waxing
Invoice Report
*************************
Recepient: Ledjon SoftTech
Program type: Custom
-----------------------------
* High water pressure washing - 0.3$
* Chasis & wheels washing - 1.5$
* Shampooing - 0.8$
* High water pressure washing - 0.3$
-----------------------------
Total price: 1.88$
Applied discount: 20%
Principles
Principle | Applied | Explanation |
---|---|---|
SRP | β | Each handler decides for itself wether to handle and/or forward the request down the chain. |
OCP | β | Different request handlers can be added, yet the clients don't have to be modified. |
LSP | β | You can supply any implementation of IWashStep into the Act method, as any subtype will have IWashAction.CleaningFactor which is used by any IVehicle . |
ISP | β | IWashAction has been seggregated from IWashStep as the vehicle doesn't care about the step's Description and Price , it simply cares about the CleaningFactor . |
DIP | β | High level modules like WashProgram or any implementation of IVehicle , do not depend on the low level modules ChasisAndWheelWashing , Waxing . They depended on the abstractions IWashStep and IWashAction respectively. |
Continue the series on Decorator.
If you found this article helpful please give it a share in your favorite forums π.
The solution project is available at GitHub.