- Published on
Design Patterns: Fluent Builder
- Authors
- Name
- Ivan Gechev
When designing object-oriented software, we often face complex objects that can have many parts. In such cases, we can use the Builder Design Pattern to simplify the object creation process by breaking it down into several steps. The pattern is particularly useful when we have many optional parameters because it makes the code for creating the object much more readable and maintainable.
In this post, we'll use coffee, because don't we turn coffee into code? We'll also use a Fluent API when implementing our builder to make the code shorter and easier to understand.
The problem
Imagine we're running a café where customers can order different types of coffee. Each coffee can have a different size and additional ingredients such as milk, sugar, and cinnamon. We want to create a system that is easy to maintain and allows for the easy creation of different types of coffee.
To solve this problem, we'll use the Fluent Builder Design Pattern.
The Coffee Class
The Coffee
class is the complex object that we want to create:
public class Coffee
{
public CoffeeSize Size { get; private set; }
public bool Milk { get; private set; }
public bool Cream { get; private set; }
public bool Sugar { get; private set; }
public bool Cocoa { get; private set; }
public bool Cinnamon { get; private set; }
public Coffee(
CoffeeSize size,
bool milk,
bool cream,
bool sugar,
bool cocoa,
bool cinnamon)
{
Size = size;
Milk = milk;
Cream = cream;
Sugar = sugar;
Cocoa = cocoa;
Cinnamon = cinnamon;
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append($"{Size} coffee");
sb.Append(Milk ? ", with milk" : "");
sb.Append(Cream ? ", with cream" : "");
sb.Append(Sugar ? ", with sugar" : "");
sb.Append(Cocoa ? ", with cocoa" : "");
sb.Append(Cinnamon ? ", with cinnamon" : "");
return sb.ToString();
}
}
It has several properties representing parts of a coffee such as size, milk, sugar, cream, cocoa and cinnamon. We define a constructor that takes all the parameters for initializing the object. We also override the ToString
method to get a nice visual representation.
The size in our case is a simple enumeration:
public enum CoffeeSize
{
Small = 0,
Medium = 1,
Large = 2,
Grande = 3
}
The Builder Interface
We define an interface that defines the methods needed for creating the different parts of our Coffee
objects:
public interface ICoffeeBuilder
{
ICoffeeBuilder AddMilk();
ICoffeeBuilder AddCream();
ICoffeeBuilder AddSugar();
ICoffeeBuilder AddCocoa();
ICoffeeBuilder AddCinnamon();
Coffee Build();
}
Each method in the interface corresponds to a property of the Coffee class. We use the methods AddMilk
, AddCream
, AddSugar
, AddCocoa
, and AddCinnamon
to set the boolean values that correspond to the preferences of our customers. We also have the Build
method that has a return type of Coffee
and we will use it to build our final coffee.
Implementing the CoffeeBuilder Interface
Now that we have defined the ICoffeeBuilder
interface, we can implement it. We'll create a concrete class called CoffeeBuilder
that implements the interface.
Here's what the implementation looks like:
public class CoffeeBuilder : ICoffeeBuilder
{
private readonly CoffeeSize _size;
private bool _milk;
private bool _cream;
private bool _sugar;
private bool _cocoa;
private bool _cinnamon;
public CoffeeBuilder(CoffeeSize size)
{
_size = size;
}
public ICoffeeBuilder AddMilk()
{
_milk = true;
return this;
}
public ICoffeeBuilder AddCream()
{
_cream = true;
return this;
}
public ICoffeeBuilder AddSugar()
{
_sugar = true;
return this;
}
public ICoffeeBuilder AddCocoa()
{
_cocoa = true;
return this;
}
public ICoffeeBuilder AddCinnamon()
{
_cinnamon = true;
return this;
}
public Coffee Build()
{
return new Coffee(
_size,
_milk,
_cream,
_sugar,
_cocoa,
_cinnamon);
}
}
The CoffeeBuilder
class implements the ICoffeeBuilder
interface and provides methods for constructing the different parts of the Coffee
object.
The constructor of our builder takes a CoffeeSize
as a parameter, The AddMilk
, AddSugar
, AddCream
, AddCocoa
, and AddCinnamon
methods set the corresponding properties to true
. Each method also returns the instance of the CoffeeBuilder
class - this way we can use it like a Fluent API.
The Build
method creates a new instance of the Coffee
object using the properties set by the builder methods and returns it.
Using the Fluent Builder to create a Coffee object
We can now use the CoffeeBuilder
to create a Coffee
object. Here's an example:
static void Main(string[] args)
{
var coffee = new CoffeeBuilder(CoffeeSize.Grande)
.AddCream()
.AddSugar()
.AddMilk()
.AddCinnamon()
.Build();
Console.WriteLine(coffee);
}
First, we create an instance of CoffeeBuilder
by passing in the CoffeeSize.Grande
parameter to its constructor. This sets the size of our coffee to be grande.
Then, we call several methods on the CoffeeBuilder
instance to specify the customer's preferences for the coffee, including adding cream, sugar, milk, and cinnamon.
Finally, we call the Build
method to construct the final Coffee
object with the specified properties, which is then assigned to the coffee
variable.
We use the Console.WriteLine
method to print the details of the coffee object to the console:
Grande coffee, with milk, with cream, with sugar, with cinnamon
Conclusion
In this post, we explored the Builder Design Pattern using a Coffee
example in C#. We used the Fluent API to make our code more concise and expressive. The pattern is very useful when there are many optional parameters, and it simplifies the object creation process by breaking it down into several steps.