CrackedRuby CrackedRuby

Overview

The Builder Pattern addresses the problem of constructing objects with numerous optional parameters or complex initialization sequences. Rather than using telescoping constructors with many parameters or requiring clients to call multiple setter methods, the Builder Pattern provides a dedicated object that handles construction through a fluent, step-by-step interface.

The pattern emerged from the Gang of Four design patterns catalog as a solution to the inflexibility of constructors. When an object requires many configuration options, constructors become unwieldy. A constructor with ten parameters forces callers to understand parameter order and provide values for options they may not care about. The Builder Pattern extracts this construction logic into a separate class, allowing clients to specify only the configuration they need.

The pattern operates through two primary components: the Builder, which provides methods for setting individual aspects of the object being constructed, and optionally a Director, which encapsulates common construction sequences. The Builder accumulates configuration through method calls, then creates the final object when requested. This separation means the same construction process can create different representations of an object.

# Without Builder Pattern - telescoping constructor
report = Report.new(
  title: "Sales Report",
  format: :pdf,
  include_charts: true,
  include_summary: true,
  page_size: :a4,
  orientation: :landscape,
  font_size: 12,
  color_scheme: :corporate
)

# With Builder Pattern - selective configuration
report = ReportBuilder.new
  .title("Sales Report")
  .format(:pdf)
  .include_charts
  .landscape
  .build

The pattern finds particular value in domain-specific languages (DSLs), configuration systems, test data generation, and any context where object construction involves complex setup or numerous optional components.

Key Principles

Separation of Construction and Representation

The fundamental principle separates how an object is built from what it represents. The Builder encapsulates construction logic while the product class represents the final object. This separation allows the same construction process to produce different types of objects, and different builders to create variations of the same product type.

class HTMLBuilder
  def initialize
    @elements = []
  end

  def add_heading(text)
    @elements << "<h1>#{text}</h1>"
    self
  end

  def add_paragraph(text)
    @elements << "<p>#{text}</p>"
    self
  end

  def build
    @elements.join("\n")
  end
end

class MarkdownBuilder
  def initialize
    @elements = []
  end

  def add_heading(text)
    @elements << "# #{text}"
    self
  end

  def add_paragraph(text)
    @elements << text
    self
  end

  def build
    @elements.join("\n\n")
  end
end

# Same construction process, different representations
def create_document(builder)
  builder.add_heading("Welcome")
         .add_paragraph("This is content")
         .build
end

Step-by-Step Construction

Builders construct objects incrementally rather than requiring all parameters at once. Each method adds one aspect of the final object, accumulating state until the build method finalizes construction. This approach provides clarity about what gets configured and supports optional parameters naturally.

The incremental nature allows validation at each step rather than only at construction time. A builder can verify that added components are compatible or meet certain constraints before accepting them. This early validation prevents invalid object creation and provides better error messages.

Method Chaining and Fluent Interfaces

Builders typically return self from configuration methods, enabling method chaining. This creates a fluent interface where multiple configuration calls read like sentences. The pattern distinguishes between configuration methods (which return the builder) and the terminal build method (which returns the product).

class QueryBuilder
  def initialize(table)
    @table = table
    @conditions = []
    @order = nil
    @limit = nil
  end

  def where(condition)
    @conditions << condition
    self  # Enable chaining
  end

  def order_by(column)
    @order = column
    self  # Enable chaining
  end

  def limit(count)
    @limit = count
    self  # Enable chaining
  end

  def build
    query = "SELECT * FROM #{@table}"
    query += " WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
    query += " ORDER BY #{@order}" if @order
    query += " LIMIT #{@limit}" if @limit
    query  # Terminal method returns product
  end
end

# Fluent usage
query = QueryBuilder.new("users")
  .where("active = true")
  .where("created_at > '2024-01-01'")
  .order_by("last_name")
  .limit(50)
  .build

Immutability Options

