Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compelling Builder example #539

Closed
ploeh opened this issue Jan 6, 2020 · 16 comments
Closed

Compelling Builder example #539

ploeh opened this issue Jan 6, 2020 · 16 comments

Comments

@ploeh
Copy link
Owner

@ploeh ploeh commented Jan 6, 2020

This issue isn't an actual issue with the blog, but a request for ideas.

I'd like to write a couple of articles about the Builder design pattern, but I need a compelling code example. I have one already, but I'd like one more.

You don't have to supply any code; just a good idea.

If you have a good idea, just write a response here.

In the following, I'll outline what I've already considered, and what I'm looking for

What I'm looking for

I'm looking for a problem that can be used for a code example. I want to present it in an article, so neither the problem nor the code can be too complicated. It must be something that a reader can grasp without too much effort.

The problem must also be a good fit for the Builder pattern. In other words, it must solve the problem of separating construction of an object from it representation.

GoF Maze

For a while, I considered (re)using the Maze example from Design Patterns, but once you start working with that example code, it's really odd. It seems to assume a rectangular layout, but as a client of the MazeBuilder API you have no way to indicate relative position. This seems to make it impossible, or at least awkward, to implement the implied CommonWall method. If anyone could suggest a reasonable way out of this conundrum, then perhaps I could use that as my example.

As far as I can tell, another problem with the Maze example is that it doesn't deliver on the book's own stated intent. It offers two polymorphic implementations of MazeBuilder, where StandardMazeBuilder seems tightly coupled to the Maze representation. The other alternative, CountingMazeBuilder, is neither compelling nor uses the Maze representation at all - it just returns a null pointer.

Typical examples

I've searched the internet for other examples, but they're most in the form of build a car from parts, build a computer from parts, or even compose a breakfast from parts. Sometimes, when 'ordinary programmers' complain that they have a hard time relating design patterns to their day-to-day work, I can't blame them.

Those are not examples of problems one runs into in ordinary enterprise software development settings.

Examples of Builder that are useful in real code, but not for an article

My troubles with coming up with a compelling examples isn't due to the pattern's lack of utility. You often see it employed by various frameworks' configuration systems. For example, the ASP.NET Core configuration system seems to me to be an example of the Builder pattern, albeit with a twist. Such configuration systems are just a tad too complicated to make an easy-to-understand example.

Cleaner examples are the UriBuilder and ConnnectionStringBuilder classes. These, on the other hand, I have to rule out because I'd like to include some refactorings in my article, and I can't easily refactor a published class.

@Tornhoof

This comment has been minimized.

Copy link

@Tornhoof Tornhoof commented Jan 6, 2020

What about Immutable Collections, i.e. https://docs.microsoft.com/en-us/dotnet/api/system.collections.immutable?view=netcore-3.1. They use builder mostly for performance reasons, that could be you refactoring approach (without the builder it is slow, with the builder it is faster to build the collections). It might be very edge case though, as you can get the builder object from the normal object.

@jalbrzym

This comment has been minimized.

Copy link

@jalbrzym jalbrzym commented Jan 6, 2020

What do you think about Email builder? The E-mail consists of several parts that have to be assembled, like recipients, subject, content, attachments, etc.

@idavis

This comment has been minimized.

Copy link

@idavis idavis commented Jan 6, 2020

I think the Ninject fluent API is a good example. The generic or ASP.net core hosting model also uses them.

@bender2k14

This comment has been minimized.

Copy link
Contributor

@bender2k14 bender2k14 commented Jan 6, 2020

Cleaner examples are the UriBuilder and ConnnectionStringBuilder classes. These, on the other hand, I have to rule out because I'd like to include some refactorings in my article, and I can't easily refactor a published class.

You could use these examples if you selected a subset of the API that you care about and then provide your own implementations of the API. As I recall, you do something similar in your code examples with ASP.NET (it looks like you are using ASP.NET types in your controller actions because the names match, but you have little to no logic behind those types).

A similar but simpler case than "UriBuilder" is "FileSystemPathBuilder". The API could potentially express root paths, directory paths, file paths, absolute paths, relative paths. A simple but compelling example (in my opinion) is "building" an absolute file path by starting with a root path, "adding" some number of directory names, and then adding a file name.

Nearly every project I have worked on has expressed file system paths with strings. I would love to use stronger types, but I have never found anything that I liked enough to justify making the switch. The best I found is is contained in the NDepend.Path namespace. See their documentation and source code.

