Introduction

Each and every design pattern comes to solve a practical problem. Without problems, there would be no design patterns. They are here to provide us maintainable and reusable solutions for various complex scenarios in application development. As defined in one of my other articles:

Software design patterns provide us, we can say, a way to organize our codebase. They represent, not an actual solution, but rather a solution template for the problems in modern software development. Great thing is that these templates are not strictly limited to the specific problem, and they can be reused whenever we see fit.

One of the popular patterns is the Builder pattern, and in this article, I will show you what it is and how to use it through an example.

This article is part of the software design patterns series, and if you are interested, take a look at the previously covered topics listed below.

The Builder Pattern

What is the builder pattern? It is one of the creational patterns and it allows us to create objects with a complex configuration in a more simple and easier manner. It encapsulates the initialization of the object we need and exposes the methods responsible for setting up the inner object configuration and properties. In the end, a configured instance is returned.

In other words, it replaces the manual initialization by using the new keyword with a lot of required parameters with specific methods responsible for setting up a portion of the required configuration.

Example

Ok, as usual, we are going to start from the bad way of doing things. Imagine that we are working on software for a taxi service and we want to, for example, store each taxi route after reaching the destination for analytics purposes. We want to store the route id, the number of passengers, the time needed to complete the route, the address of the destination, the vehicle used to transport the passenger, the vehicle driver...etc.

A lot of different parameters depending on each route, as you can see. We can split this into a couple of objects in our system.

  • Route
  • Address
  • Vehicle

The route class

public class Route
{
  public Route(int id, DateTime startTime, DateTime endTime, Address address, Vehicle vehicle)
  {
    Id = id;
    StartTime = startTime;
    EndTime = endTime;
    Address = address;
    Vehicle = vehicle;
  }

  public int Id { get; set; }
  public DateTime StartTime { get; set; }
  public DateTime EndTime { get; set; }
  public Address Address { get; set; }
  public Vehicle Vehicle { get; set; }
}

The address class

public class Address
{
  public string StreetName { get; set; }
  public int Number { get; set; }
}

The vehicle class

public class Vehicle
{
  public string Model { get; set; }
  public string Driver { get; set; }
}

At this point, without the route builder, we would instantiate our route class manually with all the required parameters. Our code would look something like this:

var route = new Route(
  1,
  DateTime.Now,
  DateTime.Now.AddMinutes(15),
  new Address()
  {
    StreetName = "My Street",
    Number = 5
  },
  new Vehicle()
  {
    Model = "Ford Fiesta",
    Driver = "John Doe"
  }
);

As you can see, even though we have a small number of parameters, our code doesn't give us a lot of flexibility. For example, imagine that we want to create a route without the end time. In this setup, it is not possible.

We would be required to create another constructor without that parameter, or to redefine the existing constructor in a way where we make the end time parameter optional. This will result in the need for updating all places in our codebase where we used the route object. This kind of implementation is certainly not easy to maintain.

Fortunately, we have the builder pattern. Let's see how the problem above gets solved by using it.

In my mind, there are two ways to implement the builder pattern. We can place the methods in each object where each of them is responsible for building itself, or we can create a builder class grouping the methods for building the root object (route) together with the inner object type properties (address, vehicle).

In this article, we will use the second approach. Our implementation looks like this:

public class RouteBuilder: IAddressBuilder, IVehicleBuilder
{
  private Route _route;

  public RouteBuilder NewRoute()
  {
    _route = new Route
    {
      StartTime = DateTime.Now
    };
    return this;
  }

  public RouteBuilder SetId(int id)
  {
    _route.Id = id;
    return this;
  }

  public RouteBuilder SetEndTime(DateTime dateTime)
  {
    _route.EndTime = dateTime;
    return this;
  }

  public IAddressBuilder SetStreetName(string name)
  {
    _route.Address = new Address()
    {
      StreetName = name
    };

    return this;
  }

  public RouteBuilder SetStreetNumber(int number)
  {
    if (_route.Address == null || string.IsNullOrEmpty(_route.Address.StreetName))
    {
      throw new Exception("Address not initialized. Please set the street name");
    }

    _route.Address.Number = number;
    return this;
  }

  public IVehicleBuilder SetVehicleModel(string model)
  {
    _route.Vehicle = new Vehicle()
    {
      Model = model
    };

    return this;
  }

  public RouteBuilder SetVehicleDriver(string driver)
  {
    if (_route.Vehicle == null || string.IsNullOrEmpty(_route.Vehicle.Model))
    {
      throw new Exception("Vehicle not initialized. Please set the vehicle model");
    }

    _route.Vehicle.Driver = driver;
    return this;
  }

  public Route Build()
  {
    // validate all required parameters
    return _route;
  }
}

This is a really simple implementation and it represents a proof of concept. In a real app, I would introduce validation and probably use the second approach, where each object is responsible for building itself

To spice things up a bit, I've added IAddressBuilder and IVehicleBuilder interfaces. These interfaces are here to limit the possible building options to the specific inner type. In other words, when we start setting up the address, we are not able to set anything else related to the route before finishing the address setup. The same goes for the vehicle.

The address builder interface

public interface IAddressBuilder
{
  IAddressBuilder SetStreetName(string name);
  RouteBuilder SetStreetNumber(int number);
}

The vehicle builder interface

public interface IVehicleBuilder
{
  IVehicleBuilder SetVehicleModel(string model);
  RouteBuilder SetVehicleDriver(string driver);
}

Our route builder implements these interfaces and the first method will, as all of the other methods, return the route builder instance but the options are going to be limited to the specific type. In our case, address or vehicle.

Now, the only thing left to do is to update our route class:

public class Route
{
  public int Id { get; set; }
  public DateTime StartTime { get; set; }
  public DateTime EndTime { get; set; }
  public Address Address { get; set; }
  public Vehicle Vehicle { get; set; }

  public static RouteBuilder Builder => new RouteBuilder();
}

At this point, we are ready to use our builder. An example usage looks like this:

var route = Route.Builder.NewRoute()
  .SetId(1)
  .SetStreetName("My Street")
  .SetStreetNumber(5)
  .SetVehicleModel("Ford Fiesta")
  .SetVehicleDriver("John Doe")
  .SetEndTime(DateTime.Now.AddMinutes(15))
  .Build();

This looks a lot cleaner if you ask me, and if you remember the problem we had with the requirement to leave out the end time, in this situation is a lot easier to update our code. We simply can omit the SetEndTime method.

Conclusion

The builder pattern gives us a lot of benefits, but it has its flaws. The main disadvantage is in error handling. In other words, the point in time when we can spot errors in our code changes with usage of this pattern.

With the builder pattern, we cannot tell if all of the required fields are set at compile-time. If there are errors, we are likely going to find about them at runtime, meaning that we would have to run our code to check for errors. To some, this can be the reason not to use this pattern.

If you liked this article, please consider supporting me by buying me a coffee. To stay tuned, follow me on twitter or subscribe here on devinduct.

Thank you for reading and see you in the next article!