Builders can construct either mutable or immutable products. For immutable products, the builder accumulates configuration in mutable state, then creates an immutable object in the build method. The builder itself may be mutable (accumulating state) or immutable (creating new builder instances at each step).

Immutable builders sacrifice performance for safety, ensuring that shared builder references cannot interfere with each other. Mutable builders offer better performance but require care when builders might be shared or reused.

Director Pattern

The optional Director component encapsulates common construction sequences. While builders provide the primitives for construction, directors combine these primitives into standard configurations. This separates the mechanics of building from common usage patterns.

class HTTPRequestBuilder
  def initialize
    @method = :get
    @headers = {}
    @body = nil
  end

  def method(http_method)
    @method = http_method
    self
  end

  def header(key, value)
    @headers[key] = value
    self
  end

  def body(content)
    @body = content
    self
  end

  def build
    HTTPRequest.new(@method, @headers, @body)
  end
end

class HTTPRequestDirector
  def initialize(builder)
    @builder = builder
  end

  def json_post(url, data)
    @builder
      .method(:post)
      .header("Content-Type", "application/json")
      .header("Accept", "application/json")
      .body(JSON.generate(data))
      .build
  end

  def authenticated_get(url, token)
    @builder
      .method(:get)
      .header("Authorization", "Bearer #{token}")
      .build
  end
end

Design Considerations

When to Apply the Builder Pattern

The Builder Pattern provides value when object construction involves multiple steps or numerous parameters. Objects with more than four or five constructor parameters become candidates for builders, particularly when many parameters are optional. The pattern excels when construction logic is complex enough to obscure the object's purpose or when multiple representations of constructed objects exist.

Consider builders when telescoping constructors (multiple constructor overloads with different parameter combinations) emerge in the codebase. Telescoping constructors force callers to understand which constructor variant to use and become unmaintainable as parameter combinations grow. Builders eliminate this problem by making each configuration option explicit.

The pattern fits domain-specific languages where object creation reads like natural language. Test fixture builders, query builders, and document builders demonstrate this. Configuration objects that combine many optional settings benefit from builders, as do objects requiring validation logic distributed across multiple properties.

When to Avoid the Builder Pattern

Simple objects with few required parameters and no optional parameters gain little from builders. A user class with only name and email parameters needs no builder - a constructor suffices. The builder adds complexity without corresponding benefit.

Objects with interdependent parameters that must be validated together make poor candidates for step-by-step construction. If parameter A's validity depends on parameter B's value, validating at each step becomes complex. In such cases, a factory method or constructor with parameter validation may work better.

Performance-critical code where object creation happens in tight loops should avoid builders unless profiling demonstrates acceptable overhead. Each builder creates an additional object that must be garbage collected. For simple objects created millions of times per second, direct constructor calls prove more efficient.

Trade-offs Against Alternative Patterns

Builders trade simplicity for flexibility. A simple constructor requires less code and fewer classes but handles optional parameters poorly. Builders require an additional class and more initial development effort but provide superior clarity for complex construction.

Factory patterns and builders address different problems. Factories encapsulate object creation logic and select between different product types based on parameters. Builders configure a single product type through step-by-step construction. A factory might use a builder internally to construct the objects it creates.

Parameter objects bundle related parameters into a single object passed to a constructor. This approach works well for cohesive parameter groups but still requires callers to construct the parameter object. Builders can construct the parameter object itself or the final product.

Static factory methods provide named constructors that clarify intent. A User.from_oauth(oauth_data) factory method explains what it creates better than User.new(oauth_data). Builders provide similar benefits through method naming but support more complex construction sequences.

# Constructor with many parameters
class EmailMessage
  def initialize(to, from, subject, body, cc = nil, bcc = nil, 
                 reply_to = nil, attachments = [], priority = :normal,
                 content_type = "text/plain")
    # ...
  end
end

