В этой статье объясняется понятие объектов стоимости. Сначала он определяет и демонстрирует различные виды ценностных объектов, затем объясняет правило конструирования действительных объектов, в то же время освещая последствия нарушения концепции. Наконец, он показывает несколько способов реализации объектов-значений в Ruby.
Хотя примеры написаны на Ruby, эта концепция может быть легко применена и к другим языкам.
Что такое объект значения?
Объект значения, как определено в P EAA :
… Их понятие равенства не основано на идентичности, вместо этого два объекта значения равны, если все их поля равны.
Это означает, что объекты значений, которые имеют одинаковые внутренние поля, должны быть равны друг другу. Значение всех полей в достаточной степени определяет равенство объекта значения.
Простейшими примерами являются примитивные объекты — Symbol
String
Integer
TrueClass
true
FalseClass
false
NilClass
nil
Range
Regexp
Например, всякий раз , когда в программе появляется 1.0
1: 1.0
var1 = :symbol
var2 = :symbol
var1 == var2 # => true
var1 = 'string'
var2 = 'string'
var1 == var2 # => true
var1 = 1.0
var2 = 1.0
var1 == var2 # => true
var1 = true
var2 = true
var1 == var2 # => true
var1 = nil
var2 = nil
var1 == var2 # => true
var1 = /reg/
var2 = /reg/
var1 == var2 # => true
var1 = 1..2
var2 = 1..2
var1 == var2 # => true
var1 == [1, 2, 3]
var2 == [1, 2, 3]
var1 == var2 # => true
var1 == { key: 'value'}
var2 == { key: 'value'}
var1 == var2 # => true
Это примеры объектов-значений с одним полем.
Значимые объекты также могут состоять из нескольких полей. Например, класс IPAddr
в стандартной библиотеке имеет три поля: @addr
@mask_addr
@family
@addr
@mask_addr
@family
Объекты IPAddr
require 'ipaddr'
ipaddr1 = IPAddr.new "192.168.2.0/24"
ipaddr2 = IPAddr.new "192.168.2.0/255.255.255.0"
ipaddr1.inspect
# => "#<IPAddr: IPv4:192.168.2.0/255.255.255.0>"
ipaddr2.inspect
#=> "#<IPAddr: IPv4:192.168.2.0/255.255.255.0>"
ipaddr1 == ipaddr2 # => true
Точно так же деньги, данные GPS, данные отслеживания, диапазон дат и т. Д. Являются подходящими кандидатами для ценностных объектов.
Приведенные выше примеры демонстрируют определение объектов-значений — объектов, равенство которых основано на их внутренних полях, а не на их идентичности.
Чтобы гарантировать, что объекты значений с одинаковыми полями будут равны друг другу всякий раз, когда он появляется в программе, существует неявное правило, которому необходимо следовать при построении объектов значений.
Правило для построения объектов стоимости
Правило, гарантирующее равенство объектов-значений во всем их жизненном цикле: атрибуты объекта-значения останутся неизменными с момента создания экземпляра до последнего состояния его существования. «… Это требуется для неявного контракта, чтобы два объекта значения, созданные равными, оставались равными». 2 Следуя этому правилу, объекты значения должны иметь неизменный интерфейс.
Иногда необходимость создания вариантов объектов-значений может нарушить правило, если они созданы без тщательной реализации. Возьмите следующий класс Money
class Money
attr_accessor :currency, :amount
def initialize(amount, currency)
@amount = amount
@currency = currency
end
end
usd = Money.new(10, 'USD')
# <Money:0x007f987f283b50 @amount=10, @currency="USD">
usd.amount = 20
usd.inspect
# <Money:0x007f987f283b50 @amount=20, @currency="USD">
Объект значения usd
@amount
Открытый метод метода amount=
Правильный подход к созданию вариантов объектов-значений заключается в реализации метода установщика для инициализации нового объекта-значения вместо изменения текущего:
class Money
# remove the public setter interface
attr_reader :currency, :amount
def initialize(amount, currency)
@amount = amount
@currency = currency
end
# a setter method to return a new value object
def amount=(other_amount)
Money.new(other_amount, currency)
end
end
usd = Money.new(10, 'USD')
usd.inspect
# <Money:0x007f9672753ba8 @amount=10, @currency="USD">
other_usd = (usd.amount = 20)
usd.inspect
# <Money:0x007f9672753ba8 @amount=20, @currency="USD">
Таким образом, после создания объекта « Money
Новые варианты создаются как различные объекты-значения вместо внесения изменений в исходный.
Как реализовать объект значения в Ruby
В заключение, чтобы реализовать объект значения, следуя приведенным выше определениям и правилам:
- Объекты значений имеют несколько атрибутов
- Атрибуты должны быть неизменными на протяжении всего жизненного цикла
- Равенство определяется его атрибутами (и его типом)
Мы уже видели реализацию объектов Money
Давайте завершим реализацию, добавив методы определения равенства.
class Money
def ==(other_money)
self.class == other_money.class &&
amount == other_money.amount &&
currency == other_money.currency
end
alias :eql? :==
def hash
[@amount, @currency].hash
end
end
usd = Money.new(10, 'USD')
usd2 = Money.new(10, 'USD')
usd == usd2 # => true
eql?
и ==
По определению объектов-значений проверяются результаты сравнения всех полей. Также необходимо отличать Money
Например:
AnotherMoney = Struct.new(:money, :currency)
other_usd = AnotherMoney.new(10, 'USD')
usd == other_usd # => false
Это достигается строкой self.class == other_money.class
Метод hash
Из документов Ruby: «… эта функция должна иметь свойство, которое a.eql?(b)
a.hash == b.hash
Помимо обычного синтаксиса класса Struct
Вот пример для реализации тех же объектов Money
Struct
class Money < Struct.new(:amount, :currency)
def amount=(other_amount)
Money.new(other_amount, currency)
end
end
usd = Money.new(10, 'USD')
usd2 = Money.new(10, 'USD')
usd.hash == usd2.hash # => true
usd == usd2 # => true
Это гораздо более кратко, чем обычное определение класса. Атрибуты объявляются через интерфейс Struct.new
Определение методов hash
==
Struct
Однако одним из недостатков использования Struct
usd = Money.new(10, 'USD')
usd.amount = 20
usd.inspect
# => <struct Money amount=10, currency="USD">
invalid_usd = Money.new(1)
invalid_usd.inspect
# => <struct Money amount=1, currency=nil>
Чтобы сохранить краткость Struct
Value
Struct
Вот пример с демонстрационной страницы библиотеки:
Point = Value.new(😡, :y)
Point.new(1)
# => ArgumentError: wrong number of arguments, 1 for 2
# from /Users/tcrayford/Projects/ruby/values/lib/values.rb:7:in `block (2 levels) in new
# from (irb):5:in new
# from (irb):5
# from /usr/local/bin/irb:12:in `<main>
p = Point.new(1, 2)
p.x = 1
# => NoMethodError: undefined method x= for #<Point:0x00000100943788 @x=0, @y=1>
# from (irb):6
# from /usr/local/bin/irb:12:in <main>
Теперь работа с объектами-значениями в Ruby должна быть простой и увлекательной.
Вывод
Начиная с определения объектов-значений, в этой статье показано использование объектов-значений от примитивного объекта до более сложных, зависящих от домена объектов. Он также погружается в неявное правило согласованного поведения объектов-значений в течение своего жизненного цикла.
Наконец, мы прошли через несколько различных способов реализации концепции объектов значений, используя обычное определение класса Ruby и класс Struct
Наконец, мы получили полезный гем Values
Предоставляет ли объяснение объектов-значений вдохновение для написания более элегантного кода? Что вы думаете об использовании объектов стоимости?
Ссылки
Значение объекта в Википедии.
Некоторое обсуждение ценностных объектов на c2.com.