如何在Ruby中编写包含method_missing方法的模块

9

我有几个扩展方法缺失的模块:

module SaysHello
    def respond_to?(method)
        super.respond_to?(method) || !!(method.to_s =~ /^hello/)
    end
    def method_missing(method, *args, &block)
        if (method.to_s =~ /^hello/)
            puts "Hello, #{method}"
        else
            super.method_missing(method, *args, &block)
        end
    end
end

module SaysGoodbye
    def respond_to?(method)
        super.respond_to?(method) || !!(method.to_s =~ /^goodbye/)
    end
    def method_missing(method, *args, &block)
        if (method.to_s =~ /^goodbye/)
            puts "Goodbye, #{method}"
        else
            super.method_missing(method, *args, &block)
        end
    end
end

class ObjectA
    include SaysHello
end

class ObjectB
    include SaysGoodbye
end

这些都很好用,例如ObjectA.new.hello_there输出"Hello, hello_there"。同样,ObjectB.new.goodbye_xxx输出"Goodbye, xxx"。使用respond_to?也可以,例如ObjectA.new.respond_to? :hello_there返回true。
然而,当您想同时使用SaysHelloSaysGoodbye时,这种方法就不太适用了。
class ObjectC
    include SaysHello
    include SaysGoodbye
end

ObjectC.new.goodbye_aaa能正常工作,但ObjectC.new.hello_a的表现却有些奇怪:

> ObjectC.new.hello_aaa
Hello, hello_aaa
NoMethodError: private method `method_missing' called for nil:NilClass
    from test.rb:22:in `method_missing' (line 22 was the super.method_missing line in the SaysGoodbye module)

输出结果正确,但随后出现错误。同时respond_to?不正确,ObjectC.new.respond_to? :hello_a返回false。

最后,添加这个类:

class ObjectD
    include SaysHello
    include SaysGoodbye

    def respond_to?(method)
        super.respond_to?(method) || !!(method.to_s =~ /^lol/)
    end


    def method_missing(method, *args, &block)
        if (method.to_s =~ /^lol/)
            puts "Haha, #{method}"
        else
            super.method_missing(method, *args, &block)
        end
    end
end

这个代码存在一些问题。ObjectD.new.lol_zzz 是可以工作的,但是 ObjectD.new.hello_aObjectD.new.goodbye_t 都会在输出正确字符串后抛出一个名称异常。同时,respond_to? 也无法识别 hello 和 goodbye 方法。

有没有一种方法可以让所有方法都正常工作?了解 method_missing、模块和 super 之间的交互方式也会非常有用。

编辑:coreyward 解决了这个问题,如果在我定义的所有方法中使用 super 而不是 super.<method-name>(args...),程序就可以正常工作。不过我不明白为什么,所以我在这里提了另一个问题。


包含的模块被添加到继承链中,它们不会覆盖或替换方法。所以如果每个method_missing都调用super,最终它们都会被调用。请参见我的下面的答案。 - ChrisPhoenix
2个回答

5

当您重新定义一个方法时,您重新定义了一个方法;就是这样。

当您使用具有method_missing方法定义的第二个模块时,您正在覆盖先前定义的method_missing。您可以通过在重新定义之前为其设置别名来保留它,但您可能要小心处理。

此外,我不知道您为什么要调用super.method_missing。一旦您的method_missing定义用完了所有技巧,您应该让Ruby知道它可以继续沿着链找到处理调用的方法,只需调用super即可(无需传递参数或指定方法名称)。

关于Super(更新)

当您调用super时,Ruby会继续沿着继承链寻找被调用方法的下一个定义,如果找到则调用并返回响应。当您调用super.method_missing时,您会调用对super()的响应中的method_missing方法。

看一个(有些愚蠢的)例子:

class Sauce
  def flavor
    "Teriyaki"
  end
end

# yes, noodles inherit from sauce:
#   warmth, texture, flavor, and more! ;)
class Noodle < Sauce
  def flavor
    sauce_flavor = super
    "Noodles with #{sauce_flavor} sauce"
  end
end

dinner = Noodle.new
puts dinner.flavor     #=> "Noodles with Teriyaki sauce"

你可以看到,super是一个像其他方法一样的方法,只是在幕后进行了一些魔法操作。如果你在这里调用super.class,你会看到String,因为"Teriyaki"是一个字符串。

现在明白了吗?


但是导入第二个模块并没有覆盖第一个模块的method_missing,否则ObjectC.new.hello_a就不会输出Hello, hello_a(加上NameError)。使用super而不是super.method_missing(...)实际上解决了问题,所有的method_missing调用和respond_to?调用现在都可以正确地工作。我不明白为什么。 - David Miani
@nanothief:我已经更新了问题,希望更详细地解释了Ruby中super的工作原理。我认为这会让你清楚一些。 - coreyward
谢谢,那就是我在使用super时遇到的问题。我一直以为super.method是在父类上调用method,而实际上它是在调用超类上当前方法的结果上调用method。 - David Miani
也许应该澄清一下,“override”的意思是“插入到继承链中”,而“super”的意思是“调用继承链上层的方法”。这就是为什么从每个method_missing调用super最终会调用所有的method_missing。 - ChrisPhoenix

2

http://www.perfectline.ee/blog/activerecord-method-missing-with-multiple-inheritance

本文解释了它的工作原理:每个新模块不会覆盖或替换方法——相反,它的方法被插入到继承链中。这就是为什么从每个 method_missing 方法调用 super 最终会调用所有 method_missing 的原因。
类在继承链中仍然是最低层,最后添加的模块靠近类。
所以:
class Foo
  include A
  include B
end

内核 -> A -> B -> Foo 的结果

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接