跳转到内容

Ruby on Rails/ActiveRecord/聚合

来自维基教科书,开放书籍,开放世界

Active Record 通过类似宏的类方法实现聚合,称为composed_of用于将属性表示为值对象。它表达了诸如“帐户[由]金钱[以及其他东西]组成”或“人[由][一个]地址组成”之类的关系。每次调用宏都会添加对如何从实体对象的属性创建值对象(当实体作为新对象或从查找现有对象初始化时)以及如何将其转换回属性(当实体保存到数据库时)的描述。

例子

  class Customer < ActiveRecord::Base
    composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
    composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
  end

客户类现在具有以下方法来操作值对象

  * Customer#balance, Customer#balance=(money)
  * Customer#address, Customer#address=(address)

这些方法将使用下面描述的值对象进行操作

 class Money
   include Comparable
   attr_reader :amount, :currency
   EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
 
   def initialize(amount, currency = "USD")
     @amount, @currency = amount, currency
   end
 
   def exchange_to(other_currency)
     exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
     Money.new(exchanged_amount, other_currency)
   end
 
   def ==(other_money)
     amount == other_money.amount && currency == other_money.currency
   end
 
   def <=>(other_money)
     if currency == other_money.currency
       amount <=> amount
     else
       amount <=> other_money.exchange_to(currency).amount
     end
   end
 end

 class Address
   attr_reader :street, :city
   def initialize(street, city)
     @street, @city = street, city
   end
 
   def close_to?(other_address)
     city == other_address.city
   end
 
   def ==(other_address)
     city == other_address.city && street == other_address.street
   end
 end

现在可以通过值对象而不是属性访问数据库中的属性。如果您选择将组合命名为与属性名称相同,这将是访问该属性的唯一方法。这适用于我们的余额属性。您可以像对待任何其他属性一样与值对象进行交互,尽管

 customer.balance = Money.new(20)     # sets the Money value object and the attribute
 customer.balance                     # => Money value object
 customer.balance.exchanged_to("DKK") # => Money.new(120, "DKK")
 customer.balance > Money.new(10)     # => true
 customer.balance == Money.new(20)    # => true
 customer.balance < Money.new(5)      # => false

值对象也可以由多个属性组成,例如地址的情况。映射的顺序将决定参数的顺序。例子

 customer.address_street = "Hyancintvej"
 customer.address_city   = "Copenhagen"
 customer.address        # => Address.new("Hyancintvej", "Copenhagen")
 customer.address = Address.new("May Street", "Chicago")
 customer.address_street # => "May Street"
 customer.address_city   # => "Chicago"

编写值对象

[编辑 | 编辑源代码]

值对象是不可变的,可以互换的对象,它们表示给定的值,例如表示 5 美元的 Money 对象。两个都表示 5 美元的 Money 对象应该相等(通过方法,例如 == 和 <=> 来自 Comparable 如果排名有意义)。这与实体对象不同,实体对象的相等性由标识决定。例如 Customer 这样的实体类可以很容易地拥有两个不同的对象,它们都具有在 Hyancintvej 上的地址。实体标识由对象或关系唯一标识符(例如主键)决定。正常ActiveRecord::Base类是实体对象。

将值对象视为不可变也很重要。不要允许 Money 对象在创建后更改其金额。创建一个具有新值的新的货币对象。Money#exchanged_to 方法就是一个例子,该方法返回一个新的值对象,而不是更改其自己的值。Active Record 不会持久化通过除编写器方法以外的其他方式更改的值对象。

Active Record 通过冻结任何分配为值对象的来强制执行不可变要求。之后尝试更改它会导致 TypeError。

华夏公益教科书