Beginner - Fluent Interfaces
This continues from method chaining with C#.
To recap, Jim found a cleaner, more efficient way to build his sandwich, but he's learned the ingredients need to go on in a specific order.
Method chaining can't enforce the order methods are called in:
class SandwichFactory
{
public SandwichFactory AddBread()
{
// implementation omitted for brevity
return this;
}
public SandwichFactory AddCheese()
{
// implementation omitted for brevity
return this;
}
}As sandwich construction experts, we want to enforce this order:
- Bread
- Cheese
- Bread
But a consumer of SandwichFactory could do this:
var createSandwich = new SandwichFactory()
.AddCheese()
.AddBread();Or something worse. Let's fix it.
Coming up with a nice fluent API requires a good bit of thought. - Martin Fowler
Depending on the API you want to provide, complexity can escalate quickly. This example stays small, to show the concepts rather than a real application. Here's the solution, followed by the reasoning:
public sealed class SandwichFactory : ISandwichBread, ISandwichCheese
{
private SandwichFactory()
{
}
public static ISandwichBread Create => new SandwichFactory();
public ISandwichCheese AddBread()
{
throw new NotImplementedException();
}
public ISandwichBread AddCheese()
{
throw new NotImplementedException();
}
}
public interface ISandwichBread
{
ISandwichCheese AddBread();
}
public interface ISandwichCheese
{
ISandwichBread AddCheese();
}- The constructor is private. Nobody should create an
instance directly, because that would expose both
AddBreadandAddCheese, which is exactly what we're trying to avoid. - There's a static entry point.
Createreturns a new instance as the interface we want to start from,ISandwichBread. - Parameters stay few. Avoid lots of parameters per method. This example uses none, but it's worth keeping in mind.
Now a consumer of SandwichFactory can't
add cheese before bread, because the interfaces only allow bread, then
cheese:
SandwichFactory.Create
.AddBread()
.AddCheese()
.AddBread();Simples!