The Fluent Builder pattern is a powerful design pattern that enables the creation of complex objects through a more readable and maintainable interface.
Why Use the Fluent Builder Pattern?
Before we dive into implementations, let’s understand why you might want to use the Fluent Builder pattern:
- Improves code readability through method chaining
- Separates the construction of complex objects from their representation
- Enforces immutability while maintaining flexible object construction
- Handles optional parameters elegantly
- Provides a clear API for object construction
Real-World Example: StringBuilder
The StringBuilder
class in .NET is an excellent example of the Fluent Builder pattern:
var message = new StringBuilder()
.Append("Hello")
.Append(" ")
.Append("World")
.AppendLine("!")
.ToString();
ReBuild the HttpClient
Class
Let's look at how the HttpClient
class is being used:
var client = new HttpClient(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 10
});
While this works, it could be more elegant. Here’s how we could implement a fluent builder for HttpClient:
public class HttpClientBuilder
{
private readonly SocketsHttpHandler _handler;
private HttpClientBuilder()
{
_handler = new SocketsHttpHandler();
}
public static HttpClientBuilder Create()
{
return new HttpClientBuilder();
}
public HttpClientBuilder WithConnectionLifetime(TimeSpan lifetime)
{
_handler.PooledConnectionLifetime = lifetime;
return this;
}
public HttpClientBuilder WithIdleTimeout(TimeSpan timeout)
{
_handler.PooledConnectionIdleTimeout = timeout;
return this;
}
public HttpClientBuilder WithMaxConnections(int maxConnections)
{
_handler.MaxConnectionsPerServer = maxConnections;
return this;
}
public HttpClient Build()
{
return new HttpClient(_handler);
}
}
Now we can create an HttpClient in a much more elegant way:
var client = HttpClientBuilder.Create()
.WithConnectionLifetime(TimeSpan.FromMinutes(10))
.WithIdleTimeout(TimeSpan.FromMinutes(5))
.WithMaxConnections(10)
.Build();
Advanced Pattern: Generic Builders with Constraints
Sometimes you need to create builders that work with generic types. Here’s an example of a generic collection builder:
public class CollectionBuilder<T> where T : class
{
private readonly List<T> _items = new List<T>();
private readonly List<Action<List<T>>> _configurations = new List<Action<List<T>>>();
public CollectionBuilder<T> Add(T item)
{
_items.Add(item);
return this;
}
public CollectionBuilder<T> AddRange(IEnumerable<T> items)
{
_items.AddRange(items);
return this;
}
public CollectionBuilder<T> Configure(Action<List<T>> configuration)
{
_configurations.Add(configuration);
return this;
}
public CollectionBuilder<T> WithFilter(Func<T, bool> predicate)
{
_configurations.Add(items =>
{
var itemsToRemove = items.Where(x => !predicate(x)).ToList();
foreach (var item in itemsToRemove)
{
items.Remove(item);
}
});
return this;
}
public IReadOnlyList<T> Build()
{
foreach (var configuration in _configurations)
{
configuration(_items);
}
return _items.AsReadOnly();
}
}
Consume it like this:
var numbers = new CollectionBuilder<string>()
.Add("1")
.Add("2")
.Add("3")
.AddRange(new[] { "4", "5", "6" })
.Configure(list => list.Sort())
.WithFilter(x => int.Parse(x) > 2)
.Build();
Best Practices and Guidelines
When implementing the Fluent Builder pattern, consider these best practices:
- Make the final object immutable
- Use descriptive method names that start with “With” or similar prefixes
- Return the builder instance from configuration methods
- Provide a clear
Build()
method - Consider making the builder’s constructor private and providing a static creation method
- Use method chaining for a more natural API
- Implement validation in the
Build()
method - Consider adding a
Reset()
method for builder reuse
Common Pitfalls to Avoid
- Overcomplicating simple object creation
- Not handling null values properly
- Making the builder mutable after building
- Not implementing proper validation
- Creating too many nested builders
- Not providing a way to reset the builder
When to Use the Fluent Builder Pattern
The Fluent Builder pattern is most useful when:
- Objects require many parameters for construction
- Object construction involves multiple steps
- You need to enforce immutability
- You want to provide a more readable API
- You need to construct objects with optional parameters
- You’re working with complex nested objects