如何在 Ruby 中实现抽象类

我知道在 Ruby 中没有抽象类的概念。但是如果需要实施,我该怎么做呢?我试过这样的方法:

class A
def self.new
raise 'Doh! You are trying to write Java in Ruby!'
end
end


class B < A
...
...
end

但是,当我尝试实例化 B 时,它会在内部调用 A.new,这会引发异常。

此外,模块不能被实例化,但是它们也不能被继承。将新方法设置为私有也不起作用。

有人有什么建议吗?

109317 次浏览

就我个人而言,我在抽象类的方法中提出 NotImplementedError。但是,由于您提到的原因,您可能希望将它保留在 new方法之外。

我不喜欢在 Ruby 中使用抽象类(几乎总有更好的方法)。如果你真的认为这是最好的技术,你可以使用下面的代码片段来说明哪些方法是抽象的:

module Abstract
def abstract_methods(*args)
args.each do |name|
class_eval(<<-END, __FILE__, __LINE__)
def #{name}(*args)
raise NotImplementedError.new("You must implement #{name}.")
end
END
# important that this END is capitalized, since it marks the end of <<-END
end
end
end


require 'rubygems'
require 'rspec'


describe "abstract methods" do
before(:each) do
@klass = Class.new do
extend Abstract


abstract_methods :foo, :bar
end
end


it "raises NoMethodError" do
proc {
@klass.new.foo
}.should raise_error(NoMethodError)
end


it "can be overridden" do
subclass = Class.new(@klass) do
def foo
:overridden
end
end


subclass.new.foo.should == :overridden
end
end

基本上,您只需使用抽象方法列表调用 abstract_methods,当它们被抽象类的实例调用时,就会引发 NotImplementedError异常。

你的方法没有错。在初始化程序中引发一个错误似乎没有问题,当然,只要您的所有子类都覆盖 initialize。但是您不希望这样定义 self.new。我会这么做:

class A
class AbstractClassInstiationError < RuntimeError; end
def initialize
raise AbstractClassInstiationError, "Cannot instantiate this class directly, etc..."
end
end

另一种方法是将所有这些功能放在一个模块中,正如您所提到的,该模块永远不能实例化。然后,将模块包含在类中,而不是从其他类继承。然而,这会打破像 super这样的东西。

这取决于您希望如何构造它,尽管模块似乎是解决“如何编写一些为其他类设计的东西以供其他类使用”问题的更清晰的解决方案

试试这个:

class A
def initialize
raise 'Doh! You are trying to instantiate an abstract class!'
end
end


class B < A
def initialize
end
end

如果您想使用一个不可实例化的类,在 A.new 方法中,在抛出错误之前检查 self == A

但实际上,模块看起来更像是您在这里想要的。例如,枚举类型在其他语言中可能是一个抽象类。从技术上讲,您不能对它们进行子类化,但是调用 include SomeModule可以实现大致相同的目标。是不是有什么原因让你觉得这样不行?

您试图为抽象类服务的目的是什么?在 Ruby 中可能有更好的方法,但是您没有给出任何细节。

我的指针是这个; 使用 混音而不是继承。

另一个答案是:

module Abstract
def self.append_features(klass)
# access an object's copy of its class's methods & such
metaclass = lambda { |obj| class << obj; self ; end }


metaclass[klass].instance_eval do
old_new = instance_method(:new)
undef_method :new


define_method(:inherited) do |subklass|
metaclass[subklass].instance_eval do
define_method(:new, old_new)
end
end
end
end
end

这依赖于正常的 #method_missing来报告未实现的方法, 但是使抽象类无法实现(即使它们有一个 initialize 方法)

class A
include Abstract
end
class B < A
end


B.new #=> #<B:0x24ea0>
A.new # raises #<NoMethodError: undefined method `new' for A:Class>

正如其他海报所说,您可能应该使用混合类,而不是抽象类。

在过去6年半的 Ruby 编程生涯中,我一次也没有使用过 需要抽象类。

如果您认为自己需要一个抽象类,那么您就是在一种提供/需要它们的语言中思考得太多了,而不是像 Ruby 那样。

正如其他人所建议的那样,混合语言更适合那些应该是接口的东西(如 Java 定义的那样) ,重新思考你的设计更适合那些“需要”来自其他语言(如 C + +)的抽象类的东西。

