Practical .NET

Move Beyond Factory Methods with the Builder Pattern in C#/Visual Basic

When your objects get sufficiently complicated to create, it's time to simplify your life by moving to the Builder pattern. The Builder pattern is not only a great pattern for creating complicated objects, it supports your application's further evolution.

The first design pattern I ever used was the Factory Method pattern. By adopting that pattern, I moved the code for creating my objects out of my constructors and into a separate method. That simplified each of my classes and made it easier to test both the class and the Factory method.

With the Factory Method pattern, when I want a Customer object, I write code like this:

Dim cust As Customer
cust = CustomerFactory.GetCustomerById("A123")
cust = CustomerFactory.CreateCustomer()
Dim custs As List(Of Customer)
custs = CustomerFactory.GetCustomersByCity("Regina")

This code reflects some common ways I implement the Factory Method pattern:

  • I have a dedicated factory class for my Factory Methods
  • The name of the factory class includes the target class's name and the word "Factory"
  • The Factory class may have one or more methods depending on how many ways I need to have Customer objects created
  • The various methods are all declared as Shared/static so that I can call them directly from the factory class

Regardless of what conventions you want to follow when defining Factory classes, the goal remains the same: To isolate the object creation code from the class itself.

Why Centralizing Object Creation is Good For You
In addition to the benefits I've already listed, isolating the object creation code also allows you to modify the class creation code without having to modify the target class. If your change to the way the object is created introduces a new bug, at least you've restricted the bug to the factory class.

You can also ensure that, no matter what application creates the class, the class is always created the same way … provided you require all developers to use your factory class to create the target class. You can achieve that by first defining your target class in the same project as the factory class that creates it and then by giving the target class a constructor that can only be used by code in the same project.

