This post is part of the series SOLID Wash Tunnel.

Definition

"Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which they operate."

- Refactoring Guru

The visitor pattern is used to separate the logic of traversing a data structure from the logic of operating on the elements of that structure. This allows for easy modification or extension of the operations performed on the elements, without having to change the structure of the data itself.

It suggests that you place the new behavior into a separate class called visitor, instead of trying to integrate it into existing classes. The original object that had to perform the behavior is now passed to one of the visitor’s methods as an argument, providing the method access to all necessary data contained within the object. The visitor method, corresponding to the target class type (since there could be several variants) then executes an operation over said object.

The visitor pattern enables capabilities like:

  • Separates the logic of traversing a data structure from the logic of operating on its elements.
  • Allows for easy modification or extension of operations performed on the elements, without changing the structure of the data.
  • Can handle multiple operations on a single data structure.
  • Supports double dispatch, which can be useful in certain situations.

But it also comes with some drawbacks like:

  • Can make code more complex.
  • Not suitable for all data structures, such as those that are frequently modified.
  • Can lead to an explosion of visitor classes if the number of operations on the data structure is high.
  • Can make it more difficult to understand the overall structure of the code.

UML class diagram. [source]


Implementation

IWashAction

The IWashAction interface is analogous to Visitor in the UML diagram. We are sepparating the IWashAction from the IVehicle, because the wash action simply executes, and does not care about wether it reaches the vehicle or not (for example a stream of water).

The IWashAction has a method called Visit which accepts an IVehicle. Like in the definition, the visitor visits an element and proceeds to take an action upon it.

public interface IWashAction
{
int CleaningFactor { get; }
public void Visit(IVehicle vehicle);
}

IVehicle

The IVehicle interface is analogous to Element in the UML diagram. It has a method called Accept which accepts a visitor of type IWashAction. It is left to the visitor to do the rest.

public interface IVehicle
{
int Dirtiness { get; set; }
PaintFinishType FinishType { get; }
void Accept(IWashAction action);
}

AirDrying, Waxing, HighPressureWashing...

AirDrying, Waxing, HighPressureWashing and may others, are analogous to the ConcreteVisitor's in the UML diagram. If we have a look at the implementation we see that they all extend the abstract class WashStep, which is an implementation of IWashStep, and in-turn inherits from IWashAction.

So steps like AirDrying are indirectly concrete implementations of the visitor. which is the IWashAction.

This has been done in accordance with the Interface Seggregation Principle, so that clients should not be forced to implement methods they don't use. A vehicle does not care about information of a wash step like: price, next step, or the step's description, it just needs to be washed.

Note the difference between the implementation of Act between Waxing and AirDrying.
Waxing refuses to let the visitor do it's thing in case of the vehicle having a matte finish type.

public class AirDrying : WashStep
{
public override int Id => 1;
public override int CleaningFactor => 1;
public override Money Price => Money.Create(0.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 "Air drying";
}
}

public class Waxing : WashStep
{
public override int Id => 7;
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";
}
}

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)
{
// With each wash action we decrease
// the dirtiness a.k.a we make the vehicle cleaner.
vehicle.Dirtiness =- CleaningFactor;
}

public virtual void Act(
IVehicle vehicle,
Action<IWashAction, bool> callback)
{
if (nextStep != null)
{
nextStep.Act(vehicle, callback);
}
}
}

public interface IWashStep : IWashAction
{
Money Price { get; }

string GetDescription();
IWashStep NextStep(IWashStep washStep);
void Act(IVehicle vehicle, Action<IWashStep, bool> callback);
}

DirtyMetallicCar, CleanMetallicCar, DirtyMatteCar, CleanMatteCar

DirtyMetallicCar, CleanMetallicCar, DirtyMatteCar, CleanMatteCar and potentially others, are analogous to the ConcreteElement's in the UML diagram. They all inherit from the abstract class Car which in-turn implements the IVehicle interface, which is the Element that the Visitor will visit.

All vehicles implement the Accept method which accepts an IWashAction and calls its Visit method by passing itself as the argument. The wash action (visitor) uses it's CleaningFactor and simulates cleaning, by means of deducting points from the Dirtiness property of the IVehicle.

It makes sense to model it like this because a vehicle, naturally has a level of dirtiness associated with it, and also a wash action, naturally has a level of cleaning factor associated with it.

public abstract class Car : IVehicle
{
public int Dirtiness { get; set; }
public PaintFinishType FinishType { get; }

public Car(int dirtiness, PaintFinishType finishType)
{
Dirtiness = dirtiness;
FinishType = finishType;
}

public void Accept(IWashAction action)
{
action.Visit(this);
}
}

