Builder isomorphisms by Mark Seemann
The Builder pattern is equivalent to the Fluent Builder pattern.
This article is part of a series of articles about software design isomorphisms. An isomorphism is when a bi-directional lossless translation exists between two representations. Such translations exist between the Builder pattern and two variations of the Fluent Builder pattern. Since the names sound similar, this is hardly surprising.
Given an implementation that uses one of those three patterns, you can translate your design into one of the other options. This doesn't imply that each is of equal value. When it comes to composability, both versions of Fluent Builder are superior to the classic Builder pattern.
A critique of the Maze Builder example #
In these articles, I usually first introduce the form presented in Design Patterns. The code example given by the Gang of Four is, however, problematic. I'll start by pointing out the problems and then proceed to present a simpler, more useful example.
The book presents an example centred on a MazeBuilder
abstract class. The original example is in C++, but I here present my C# interpretation:
public abstract class MazeBuilder { public virtual void BuildMaze() { } public virtual void BuildRoom(int room) { } public virtual void BuildDoor(int roomFrom, int roomTo) { } public virtual Maze GetMaze() { return null; } }
As the book states, "the maze-building operations of MazeBuilder do nothing by default. They're not declared pure virtual to let derived classes override only those methods in which they're interested." This means that you could technically write a derived class that overrides only BuildRoom
. That's unlikely to be useful, since GetMaze
still returns null
.
Moreover, the presence of the BuildMaze
method indicates sequential coupling. A client (a Director, in the pattern language of Design Patterns) is supposed to first call BuildMaze
before calling any of the other methods. What happens if a client forgets to call BuildMaze
? What happens if client code calls the method after some of the other methods. What happens if it calls it multiple times?
Another issue with the sample code is that it's unclear how it accomplishes its stated goal of separating "the construction of a complex object from its representation." The StandardMazeBuilder
presented seems tightly coupled to the Maze
class to a degree where it's hard to see how to untangle the two. The book fails to make a compelling example by instead presenting a CountingMazeBuilder
that never implements GetMaze
. It never constructs the desired complex object.
Don't interpret this critique as a sweeping dismissal of the pattern, or the book in general. As this article series implies, I've invested significant energy in it. I consider the book seminal, but we've learned much since its publication in 1994. A common experience is that not all of the patterns in the book are equally useful, and of those that are, some are useful for different reasons than the book gives. The Builder pattern is an example of that.
The Builder pattern isn't useful only because it enables you to "separate the construction of a complex object from its representation." It's useful because it enables you to present an API that comes with good default behaviour, but which can be tweaked into multiple configurations. The pattern is useful even without polymorphism.
HTTP request Builder #
The HttpRequestMessage class is a versatile API with good default behaviour, but it can be a bit awkward if you want to make an HTTP request with a body and particular headers. You can often get around the problem by using methods like PostAsync on HttpClient, but sometimes you need to drop down to SendAsync. When that happens, you need to build your own HttpRequestMessage
objects. A Builder can encapsulate some of that work.
public class HttpRequestMessageBuilder { private readonly Uri url; private object? jsonBody; public HttpRequestMessageBuilder(string url) : this(new Uri(url)) { } public HttpRequestMessageBuilder(Uri url) { this.url = url; Method = HttpMethod.Get; } public HttpMethod Method { get; set; } public void AddJsonBody(object jsonBody) { this.jsonBody = jsonBody; } public HttpRequestMessage Build() { var message = new HttpRequestMessage(Method, url); BuildBody(message); return message; } private void BuildBody(HttpRequestMessage message) { if (jsonBody is null) return; string json = JsonConvert.SerializeObject(jsonBody); message.Content = new StringContent(json); message.Content.Headers.ContentType.MediaType = "application/json"; } }
Compared to Design Patterns' example, HttpRequestMessageBuilder
isn't polymorphic. It doesn't inherit from a base class or implement an interface. As I pointed out in my critique of the MazeBuilder
example, polymorphism doesn't seem to be the crux of the matter. You could easily introduce a base class or interface that defines the Method
, AddJsonBody
, and Build
members, but what would be the point? Just like the MazeBuilder
example fails to present a compelling second implementation, I can't think of another useful implementation of a hypothetical IHttpRequestMessageBuilder
interface.
Notice that I dropped the Build prefix from most of the Builder's members. Instead, I reserved the word Build
for the method that actually creates the desired object. This is consistent with most modern Builder examples I've encountered.
The HttpRequestMessageBuilder
comes with a reasonable set of default behaviours. If you just want to make a GET
request, you can easily do that:
var builder = new HttpRequestMessageBuilder(url); HttpRequestMessage msg = builder.Build(); HttpClient client = GetClient(); var response = await client.SendAsync(msg);
Since you only call the builder
's Build
method, but never any of the other members, you get the default behaviour. A GET
request with no body.
Notice that the HttpRequestMessageBuilder
protects its invariants. It follows the maxim that you should never be able to put an object into an invalid state. Contrary to Design Patterns' StandardMazeBuilder
, it uses its constructors to enforce an invariant. Regardless of what sort of HttpRequestMessage
you want to build, it must have a URL. Both constructor overloads require all clients to supply one. (In order to keep the code example as simple as possible, I've omitted all sorts of precondition checks, like checking that url
isn't null, that it's a valid URL, and so on.)
If you need to make a POST
request with a JSON body, you can change the defaults:
var builder = new HttpRequestMessageBuilder(url); builder.Method = HttpMethod.Post; builder.AddJsonBody(new { id = Guid.NewGuid(), date = "2020-03-22 19:30:00", name = "Ælfgifu", email = "ælfgifu@example.net", quantity = 1 }); HttpRequestMessage msg = builder.Build(); HttpClient client = GetClient(); var response = await client.SendAsync(msg);
Other combinations of Method
and AddJsonBody
are also possible. You could, for example, make a DELETE
request without a body by only changing the Method
.
This incarnation of HttpRequestMessageBuilder
is cumbersome to use. You must first create a builder
object and then mutate it. Once you've invoked its Build
method, you rarely need the object any longer, but the builder
variable is still in scope. You can address those usage issues by refactoring a Builder to a Fluent Builder.
HTTP request Fluent Builder #
In the Gang of Four Builder pattern, no methods return anything, except the method that creates the object you're building (GetMaze
in the MazeBuilder
example, Build
in the HttpRequestMessageBuilder
example). It's always possible to refactor such a Builder so that the void
methods return something. They can always return the object itself:
public HttpMethod Method { get; private set; } public HttpRequestMessageBuilder WithMethod(HttpMethod newMethod) { Method = newMethod; return this; } public HttpRequestMessageBuilder AddJsonBody(object jsonBody) { this.jsonBody = jsonBody; return this; }
Changing AddJsonBody
is as easy as changing its return type and returning this
. Refactoring the Method
property is a bit more involved. It's a language feature of C# (and a few other languages) that classes can have properties, so this concern isn't general. In languages without properties, things are simpler. In C#, however, I chose to make the property setter private and instead add a method that returns HttpRequestMessageBuilder
. Perhaps it's a little confusing that the name of the method includes the word method, but keep in mind that the method in question is an HTTP method.
You can now create a GET
request with a one-liner:
HttpRequestMessage msg = new HttpRequestMessageBuilder(url).Build();
You don't have to declare any builder
variable to mutate. Even when you need to change the defaults, you can just start with a builder and keep on chaining method calls:
HttpRequestMessage msg = new HttpRequestMessageBuilder(url) .WithMethod(HttpMethod.Post) .AddJsonBody(new { id = Guid.NewGuid(), date = "2020-03-22 19:30:00", name = "Ælfgifu", email = "ælfgifu@example.net", quantity = 1 }) .Build();
This creates a POST
request with a JSON message body.
We can call this pattern Fluent Builder because this version of the Builder pattern has a Fluent Interface.
This usually works well enough in practice, but is vulnerable to aliasing. What happens if you reuse an HttpRequestMessageBuilder
object?
var builder = new HttpRequestMessageBuilder(url); var deleteMsg = builder.WithMethod(HttpMethod.Delete).Build(); var getMsg = builder.Build();
As the variable names imply, the programmer responsible for these three lines of code incorrectly believed that without the call to WithMethod
, the builder
will use its default behaviour when Build
is called. The previous line of code, however, mutated the builder
object. Its Method
property remains HttpMethod.Delete
until another line of code changes it!
HTTP request Immutable Fluent Builder #
You can disarm the aliasing booby trap by making the Fluent Builder immutable. A good first step in that refactoring is making sure that all class fields are readonly
:
private readonly Uri url; private readonly object? jsonBody;
The url
field was already marked readonly
, so the change only applies to the jsonBody
field. In addition to the class fields, don't forget any automatic properties:
public HttpMethod Method { get; }
The HttpMethod
property previously had a private
setter, but this is now gone. It's also strictly read only.
Now that all data is read only, the only way you can 'change' values is via a constructor. Add a constructor overload that receives all data and chain the other constructors into it:
public HttpRequestMessageBuilder(string url) : this(new Uri(url)) { } public HttpRequestMessageBuilder(Uri url) : this(url, HttpMethod.Get, null) { } private HttpRequestMessageBuilder(Uri url, HttpMethod method, object? jsonBody) { this.url = url; Method = method; this.jsonBody = jsonBody; }
I'm usually not keen on allowing null
arguments, but I made the all-encompassing constructor private
. In that way, at least no client code gets the wrong idea.
The optional modification methods can now only do one thing: return a new object:
public HttpRequestMessageBuilder WithMethod(HttpMethod newMethod) { return new HttpRequestMessageBuilder(url, newMethod, jsonBody); } public HttpRequestMessageBuilder AddJsonBody(object jsonBody) { return new HttpRequestMessageBuilder(url, Method, jsonBody); }
The client code looks the same as before, but now you no longer have an aliasing problem:
var builder = new HttpRequestMessageBuilder(url); var deleteMsg = builder.WithMethod(HttpMethod.Delete).Build(); var getMsg = builder.Build();
Now deleteMsg
represents a Delete
request, and getMsg
truly represents a GET
request.
Since this variation of the Fluent Builder pattern is immutable, it's natural to call it an Immutable Fluent Builder.
You've now seen how to refactor from Builder via Fluent Builder to Immutable Fluent Builder. If these three pattern variations are truly isomorphic, it should also be possible to move in the other direction. I'll leave it as an exercise for the reader to do this with the HTTP request Builder example. Instead, I will briefly discuss another example that starts at the Fluent Builder pattern.
Test Data Fluent Builder #
A prominent example of the Fluent Builder pattern would be the set of all Test Data Builders. I'm going to use the example I've already covered. You can visit the previous article for all details, but in summary, you can, for example, write code like this:
Address address = new AddressBuilder().WithCity("Paris").Build();
This creates an Address
object with the City
property set to "Paris"
. The Address
class comes with other properties. You can trust that the AddressBuilder
gave them values, but you don't know what they are. You can use this pattern in unit tests when you need an Address
in Paris, but you don't care about any of the other data.
In my previous article, I implemented AddressBuilder
as a Fluent Builder. I did that in order to stay as true to Nat Pryce's original example as possible. Whenever I use the Test Data Builder pattern in earnest, however, I use the immutable variation so that I avoid the aliasing issue.
Test Data Builder as a Gang-of-Four Builder #
You can easily refactor a typical Test Data Builder like AddressBuilder
to a shape more reminiscent of the Builder pattern presented in Design Patterns. Apart from the Build
method that produces the object being built, change all other methods to void
methods:
public class AddressBuilder { private string street; private string city; private PostCode postCode; public AddressBuilder() { this.street = ""; this.city = ""; this.postCode = new PostCodeBuilder().Build(); } public void WithStreet(string newStreet) { this.street = newStreet; } public void WithCity(string newCity) { this.city = newCity; } public void WithPostCode(PostCode newPostCode) { this.postCode = newPostCode; } public void WithNoPostcode() { this.postCode = new PostCode(); } public Address Build() { return new Address(this.street, this.city, this.postCode); } }
You can still build a test address in Paris, but it's now more inconvenient.
var addressBuilder = new AddressBuilder(); addressBuilder.WithCity("Paris"); Address address = addressBuilder.Build();
You can still use multiple Test Data Builders to build more complex test data, but the classic Builder pattern doesn't compose well.
var invoiceBuilder = new InvoiceBuilder(); var recipientBuilder = new RecipientBuilder(); var addressBuilder = new AddressBuilder(); addressBuilder.WithNoPostcode(); recipientBuilder.WithAddress(addressBuilder.Build()); invoiceBuilder.WithRecipient(recipientBuilder.Build()); Invoice invoice = invoiceBuilder.Build();
These seven lines of code creates an Invoice
object with a address without a post code. Compare that with the Fluent Builder example in the previous article. This is a clear example that while the variations are isomorphic, they aren't equally useful. The classic Builder pattern isn't as practical as one of the Fluent variations.
You might protest that this variation of AddressBuilder
, InvoiceBuilder
, etcetera isn't equivalent to the Builder pattern. After all, the Builder shown in Design Patterns is polymorphic. That's really not an issue, though. Just extract an interface from the concrete builder:
public interface IAddressBuilder { Address Build(); void WithCity(string newCity); void WithNoPostcode(); void WithPostCode(PostCode newPostCode); void WithStreet(string newStreet); }
Make the concrete class implement the interface:
public class AddressBuilder : IAddressBuilder
You could argue that this adds no value. You'd be right. This goes contrary to the Reused Abstractions Principle. I think that the same criticism applies to Design Patterns' original description of the pattern, as I've already pointed out. The utility in the pattern comes from how it gives client code good defaults that it can then tweak as necessary.
Summary #
The Builder pattern was originally described in Design Patterns. Later, smart people like Nat Pryce figured out that by letting each mutating operation return the (mutated) Builder, such a Fluent API offered superior composability. A further improvement to the Fluent Builder pattern makes the Builder immutable in order to avoid aliasing issues.
All three variations are isomorphic. Work that one of these variations afford is also afforded by the other variations.
On the other hand, the variations aren't equally useful. Fluent APIs offer superior composability.
Next: Church encoding.
Comments
It is also possible to write that one-liner with the original (non-fluent) builder implementation. Did you mean to show how it is possible with the fluent builder implementation to create a
DELETE
request with a one-liner? You have such an example two code blocks later.Tyson, you are, of course, right. The default behaviour could also have been a one-liner with the non-fluent design. Every other configuration, however, can't be a one-liner with the Gang-of-Four pattern, while it can in the Fluent guise.
Among the example uses of your
HttpRequestMessageBuilder
, I see three HTTP verbs used:GET
,DELETE
, andPOST
. Furthermore, a body is added if and only if the method isPOST
. This matches my expectations gained from my limited experience doing web programming. If aGET
orDELETE
request had a body or if aPOST
request did not have a body, then I would suspect that such behavior was a bug.For the sake of a question that I would like to ask, let's suppose that a body must be added if and only if the method is
POST
. Under this assumption,HttpRequestMessageBuilder
can create invalid messages. For example, it can create aGET
request with a body, and it can create aPOST
request without a body. Under this assumption, how would you modify your design so that only valid messages can be created?Tyson, thank you for another inspiring question! It gives me a good motivation to write about polymorphic Builders. I'll try to address this question in a future article.
Tyson, I've now attempted to answer your question in a new article.