Статьи

Объекты значения, объясненные с помощью Ruby

val_obj В этой статье объясняется понятие объектов стоимости. Сначала он определяет и демонстрирует различные виды ценностных объектов, затем объясняет правило конструирования действительных объектов, в то же время освещая последствия нарушения концепции. Наконец, он показывает несколько способов реализации объектов-значений в Ruby.

Хотя примеры написаны на Ruby, эта концепция может быть легко применена и к другим языкам.

Что такое объект значения?

Объект значения, как определено в P EAA :

… Их понятие равенства не основано на идентичности, вместо этого два объекта значения равны, если все их поля равны.

Это означает, что объекты значений, которые имеют одинаковые внутренние поля, должны быть равны друг другу. Значение всех полей в достаточной степени определяет равенство объекта значения.

Простейшими примерами являются примитивные объекты — SymbolStringIntegerTrueClasstrueFalseClassfalseNilClassnilRangeRegexp Например, всякий раз , когда в программе появляется 1.01: 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 Вот пример для реализации тех же объектов MoneyStruct

 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>

Чтобы сохранить краткость StructValueStruct Вот пример с демонстрационной страницы библиотеки:

 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

Предоставляет ли объяснение объектов-значений вдохновение для написания более элегантного кода? Что вы думаете об использовании объектов стоимости?

  1. «Равный» используется в значении равенства ( ==eql?equal? ↩
  2. http://en.wikipedia.org/wiki/Value_object ↩

Ссылки

Значение объекта в Википедии.

Некоторое обсуждение ценностных объектов на c2.com.