[go: up one dir, main page]

DEV Community

Cover image for Design Patterns: Builder
Russ Hammett
Russ Hammett

Posted on • Originally published at blog.kritner.com on

Design Patterns: Builder

The builder is a creational pattern that can be used to construct “more complex” objects, without having to directly new them up in calling code.

Though the builder pattern is not my most used creational pattern (that’s the Factory), it is one that I often rely on for the creation of more complex objects than what the factory easily allows for.

The Builder Pattern

From Wikipedia:

The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation. It is one of the Gang of Four design patterns.

I can’t think of an exceedingly straight forward example that truly shows of the power of the builder, but I am going to throw something together as a demonstration. Also note that a lot of the .net core app bootstrapping works around the concept of a builder, IHostBuilder comes immediately to mind.

Abstraction

public interface IAddress
{
    string Address1 { get; }
    string Address2 { get; }
    string Address3 { get; }
    string City { get; }
    string State { get; }
    string Zip { get; }
}

public interface IAddressBuilder
{
    IAddressBuilder WithAddress1(string value);
    IAddressBuilder WithAddress2(string value);
    IAddressBuilder WithAddress3(string value);
    IAddressBuilder WithCity(string value);
    IAddressBuilder WithState(string value);
    IAddressBuilder WithZip(string value);
    IAddress Build();
}
Enter fullscreen mode Exit fullscreen mode

The above shows a bit of what the pattern can accomplish, but keep in mind this is going to be nothing like what the IHostBuilder will give you access to from the framework.

Let’s break down one of the similar methods exposed by the interface: IAddressBuilder WithAddress1(string value);. This method takes in a string value and returns an IAddressBuilder. What does that mean exactly? That means it exposes a fluent api!

Wait, what’s a fluent api? Fluent APIs allow for “method chaining”, which can look like this:

public void DoStuff()
{
    var address = builder
        .WithAddress1("123 Anywhere Blvd.")
        .WithCity("Chicago")
        .WithState("IL")
        .WithZip("60652")
        .Build();
}
Enter fullscreen mode Exit fullscreen mode

Because each “non build” method within IAddressBuilder returns an IAddressBuilder that allows for the “chaining” of method calls on the builder as seen above, finally culminating in the built IAddress when .Build() is called.

Implementation

internal class Address : IAddress
{
    public string Address1 { get; }
    public string Address2 { get; }
    public string Address3 { get; }
    public string City { get; }
    public string State { get; }
    public string Zip { get; }

    public Address(string address1, string address2, string address3, string city, string state, string zip)
    {
        Address1 = address1;
        Address2 = address2;
        Address3 = address3;
        City = city;
        State = state;
        Zip = zip;
    }
}

public class AddressBuilder : IAddressBuilder
{
    private string _address1;
    private string _address2;
    private string _address3;
    private string _city;
    private string _state;
    private string _zip;

    public IAddressBuilder WithAddress1(string value)
    {
        _address1 = value;
        return this;
    }

    public IAddressBuilder WithAddress2(string value)
    {
        _address2 = value;
        return this;
    }

    public IAddressBuilder WithAddress3(string value)
    {
        _address3 = value;
        return this;
    }

    public IAddressBuilder WithCity(string value)
    {
        _city = value;
        return this;
    }

    public IAddressBuilder WithState(string value)
    {
        _state = value;
        return this;
    }

    public IAddressBuilder WithZip(string value)
    {
        _zip = value;
        return this;
    }

    public IAddress Build()
    {
        return new Address(_address1, _address2, _address3, _city, _state, _zip);
    }
}
Enter fullscreen mode Exit fullscreen mode

The above should be pretty straightforward looking, the only thing to me that seems “weird” if you haven’t seen it before is the return this; within all the With[x] implementations. The return this; accomplishes the method chaining discussed earlier in that “the instance (this) is returned from the method, and can immediately be invoked again”.

Here’s an example of it running:

Example

Reasons to use this pattern

It may not be immediately obvious why you would want to use this pattern, so hopefully this section will help shed some light!

  • Can handle more complex object construction over a factory. A factory will generally (hopefully) have one to two different parameters that determine what sort of instance to return; a builder’s API can easily support more without becoming overwhelming.
  • “Optional” parameters are simpler for object construction, just invoke that part of the builder.
  • “Default” implementations are possible within the builder implementations (not shown in the above example).
    • Perhaps not super useful for “production” code, but I’ve used more than a handful of times for testing code.
  • “Readonly” classes possible w/o needing to expose a constructor handling every iteration of parameter combinations
    • For this scenario you’ll still likely want a “all encompassing constructor” that your builder can call, but the builder is the thing that worries about “plugging the right parameters into the right ‘slot’ within the constructor”. Consumers just call the (hopefully) well named “With” methods on the builder.
  • Not covered in this post, or in the factory post, but both of these patterns can return “concretes” that are internal to their assemblies, thus ensuring objects are constructed by their builder/factory rather than newed up directly in code.
  • Builders can contain “validation” logic, and can take in dependencies that you wouldn’t otherwise necessarily want in a POCO.
    • If the above builder had a constructor that took in something like a IStateValidator, it could be utilized when setting the state (or as a Build() step)
  • This one’s probably going to be confusing, but I have used it in the past, I don’t know if it’s good, but it’s at least clever?
    • Builders can rather than returning the object to be built, return another different builder, depending on the state of the builder.
    • Imagine an instance where some options are valid in scenario A, and other options are valid in Scenario B.
    • You wouldn’t necessarily want the consumer of the builder to be able to set invalid options on your builder, so you can introduce additional builders (scenario A and scenario B) returned by your root builder.
    • Example of what I mean: https://stackoverflow.com/questions/51392145/conditional-variable-scope-in-c-sharp/51392536#51392536

Important notes about builders

  • Scope - I generally try to stick to a singleton type scope as much as possible, as there’s no need to have multiple instances of a class around unless necessary. In the builders can it is necessary to have multiple instances around! Builders by their nature contain state, and as such should never be in a singleton scope, as multiple contexts acting upon a singleton builder can lead to some very unexpected behavior
  • “Target audience” - I’m not sure if that’s the right word, but when looking at a builder vs a factory, I tend to think the target audience for the two patterns differs. The factory usually takes in a parameter or two to determine what instance of “thing” to return. In a builder, more often than not, I just have one “thing” that can be returned, there’s just a lot of options that can go into the construction of said thing.

References

Top comments (0)