Polymorphic Builder by Mark Seemann
Keeping illegal states unrepresentable with the Builder pattern.
As a reaction to my article on Builder isomorphisms Tyson Williams asked:
I'm happy to receive that question, because I struggled to find a compelling example of a Builder where polymorphism seems warranted. Here, it does."If a
GET
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?"
Valid combinations #
Before showing code, I think a few comments are in order. As far as I'm aware, the HTTP specification doesn't prohibit weird combinations like a GET
request with a body. Still, such a combination is so odd that it seems fair to design an API to prevent this.
On the other hand I think that a POST
request without a body should still be allowed. It's not too common, but there are edge cases where this combination is valid. If you want to cause a side effect to happen, a GET
is inappropriate, but sometimes all you want do to is to produce an effect. In the Restful Web Services Cookbook Subbu Allamaraju gives this example of a fire-and-forget bulk task:
POST /address-correction?before=2010-01-01 HTTP/1.1
As he puts it, "in essence, the client is "flipping a switch" to start the work."
I'll design the following API to allow this combination, also because it showcases how that sort of flexibility can still be included. On the other hand, I'll prohibit the combination of a request body in a GET
request, as Tyson Williams suggested.
Expanded API #
I'll expand on the HttpRequestMessageBuilder
example shown in the previous article. As outlined in another article, apart from the Build
method the Builder really only has two capabilities:
- Change the HTTP method
- Add (or update) a JSON body
- Add or change the
Accept
header - Add or change a
Bearer
token
HttpRequestMessageBuilder
class now looks like this:
public class HttpRequestMessageBuilder { private readonly Uri url; private readonly object? jsonBody; private readonly string? acceptHeader; private readonly string? bearerToken; public HttpRequestMessageBuilder(string url) : this(new Uri(url)) { } public HttpRequestMessageBuilder(Uri url) : this(url, HttpMethod.Get, null, null, null) { } private HttpRequestMessageBuilder( Uri url, HttpMethod method, object? jsonBody, string? acceptHeader, string? bearerToken) { this.url = url; Method = method; this.jsonBody = jsonBody; this.acceptHeader = acceptHeader; this.bearerToken = bearerToken; } public HttpMethod Method { get; } public HttpRequestMessageBuilder WithMethod(HttpMethod newMethod) { return new HttpRequestMessageBuilder( url, newMethod, jsonBody, acceptHeader, bearerToken); } public HttpRequestMessageBuilder AddJsonBody(object jsonBody) { return new HttpRequestMessageBuilder( url, Method, jsonBody, acceptHeader, bearerToken); } public HttpRequestMessageBuilder WithAcceptHeader(string newAcceptHeader) { return new HttpRequestMessageBuilder( url, Method, jsonBody, newAcceptHeader, bearerToken); } public HttpRequestMessageBuilder WithBearerToken(string newBearerToken) { return new HttpRequestMessageBuilder( url, Method, jsonBody, acceptHeader, newBearerToken); } public HttpRequestMessage Build() { var message = new HttpRequestMessage(Method, url); BuildBody(message); AddAcceptHeader(message); AddBearerToken(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"; } private void AddAcceptHeader(HttpRequestMessage message) { if (acceptHeader is null) return; message.Headers.Accept.ParseAdd(acceptHeader); } private void AddBearerToken(HttpRequestMessage message) { if (bearerToken is null) return; message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); } }
Notice that I've added the methods WithAcceptHeader
and WithBearerToken
, with supporting implementation. So far, those are the only changes.
It enables you to build HTTP request messages like this:
HttpRequestMessage msg = new HttpRequestMessageBuilder(url) .WithBearerToken("cGxvZWg=") .Build();
Or this:
HttpRequestMessage msg = new HttpRequestMessageBuilder(url) .WithMethod(HttpMethod.Post) .AddJsonBody(new { id = Guid.NewGuid(), date = "2021-02-09 19:15:00", name = "Hervor", email = "hervor@example.com", quantity = 2 }) .WithAcceptHeader("application/vnd.foo.bar+json") .WithBearerToken("cGxvZWg=") .Build();
It still doesn't address Tyson Williams' requirement, because you can build an HTTP request like this:
HttpRequestMessage msg = new HttpRequestMessageBuilder(url) .AddJsonBody(new { id = Guid.NewGuid(), date = "2020-03-22 19:30:00", name = "Ælfgifu", email = "ælfgifu@example.net", quantity = 1 }) .Build();
Recall that the default HTTP method is GET
. Since the above code doesn't specify a method, it creates a GET
request with a message body. That's what shouldn't be possible. Let's make illegal states unrepresentable.
Builder interface #
Making illegal states unrepresentable is a catch phrase coined by Yaron Minsky to describe advantages of statically typed functional programming. Unintentionally, it also describes a fundamental tenet of object-oriented programming. In Object-Oriented Software Construction Bertrand Meyer describes object-oriented programming as the discipline of guaranteeing that an object can never be in an invalid state.
In the present example, we can't allow an arbitrary HTTP Builder object to afford an operation to add a body, because that Builder object might produce a GET
request. On the other hand, there are operations that are always legal: adding an Accept
header or a Bearer
token. Because these operations are always legal, they constitute a shared API. Extract those to an interface:
public interface IHttpRequestMessageBuilder { IHttpRequestMessageBuilder WithAcceptHeader(string newAcceptHeader); IHttpRequestMessageBuilder WithBearerToken(string newBearerToken); HttpRequestMessage Build(); }
Notice that both the With[...]
methods return the new interface. Any IHttpRequestMessageBuilder
must implement the interface, but is free to support other operations not part of the interface.
HTTP GET Builder #
You can now implement the interface to build HTTP GET
requests:
public class HttpGetMessageBuilder : IHttpRequestMessageBuilder { private readonly Uri url; private readonly string? acceptHeader; private readonly string? bearerToken; public HttpGetMessageBuilder(string url) : this(new Uri(url)) { } public HttpGetMessageBuilder(Uri url) : this(url, null, null) { } private HttpGetMessageBuilder( Uri url, string? acceptHeader, string? bearerToken) { this.url = url; this.acceptHeader = acceptHeader; this.bearerToken = bearerToken; } public IHttpRequestMessageBuilder WithAcceptHeader(string newAcceptHeader) { return new HttpGetMessageBuilder(url, newAcceptHeader, bearerToken); } public IHttpRequestMessageBuilder WithBearerToken(string newBearerToken) { return new HttpGetMessageBuilder(url, acceptHeader, newBearerToken); } public HttpRequestMessage Build() { var message = new HttpRequestMessage(HttpMethod.Get, url); AddAcceptHeader(message); AddBearerToken(message); return message; } private void AddAcceptHeader(HttpRequestMessage message) { if (acceptHeader is null) return; message.Headers.Accept.ParseAdd(acceptHeader); } private void AddBearerToken(HttpRequestMessage message) { if (bearerToken is null) return; message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); } }
Notice that the Build
method hard-codes HttpMethod.Get
. When you're using an HttpGetMessageBuilder
object, you can't modify the HTTP method. You also can't add a request body, because there's no API that affords that operation.
What you can do, for example, is to create an HTTP request with an Accept
header:
HttpRequestMessage msg = new HttpGetMessageBuilder(url) .WithAcceptHeader("application/vnd.foo.bar+json") .Build();
This creates a request with an Accept
header, but no Bearer
token.
HTTP POST Builder #
As a peer to HttpGetMessageBuilder
you can implement the IHttpRequestMessageBuilder
interface to support POST
requests:
public class HttpPostMessageBuilder : IHttpRequestMessageBuilder { private readonly Uri url; private readonly object? jsonBody; private readonly string? acceptHeader; private readonly string? bearerToken; public HttpPostMessageBuilder(string url) : this(new Uri(url)) { } public HttpPostMessageBuilder(Uri url) : this(url, null, null, null) { } public HttpPostMessageBuilder(string url, object jsonBody) : this(new Uri(url), jsonBody) { } public HttpPostMessageBuilder(Uri url, object jsonBody) : this(url, jsonBody, null, null) { } private HttpPostMessageBuilder( Uri url, object? jsonBody, string? acceptHeader, string? bearerToken) { this.url = url; this.jsonBody = jsonBody; this.acceptHeader = acceptHeader; this.bearerToken = bearerToken; } public IHttpRequestMessageBuilder WithAcceptHeader(string newAcceptHeader) { return new HttpPostMessageBuilder( url, jsonBody, newAcceptHeader, bearerToken); } public IHttpRequestMessageBuilder WithBearerToken(string newBearerToken) { return new HttpPostMessageBuilder( url, jsonBody, acceptHeader, newBearerToken); } public HttpRequestMessage Build() { var message = new HttpRequestMessage(HttpMethod.Post, url); BuildBody(message); AddAcceptHeader(message); AddBearerToken(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"; } private void AddAcceptHeader(HttpRequestMessage message) { if (acceptHeader is null) return; message.Headers.Accept.ParseAdd(acceptHeader); } private void AddBearerToken(HttpRequestMessage message) { if (bearerToken is null) return; message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); } }
This class affords various constructor overloads. Two of them don't take a JSON body, and two of them do. This supports both the case where you do want to supply a request body, and the edge case where you don't.
I didn't add an explicit WithJsonBody
method to the class, so you can't change your mind once you've created an instance of HttpPostMessageBuilder
. The only reason I didn't, though, was to save some space. You can add such a method if you'd like to. As long as it's not part of the interface, but only part of the concrete HttpPostMessageBuilder
class, illegal states are still unrepresentable. You can represent a POST
request with or without a body, but you can't represent a GET
request with a body.
You can now build requests like this:
HttpRequestMessage msg = new HttpPostMessageBuilder( url, new { id = Guid.NewGuid(), date = "2021-02-09 19:15:00", name = "Hervor", email = "hervor@example.com", quantity = 2 }) .WithAcceptHeader("application/vnd.foo.bar+json") .WithBearerToken("cGxvZWg=") .Build();
This builds a POST
request with both a JSON body, an Accept
header, and a Bearer
token.
Is polymorphism required? #
In my previous Builder article, I struggled to produce a compelling example of a polymorphic Builder. It seems that I've now mended the situation. Or have I?
Is the IHttpRequestMessageBuilder
interface really required?
Perhaps. It depends on your usage scenarios. I can actually delete the interface, and none of the usage examples I've shown here need change.
On the other hand, had I written helper methods against the interface, obviously I couldn't just delete it.
The bottom line is that polymorphism can be helpful, but it still strikes me as being non-essential to the Builder pattern.
Conclusion #
In this article, I've shown how to guarantee that Builders never get into invalid states (according to the rules we've arbitrarily established). I used the common trick of using constructors for object initialisation. If a constructor completes without throwing an exception, we should expect the object to be in a valid state. The price I've paid for this design is some code duplication.
You may have noticed that there's duplicated code between HttpGetMessageBuilder
and HttpPostMessageBuilder
. There are ways to address that concern, but I'll leave that as an exercise.
For the sake of brevity, I've only shown examples written as Immutable Fluent Builders. You can refactor all the examples to mutable Fluent Builders or to the original Gang-of-Four Builder pattern. This, too, will remain an exercise for the interested reader.
Comments
I know of essentially one occurrence in .NET. Starting with
IEnumerable<T>
, calling either of the extension methods OrderBy or OrderByDescending returnsIOrderedEnumerable<T>
, which has the additional extension methods ThenBy and ThenByDescending.Quoting your recent Builder isomorphisms post.
I also find the builder pattern useful because its methods typically accept one argument one at a time. The builders in your recent posts are like this. The
OrderBy
andThenBy
methods and theirDescending
alternatives in .NET are also examples of this.However, some of the builders in your recent posts have some constructors that take multiple arguments. That is the situation that I was trying to address when I asked
This could be a kata variation: all public functions accept at most one argument. So
Foo(a, b)
would not be allowed butFoo.WithA(a).WithB(b)
would. In an issue on this blog's GitHub, jaco0646 nicely summerized the reasoning that could lead to applying this design philosophy to production code by sayingThat comment by jaco0646 also supplied names by which this type of design is known. Those names (with the same links from the comment) are Builder with a twist or Step Builder. This is great, because I didn't have any good names. (I vaguely recall once thinking that another name was "progressive API" or "progressive fluent API", but now when I search for anything with "progressive", all I get are false positives for progressive web app.
When replacing a multi-argument constructor with a sequence of function calls that each accept one argument, care must be taken to ensure that illegal state remains unrepresentable. My general impression is that many libraries have designed such APIs well. The two that I have enough experience with to recommend as good examples of this design are the fluent configuration API in Entity Framework and Fluent Assertions. As I said before, the most formal treatment I have seen about this type of API design was in this blog post.
Tyson, apart from as a kata constraint, is there any reason to prefer such a design?
I'll be happy to give it a more formal treatment if there's reasonable scenario. Can you think of one?
I don't find the motivation given by jaco0646 convincing. If you have more than a handful of required parameters, I agree that that's an issue with complexity, but I don't agree that the solution is to add more complexity on top of it. Builders add complexity.
At a glance, though, with something like
Foo.WithA(a).WithB(b)
it seems to me that you're essentially reinventing currying the hard way around.Related to the overall Builder discussion (but not to currying) you may also find this article and this Stack Overflow answer interesting.
Yes. Just like you, I want to write small functions. In that post, you suggest an arbitrary maximum of 24 lines. One thing I find fascinating about functional programming is how useful the common functions are (such as
map
) and how they are implemented in only a few lines (often just one line). There is a correlation between the number of function arguments and the length of the function. So to help control the length of a function, it helps to control the number of arguments to the functions. I think Robert Martin has a similar argument. When talking about functions in chapter 3 of Clean Code, his first section is about writing small functions and a later section about function arguments open by sayingIn the C# code
a.Foo(b)
,Foo
is an instance method that "feels" like it only has one argument. In reality, its two inputs area
andb
, and that code uses infix notation. The situation is similar in the F# codea |> List.map f
. The functionList.map
(as well as the operator|>
) has two arguments and is applied using infix notation. I try to avoid creating functions that have more than two arguments.I am not sure how you are measuring complexity. I like to think that there are two types of complexity: local and global. For the sake of argument, let's suppose
Indeed, that is a nice article. Finite state machines/automata (both deterministic and nondeterministic) have the same expressiveness as regular expressions.
It is. As a regular expression, it would be something like
AB
. I was just trying to give a simple example. The point of the article you shared is that the builder pattern is much more expressive. I have previously shared a similar article, but I like yours better. Thanks :)Wow. That is extremely cleaver! I would never thought of that. Thank you very much for sharing.
As I said above, I often try to find ways to minimize the maximum complexity of the code that I write. In this case, the reason that I originally asked you about the builder pattern is that I was trying to improve the API for creating a binding in Elmish.WPF. The tutorial has a great section about bindings. There are many binding types, and each has multiple ways to create it. Most arguments are required and some are optional.
Here is a closed issue that was created during the transition to the current binding API, which uses method overloading. In an attempt to come up with a better API, I suggested that we could use your suggestion to replace overloading with discriminated unions, but my co-maintainer wasn't convinced that it would be better.
Three days later, I increased the expressiveness of our bindings in this pull request. Conceptually it was a small change; I added a single optional argument. For a regular expression, such a change is trivial. However, in my case it was a delta of +300 lines of mind-numbingly boring code.
I agree with my co-maintainer that the current binding API is pretty good for the user. On the implementation side though, I am not satisfied. I want to find something better without sacrificing (and maybe even improving) the user's experience.