1

我正在尝试进行以下工作,但我显然遗漏了一些东西:

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并将方法名称作为字符串传递,但我想避免这种情况。有任何想法吗?

提前致谢

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

4

4 回答 4

3

TL;博士

首先,您最初发布的代码没有定义对象之间的正确关系或协作,这是问题的一部分。其次,需要捕获和转发块的方式通常是相当不直观的,因此使用块而不是简单的依赖注入(例如,使用特定的 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 对象。你的旅费可能会改变。

于 2021-11-14T10:00:24.327 回答
2

您正在寻找:

instance_eval(&data)

object.instance_eval评估块,但self在该块内(通常self是创建上下文块的)替换为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

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

于 2021-11-14T10:24:38.937 回答
1

这是使事情变得简单的选项:

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
于 2021-11-14T09:26:30.510 回答
0

在我看来,这个组织是不正确的。

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

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
于 2021-11-14T07:51:20.460 回答