Another thing I find very interesting about file system IO is properly separating the code according to its "concern".

  1. Since I have never used a strong type to represent paths, some of the code is logic and data representation (via string) for creating paths of various kinds (file vs directory and absolute vs relative) and checking if they are valid. This code can be complicated because it might depend on the operating system (Windows vs Unix) and while maybe it could be implemented purely in theory, it often seems easier to do so impurely (i.e. to check if a directory name is valid, just try to create a directory by that name). In particular, any logic here is not "business logic".
  2. Some of the code is impure behavior (such as getting all the names of the directories in a given directory).
  3. Some of the code is "business logic" (such as checking for the existence of a directory name matching a certain pattern given a sequence of directory names).
@dannyfhalpotia

This comment has been minimized.

Copy link

@dannyfhalpotia dannyfhalpotia commented Jan 6, 2020

How about for creating test data in unit tests? So e.g. for building a Person object you would go new PersonBuilder.Build(); and it would build a Person object with "default" properties (specified in the ctor). Then you expose each of the Person object's properties as "With" methods so you could go new PersonBuilder().WithFirstName("Danny").Build(); and then you will have a Person object with first name Danny. I've found the builder pattern to be really useful in these cases when I need to build an object quickly many times and can quickly tweak one property without having to set all the others again.

Something similar to what is being described here: https://medium.com/@arleypadua/builder-pattern-applied-to-testing-60e009c427c6

For complex properties you could also use the builder pattern e.g.

new PersonBuilder()
    .WithAge(28)
    .WithAddress(new AddressBuilder().WithZipCode(1159).Build())
    .Build();
@ploeh

This comment has been minimized.

Copy link
Owner Author

@ploeh ploeh commented Jan 7, 2020

Thank you, everyone, for your suggestions. A few responses to some of them:

@jalbrzym, an EmailBuilder is an easy-to-understand example, but I hardly feel that it seems warranted. How does a Builder improve on a design where you have an Email class with properties you can set or leave empty as needed?

@dannyfhalpotia, the Test Data Builder pattern is the example I already have on hand. You couldn't know that, because I never wrote that 😄

@bender2k14, a FileSystemPathBuilder isn't a bad suggestion. I think that I'll go with another example, but of the ones so far suggested, I like this one best 👍

In case you're wondering...

For anyone interested, I'll briefly explain why I don't want to use the FileSystemPathBuilder example: When writing a technical article, I need to consider how the 'average' reader will read it. When it comes to example code, the example must be complicated enough that the reader feels that what the article tries to explain is warranted. On the other hand, it can't be too complicated.

Likewise, I need to pick an example domain that's easy to grasp, but not too familiar to the normal reader. I usually need to simplify things to keep my examples easy to understand, and that's okay for most readers - as long as they aren't experts in that particular field. That's why I love the online restaurant reservation scenario so much. Most people immediately understand what it's about, but few have deep insights.

My hesitation in using FileSystemPathBuilder is that it's a domain that most programmers know a lot about. Probably more than me, because file systems never much interested me. Thus, I'm concerned that I may present some example code that readers will find stupid or naive. This could prevent them from getting the point about the Builder pattern.

Ironically, the example that I currently have in mind is an HttpRequestMessageBuilder. You may say that many readers will also know how to create HTTP requests, but contrary to file systems, I've worked a lot in this space.

@jaco0646

This comment has been minimized.

Copy link

@jaco0646 jaco0646 commented Jan 9, 2020

In chapter 2 of Effective Java, Josh Bloch builds, "the Nutrition Facts label that appears on packaged foods." Regarding the pattern he uses, Bloch says, "It is a form of the Builder pattern [Gamma95, p. 97]."

I think Bloch's pattern may be more popular, because it appears far simpler than the GoF version. As an example, one may wonder why the Builder pattern needs a Director.

@Tyrrrz

This comment has been minimized.

Copy link

@Tyrrrz Tyrrrz commented Jan 12, 2020

Most of the time I'm using builder pattern for configuration classes. I like to use immutable objects, which is important with configuration classes because you don't want it to be changed in the middle of the application's execution and cause unpredictable results. Unfortunately, configurations can have a looot of properties and initializing them all via constructor as a library consumer can be annoying, especially if you just want to set one of them and leave the rest as defaults. Builder pattern solves that.
Here's an example: https://github.com/Tyrrrz/CliFx/blob/master/CliFx/CliApplicationBuilder.cs

