TL;DR
首先,您最初发布的代码没有定义对象之间的正确关系或协作,这是问题的一部分。其次,需要捕获和转发块的方式通常是相当不直观的,因此使用块而不是简单的依赖注入(例如,使用特定的 Animal 初始化一个新的 Person 以与之协作)或消息传递背后的推理使得这比它需要的要难一些。简单通常更好,而且通常更容易在以后进行调试和扩展。
也就是说,下面的重新设计在语义上更加清晰,而且还展示了如何在协作者之间将块作为 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 与 Animal 交互,并且该 Person 可以与您选择指定的任何类型的 Animal 物种对话,而无需继承 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 键关闭短语的元素,以根据其种类做出不同的反应。不过,大多数情况下,这个新代码解决了如何传递一个表示动物反应的可选块而不将对象耦合得太紧的问题。
从语义上讲,一个人应该知道他们正在与什么动物交谈,以及他们希望动物会做什么回应。传递一个块是否真的是代表人的期望或动物的反应的最佳方式是更有争议的,我个人会选择更多地关注基于物种和短语 而不是传递 Proc 对象。您的里程可能会有所不同。