# Factory method
class EmailMessage
  def self.simple_text(to, from, subject, body)
    new(to, from, subject, body, nil, nil, nil, [], :normal, "text/plain")
  end
end

# Builder approach
class EmailMessageBuilder
  def to(address)
    @to = address
    self
  end

  def from(address)
    @from = address
    self
  end

  def subject(text)
    @subject = text
    self
  end

  def body(text)
    @body = text
    self
  end

  def cc(address)
    @cc = address
    self
  end

  def attach(file)
    (@attachments ||= []) << file
    self
  end

  def priority(level)
    @priority = level
    self
  end

  def build
    EmailMessage.new(@to, @from, @subject, @body, @cc, @bcc,
                     @reply_to, @attachments || [], @priority || :normal,
                     @content_type || "text/plain")
  end
end

Mutability Considerations

Builder mutability affects how builders get shared and reused. Mutable builders maintain state across method calls, making them unsuitable for concurrent use or sharing between construction processes. Once a mutable builder creates an object, resetting it for reuse requires explicit reset logic.

Immutable builders create new builder instances at each step, preventing interference between usages. This safety comes at the cost of creating more objects and potential performance overhead. Immutable builders work naturally with concurrent code since no shared mutable state exists.

The product's mutability relates to but differs from the builder's mutability. A mutable builder can construct immutable products by copying its accumulated state into an immutable object during build. An immutable builder can construct mutable products by passing its state to a mutable object constructor.

Ruby Implementation

Method Chaining with Self Return

Ruby builders rely heavily on returning self from configuration methods to enable chaining. This pattern integrates naturally with Ruby's syntax, creating readable configuration sequences. The final build method breaks the chain by returning the constructed product.

class ConfigurationBuilder
  def initialize
    @config = {}
  end

  def database(connection_string)
    @config[:database] = connection_string
    self
  end

  def cache(type, options = {})
    @config[:cache] = { type: type, options: options }
    self
  end

  def logging(level)
    @config[:logging_level] = level
    self
  end

  def build
    Configuration.new(@config)
  end
end

config = ConfigurationBuilder.new
  .database("postgresql://localhost/myapp")
  .cache(:redis, host: "localhost", port: 6379)
  .logging(:info)
  .build

Tap Method for Inline Configuration

Ruby's tap method provides an alternative to explicit method chaining by yielding the builder to a block. This approach groups related configurations and enables conditional logic during building without breaking the chain.

class ServerBuilder
  attr_reader :host, :port, :ssl_enabled, :ssl_cert

  def initialize
    @host = "localhost"
    @port = 8080
    @ssl_enabled = false
  end

  def host(value)
    @host = value
    self
  end

  def port(value)
    @port = value
    self
  end

  def enable_ssl(cert_path)
    @ssl_enabled = true
    @ssl_cert = cert_path
    self
  end

  def build
    Server.new(@host, @port, @ssl_enabled, @ssl_cert)
  end
end

# Using tap for grouped configuration
server = ServerBuilder.new.tap { |builder|
  builder.host("api.example.com")
  builder.port(443)
  builder.enable_ssl("/path/to/cert.pem") if ENV['PRODUCTION']
}.build

Block-Based Builder DSL

Ruby blocks enable elegant builder DSLs where the builder yields itself to a block, allowing configuration without explicit variable references. This pattern creates highly readable construction code that looks like configuration files.

class RouteBuilder
  def initialize
    @routes = []
  end

  def get(path, &handler)
    @routes << { method: :get, path: path, handler: handler }
    self
  end

  def post(path, &handler)
    @routes << { method: :post, path: path, handler: handler }
    self
  end

  def namespace(prefix, &block)
    nested_builder = RouteBuilder.new
    nested_builder.instance_eval(&block)
    @routes.concat(nested_builder.build.map { |r| 
      r.merge(path: "#{prefix}#{r[:path]}") 
    })
    self
  end

  def build
    @routes
  end

  def self.define(&block)
    builder = new
    builder.instance_eval(&block)
    builder.build
  end
