访问 ruby 个对象内对象的属性

Access attributes on objects inside ruby objects

我正在构建一个 LineItemGenerator 对象,其目的是在给定所需属性的情况下生成属性值数组。

问题是给定的对象有对象作为属性。所以给出的 "attributes" 实际上是 "nested attributes".

目标是从 item 访问请求的嵌套属性,在本例中是 item.nameitem.style.name 通过创建一些输入数据结构并使用一些算法。

目前,我将 "nested attributes" 输入数据结构表示为数组数组,nested_attributes

我执行繁重工作的算法称为 #generate

它采用原始 itemnested_attributes。接下来,它映射 nested_attributes,通过在每次迭代中将消息 send 发送到原始 item,将每个 nested_attribute 减少为 "attribute"。

class Style
  attr_reader :name
  def initialize name:
    @name = name
  end
end

class Item
  attr_reader :name, :style
  def initialize name:, style:
    @name = name
    @style = style
  end
end

class LineItemGenerator
  def generate item:, nested_attributes:
    nested_attributes.map do |nested_attribute|
      nested_attribute.reduce(item) do |obj, attribute| # <-- algorithm using #reduce to burrow in
        obj.send(attribute)
      end
    end
  end
end


require 'minitest/autorun'

class SomeTest < Minitest::Test
  def test_it_returns_the_right_line_item
    style = Style.new name: 'cool'
    item = Item.new name: 'pants', style: style

    # input data structure is array or arrays
    nested_attributes = [[:name], [:style, :name]]
    input = { item: item, nested_attributes: nested_attributes}
    output = LineItemGenerator.new.generate input
    assert_equal ['pants', 'cool'], output
  end
end

我对实现更具声明性和表现力的输入数据结构和算法的新方法感到好奇。上面的评论中提到了两个感兴趣的部分。

使用 #inject 感觉很奇怪,因为我实际上只是想将可变数量的 send 调用链接在一起。例如:

item = Item.new name: 'pants', style: Style.new(name: 'cool')
p item.send(:style).send(:name) #=> "cool"

在这种情况下,是否有一些 Enumerable 方法是更好的选择?我的输入数据结构有更好的选择吗?

这对我来说更像是一个软件设计问题,所以这就是我从设计角度处理它的方式。

  • 一次从一个组件的角度推理。
  • 将我们试图传达的内容与其实施方式分开

The LineItemGenerator's job is to generate an array of attribute values for an item given the desired attributes.

基于此,LineItemGenerator

  • 获取具有属性的项目
  • 实施 generate_attribute_values 给定所需属性列表

这可能看起来像:

LineItemGenerator.new(@item).generate_attribute_values(:name, :style)

我会删除 generate,因为它在这里似乎不是正确的词。我们只是检索和过滤现有值,而不是创建新的属性值对象。

LineItemGenerator.new(@item).attribute_values(:name, :style)

在这一点上,我考虑 Item 应该向我们的 LineItemGenerator 公开什么。

  • 项目有属性
  • 属性有值,这意味着它们也应该有名称。

鉴于这种理解,我可以将 LineItemGenerator 实现为:

class LineItemGenerator
  def initialize(item)
    @item = item
  end

  def attribute_values(*attribute_names)
    @item.attributes.select { |attribute| attribute_names.include?(attribute.name) }.map(&:value)
  end
end

此时,有两个合约需要完成:

  1. 项目需要实施#attributes
  2. item.attributes 需要 return 一组响应 #name#value
  3. 的对象

现在,让我们从一个项目的角度来思考。 - 一个项目有很多属性(例如名称和样式)。 - 相关的属性值可以在Item 对象上定义或委托给其他对象。

合同 1 很容易实现:

class Item
  attr_reader :attributes
end

合同 2 更灵活一些,因为它可以在项目或单个属性 classes 上实现。如果某个属性不是应用程序中的第一个 class 关注点,我会在 Item 上实施它。

class Item
  attr_reader :attributes

  Attribute = Struct.new(:name, :value)

  def initialize(name:, style:)
    @attributes = [
      Attribute.new(name: :name, value: name),
      Attribute.new(name: :style, value: style) 
    ]
  end
end

如果系统的某些其他部分需要与属性交互作为第一个 class 问题:

# TODO: DRY up using inheritance or modules
class Style
  attr_reader :value
  def initialize value:
    @value = value
  end

  def name
    :style
  end
end

class ItemName
  attr_reader :value
  def initialize value:
    @value = value
  end

  def name
    :name
  end
end

class Item
  attr_reader :name, :style, :attributes
  def initialize item_name:, style:
    @name = item_name
    @style = style

    @attributes = [@name, @style]
  end
end