【问题标题】:Passing around procs between different objects在不同对象之间传递过程
【发布时间】:2021-12-25 20:40:14
【问题描述】:

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

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

提前致谢

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

【问题讨论】:

  • instance_eval(&amp;data)替换data.call
  • 正是我正在寻找/缺少的东西。如果你把它变成一个答案,我会确保它是被接受的。谢谢!
  • Proc 是一个闭包,所以#fetch 不是 Person#fetch,这就是为什么你会从 Animal 中得到异常。你可以解决这个问题,但不要忽视问题开始与闭包定义自我和封装绑定的方式的概念,这使得它们在不同情况下既有用又麻烦。跨度>

标签: ruby proc instance-eval


【解决方案1】:

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 对象。您的里程可能会有所不同。

【讨论】:

  • 感谢您调查我的问题。您的回答非常彻底,但它略微忽略了一点,即块作为参数传递时的行为方式。我编辑了我的问题,添加了免责声明以更好地传达我的意图,但在您发布答案后我这样做了。对此表示歉意。 FWIW 在现实世界的场景中,我会采用您建议的解决方案。
【解决方案2】:

您正在寻找:

instance_eval(&data)

object.instance_eval 评估块,但用object 替换该块内的self(通常是创建上下文块的self):

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

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

【讨论】:

    【解决方案3】:

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

    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
    

    【讨论】:

    • 谢谢?!我想到的是@broisatse 的解决方案(这实际上是您提出的第三个解决方案)。但是,我最终选择了您提出的第二个选项作为最实用的选项(符号选项)。
    【解决方案4】:

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

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

    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
    

    【讨论】:

    • 谢谢,但您在这里错过了重点。我不打算重新组织代码。我只是在寻找一种方法来传递未在上下文 A (Animal) 中定义但在上下文 B (Person) 中定义的方法。
    • 不,我意识到重点不是重组,问题中甚至没有提到这一点。我的陈述是,它可能没有正确组织。 (还有其他答案也反映了这一观察。)您的目标已明确说明,如果阅读了问题,几乎不可能错过。充其量,我在技术上没有回答这个问题,解决了一个(对我来说)更明显的问题,同时提供了一个答案来传递一个不需要知道另一个对象的块。如果我们使该方法成为对象,我们可以传递一个方法。请参阅Object#method 文档。
    • “我只是在寻找一种方法来传递一种方法”这不是问题,也不是编辑。
    猜你喜欢
    • 1970-01-01
    • 2012-02-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多