Ruby继承和覆盖类方法

4
我已经设置了如下所示的两个类:
class Parent

  def self.inherited(child)
    child.custom_class_method
  end

  def self.custom_class_method
    raise "You haven't implemented me yet!"
  end
end

class Child < Parent

  def self.custom_class_method
    "hello world"
  end
end

似乎在评估继承关系Child < Parent时,会调用self.inherited,然后引发Parent版本的self.custom_class_method,而不是Child版本的self.custom_class_method。这是一个问题,因为我没有得到预期的"hello world",而是抛出了一个错误,说"You haven't implemented me yet!"

难道只有当Parentself.inherited执行完毕后,才会评估Childself.custom_class_method吗?如果是这样的话,也许有一种解决方法吗?我应该不在父类上放置raise检查吗?


这看起来很奇怪。获取父类的 custom_class_method 的唯一方法应该是调用 super。否则,只需调用 Child.custom_class_method 就应该输出 "hello world"。能否提供更深入的日志记录? - Cole Bittel
嗯,我同意应该这样做!但是似乎我可以通过将代码块简单地复制粘贴到“irb”控制台中来复制此操作。由于这个原因,在评估“Child”类时会出现错误。 - aMat
3个回答

4
我认为这应该能够澄清问题:
class Parent
  def self.inherited(child)
    puts "Inherited"
  end
end

class Child < Parent
  puts "Starting to define methods"
  def self.stuff; end
end

输出结果清楚地表明,在打开新类时立即调用了.inherited,而不是在关闭它时调用。因此,正如你所猜测的那样,在尝试调用Child.custom_class_method时,它并不存在 - 所有.inherited看到的都是一个空白的状态。
(至于如何解决这个问题...很遗憾,如果没有更多关于你要做什么的见解,我无法给出答案。)

哦,聪明,我没想到可以这样调试它!那么在这里定义抽象方法的最佳方式是什么?我只是避免引发错误吗(说实话,这让我感到不舒服)。 - aMat
基本上我想做的是创建一个父类,能够在self.inherited块中执行代码,这些代码在每个子类中略有不同。但是为了确保我们不会出现错误,我希望强制每个继承的子类都定义custom_method(这就是为什么我让父类引发异常的原因)。 - aMat
1
啊,我明白了。看起来我的思维过程本质上不遵循继承的设计原则(我想这也没关系)。我希望做的是将各种类正在执行的逻辑(它们每个都在类级别上建立与其自定义数据库的连接)抽象成一个父类。一旦继承,父类将询问子类他们想要连接到哪个数据库,并为他们完成连接。这样就可以将这个逻辑抽象化并保持DRY,而不是在每个模型中重复。 - aMat
不确定我是否理解,所以让我确认一下:子模型是需要连接到不同数据库的数据模型,并期望抽象父模型处理连接吗?这不是DRY,这是责任混淆。抽象模型仍应该是一个模型;它不应该知道如何连接到数据库(尤其不是多个数据库)。我会让DatabaseManager(或类似的东西)处理数据库连接,并让模型请求适当的连接,在每个模型中只需一行代码。(再次说明,由于我不了解您的设计细节,所以这里非常模糊。) - Amadan
1
是的,抱歉表述不够清晰,但您的建议实际上非常正确。谢谢!所以在每个子进程中,我将调用 DatabaseManager.connect_to(custom_database)。再次感谢! - aMat
显示剩余2条评论

3

模板模式/惰性初始化可能有助于解决您的问题。该代码假设子类之间的不同之处是数据库连接信息,可能只是表名,也可能是完全不同的数据库。父类拥有创建和维护连接的所有代码,使子类仅负责提供不同的内容。

class Parent
  def connection
    @connection ||= make_connection
  end

  def make_connection
    puts "code for making connection to database #{database_connection_info}"
    return :the_connection
  end

  def database_connection_info
    raise "subclass responsibility"
  end
end

class Child1 < Parent
  def database_connection_info
    {host: '1'}
  end
end

class Child2 < Parent
  def database_connection_info
    {host: '2'}
  end
end

child1 = Child1.new
child1.connection  # => makes the connection with host: 1
child1.connection  # => uses existing connection

Child2.new.connection  # => makes connection with host: 2

太棒了,谢谢!作为一个设计问题,您认为这种方式与将其抽象成“DatabaseManager”模型并在每个类中调用“DatabaseManager.make_connection”相比,哪种更好/更差? - aMat
很好的问题。委托与继承是一个古老的问题。Rails通过继承实现持久性(技术上可以使用模块,但通常通过继承完成)。许多人认为持久性不应该是类的职责,而应该采取其他方向,例如数据映射器模式(https://en.wikipedia.org/wiki/Data_mapper_pattern)。即使您属于持久性是职责阵营,您仍然可能希望将数据库行为抽象化到类之外--也许是连接池。有很好的库可用,例如sequel gem。 - Brian
我并不固执于 DatabaseManager 这个想法,所以这是另一个很好的建议。正如我所说,我的回答必须含糊不清,因为你比我更了解你的项目。我试图表达的主要观点是你不能在 inherit 上这样做。你可以使用外部管理器,或者像这里一样继承方法,甚至可以在祖先上使用 initialize;但是 inherited 有另一个目的。 - Amadan
如何实现相反的效果,即使调用该方法,而不是引发“子类责任”的错误,也让其无法实现? - jgomo3

0

你需要将 self.inherited 包装在 Thread.new 中。

示例:

class Foo
  def self.inherited klass
    begin
      puts klass::BAZ
    rescue => error
      # Can't find and autoload module/class "BAZ".
      puts error.message
    end

    Thread.new do
      # 123
      puts klass::BAZ
    end
  end
end

class Bar < Foo
  BAZ = 123
end


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