end

# DSL usage
routes = RouteBuilder.define do
  get "/users" do
    # handler code
  end

  post "/users" do
    # handler code
  end

  namespace "/api/v1" do
    get "/posts" do
      # handler code
    end
  end
end

Struct-Based Simple Builders

For simpler cases, Ruby's Struct provides a lightweight builder alternative. Defining a Struct with keyword arguments creates an object with builder-like initialization, though without method chaining.

# Struct as a simple builder
ReportConfig = Struct.new(
  :title,
  :format,
  :include_charts,
  :include_summary,
  :page_size,
  keyword_init: true
) do
  def self.build(**attributes)
    new(
      title: attributes[:title] || "Untitled Report",
      format: attributes[:format] || :pdf,
      include_charts: attributes.fetch(:include_charts, true),
      include_summary: attributes.fetch(:include_summary, true),
      page_size: attributes[:page_size] || :a4
    )
  end
end

config = ReportConfig.build(
  title: "Q4 Results",
  format: :html,
  include_charts: false
)

Keyword Arguments as Builder Alternative

Ruby's keyword arguments provide native support for optional parameters with defaults, reducing the need for builders in simpler cases. This approach works well when construction requires no complex logic and all parameters are independent.

class Report
  def initialize(title:, format: :pdf, include_charts: true, 
                 include_summary: true, page_size: :a4)
    @title = title
    @format = format
    @include_charts = include_charts
    @include_summary = include_summary
    @page_size = page_size
  end
end

# Clear initialization without builder
report = Report.new(
  title: "Sales Report",
  format: :html,
  include_charts: false
)

Validation in Ruby Builders

Ruby builders can perform validation at each configuration step or defer validation until build time. Immediate validation provides faster feedback but complicates builder methods. Deferred validation simplifies configuration methods but delays error detection.

class DatabaseConnectionBuilder
  def initialize
    @config = {}
  end

  def host(value)
    raise ArgumentError, "Host cannot be empty" if value.to_s.strip.empty?
    @config[:host] = value
    self
  end

  def port(value)
    raise ArgumentError, "Port must be between 1 and 65535" unless (1..65535).cover?(value)
    @config[:port] = value
    self
  end

  def database(value)
    @config[:database] = value
    self
  end

  def build
    # Deferred validation of required fields
    raise "Host is required" unless @config[:host]
    raise "Database is required" unless @config[:database]
    
    # Set defaults
    @config[:port] ||= 5432
    
    DatabaseConnection.new(@config)
  end
end

Common Patterns

Classic Builder Pattern

The classic Gang of Four builder separates the builder interface from concrete implementations. An abstract builder defines the construction interface, and concrete builders implement specific variations. A director orchestrates the construction process.

class DocumentBuilder
  def add_title(text); raise NotImplementedError; end
  def add_section(heading, content); raise NotImplementedError; end
  def add_image(path); raise NotImplementedError; end
  def get_result; raise NotImplementedError; end
end

class PDFDocumentBuilder < DocumentBuilder
  def initialize
    @document = PDFDocument.new
  end

  def add_title(text)
    @document.add_element(PDFTitle.new(text, size: 24))
  end

  def add_section(heading, content)
    @document.add_element(PDFHeading.new(heading, level: 2))
    @document.add_element(PDFText.new(content))
  end

  def add_image(path)
    @document.add_element(PDFImage.new(path))
  end

  def get_result
    @document
  end
end

class HTMLDocumentBuilder < DocumentBuilder
  def initialize
    @html = []
  end

  def add_title(text)
    @html << "<h1>#{text}</h1>"
  end

  def add_section(heading, content)
    @html << "<h2>#{heading}</h2>"
    @html << "<p>#{content}</p>"
  end

  def add_image(path)
    @html << "<img src='#{path}' />"
  end

  def get_result
    @html.join("\n")
  end
