This post is part of the series SOLID Wash Tunnel.

Definition

"Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors."

- Refactoring Guru

The decorator lets you structure your business logic into layers. By doing so you create a wrapper object for each layer and compose objects with various combinations of this logic, at runtime. The client code can treat all of them in the same way, since they all implement the same interface.

It becomes very useful in situations where you can not extend a given class, or extension is not the right strategy. Decorator enables wrapping the class and extending its behaviour. It also promotes code re-use, especially in situations like logging.

The decorator pattern enables capabilities like:

  • Layer behaviours on top of each other.
  • Extending class behavior without making a new subclass.
  • Plays along really well with SRP and OCP principles.
  • Provides greater flexibility than static inheritance, since it can add or remove responsibilities from an object, at runtime.

But it also comes with some drawbacks like:

  • It can be hard to remove a specific decorator from the decorator stack.
  • The architecture tends to lean more towards the complex side.
  • Can be a pain to maintain if the interface changes frequently (but this speaks more about the interface itself, rather than the pattern).

It is one of the most useful design patterns out there, and in my opinion the pros outweigh the cons by a mile!

UML class diagram. [source]


Implementation

IWashTunnel

The IWashTunnel interface is analogous to Component in the UML diagram. It is an abstraction of an object that represents our wash tunnel.

public interface IWashTunnel
{
IWashTunnelState State { get; set; }
void Wash(IVehicle vehicle, IWashProgram program);
}

WashTunnel

The WashTunnel class is analogous to ConcreteComponent in the UML diagram. It is an implementation of IWashTunnel.

public class WashTunnel : IWashTunnel
{
public IWashTunnelState State { get; set; }

public WashTunnel(
ISignalTransmitter transmitter,
IWashStepNotifier notifier)
{
State = new AvailableState(this, transmitter, notifier);
}

public void Wash(IVehicle vehicle, IWashProgram program)
{
State.Handle(vehicle, program);
}
}

SmartWashTunnel

In the UML diagram the Decorator is represented as a base class (an abstract one), but it doesn't have to be! We can make the decorator a concrete implementation right away.

SmartWashTunnel being the decorator, follows the patterns structure where the class accepts an IWashTunnel as a constructor argument, but also implements the interface itself.

public class SmartWashTunnel : IWashTunnel   // Implements
{
private readonly IWashTunnel _washTunnel;
private readonly ISignalTransmitter _transmitter;
private readonly IDirtinessSensor _sensor;

public IWashTunnelState State
{
get => _washTunnel.State;
set => _washTunnel.State = value;
}

public SmartWashTunnel(
IWashTunnel washTunnel,
ISignalTransmitter transmitter,
IDirtinessSensor sensor)
{
_washTunnel = washTunnel;
_transmitter = transmitter;
_sensor = sensor;
}

public void Wash(IVehicle vehicle, IWashProgram program)
{
if (!_sensor.IsDirty(vehicle))
{
// Intercepts the regular call to 'Wash'
_transmitter.Transmit(new VehicleAlreadyCleanSignal());
return;
}

_washTunnel.Wash(vehicle, program);
}
}

The Wash method is intercepted by a check on the IsDirty method, provided by the IDirtinessSensor which is also injected into the constructor. If the sensor says that the vehicle is not dirty, the SmartWashTunnel transmits a VehicleAlreadyCleanSignal and returns prematurely. On the other hand if the vehicle is dirty, than the call is forwarded to the injected implementation of IWashTunnel.

Meanwhile the State property always forwards calls to the injected IWashTunnel in the constructor, because no behavioural changes are needed. We'll go into more details about the state transitions when we elaborate the State pattern.

Dirtiness Sensor

The dirtiness sensor has nothing to do with the decorator perse, but we are still showing the implementation just for the sake of understanding the total program flow. The threshold of dirtiness is preset to 3, but the sensor can be calibrated to detect a dirty or clean vehicle and that is done during compile time.

public interface IDirtinessSensor
{
/// <summary>
/// Calibrate this sensor to react on a certain level of dirtiness
/// </summary>
/// <param name="threshold">The level above which the vehicle is considered 'dirty'.</param>
IDirtinessSensor Calibrate(int threshold);
bool IsDirty(IVehicle vehicle);
}

