Classic Implementation
Definision
The Builder Pattern decouples the construction of a complex object from its representation, allowing the same construction process to create different representations.
Pattern Structure
- Abstract Builder (Builder): Defines an abstract interface for creating parts of a Product object. It specifies what needs to be built but not how.
- Concrete Builder: Implements the builder interface. It constructs and assembles parts of the product, defines the specific representation, and provides a way to retrieve the constructed object.
- Director: Constructs an object using the Builder interface. It knows the sequence of assembly but is unaware of the specific product details.
- Product: The complex object under construction. It includes classes that define the components and their asseembly.
Example: Assembling Vehicles
Product and Abstract Builder
// The complex object we want to build
public class Vehicle
{
public IList<string> Wheels { get; set; }
public IList<string> Lights { get; set; }
}
// Abstract builder defining the steps
public abstract class VehicleAssembler
{
public Vehicle CurrentVehicle { get; protected set; }
public void Initialize()
{
CurrentVehicle = new Vehicle();
}
public abstract void InstallWheels();
public abstract void InstallLights();
}
Concrete Builders
// Builder for a Car
public class CarAssembler : VehicleAssembler
{
public override void InstallWheels()
{
CurrentVehicle.Wheels = new List<string> { "FL", "FR", "RL", "RR" };
}
public override void InstallLights()
{
CurrentVehicle.Lights = new List<string> { "Headlight", "Headlight", "Taillight", "Taillight" };
}
}
// Builder for a Bicycle
public class BikeAssembler : VehicleAssembler
{
public override void InstallWheels()
{
CurrentVehicle.Wheels = new List<string> { "Front", "Rear" };
}
public override void InstallLights()
{
CurrentVehicle.Lights = null; // Bicycles might not have lights
}
}
Director
// The Director orchestrates the building process
public class AssemblyLine
{
public Vehicle Assemble(VehicleAssembler assembler)
{
assembler.Initialize();
assembler.InstallWheels();
assembler.InstallLights();
return assembler.CurrentVehicle;
}
}
Usage
var line = new AssemblyLine();
var car = line.Assemble(new CarAssembler());
// car.Wheels.Count == 4
var bike = line.Assemble(new BikeAssembler());
// bike.Wheels.Count == 2
Pros and Cons
Advantages:
- Allows fine-grained control over the construction steps and their order.
- Isolates complex construction code from the business logic.
- Supports constructing different representations using the same process.
Disadvantages:
- Increases code complexity by introducing multiple new classes.
- The Director can become a bottleneck if the construction logic changes frequently.
Attribute-Driven Builder
This approach uses custom attributes and reflection to define the build steps directly on the product class, eliminating the need for separate Builder classes.
Attribute Definition
[AttributeUsage(AttributeTargets.Method)]
public class AssemblyStepAttribute : Attribute
{
public int Order { get; }
public int RepeatCount { get; }
public AssemblyStepAttribute(int order, int count = 1)
{
Order = order;
RepeatCount = count;
}
}
Reflection Helper
public class StepScanner
{
static Dictionary<Type, List<MethodInfo>> _cache = new Dictionary<Type, List<MethodInfo>>();
public List<MethodInfo> GetSteps(Type type)
{
if (_cache.ContainsKey(type)) return _cache[type];
var methods = type.GetMethods()
.Where(m => m.GetCustomAttribute<AssemblyStepAttribute>() != null)
.OrderBy(m => m.GetCustomAttribute<AssemblyStepAttribute>().Order)
.ToList();
_cache[type] = methods;
return methods;
}
}
Generic Builder
public class DynamicBuilder<T> where T : new()
{
public T Construct()
{
var scanner = new StepScanner();
var steps = scanner.GetSteps(typeof(T));
T instance = new T();
foreach (var step in steps)
{
var attr = step.GetCustomAttribute<AssemblyStepAttribute>();
for (int i = 0; i < attr.RepeatCount; i++)
{
step.Invoke(instance, null);
}
}
return instance;
}
}
Product with Attributes
class Sedan
{
public List<string> Components { get; set; } = new List<string>();
[AssemblyStep(1)]
public void AttachChassis() => Components.Add("Chassis");
[AssemblyStep(2, 4)]
public void AddTire() => Components.Add("Tire");
}
Reversible Builder (Teardown)
Sometimes objects need to be dismantled as well as built. This interface supports both operations.
public interface IReversibleBuilder<T>
{
T Assemble();
T Dismantle();
}
public class DeviceBuilder : IReversibleBuilder<Device>
{
private Device _device = new Device();
public Device Assemble()
{
_device.Parts = new List<string> { "CPU", "RAM", "SSD" };
return _device;
}
public Device Dismantle()
{
_device.Parts.Clear();
return _device;
}
}
public class Device { public List<string> Parts { get; set; } }
Fluent Builder (Chaining)
The Fluent interface allows for readable, chained method calls during object creation. This is useful for objects with many optional parameters.
public class ServerConfig
{
public string IP { get; }
public int Port { get; }
public bool IsSecure { get; }
private ServerConfig(Builder builder)
{
IP = builder.IPAddress;
Port = builder.PortNumber;
IsSecure = builder.Secure;
}
public class Builder
{
public string IPAddress { get; private set; }
public int PortNumber { get; private set; }
public bool Secure { get; private set; }
public Builder UseIP(string ip)
{
IPAddress = ip;
return this;
}
public Builder OnPort(int port)
{
PortNumber = port;
return this;
}
public Builder EnableSSL()
{
Secure = true;
return this;
}
public ServerConfig Build()
{
return new ServerConfig(this);
}
}
}
// Usage:
// var config = new ServerConfig.Builder().UseIP("127.0.0.1").OnPort(8080).EnableSSL().Build();
Event-Based Builder (AOP Style)
By exposing events during the construction liefcycle, we can inject cross-cutting concerns like logging or validation without modifying the builder logic.
public class ObservableBuilder<T> where T : new()
{
public event Action<T> OnConstructionStarted;
public event Action<T, string> OnStepCompleted;
public T Build()
{
T instance = new T();
OnConstructionStarted?.Invoke(instance);
// Simulate steps
AddComponent(instance, "Engine");
AddComponent(instance, "Wheels");
return instance;
}
private void AddComponent(T target, string component)
{
// Logic to add component...
OnStepCompleted?.Invoke(target, component);
}
}