end

class DocumentDirector
  def initialize(builder)
    @builder = builder
  end

  def construct_report(title, sections, images)
    @builder.add_title(title)
    sections.each { |heading, content| @builder.add_section(heading, content) }
    images.each { |path| @builder.add_image(path) }
    @builder.get_result
  end
end

# Usage
pdf_builder = PDFDocumentBuilder.new
director = DocumentDirector.new(pdf_builder)
pdf = director.construct_report(
  "Annual Report",
  [["Overview", "Company overview..."], ["Financials", "Financial data..."]],
  ["chart1.png", "chart2.png"]
)

Fluent Builder Pattern

Fluent builders emphasize readability through method chaining and natural language method names. This pattern prioritizes the client-side code's appearance over implementation simplicity.

class EmailBuilder
  def initialize
    @to = []
    @cc = []
    @attachments = []
  end

  def to(address)
    @to << address
    self
  end

  def cc(address)
    @cc << address
    self
  end

  def from(address)
    @from = address
    self
  end

  def subject(text)
    @subject = text
    self
  end

  def body(text)
    @body = text
    self
  end

  def attach(filename)
    @attachments << filename
    self
  end

  def with_priority(level)
    @priority = level
    self
  end

  def build
    Email.new(
      to: @to,
      cc: @cc,
      from: @from,
      subject: @subject,
      body: @body,
      attachments: @attachments,
      priority: @priority
    )
  end
end

# Highly readable construction
email = EmailBuilder.new
  .from("sender@example.com")
  .to("recipient@example.com")
  .cc("manager@example.com")
  .subject("Project Update")
  .body("Here is the latest status...")
  .attach("report.pdf")
  .attach("slides.pptx")
  .with_priority(:high)
  .build

Step Builder Pattern

Step builders enforce construction order by returning different builder types at each step. This pattern ensures clients cannot omit required configuration or configure steps in the wrong order. Each step's builder provides only the methods valid for that step.

class PaymentBuilder
  def self.for_amount(amount)
    AmountStep.new(amount)
  end

  class AmountStep
    def initialize(amount)
      @amount = amount
    end

    def from_account(account)
      AccountStep.new(@amount, account)
    end
  end

  class AccountStep
    def initialize(amount, account)
      @amount = amount
      @account = account
    end

    def to_recipient(recipient)
      RecipientStep.new(@amount, @account, recipient)
    end
  end

  class RecipientStep
    def initialize(amount, account, recipient)
      @amount = amount
      @account = account
      @recipient = recipient
    end

    def with_reference(reference)
      @reference = reference
      self
    end

    def execute
      Payment.new(
        amount: @amount,
        account: @account,
        recipient: @recipient,
        reference: @reference
      )
    end
  end
end

# Enforced step order
payment = PaymentBuilder
  .for_amount(1000.00)
  .from_account("123-456-789")
  .to_recipient("recipient@example.com")
  .with_reference("Invoice #1234")
  .execute

Test Data Builder Pattern

Test data builders create objects for testing with sensible defaults and selective overrides. This pattern reduces test setup code and improves test readability by making only relevant attributes explicit.

class UserBuilder
  def initialize
    @attributes = {
      name: "Test User",
      email: "test@example.com",
      role: :user,
      active: true,
      created_at: Time.now
    }
  end

  def with_name(name)
    @attributes[:name] = name
    self
  end

  def with_email(email)
    @attributes[:email] = email
    self
  end

  def as_admin
    @attributes[:role] = :admin
    self
  end

  def inactive
    @attributes[:active] = false
    self
  end

  def created_at(time)
    @attributes[:created_at] = time
    self
  end

  def build
    User.new(@attributes)
  end

  def create
    User.create(@attributes)
  end
end

