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。