This post is part of the series SOLID Wash Tunnel.
Introduction
This is the second post of the mini-series on the Fluent Builder pattern. If you have not read the previous article, I would highly suggest you to do so.
We will keep the UML diagram of the builder pattern as a point of reference.
Implementation of Custom Wash Program builder
The client has the option to build a wash program itself. Any wash program exposes a user-friendly name, and a collection of wash steps that it offers.
public interface IWashProgram
{
string Name { get; }
IEnumerable<IWashStep> GetWashSteps();
}
There are already some built-in wash programs from which the client can choose from. They expose their metadata and the wash steps that they offer, but the steps can not be added, removed or modified.
public class FastWashProgram : IWashProgram
{
public string Name => "Fast";
private readonly IWashStepFactory _washStepFactory;
public FastWashProgram(IWashStepFactory washStepFactory)
{
_washStepFactory = washStepFactory;
}
public IEnumerable<IWashStep> GetWashSteps() =>
new List<IWashStep>()
{
_washStepFactory.Create(WashStepType.ChasisAndWheelWashing),
_washStepFactory.Create(WashStepType.HighPressureWashing),
_washStepFactory.Create(WashStepType.AirDrying)
};
}
public class EconomicWashProgram : IWashProgram
{
public string Name => "Economic";
private readonly IWashStepFactory _washStepFactory;
public EconomicWashProgram(IWashStepFactory washStepFactory)
{
_washStepFactory = washStepFactory;
}
public IEnumerable<IWashStep> GetWashSteps() =>
new List<IWashStep>()
{
_washStepFactory.Create(WashStepType.ChasisAndWheelWashing),
_washStepFactory.Create(WashStepType.Shampooing),
_washStepFactory.Create(WashStepType.HighPressureWashing),
_washStepFactory.Create(WashStepType.SingleColorFoaming),
_washStepFactory.Create(WashStepType.HighPressureWashing),
_washStepFactory.Create(WashStepType.AirDrying)
};
}
public class AllRounderWashProgram : IWashProgram
{
public string Name => "All rounder";
private readonly IWashStepFactory _washStepFactory;
public AllRounderWashProgram(IWashStepFactory washStepFactory)
{
_washStepFactory = washStepFactory;
}
public IEnumerable<IWashStep> GetWashSteps()
{
foreach (WashStepType type in Enum.GetValues(typeof(WashStepType)))
{
yield return _washStepFactory.Create(type);
}
}
}
The CustomWashProgram
in the order hand, by default, has no wash steps to offer. It accepts an IEnumerable<IWashStep>
in its constructor, and returns them when they are requested.
public class CustomWashProgram : IWashProgram
{
public string Name => "Custom";
private IEnumerable<IWashStep> _washSteps;
public CustomWashProgram(IEnumerable<IWashStep> washSteps)
{
_washSteps = washSteps;
}
public IEnumerable<IWashStep> GetWashSteps() => _washSteps;
}
In part one of this mini-series, we introduced the IUserPanel
interface which contained two methods one of which accepted an IWashProgram
, namely SelectCustomizedProgram
. This enabled the client to build their own wash program and apply that one to wash their vehicle.
public interface IUserPanel
{
ICustomerInformationCollector SelectBuiltInProgram(ProgramType type);
ICustomerInformationCollector SelectCustomizedProgram(IWashProgram program);
}
The builder interface exposes methods to conveniently add wash steps to their to-be build program, without knowing the internals of the wash steps. The same wash step can be added multiple times. This makes sense because even in some of our built-in programs we have some steps appearing multiple times.
For example, in the "Economic" program, the wash step HighPressureWashing
appears twice. Once after Shampooing
, and once after SingleColorFoaming
.
The interface exposes 4 methods, each with a purpose:
- The overload of
Add
accepting aWashStepType
enum, is used to add anIWashStep
that is built-in. - The overload of
Add
accepting anIWashStep
, is used to add a customIWashStep
. AddAll
is used to bulk add all available built-in wash steps.Build
simply builds the endIWashProgram
and returns it to the client.
public interface ICustomWashProgramBuilder
{
ICustomWashProgramBuilder Add(WashStepType type);
ICustomWashProgramBuilder Add(IWashStep washStep);
ICustomWashProgramBuilder AddAll();
IWashProgram Build();
}
public enum WashStepType
{
ChasisAndWheelWashing,
Shampooing,
HighPressureWashing,
SingleColorFoaming,
ThreeColorFoaming,
Waxing,
AirDrying
}
The client builds their wash program through the interface, not through the concrete implementation - ⚠️ Dependency Inversion Principle.
Tip: Declare the builder implementation as internal.
I suggest declaring the builder implementation as internal
(granted if the programming language of choice supports it).
The client does not need it, which implies it should not be able to reference it. This would lower the chances of making breaking changes in the future, because there is no consumer of this class.
For sake of simplicity we will leave it as public
.
public class CustomWashProgramBuilder : ICustomWashProgramBuilder
{
private readonly List<IWashStep> _washSteps;
private readonly IWashProgramFactory _programFactory;
private readonly IWashStepFactory _washStepFactory;
public CustomWashProgramBuilder(
IWashProgramFactory programFactory,
IWashStepFactory washStepFactory)
{
_washSteps = new List<IWashStep>();
_programFactory = programFactory;
_washStepFactory = washStepFactory;
}
public ICustomWashProgramBuilder Add(WashStepType type)
{
_washSteps.Add(_washStepFactory.Create(type));
return this;
}
public ICustomWashProgramBuilder Add(IWashStep washStep)
{
_washSteps.Add(washStep);
return this;
}
public ICustomWashProgramBuilder AddAll()
{
foreach (WashStepType type in Enum.GetValues(typeof(WashStepType)))
{
_washSteps.Add(_washStepFactory.Create(type));
}
return this;
}
public IWashProgram Build()
{
if (_washSteps.Count == 0)
throw new InvalidOperationException("A custom wash program must have at least one wash step.");
IWashProgram program = _programFactory.Create(ProgramType.Custom, _washSteps.ToArray());
_washSteps.Clear();
return program;
}
}
Preventing mutation after build
Notice how we clear the variable _washSteps
upon calling Build
. This is important to ensure that no modifications can be made after the custom program has been build.
Take for example the following code.
var builder = container.GetService<ICustomWashProgramBuilder>();
IWashProgram customProgram1 = builder
.Add(WashStepType.ChasisAndWheelWashing)
.Add(WashStepType.Shampooing)
.Build();
IWashProgram customProgram2 = builder
.Add(WashStepType.AirDrying)
.Build();
What would be the total number of wash steps in
customProgram1
andcustomProgram2
, if we did not clear_washSteps
upon callingBuild
?
The answer is both customProgram1
and customProgram2
would contain a total of 3 wash steps. The reason is because both use the same builder
instance. Which would be an incorrect behaviour!
By clearing _washSteps
we ensure that customProgram1
will end up having a total of 2 wash steps (ChasisAndWheelWashing
, Shampooing
), and customProgram2
will have a total of 1 wash steps (AirDrying
).
Wash Program & Wash Step factories
Since we have already elaborated the Simple Factory pattern. We will not go in-depth in this article, but only showcase the implementations of IWashProgramFactory
and IWashStepFactory
.
public class WashProgramFactory : IWashProgramFactory
{
private readonly Lazy<IDictionary<ProgramType, Func<IWashStep[], IWashProgram>>> _programsMap;
public WashProgramFactory(IDictionary<ProgramType, Func<IWashStep[], IWashProgram>> programs)
{
_programsMap = new Lazy<IDictionary<ProgramType, Func<IWashStep[], IWashProgram>>>(programs);
}
public IWashProgram Create(ProgramType type, params IWashStep[] washSteps)
{
if (!_programsMap.Value.TryGetValue(type, out Func<IWashStep[], IWashProgram> _func))
{
throw new NotSupportedException($"Wash program type {type} is not supported!");
}
return _func.Invoke(washSteps);
}
}
public class WashStepFactory : IWashStepFactory
{
private readonly Lazy<IDictionary<WashStepType, Func<IWashStep>>> _washStepsMap;
public WashStepFactory(IDictionary<WashStepType, Func<IWashStep>> washSteps)
{
_washStepsMap = new Lazy<IDictionary<WashStepType, Func<IWashStep>>>(washSteps);
}
public IWashStep Create(WashStepType type)
{
if (!_washStepsMap.Value.TryGetValue(type, out Func<IWashStep> _func))
{
throw new NotSupportedException($"Wash step type {type} is not supported!");
}
return _func.Invoke();
}
}
The different types of IWashProgram
and IWashStep
are loaded from the ConfigMap
class.
internal static class ConfigMap
{
internal static IDictionary<ProgramType, Func<IWashStep[], IWashProgram>>
GetWashPrograms(IWashStepFactory factory) =>
new Dictionary<ProgramType, Func<IWashStep[], IWashProgram>>()
{
{ ProgramType.Custom, (ws) => new CustomWashProgram(ws) },
{ ProgramType.Fast, _ => new FastWashProgram(factory) },
{ ProgramType.Economic, _ => new EconomicWashProgram(factory) },
{ ProgramType.AllRounder, _ => new AllRounderWashProgram(factory) }
};
internal static IDictionary<WashStepType, Func<IWashStep>>
GetWashSteps() =>
new Dictionary<WashStepType, Func<IWashStep>>()
{
{ WashStepType.ChasisAndWheelWashing, () => new ChasisAndWheelWashing() },
{ WashStepType.Shampooing, () => new Shampooing() },
{ WashStepType.HighPressureWashing, () => new HighPressureWashing() },
{ WashStepType.SingleColorFoaming, () => new SingleColorFoaming() },
{ WashStepType.ThreeColorFoaming, () => new ThreeColorFoaming() },
{ WashStepType.Waxing, () => new Waxing() },
{ WashStepType.AirDrying, () => new AirDrying() }
};
}
And the service registrations are performed like follows.
public static IContainer AddWashTunnel(this IContainer container)
{
IWashStepFactory washStepFactory = new WashStepFactory(ConfigMap.GetWashSteps());
container.AddSingleton(() => washStepFactory);
container.AddSingleton<IWashProgramFactory>(() => new WashProgramFactory(ConfigMap.GetWashPrograms(washStepFactory)));
}
Invoking the builder
The CustomWashProgramBuilder
is invoked through the ICustomWashProgramBuilder
interface from Program.cs
which is located in the SOLIDWashTunnel.Customers
project.
var panel = container.GetService<IUserPanel>();
var builder = container.GetService<ICustomWashProgramBuilder>();
// Option 1: Adding specific wash steps.
IWashProgram customProgram = builder
.Add(WashStepType.ChasisAndWheelWashing)
.Add(WashStepType.Shampooing)
.Add(WashStepType.HighPressureWashing)
.Build();
// Option 2: Adding all available wash steps.
IWashProgram customProgram = builder
.AddAll()
.Build();
panel.SelectCustomizedProgram(customProgram)
.AsIndividual("Ledjon", "Behluli", Currency.USD)
.Start(Vehicle, PrintInvoice());
Analogy to the builder pattern
Lets connect the concepts defined in the UML diagram of the builder pattern, to the CustomWashProgramBuilder
.
ICustomWashProgramBuilder
interfaces is analogous toBuilder
. It represents the abstraction.CustomWashProgramBuilder
is analogous toConcreteBuilder
. It represents the concrete implementation ofBuilder
.Program.cs
(or calling code) is analogous toDirector
.IWashProgram
generated upon callingBuild
is analogous toProduct
.
Principles
Principle | Applied | Explanation |
---|---|---|
SRP | ✅ | CustomWashProgramBuilder has one responsibility, that is to build an IWashProgram . |
OCP | ✅ | CustomWashProgramBuilder is closed for modification, but open for extension. Even if new wash steps are introduced (either built-in or external) the class does not have to change. |
LSP | ❌ | There is no inheritance involved, so LSP has no applicability. |
ISP | ✅ | CustomWashProgramBuilder makes use of all the methods of ICustomWashProgramBuilder . |
DIP | ✅ | The high level module CustomWashProgramBuilder does not depend on the low level modules which are any derived types of IWashStep , WashStepFactory , and WashProgramFactory . It dependes on the abstractions IWashStep , IWashStepFactory , and IWashProgramFactory respectively. |
Continue the series on Fluent Builder (part 3/3).
If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.