@Tyrrrz

This comment has been minimized.

Copy link

@Tyrrrz Tyrrrz commented Jan 12, 2020

Another example is when you're writing an API that returns a contract, which also happens to be an immutable object. Let's say that object has 3 fields, but you can't retrieve the corresponding data for all of them at once. For example, you may need to make 1 HTTP request to get the first two fields and another HTTP request to get the third. That, and the fact that the returned object is immutable, means that you can't really split those two requests in separate methods. You can of course use "temporary" objects to hold the data or tuples, which may be a viable way but I find that in C# it's always easier to use the builder pattern.

Illustration:

public class Contract
{
    public string Foo { get; }
    public string Bar { get; }
    public string Baz { get; }

    public Contract(string foo, string bar, string baz) { /* ... */ }
}

// Some API method
public Contract GetContract()
{
    var response1 = MakeFirstRequest();
    var foo = response1["foo"];
    var bar = response1["bar"];

    var response2 = MakeSecondRequest();
    var baz = response2["baz"];

    return new Contract(foo, bar, baz);
}

As you can see, we can't easily split these two requests out, even though it's easy to image the code getting bigger and more chaotic as the data extraction gets more complicated or there are more fields to fill.

With builder pattern we can do:

public ContractBuilder FillFooBar(ContractBuilder builder)
{
    var response = MakeFirstRequest();
    return builder.SetFoo(response["foo"]).SetBar(response["bar"]);
}

public ContractBuilder FillBaz(ContractBuilder builder)
{
    var response = MakeSecondRequest();
    return builder.SetBaz(response["baz"]);
}

public Contract GetContract()
{
    var builder = new ContractBuilder();
    FillFooBar(builder);
    FillBaz(builder);
    
    return builder.Build();
}

Sorry for the convoluted example but I hope it gets the point across.

@bender2k14

This comment has been minimized.

Copy link
Contributor

@bender2k14 bender2k14 commented Jan 12, 2020

My impression is that the case of "defaults exist for all fields/properties" is well understood. In the "base case", this is "just" F#'s record-style copy and update expression that uses the common keyword with. In the "recursive" case, in which some field/property is also a "record", then functional lens is the standard answer (though I have never used a lens yet).

The interesting case to me is when some field/properties/values are required but there is no (reasonable) default with which to start.

I am very excited to see what Mark has to say on this topic of builder patterns.

@Tyrrrz

This comment has been minimized.

Copy link

@Tyrrrz Tyrrrz commented Jan 12, 2020

@bender2k14 yeah, the builder pattern replaces the with expression in the first example, however the second example is a bit more nuanced. In the second example, there is no default foo, bar, baz, but the builder pattern helps separate potentially complex logic when initializing these fields.

@ploeh

This comment has been minimized.

Copy link
Owner Author

@ploeh ploeh commented Jan 22, 2020

Once again thank you, all, for your suggestions. I've now written the articles that I intended to write, so I'm going to close this issue.

Feel free to continue the discussions, if you wish 😄

@ploeh ploeh closed this Jan 22, 2020
@bender2k14

This comment has been minimized.

Copy link
Contributor

@bender2k14 bender2k14 commented Feb 13, 2020

...I've now written the articles that I intended to write...

If you don't mind me asking, how many articles do you intend to publish on this topic? (I am interested to know when the content on this topic is "over".)

@jaco0646

This comment has been minimized.

Copy link

@jaco0646 jaco0646 commented Feb 13, 2020

Reading the comparison of Builder flavors in the article reminded me of another flavor, sometimes called Builder with a twist or Step Builder.

Popular advice for a builder with required parameters is to put those in a constructor; but with more than a handful of required parameters, we return to the original problem: too much complexity in a constructor.

@bender2k14

This comment has been minimized.

Copy link
Contributor

@bender2k14 bender2k14 commented Feb 13, 2020

Those are good links @jaco0646. Thanks for sharing :)

In a previous conversation about builders (that started with this comment), I shared this link in this comment, which is very similar to the links you have shared.

@ploeh

This comment has been minimized.

Copy link
Owner Author

@ploeh ploeh commented Feb 13, 2020

If you don't mind me asking, how many articles do you intend to publish on this topic?

I've one more queued up. Then you probably have some questions, and we can take if from there 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
8 participants
You can’t perform that action at this time.