在不同对象之间传递过程

Passing around procs between different objects

我正在努力完成以下工作,但我显然遗漏了一些东西:

class Person
  def fetch
    puts 'Fetch it boy!'
  end

  def action(data)
    data.call
  end
end

class Animal
  def play
    Person.new.action(proc { fetch })
  end
end

Animal.new.play  # undefined local variable or method `fetch' for #<Animal:0x0000000001b172d0>

我的目标是让 Person 执行 Animal 选择的方法。我也尝试过使用 instance_exec 但无济于事。

现在我知道我可以使用 eval 并将方法名称作为字符串传递,但我想避免这种情况。 有什么想法吗?

提前致谢

免责声明(各种):
请不要提出代码重组或重写。这个问题的目的是为了更好地理解块在对象之间传递时的行为。

我认为组织不正确。

即使采用这种解决方案,动物仍在控制人,而我们通常假设人将启动获取命令。

class Person

  def initialize(name = 'Happy Pet Owner', pet = Animal.new)
    @name = name
    @pet = pet
  end

  def fetch
    'Fetch it boy!'
  end

  def action(&block)
    print "#{@name} says: ", fetch
    block.call
  end

end

class Animal

  def play
    Person.new.action { puts " Fido chases ball." }
  end

end

Animal.new.play

下面是让事情变得简单的选项:

class Animal
  def play
    Person.new.fetch
  end
end

如果你真的需要传递一个方法名作为值,最自然的可能是使用符号(不是字符串):

class Person
  def fetch
    puts 'fetched'
  end

  def action(a)
    self.send(a)
  end
end

class Animal
  def play
    Person.new.action(:fetch)
  end
end

Animal.new.play

或者您也可以这样做:

class Person
  def fetch
    puts 'fetched'
  end

  def action(a)
    a.bind(self).call
  end
end

class Animal
  def play
    fetch = Person.instance_method(:fetch)
    Person.new.action(fetch)
  end
end

TL;DR

首先,您最初发布的代码没有定义对象之间的正确关系或协作,这是问题的一部分。其次,需要捕获和转发块的方式通常是相当不直观的,因此使用块而不是简单的依赖注入(例如初始化一个新的 Person 与特定的 Animal 进行协作)或消息传递背后的推理使得这比它需要的要难一些。简单通常更好,并且通常更容易在以后调试和扩展。

也就是说,以下重新设计在语义上更清晰,而且还展示了如何在协作者之间将块作为 Proc 对象转发。 Person 现在接受一个可选块,并且当存在时将该块作为 Proc 对象传递给 Animal 上可以调用它的方法。

您的代码已重新设计

考虑以下重新设计,它努力将正确的行为附加到正确的对象上:

class Person
  def speaks_to_animal species, phrase, &reaction
    animal = Animal.new species
    animal.reacts_to phrase, &reaction
  end
end

class Animal
  attr_reader :species

  def initialize species
    @species = species.to_s
  end

  def reacts_to phrase
    case species
    when 'lion'; pp "The #{species} eats you."
    when 'dog'
      block_given? ? yield : pp("The #{species} barks at you.")
    else pp "The #{species} doesn't know what to do."
    end
  end
end

特别是,这里的目标是重新设计代码,使 Person#speaks_to_animal 而 Animal#reacts_to_phrase 由 Person 说出。这会保留其所属的行为,因为 Person 不需要 #fetch 并且 Animal 不需要知道任何有关 Person 对象内部的信息即可进行协作。

作为副产品,这种重新设计提供了更大的灵活性。您的块现在是可选的,当它们被传递给 Person 时,它们会被 Animal 转发和调用,这似乎是您原始代码的意图。 您现在通过一个人与一个动物互动,并且那个人可以与您选择指定的任何类型的动物物种交谈,而无需子类化动物或硬编码反应。例如:

person = Person.new
person.speaks_to_animal :dog,  "Fetch, boy!"
person.speaks_to_animal(:dog,  "Fetch, boy!") do
  pp "The dog brings the stick back to you."
end
person.speaks_to_animal :lion, "Fetch, boy!"

如果你不把方块传给,那狗就不知道怎么办了,只会朝你狂吠。如果您将行为期望作为一个块传递,该块将被转发到 animal#reacts_to 并通过 yield 调用它。当然,如果你让狮子来玩接球,那就要出大事了。

重新包装对象之间的行为和关系允许您做各种各样的事情,例如关闭 person 所说的短语的元素以启用更复杂的响应,或者允许animal 关闭短语的元素以根据其物种做出不同的响应。不过,大多数情况下,这段新代码解决了如何在不将对象耦合得太紧密的情况下传递表示动物反应的可选块的问题。

在语义上,一个人 应该 知道他们在和什么动物说话,以及他们希望动物做出什么回应。传递一个方块是否真的是代表人的期望或动物反应的最佳方式更有争议,我个人会选择更多地关注基于物种phrase 而不是传递 Proc 对象。您的里程可能会有所不同。

您正在寻找:

instance_eval(&data)

object.instance_eval 评估块,但用 object:

whoami = proc { self }

whoami.call => main
1.instance_eval(&whoami) => 1 

但是请注意,instance_eval 还将对象作为参数传递给块,如果您想传递 lambda,这可能会有问题:

whoami = lambda { self }
1.instance_eval(&whoami) #=> ArgumentError (wrong number of arguments (given 1, expected 0))

还有一个类似的方法:instance_exec。不仅它没有将 self 作为参数传递:

whoami = lambda { self }
1.instance_exec(&whoami) #=> 1

但它还允许您传递其他参数:

add_number = lambda { |number| self + number }
1.instance_exec(3, &add_number) #=> 4

当然,这两种方法都需要格外小心且非常谨慎地使用 - 当您想以声明方式声明 class-wide 挂钩以在每个实例上执行时,它们非常有用。它们不应该用作两个对象之间交互的手段,除非你真的知道你在做什么并且可以证明它不验证封装。