Creational Design Patterns - Part 4 - Builder Pattern
Deep Dive: The Builder Pattern
Constructing Complex Objects Step-by-Step.
What is the Builder Pattern?
The Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code. It separates the construction of a complex object from its representation, so that the same construction process can create different representations. This is particularly useful for objects with many optional parameters or a complicated setup process. The pattern often includes four main participants: the `Product` (the complex object being built), the `Builder` (the interface for creating parts of the product), the `ConcreteBuilder` (the implementation of the builder), and the optional `Director` (which defines the order of construction steps).
Pros
- Allows you to vary a product's internal representation.
- Encapsulates code for construction and representation.
- Provides better control over the construction process.
- Avoids the "Telescoping Constructor" anti-pattern.
Cons
- Increases the number of classes required (Builder, Concrete Builders, etc.).
- Can be more verbose than a simple constructor for simple objects.
Use Case: Constructing a Payment Request
The Problem
Consider creating a `PaymentRequest` object. Some fields are always required (like `Amount` and `Currency`), but many others are optional (`RecipientDetails`, `Metadata`, `FraudCheckId`). Using a standard constructor would be messy:
// Telescoping Constructor - hard to read and error-prone
new PaymentRequest(100.00m, "USD", null, null, "order_123", null);
This is hard to read, and it's easy to mix up the order of the `null` parameters. It also doesn't allow for making the object immutable after creation.
The Solution
The Builder pattern solves this by creating a dedicated `PaymentRequestBuilder` class. This builder guides the user through the creation process step-by-step. The client calls methods like `WithAmount()`, `WithRecipient()`, etc. An optional `Director` class can be used to encapsulate common construction sequences. Finally, the client calls a `Build()` method on the builder, which returns the final, immutable `PaymentRequest` object.
C# Implementation
Here is a C# implementation for building a `PaymentRequest`. We include the `Product`, `Builder`, and an optional `Director` to show how to construct predefined request types.
// --- 1. The Product ---
public class PaymentRequest { /* ... (as before) ... */ }
// --- 2. The Builder Interface ---
public interface IPaymentRequestBuilder { /* ... (as before) ... */ }
// --- 3. The Concrete Builder ---
public class PaymentRequestBuilder : IPaymentRequestBuilder { /* ... (as before) ... */ }
// --- 4. The Director (Optional) ---
// Encapsulates common, complex construction logic.
public class PaymentRequestDirector
{
public void ConstructStandardDomesticPayment(IPaymentRequestBuilder builder)
{
builder.WithAmount(100.00m)
.WithCurrency("USD")
.WithRecipient("DomesticMerchant");
}
public void ConstructInternationalPaymentWithMetadata(IPaymentRequestBuilder builder, decimal amount)
{
builder.WithAmount(amount)
.WithCurrency("EUR")
.WithRecipient("InternationalVendor")
.WithMetadata("cross_border_tx=true");
}
}
// --- 5. Client Code ---
public class PaymentClient
{
public void SubmitPayments()
{
var director = new PaymentRequestDirector();
var builder = new PaymentRequestBuilder();
// Use the director to build a standard payment
director.ConstructStandardDomesticPayment(builder);
PaymentRequest domesticRequest = builder.Build();
// Use the director for a different, more complex payment
director.ConstructInternationalPaymentWithMetadata(builder, 500.00m);
PaymentRequest internationalRequest = builder.Build();
// The client can also act as its own director for custom one-offs
PaymentRequest customRequest = new PaymentRequestBuilder()
.WithAmount(10.00m)
.WithCurrency("JPY")
.Build();
}
}
Common Mistakes to Avoid
- Mutable Product: A common mistake is to allow the `Product` to be modified after it's built. The `Build()` method should be the final step, returning an immutable object to ensure its state is consistent.
- Builder as a "God Object": The builder should only be concerned with constructing the product. Adding business logic or other responsibilities to it violates the Single Responsibility Principle.
- Misunderstanding the Director: The `Director` is optional. For simple, one-off constructions, the client can act as the director. The `Director` class is only needed when you have several common, predefined ways to construct the product that you want to reuse.
Prerequisite C# Topics to Understand
To fully grasp this pattern's C# implementation, you should be comfortable with these concepts:
- Method Chaining (Fluent Interfaces)
- The practice of having methods return the instance of the object they belong to (`return this;`). This allows you to chain method calls together in a single, highly readable statement, as seen in the client code: `new Builder().WithA().WithB().Build()`.
- Internal vs. Private Constructors
- A `private` constructor can only be called from within the same class. An `internal` constructor can be called from any code within the same assembly (project). Using an internal constructor for the Product allows the Builder (which is in the same project) to create it, while still preventing code outside the project from doing so.
- Object Immutability
- An object is immutable if its state cannot be modified after it is created. The Builder pattern facilitates this by allowing the Product's properties to have `private set` or be `init`-only, as all values are provided at construction time via the `Build()` method.
Comments
Post a Comment