从 Ruby 中的超类初始化方法返回不同子类的对象



所以我有多个 Thing 的子类,在这里表示为 ThingA 和 ThingB。
这里有几件事应该是理所当然的:

  • 一个事物从来不是直接创建的——即Thing.new

  • ThingA 在初始化时必须通过测试,否则它应该是 ThingB

  • 可以安全地假定 ThingB 是 ThingB

这是我的层次结构的草图:

class Thing
  def initialize( var = 'yes' )
    @var = var    
    if !self.verify?
      ThingB.new( var )
    elsif self.class != ThingB
      #code for ThingA
      @Aness = 'huge' 
    end
    #code for ThingA & ThingB
    puts 'END'
  end
  def verify?
    if self.class == ThingA
      @var == 'yes'
    else
      true
    end
  end
end
class ThingA < Thing
end
class ThingB < Thing
end

我的问题是,我怎样才能得到

ThingA.new( 'no' )

改为返回ThingB

这真的很烦我,因为我使用非常相似的代码,但不知何故我失去了所需的功能。 通过上述内容,我得到以下输出:

[21] pry(main)> ThingA.new
END
=> #<ThingA:0x60bd4b0 @Aness="huge", @var="yes">
#this is fine
[22] pry(main)> ThingB.new
END
=> #<ThingB:0x53ba6b8 @var="yes">
#this also
[23] pry(main)> ThingA.new( 'no' )
END
END
=> #<ThingA:0x64bec40 @var="no">
#this should be ThingB

"END"打印两次,表示 ThingB 已初始化,但它不会代替原始 ThingA 返回。 相反,我有一个没有Aness的东西A。

如前所述,我有非常相似的代码,可以根据需要运行,而无需使用throw或任何东西 - 我不知何故破坏了。

使用 return 只会阻止第一次初始化结束,并且仍然返回原始对象。

我不一定提倡这是设计系统的正确方法,但有两个原因导致你写的东西没有按照你的预期工作。

首先,即使在"简单"的情况下,上述操作也永远不会导致返回值为 ThingB;initialize 方法的最后一行是puts调用,puts始终具有 nil 的返回值,因此在"正常"方法的简单情况下,您的返回值仍然不会是 ThingB 实例, 这将是nil.

但是,正如你所说,

使用 return 只会阻止第一次初始化结束,并且仍然返回原始对象。

我假设你的意思是在初始化方法中使用显式return,就像这个假设的代码:

class Thing
  def initialize( var = 'yes' )
    @var = var    
    if !self.verify?
      return ThingB.new( var ) # explicit return
    elsif self.class != ThingB
      #code for ThingA
      @Aness = 'huge' 
    end
    #code for ThingA & ThingB
    puts 'END'
  end
  def verify?
    if self.class == ThingA
      @var == 'yes'
    else
      true
    end
  end
end

那么为什么这不起作用呢?答案是微妙的,但最终很简单,并且是理解Ruby的关键(我认为(:你不是在代码中调用initialize,而是在调用new。New 不能只返回任何初始化返回的内容,因为这样你的原始类定义(没有显式返回(就会使 ThingA.new 返回nil![*]

new实际工作的方式更像是这样的:

class Thing
  def self.new(*args)
    obj = self.allocate
    obj.initialize(*args) # sort of; initialize is private
    return obj
  end
end

你会注意到 initialize 的返回值被完全忽略了;这是一件好事,如果不是这样,我们将不得不让每个初始值设定项繁琐地返回 self ,并且每次忘记时都会出错。

所以,如果你想让ThingA.new返回一个ThingB的实例,不需要修改ThingA#initialize,需要修改ThingA.new

class Thing
end
class ThingA < Thing
  def self.verify?(var)
    var == 'yes'
  end
  def self.new(var = 'yes')
    if self.verify?(var)
      super
    else
      ThingB.new(var)
    end
  end
  def initialize(var)
    @Aness = 'huge'
  end
end
class ThingB < Thing
end

我应该强调,这对你的代码来说不一定是明智的。但我确实认为知道如何做,以及它为什么有效,对于理解 Ruby 很重要。


[*]:同样,不是因为它缺少显式返回,而是因为它隐式返回最后一个计算表达式的值,即 puts 'END' ,并且puts总是返回 nil

class ThingB
  def initialize(var = "yes")
    @var = var
    puts "END"
  end
end
class ThingA < ThingB
  def initialize(var = "yes")
    #code for ThingA
    @Aness = "huge"
    super
  end
  class <<self
    alias old_new new
    def new(var = "yes")
      verify?(var) ? ThingA.old_new(var) : ThingB.new(var)
    end
    def verify?(var)
      var == "yes"
    end
  end
end

你正在寻找的东西已经存在了一段时间,被称为工厂模式(参见维基百科文章 https://en.wikipedia.org/wiki/Factory_method_pattern(:

"在基于类的编程中,工厂方法模式是一种创建模式,它使用工厂方法来处理创建对象的问题,而不必指定将要创建的对象的确切类。

如果在创建之前不知道实例的类,则不应使用构造函数来创建它。 相反,您应该使用另一种方法来执行测试并创建相应类的实例。

在稳定、严格控制的类层次结构中,此方法可以是Thing类上的类方法:

class Thing
  def self.create
    if something
      Thing1.new
    else
      Thing2.new
    end
  end
end

造成了更一般的类对它更具体的专业化的依赖,这通常不是一个好主意,但如果你完全控制子类可能还不错。

如果循环依赖存在问题(例如,在定义之前使用了 Thing2(,那么您可以在定义 Thing、Thing1 和 Thing2 后创建一个最小的工厂类或模块:

require 'thing'
require 'thing1'
require 'thing2'
class ThingFactory
  def self.create
    # ...same logic as before
  end
end

另一种方法是在其他地方(即,在不同的、不相关的类中(创建另一个方法来做同样的事情。


关于你关于这是供新手使用的 REPL 的评论,你可以很简单地完成这个;你可以创建一个包含new方法的模块:

module Thing
  def self.new
    some_condition ? Thing1.new : Thing2.new
  end
end

在一般编程中,这是一个非常糟糕的主意,因为它对使用您的代码的开发人员非常具有误导性; Thing看起来像是使用常规new方法实例化的类,但事实并非如此。

更好的方法是仅要求用户了解在这种情况下,他们需要调用ThingFactory.create而不是ThingX.new

但是,如果您确实确定隐藏此区别不会混淆和阻碍您的用户,则在这种情况下,此策略可能是可以接受的。

最新更新