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.

UML class diagram. [source]


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


PrincipleAppliedExplanation
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.