在实例化时将一个类转换为子类

3
我正在编写一个用于查询Mediawiki API的框架。我有一个Page类,代表维基上的文章,并且我也有一个Category类,它是一个具有更多特定方法(例如能够计算类别成员数量)的Page。我还有一个Page#category?方法,通过查询API来确定文章的命名空间,从而确定实例化的Page对象是否实际上代表了Mediawiki类别页面。
class Page
  def initialize(title)
    # do initialization stuff
  end

  def category?
    # query the API to get the namespace of the page and then...
    namespace == CATEGORY_NAMESPACE
  end
end

class Category < Page
  # ...
end

我希望能够检测到使用我的框架的用户尝试使用Page对象实例化Mediawiki类别(即 Page.new(“Category:My Category”)),如果是这样,则直接从Page构造函数实例化Category对象,而不是Page对象。
我觉得这应该是可能的,因为它类似于Rails中的单表继承,但我不确定如何使其正常工作。

1
Page 中的 category? 方法意味着祖先类知道子类,这在面向对象编程方面似乎不是一个好主意。另外,您能否稍微澄清一下您的最后一句话? - Mladen Jablanović
@Mladen:不确定Ruby语法,但似乎他想要Category foo = new Page()而不是Page bar = new Category() - Brian S
3
那么他可能应该看一下https://dev59.com/u3RB5IYBdhLWcg3wAjXR,通常还要在谷歌上搜索一些关于工厂模式的内容。 - Mladen Jablanović
抱歉,我猜在下班前发布问题并且离开电脑10个小时可能不是一个好主意 :p 我会编辑我的问题以便更加清晰明了。 - Daniel Vandersluis
@Mladen @Brian 现在应该更清楚了 :) - Daniel Vandersluis
3个回答

6

好的,有几件事情需要注意:

你不能将类 A 的实例转换为其子类 B 的实例,至少不是自动地。 B 可能(并且通常)包含在 A 中不存在的属性,可能具有完全不同的构造函数等。因此,据我所知,没有面向对象语言可以允许你以这种方式“转换”类。

即使在静态类型语言中,当你实例化 B,然后将其分配给类型为 A 的变量 a时,它仍然是 B 的实例,它根本没有转换为其祖先类。

Ruby 是一种具有强大反射功能的动态语言,因此您始终可以在运行时决定要实例化哪个类 - 请参阅以下内容:

puts "Which class to instantiate: "
class_name = gets.chomp
klass = Module.const_get class_name
instance = klass.new

所以,在这里不需要进行任何转换 - 只需在第一次实例化需要的类。

另外一件事:正如我在评论中提到的,方法category?是错误的,因为它违反了面向对象编程原则。在Ruby中,您可以 - 也应该 - 使用方法is_a?,因此您的检查将如下:

if instance.is_a? Category
  puts 'Yes, yes, it is a category!'
else
  puts "Nope, it's something else."
end

这只是冰山一角,关于实例化不同类还有很多内容。我在评论中链接了另一个问题,它可能是一个很好的起点,但那里的一些代码示例可能会让你感到困惑。但是理解它们绝对是值得的。
编辑:重新阅读您更新的问题后,我认为您正确的方式是创建一个工厂类,并让它检测和实例化不同的页面类型。因此,用户不会直接调用Page.new,而是调用类似于这样的东西。
MediaWikiClient.get_page "Category:My Category"

get_page方法将实例化相应的类。


我在编辑中尝试让它更清晰,但是 Page#category? 并不检查对象的类别,而是使用页面信息来确定其所属的命名空间。我不知道还有哪里可以放置它... - Daniel Vandersluis
无论如何,我想我并不是很熟悉工厂模式,但那似乎是正确的方法,谢谢。 - Daniel Vandersluis
啊,抱歉,我没意识到你在谈论维基媒体命名空间,而不是 Ruby 命名空间! :) 无论如何,检查 WM 命名空间并相应地实例化适当的类的地方应该是工厂。当然,你也可以将工厂方法放入你的 Page 类中,再看看其他问题 - Brian Campbell 在那里给出了很好的答案和代码示例。 - Mladen Jablanović
据我所知,没有任何面向对象的编程语言可以让你以这种方式“转换”类。当然,总有例外!在这种情况下,CLOS 有一个名为 CHANGE-CLASS 的函数可以实现这一点。还有一些通用函数可用于控制它的具体工作方式。 - Ken

3

为什么不这样做呢?能够做到这一点就足够理由去做!

class Page
  def self.new(title)
    if self == Page and is_category?(title)
      Category.new(title)
    else
      super
    end
  end

  def self.is_category?(title)
    # ... (query the API etc.)
  end

  def initialize(title)
    # do initialization stuff
  end

  def category?
    # query the API to get the namespace of the page and then...
    namespace == CATEGORY_NAMESPACE
  end
end

class Category < Page
  # ...
end

1
你可以定义一个方法来实例化类并返回该实例。这被称为 工厂模式
class PageFactory
  def create(title) # the pattern uses "create".. but "new" is more Ruby' style
    namespace = title[/\A[^:]+(?=:)/]
     # matches the prefix, up to and excluding the first colon.
    if namespace == CATEGORY_NAMESPACE
      Category.new(title)
    else
      Page.new(title)
    end
  end
end

class ClientClass
  def do_something()
    factory = PageFactory.new
    my_page = factory.create('Category:Foo') 
    my_page.do_something()
  end
end


这样不行。PageFactory中的new方法是一个实例方法,而你在doSomething中调用了类方法。此外,重新定义new方法也不是一个好主意,因为它被用于在其被调用的类上实例化该类。顺便说一下,Ruby的约定是使用下划线分隔的方法名,而不是驼峰式命名法。 - Mladen Jablanović

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