# Test usage - only specify what matters
describe "User permissions" do
  it "allows admins to delete posts" do
    admin = UserBuilder.new.as_admin.build
    expect(admin.can_delete_posts?).to be true
  end

  it "prevents inactive users from logging in" do
    user = UserBuilder.new.inactive.build
    expect(user.can_login?).to be false
  end
end

Practical Examples

HTTP Request Builder

Building HTTP requests involves numerous optional headers, authentication schemes, body formats, and timeout settings. A builder provides a clean interface for constructing requests without exposing HTTP library details.

class HTTPRequestBuilder
  def initialize(url)
    @url = url
    @method = :get
    @headers = {}
    @query_params = {}
    @body = nil
    @timeout = 30
    @follow_redirects = true
  end

  def method(http_method)
    @method = http_method
    self
  end

  def header(key, value)
    @headers[key] = value
    self
  end

  def headers(hash)
    @headers.merge!(hash)
    self
  end

  def query(key, value)
    @query_params[key] = value
    self
  end

  def json_body(data)
    @body = JSON.generate(data)
    @headers["Content-Type"] = "application/json"
    self
  end

  def form_body(data)
    @body = URI.encode_www_form(data)
    @headers["Content-Type"] = "application/x-www-form-urlencoded"
    self
  end

  def basic_auth(username, password)
    credentials = Base64.strict_encode64("#{username}:#{password}")
    @headers["Authorization"] = "Basic #{credentials}"
    self
  end

  def bearer_token(token)
    @headers["Authorization"] = "Bearer #{token}"
    self
  end

  def timeout(seconds)
    @timeout = seconds
    self
  end

  def no_redirects
    @follow_redirects = false
    self
  end

  def build
    HTTPRequest.new(
      url: @url,
      method: @method,
      headers: @headers,
      query_params: @query_params,
      body: @body,
      timeout: @timeout,
      follow_redirects: @follow_redirects
    )
  end
end

# Building various request types
get_request = HTTPRequestBuilder.new("https://api.example.com/users")
  .query("page", 1)
  .query("per_page", 50)
  .bearer_token("abc123")
  .timeout(10)
  .build

post_request = HTTPRequestBuilder.new("https://api.example.com/users")
  .method(:post)
  .json_body({ name: "John Doe", email: "john@example.com" })
  .bearer_token("abc123")
  .build

SQL Query Builder

Query builders abstract SQL construction, providing type safety and preventing SQL injection. The builder validates query components and generates properly escaped SQL.

class SQLQueryBuilder
  def initialize(table)
    @table = table
    @select_columns = ["*"]
    @joins = []
    @conditions = []
    @group_by = []
    @having = []
    @order_by = []
    @limit_value = nil
    @offset_value = nil
  end

  def select(*columns)
    @select_columns = columns
    self
  end

  def join(table, on_clause)
    @joins << "JOIN #{table} ON #{on_clause}"
    self
  end

  def left_join(table, on_clause)
    @joins << "LEFT JOIN #{table} ON #{on_clause}"
    self
  end

  def where(condition, *params)
    @conditions << [condition, params]
    self
  end

  def group_by(*columns)
    @group_by.concat(columns)
    self
  end

  def having(condition)
    @having << condition
    self
  end

  def order_by(column, direction = :asc)
    @order_by << "#{column} #{direction.to_s.upcase}"
    self
  end

  def limit(count)
    @limit_value = count
    self
  end

  def offset(count)
    @offset_value = count
    self
  end

  def build
    query = "SELECT #{@select_columns.join(', ')} FROM #{@table}"
    query += " #{@joins.join(' ')}" unless @joins.empty?
    
    unless @conditions.empty?
      where_clauses = @conditions.map { |cond, _| cond }
      query += " WHERE #{where_clauses.join(' AND ')}"
    end
    
    query += " GROUP BY #{@group_by.join(', ')}" unless @group_by.empty?
    query += " HAVING #{@having.join(' AND ')}" unless @having.empty?
    query += " ORDER BY #{@order_by.join(', ')}" unless @order_by.empty?
    query += " LIMIT #{@limit_value}" if @limit_value
    query += " OFFSET #{@offset_value}" if @offset_value
    
    # Return query and collected parameters
    params = @conditions.flat_map { |_, p| p }
    [query, params]
  end