The constructor doesn't have to require any parameters or contain any code: It's merely necessary that the constructor exists. By declaring the constructor as internal (in C#) or Friend (in Visual Basic) you ensure the constructor can't be accessed by a client external to the project creating the class. Your compiler will refuse to let a client instantiate a class if the client can't access the constructor. The constructor can, of course, still be accessed by the methods in the factory class in the same project.

Here's the Visual Basic for a Customer class that must be created by a Factory class:

Public Class Customer
  Friend Sub New()
  End Sub

And here it is in C#:

public class Customer
  internal Customer() {  }

The Factory Method that would return an empty Customer object could be as simple as this:

Public Shared Function CreateCustomer() As Customer
  Return New Customer
End Function

With these changes, you've ensured consistency in how your Customer class is created by client applications.

Why You Need a Builder Class
The problem is that, over time, things didn't stay that simple. Under the influence of the Single Responsibility Principle, the Customer object will acquire a SalesOrders collection that holds all the SalesOrders that Customer has purchased. The next step in the Customer's evolution will be incorporating a set of Address objects representing the different kinds of addresses a Customer may have (shipping, billing, contact and so on). Next, the Customer might acquire a BillingTerms object that describes how the Customer will pay its bills.

What was once a single object is now a composite of several different objects, traveling together through your application.

This leads to a new issue: Handling all the different ways for which your Customer composite might be used. Most client applications won't need the whole Customer composite. On many occasions, for example, a client will need a Customer object but won't need to populate the Customer's SalesOrders; a client that's not making a sale to a Customer probably doesn't need to retrieve the BillingTerms information and so on. Even more frequently, when a client does need a Customer component, often the client will only need a read-only version of the component.

To support this reality, you'll want "purpose-built" Customer objects that retrieve only the components of the composite that the application needs (and only makes them updateable when it's useful). This makes your application more efficient (less database access), simplifies your update logic (you don't have to update what isn't there or couldn't be changed) and reduces data contention (see previous). The problem is that that there's probably not a lot of overlap among applications: Each application has its own purposes and is going to need its own special version of the Customer object.

As a result, if you stick with the Factory Method pattern, your factory classes will start to accumulate an endless number of methods, each of which assemble the composite for different purposes: GetCustomerForSales, GetCustomerForBilling and so on. When combined with the number of different ways that you need to retrieve Customers (that is, get by city, get by id and so on) the number of methods will grow exponentially.

Designing a Builder
At this point, it becomes worthwhile to consider implementing the Builder pattern. The Builder object creates an object incrementally by allowing the client to select the options to be used inside the Factory Method. Conventionally, the Builder is a standalone class with multiple Boolean properties, one for each option. For the standard implementation of the Builder pattern, the class includes a method or property (conventionally called Result) that hands back an object built according to the selected options. A Builder object for the Customer class might look like Listing 1.

Listing 1: A Simple Builder Object

Public Class CustomerBuilder
  Private custId As String

  Public Sub New(custId As String)
    Me.custId = custId
  End Sub
  Public Property Billing As Boolean
  Public Property Addresses As Boolean
  Public Property SalesOrders As Boolean
  '...More options...
  Public ReadOnly Property Result As Customer
    Get
      Dim cust As Customer
      cust = CustomerFactory.GetCustomerById(Me.custId)
      If Me.Billing Then
        cust.BillingTerms = BillingFactory.GetTermsByCustomerId(custId)
      End If
      If Me.Addresses Then
        cust.Addresses = BillingFactory.GetAddressByCustomerId(custId)
      End If
      '...More options...
      Return cust
    End Get
  End Property
End Class

A client that uses the CustomerBuilder object might write code like this:

Dim buildCust As CustomerBuilder
buildCust = New CustomerBuilder("A123")
buildCust.Billing = True
Dim cust As Customer
cust = buildCust.Result

One of the nicer features of the BuilderObject pattern is that, as the Customer object becomes more complicated by acquiring more components, you extend the Builder by writing new code: You add a new property to support the new component and add a new If block in the Result member to generate the new component.

Variations
There a number of variations on the Builder object pattern. For example, if the Customer object is going to acquire more components, defining an interface or base class for the Builder would make sense as the number of clients increase. With my first design adding a new member to the Builder will force you to recompile all the clients -- not an onerous burden if there's only one or two clients. With an interface, however, you could simply design a new interface that inherits from the original and include the new properties in that interface. Only the clients that need the new component would need to be recompiled and, presumably, they'd need to be changed anyway.

Nor do your properties have to be Booleans. The Billing and SalesOrder properties might accept enumerated values indicating whether the updateable or read-only versions of those classes are required.

The Addresses property might be better implemented as a collection of AddressTypes, also defined in an Enum (shipping, billing and so on). A client could then add the appropriate AddressTypes to the collection and, in the result method, you would retrieve just the addresses requested. You'd want to trade that flexibility off against performance -- an implementation that provided this feature by making a separate trip to the database for each address might not be "better" (especially if the typical client wants all the addresses).

Implementing the Result member as a method is another option (probably giving the method a name like GetResult). In that variation, you might pass the CustomerId to the GetResult method rather than to the Builder object's constructor.

Multiple Builders
Of course, this assumes that you only need one Builder object. In a more complex system there might be a variety of different kinds of Customers (PremiumCustomer, InternationalCustomer and so on) all of which inherit from a BaseCustomer object (or implement an ICustomer interface). At some point, the code inside the Result member might become sufficiently complicated that it would make sense to create dedicated Builders for each type of Customer. In that case, defining either an interface or a base class for the CustomerBuilder would let you create dedicated Builders that all look alike to the client (this is a variation on the Abstract Factory pattern).

An interface that would support my simple CustomerBuilder would look like this:

Public Interface ICustomerBuilder
  Public Property Billing As Boolean
  Public Property Addresses As Boolean
  Public Property SalesOrders As Boolean
  '...more options...
  Public ReadOnly Property Result As BaseCustomer

A builder that implements this interface would look like Listing 2.

Listing 2: A Dedicated CustomerBuilder Class

Public Class InternationalCustomerBuilder
  Implement ICustomerBuilder
  '...Constructor...
  '...Options...
  Public ReadOnly Property Result As BaseCustomer Implements ICustomerBuilder.Result
    Get
      Dim cust As BaseCustomer
      '...Custom code to create InternationalCustomer
      Return cust
    End Get
  End Property
End Class

Clients would then be responsible for instantiating the right CustomerBuilder object. A client that needs an International Customer would write code like this:

Dim custBuild As ICustomerBuilder
custBuild = New InternationalCustomerBuilder("A123")
custBuild.Billing = True
Dim cust As BaseCustomer
cust = custBuild.Result

One of the major benefits of this design is that, as new types of Customers are defined, you wouldn't need to modify existing CustomerBuilders. Instead, you'd create a new CustomerBuilder that implements the ICustomerBuilder interface (following Vogel's first law of programming: You don't screw with working code). If the new CustomerBuilder requires new options, you can create a new interface that inherits from ICustomerBuilder and adds the new properties for those options.

Again, there are variations. If there's some common code that's shared among all the CustomerBuilders, it would make sense to create a BaseCustomerBuilder object to hold that code. The client code would look the same as my last example but would declare the custBuild variable as BaseCustomerBuilder instead of ICustomerBuilder.

If the number of CustomerBuilders is expected to grow, it might make sense to create a "Builder factory" class. That class, passed an enumerated Customer type, would return the right CustomerBuilder. This would free the client from having to figure which Builder class to instantiate.

As an example, this client code uses a CustomerBuilderFactory that returns a CustomerBuilder object that implements the ICustomerBuilder interface (the code also incorporates some of the other variations discussed earlier). The client doesn't know and doesn't care what kind of CustomerBuilder object it gets:

Dim custBuild As ICustomerBuilder 
custBuild = CustomerBuilderFactory.GetBuilder(CustomerTypes.Premium)
custBuild.Billing = True
custBuild.Addresses.Add(CustAddressEnum.Billing)
Dim cust As BaseCustomer
cust = custBuild.GetResult("A123")

In any application, you'll only ever need some subset of these options (and some of these options -- choosing between a Result property or GetResult method -- reflect lifestyle choices rather than critical design issues). What's important to remember is that the goal isn't to create a complicated system of Builder objects. Instead, the goal is to create a system of simple, easily testable objects, each of which does one thing well and contains clear and obvious code.

The reality is that you can't eliminate complexity in your applications. You can, however, assemble your complex applications out of individually simple objects.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube