Menu
CoddyTech

构建器模式

本课属于 Coddy Ruby 学习旅程的“面向对象编程”单元。第 48 课,共 57 课。

构建器模式会逐步创建复杂对象。每一步都是一个方法,用于保存一个选择并返回构建器,因此这些步骤可以链式调用;build 会检查这些选择,并返回创建完成的对象:

class Pizza
  def initialize(size, toppings)
    @size = size
    @toppings = toppings
  end

  def to_s
    "#{@size} pizza with #{@toppings.join(", ")}"
  end
end

class PizzaBuilder
  def initialize
    @toppings = []
  end

  def size(value)
    @size = value
    self
  end

  def topping(name)
    @toppings << name
    self
  end

  def build
    raise ArgumentError, "size is required" if @size.nil?

    Pizza.new(@size, @toppings)
  end
end

puts PizzaBuilder.new.size("large").topping("cheese").topping("ham").build

输出:

large pizza with cheese, ham

由于构建器只会在 build 中创建对象,因此它可以一次性检查整个组合,例如是否包含必需部分,或是否同时选择了两个不能搭配的选项:

class TicketBuilder
  def seat(value)
    @seat = value
    self
  end

  def standing
    @standing = true
    self
  end

  def build
    raise ArgumentError, "seat and standing exclude each other" if @seat && @standing

    @standing ? "standing ticket" : "ticket for seat #{@seat || "any"}"
  end
end

puts TicketBuilder.new.seat("B7").build
puts TicketBuilder.new.standing.build
begin
  TicketBuilder.new.seat("A1").standing.build
rescue ArgumentError => e
  puts e.message
end

输出:

ticket for seat B7
standing ticket
seat and standing exclude each other

一种常见的 Ruby 写法是将构建器交给代码块。类方法会创建构建器,将它传入代码块,并在最后调用 build:

class Email
  attr_accessor :to, :subject, :lines

  def self.compose
    email = new
    email.lines = []
    yield email
    email
  end

  def to_s
    "To: #{to}\nSubject: #{subject}\n\n#{lines.join("\n")}"
  end
end

mail = Email.compose do |e|
  e.to = "ada@x.io"
  e.subject = "Hi"
  e.lines << "Hello Ada,"
  e.lines << "see you soon."
end
puts mail

输出:

To: ada@x.io
Subject: Hi

Hello Ada,
see you soon.

对于少量可选值,Ruby 的带默认值关键字参数通常就够用了。如果某些部分可以重复、顺序很重要,或者需要在对象创建前检查组合是否有效,那么构建器就很有用:

Pizza.new(size: "large", crust: "thin")          # 选项不多:关键字

Query.new.from("users").where("age > 18").where("active = 1").limit(10).to_s
challenge icon

挑战

简单

完成 PizzaBuilder 和 Pizza。构建器的步骤 size(value)、crust(value) 和可重复调用的 topping(name) 会返回构建器。没有指定尺寸时,build 会抛出带有 size is required 的 ArgumentError;如果没有选择饼皮,则使用 classic,并返回一个 Pizza。Pizza#to_s 会返回 large thin pizza with cheese, ham 或 small classic pizza with no toppings。

每行输入代表一个订单,例如 size=large crust=thin topping=cheese topping=ham。提供的代码会按顺序将每个部分传给构建器,并通过链式调用完成,然后输出披萨或错误消息。

你的代码写在 pizza.rb 和 pizza_builder.rb 中。main.rb 包含提供的输入和输出代码,不能修改。

亲手试试

require_relative 'pizza'
require_relative 'pizza_builder'

# 提供的输入/输出代码:保持原样
input = []
while (line = gets)
  input << line.chomp
end
input.each do |line|
  builder = line.split.reduce(PizzaBuilder.new) do |b, part|
    step, value = part.split("=")
    b.public_send(step, value)
  end
  begin
    puts builder.build
  rescue ArgumentError => e
    puts e.message
  end
end
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Ruby 在线编译器