public class DirtinessSensor : IDirtinessSensor
{
private int _threshold = 3;

public IDirtinessSensor Calibrate(int threshold)
{
_threshold = threshold;
return this;
}

public bool IsDirty(IVehicle vehicle)
{
return vehicle.Dirtiness > _threshold;
}
}

Service Registration

We can make use of the decorator SmartWashTunnel by means of applying an extra fluent action AddSmartFeatures, which is exposed as an extension method. It also calibrates the sensor's dirtiness threshold to 5.

public static class ServiceRegistrations
{
/// <summary>
/// Registers all components needed for a normal wash tunnel.
/// The tunnel will wash a vehicle regardless if it is 'clean' or 'dirty'.
/// </summary>
public static IContainer AddWashTunnel(this IContainer container)
{
...
}

/// <summary>
/// Registers all smart features of a smart wash tunnel.
/// The tunnel will wash a vehicle only if it considered to be 'dirty'.
/// </summary>
public static IContainer AddSmartFeatures(this IContainer container)
{
container.AddTransient(() => new DirtinessSensor().Calibrate(5));
container.Decorate<IWashTunnel, SmartWashTunnel>();

return container;
}
}

Execution results

If we run the program for two cars where one of them is considered to be dirty, and the other one is considered clean, we see that the dirty one gets washed and the clean one is rejected, as it is already clean! Also, note how the same clean car is washed when the regular (not decorated) tunnel is used.

static void Main(string[] args)
{
Run(new DirtyMetallicCar(), isSmart: true);
Run(new CleanMetallicCar(), isSmart: true);
Run(new CleanMetallicCar(), isSmart: false);
}

static void Run(IVehicle vehicle, bool isSmart)
{
IContainer container = new Container().AddWashTunnel();

if (isSmart)
{
container.AddSmartFeatures();
}

container
.AddSmsNotifications("(917) 208-4154")
.AddMobileAppNotifications("ledjon-behluli");

var panel = container.GetService<IUserPanel>();

panel.SelectBuiltInProgram(ProgramType.Fast)
.AsIndividual("Ledjon", "Behluli", 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");
};
Dirty Car + Smart Tunnel
[SmsClient] [(917) 208-4154] [APPLIED]: Chasis & wheels washing
[MobileAppClient] [ledjon-behluli] [APPLIED]: Chasis & wheels washing
[SmsClient] [(917) 208-4154] [APPLIED]: High water pressure washing
[MobileAppClient] [ledjon-behluli] [APPLIED]: High water pressure washing
[SmsClient] [(917) 208-4154] [APPLIED]: Air drying
[MobileAppClient] [ledjon-behluli] [APPLIED]: Air drying

Invoice Report
*************************
Recepient: Ledjon Behluli
Program type: Fast
-----------------------------
 * Chasis & wheels washing - 1.5$
 * High water pressure washing - 0.3$
 * Air drying - 0.5$
-----------------------------
Total price: 2.3$
Applied discount: 0%
Clean Car + Smart Tunnel
Invoice Report
*************************
No wash step was applied since the vehicle is already clean!
Clean Car + Regular Tunnel
[SmsClient] [(917) 208-4154] [APPLIED]: Chasis & wheels washing
[MobileAppClient] [ledjon-behluli] [APPLIED]: Chasis & wheels washing
[SmsClient] [(917) 208-4154] [APPLIED]: High water pressure washing
[MobileAppClient] [ledjon-behluli] [APPLIED]: High water pressure washing
[SmsClient] [(917) 208-4154] [APPLIED]: Air drying
[MobileAppClient] [ledjon-behluli] [APPLIED]: Air drying

Invoice Report
*************************
Recepient: Ledjon Behluli
Program type: Fast
-----------------------------
 * Chasis & wheels washing - 1.5$
 * High water pressure washing - 0.3$
 * Air drying - 0.5$
-----------------------------
Total price: 2.3$
Applied discount: 0%

Principles


PrincipleAppliedExplanation
SRPSmartWashTunnel adds one extra feature to skip washing clean vehicles.
OCPDifferent features can be added, yet the underlying implementation of IWashTunnel doesn't have to be modified.
LSPYou can supply any implementation of IWashTunnel into the constructor of SmartWashTunnel.
ISPDoesn't apply since SmartWashTunnel has to implement all methods & properties of IWashTunnel, even if they are pass-through like State.
DIPThe higher level module SmartWashTunnel, does not depend on the lower level module WashTunnel. It depends on the abstraction IWashTunnel.

Continue the series on State.

If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.