end

# Complex query construction
query, params = SQLQueryBuilder.new("orders")
  .select("orders.id", "orders.total", "users.name")
  .join("users", "users.id = orders.user_id")
  .where("orders.status = ?", "completed")
  .where("orders.created_at > ?", Date.today - 30)
  .group_by("users.id")
  .having("SUM(orders.total) > 1000")
  .order_by("orders.created_at", :desc)
  .limit(20)
  .build

Configuration Builder

Application configuration involves many optional settings with sensible defaults. A builder provides a clear API for configuration while handling validation and default values.

class AppConfigBuilder
  def initialize
    @config = {
      database: {},
      cache: {},
      logging: {},
      features: {}
    }
  end

  def database_url(url)
    @config[:database][:url] = url
    self
  end

  def database_pool_size(size)
    @config[:database][:pool_size] = size
    self
  end

  def redis_cache(host: "localhost", port: 6379, db: 0)
    @config[:cache] = {
      type: :redis,
      host: host,
      port: port,
      db: db
    }
    self
  end

  def memory_cache(max_size: 1000)
    @config[:cache] = {
      type: :memory,
      max_size: max_size
    }
    self
  end

  def log_level(level)
    @config[:logging][:level] = level
    self
  end

  def log_to_file(path)
    @config[:logging][:file] = path
    self
  end

  def enable_feature(name)
    @config[:features][name] = true
    self
  end

  def disable_feature(name)
    @config[:features][name] = false
    self
  end

  def build
    # Apply defaults
    @config[:database][:pool_size] ||= 5
    @config[:logging][:level] ||= :info
    @config[:cache][:type] ||= :memory
    
    # Validate
    raise "Database URL is required" unless @config[:database][:url]
    
    AppConfig.new(@config)
  end
end

# Environment-specific configuration
config = AppConfigBuilder.new
  .database_url(ENV['DATABASE_URL'])
  .database_pool_size(20)
  .redis_cache(host: ENV['REDIS_HOST'], port: 6379)
  .log_level(:debug)
  .log_to_file("log/app.log")
  .enable_feature(:new_dashboard)
  .enable_feature(:api_v2)
  .build

Reference

Builder Method Classification

Method Type Purpose Return Value Example
Configuration Sets builder state Builder (self) with_title, add_header
Validation Checks configured state Builder or raises validate_email
Transformation Modifies accumulated state Builder (self) normalize, sanitize
Terminal Creates final product Product object build, create, execute
Reset Clears builder state Builder (self) reset, clear

Pattern Comparison

Pattern Purpose Required Fields Validation Complexity
Classic Builder Multiple representations Interface defines At each step High
Fluent Builder Readability Optional Deferred Medium
Step Builder Enforced order All required Step transitions High
Test Builder Test data All optional Minimal Low
DSL Builder Domain language Context dependent Flexible Medium

When to Use Builder Pattern

Scenario Use Builder Alternative
5+ parameters Yes Constructor still manageable
Many optional params Yes Keyword arguments
Complex construction Yes Factory method
Multiple representations Yes Abstract factory
Construction order matters Yes with steps Builder unnecessary
Immutable products Yes Constructor with validation
DSL requirements Yes No better alternative
Simple object No Direct constructor

Common Builder Idioms

Idiom Implementation Use Case
Method chaining Return self Fluent interface
Block configuration Yield self to block Grouped settings
Validation methods Check before setting Early error detection
Default values Set in build method Optional parameters
Named builders Class methods Common configurations
Immutable products Copy state in build Thread-safe products
Builder reset Clear internal state Builder reuse
Nested builders Builders for components Complex hierarchies