2022年更新: 我已经使用 Ruby 20年了,不再需要使用抽象类。所有评论我的回复的人说的每一件事都是通过实际学习 Ruby 和使用适当的工具来解决的,比如模块(它们甚至提供了通用的实现)。在我管理的团队中,有些人创建的类的基本实现会失败(比如抽象类) ,但这些基本上都是编码上的浪费,因为在生产中,NoMethodError会产生与 AbstractClassError完全相同的结果。

class A
private_class_method :new
end


class B < A
public_class_method :new
end

在这里插一句,我认为没有理由阻止某人实例化抽象类 特别是因为他们可以动态地向它添加方法

鸭式类型语言,比如 Ruby,在运行时使用方法的存在/不存在或行为来决定是否应该调用它们。因此,你的问题,因为它适用于一个抽象的 方法,是有意义的

def get_db_name
raise 'this method should be overriden and return the db name'
end

这应该就是故事的结局了。在 Java 中使用抽象类的唯一原因是坚持某些方法是“填充的”,而其他方法的行为是在抽象类中进行的。在 Duck-type 语言中,重点是方法,而不是类/类型,因此您应该将您的担心转移到这个级别。

在您的问题中,您基本上是试图从 Java 重新创建 abstract关键字,这是用 Ruby 做 Java 的代码味道。

我是这样做的,所以它在子类上重新定义 new,从而在非抽象类上找到一个 new。 我仍然不认为在 Ruby 中使用抽象类有什么实际意义。

puts 'test inheritance'
module Abstract
def new
throw 'abstract!'
end
def inherited(child)
@abstract = true
puts 'inherited'
non_abstract_parent = self.superclass;
while non_abstract_parent.instance_eval {@abstract}
non_abstract_parent = non_abstract_parent.superclass
end
puts "Non abstract superclass is #{non_abstract_parent}"
(class << child;self;end).instance_eval do
define_method :new, non_abstract_parent.method('new')
# # Or this can be done in this style:
# define_method :new do |*args,&block|
# non_abstract_parent.method('new').unbind.bind(self).call(*args,&block)
# end
end
end
end


class AbstractParent
extend Abstract
def initialize
puts 'parent initializer'
end
end


class Child < AbstractParent
def initialize
puts 'child initializer'
super
end
end


# AbstractParent.new
puts Child.new


class AbstractChild < AbstractParent
extend Abstract
end


class Child2 < AbstractChild


end
puts Child2.new

你可以试试3颗红宝石:
接口
摘要
简单摘要

我的2: 我选择了一个简单、轻量级的 DSL 混合:

module Abstract
extend ActiveSupport::Concern


included do


# Interface for declaratively indicating that one or more methods are to be
# treated as abstract methods, only to be implemented in child classes.
#
# Arguments:
# - methods (Symbol or Array) list of method names to be treated as
#   abstract base methods
#
def self.abstract_methods(*methods)
methods.each do |method_name|


define_method method_name do
raise NotImplementedError, 'This is an abstract base method. Implement in your subclass.'
end


end
end


end


end


# Usage:
class AbstractBaseWidget
include Abstract
abstract_methods :widgetify
end


class SpecialWidget < AbstractBaseWidget
end


SpecialWidget.new.widgetify # <= raises NotImplementedError

当然,在这种情况下,为初始化基类添加另一个错误是微不足道的。

还有这个小的 abstract_type gem,允许以一种不引人注目的方式声明抽象类和模块。

示例(来自 README.md文件) :

class Foo
include AbstractType


# Declare abstract instance method
abstract_method :bar


# Declare abstract singleton method
abstract_singleton_method :baz
end


Foo.new  # raises NotImplementedError: Foo is an abstract type
Foo.baz  # raises NotImplementedError: Foo.baz is not implemented


# Subclassing to allow instantiation
class Baz < Foo; end


object = Baz.new
object.bar  # raises NotImplementedError: Baz#bar is not implemented

对于 Rails 世界中的任何人来说,将 ActiveRecord 模型实现为一个抽象类都是通过在模型文件中声明完成的:

self.abstract_class = true

虽然这感觉不像 Ruby,但你可以这样做:

class A
def initialize
raise 'abstract class' if self.instance_of?(A)


puts 'initialized'
end
end


class B < A
end

结果:

>> A.new
(rib):2:in `main'
(rib):2:in `new'
(rib):3:in `initialize'
RuntimeError: abstract class
>> B.new
initialized
=> #<B:0x00007f80620d8358>
>>