public class DirtyMetallicCar : Car
{
public DirtyMetallicCar()
: base(10, PaintFinishType.Metallic) { }
}

public class CleanMetallicCar : Car
{
public CleanMetallicCar()
: base(0, PaintFinishType.Metallic) { }
}

public class DirtyMatteCar : Car
{
public DirtyMatteCar()
: base(10, PaintFinishType.Matte) { }
}

public class CleanMatteCar : Car
{
public CleanMatteCar()
: base(0, PaintFinishType.Matte) { }
}

UserPanel

What really is considered to be the Client in the UML diagram is left for discussion, because of this software being an end-to-end solution, rather than a simplified example. Which is great because it shows the dynamic nature of a program, when multiple design patterns work together to achive an end goal.

Let's elaboarate on why UserPanel could be considered the Client. The UserPanel class is an implementation of the IWashProcessStarter, which makes sense as usually the process of washing a vehicle is initiated by a user panel of some sort.

The '...' represents code that is irrelevant in this context.

public class UserPanel : 
IUserPanel,
ICustomerInformationCollector,
IWashProcessStarter

{
...

public void Start(IVehicle vehicle, Action<string> invoiceCallback)
{
_transmitter.Transmit(
new VehicleWashingStartedSignal(vehicle, invoiceCallback));
}
}

public interface IWashProcessStarter
{
void Start(IVehicle vehicle, Action<string> invoiceCallback);
}

The UserPanel simply transmits a new VehicleWashingStartedSignal and control is returned to the caller. If we look at the handler VehicleWashingStartedSignalHandler we can see it uses the current implementation of IWashTunnel, and passes along the infos of the vehicle and the selected program by calling the tunnel's Wash method.

public class VehicleWashingStartedSignal : ISignal
{
...

private class VehicleWashingStartedSignalHandler :
ISignalHandler<VehicleWashingStartedSignal>
{
...

public void Handle(VehicleWashingStartedSignal signal)
{
...

if (_memory.TryGet("WPSS",
out WashProgramSelectedSignal _signal))
{
_washTunnel.Wash(signal.Vehicle, _signal.Program);
}
}
}
}

Within the WashTunnel implementation, the vehicle and the program are passed to its current state which handles the rest.

public class WashTunnel : IWashTunnel
{
...

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

If the tunnel is free to wash a new vehicle, than the state is AvailableState. We see that the Handle method does the following.

  1. Takes an IWashProgram and IVehicle (remember this the 'element').
  2. Uses the passed program to retrieve a collection of IWashStep's (remember a step is an action, thereby it's a 'visitor').
  3. Uses the chain of responsibility to progressively pass along the wash steps.
  4. Takes the very first wash step and calls Act upon the vehicle.
public class AvailableState : 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) =>
...
));

...
}
}

If we look at the overriden method of say AirDrying, we can see that it is in here where the element's Accept method is called, and the passed argument this (the visitor itself).

public class AirDrying : WashStep
{
...

public override void Act(
IVehicle vehicle,
Action<IWashAction, bool> callback)
{
vehicle.Accept(this);
...
}
}

So you can see that it is debatable what really is considered to be the client, the UserPanel or the derived classes of WashStep. What really is important is understanding the flow, and watching how multiple patterns are working together.

IMemory

Any implementation of IMemory such as RandomAccessMemory can be considered to be the ObjectStructure in the UML diagram. The ObjectStructure is nothing more than a collection of some sort, that holds together a bunch of elements which in our case are the IVehicle's.

We saw in the UserPanel that when a new vehicle is supposed to be washed, the starter is called which simply takes the vehicle and sends a signal. The handler of that signal which is VehicleWashingStartedSignalHandler uses the injected implementation of IMemory to retrieve a signal (stored in memory), which in-turn contains infos about the vehicle, that can be passed to the tunnel in order for it to be washed.

We are not going to show the code of RandomAccessMemory as it is very abstract and doesn't provide any value in the context of this article.

Principles


PrincipleAppliedExplanation
SRPEach IWashStep is responsible for a single piece of the whole washing process, for any kind of IVehicle.
OCPDifferent IWashStep's can be added, yet any implementation of IVehicle doesn't have to be modified. New behaviour can be added by means of creating a new visitor.
LSPYou can subsitute any implementation of IWashAction to the Accept method of the IVehicle, because all wash actions have a CleaningFactor associated with them.
ISPIWashAction has been sepparated from IWashStep because clients should not be forced to implement methods they don't use (price, next step, step description).
DIPThe higher level modules like AirDrying, Waxing or any implementation of IWashAction, do not depend on the lower level modules like DirtyMetallicCar, DirtyMatteCar. They depend on the abstraction IVehicle.

Continue the series on Adapter Pattern [Comming soon ...]

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