如何在Ruby中实现一个抽象类?


121

我知道红宝石中没有抽象类的概念。但是,如果根本需要实施,该怎么做?我尝试过类似...

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

class B < A
  ...
  ...
end

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

同样,模块不能被实例化,但是它们也不能被继承。将新方法设为私有也将不起作用。有指针吗?


1
可以混入模块,但我想您是否出于某些其他原因需要经典继承?
Zach

6
并不是我需要实现一个抽象类。我想知道如何做到这一点,如果根本需要这样做的话。编程问题。而已。
Chirantan

127
raise "Doh! You are trying to write Java in Ruby"
Andrew Grimm

Answers:


61

我不喜欢在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将引发异常。


实际上,这实际上更像一个界面,但我明白了。谢谢。
Chirantan

6
这听起来不像是一个有效的用例,NotImplementedError它实际上意味着“依赖于平台,您的平台不可用”。 参见docs
skalee 2014年

6
您正在用元编程替换单行方法,现在需要包括一个mixin并调用一个方法。我认为这完全不是声明性的。
Pascal

1
我编辑了此答案修复程序,并对代码中的一些错误进行了修正,并对其进行了更新,因此现在可以运行。yehudakatz.com/2009/11/12/better-ruby-idioms
Magne

1
@ManishShrivastava:请立即查看此代码,以获取有关在此处使用END与结束的重要性的评论
Magne

113

我只是想在这里晚些时候发出警报,我认为没有理由阻止某人实例化抽象类,尤其是因为他们可以动态地向其添加方法

鸭式语言(如Ruby)在运行时使用方法的存在/不存在或行为来确定是否应调用它们。因此,您的问题(适用于抽象方法)很有意义

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

那应该是故事的结局。在Java中使用抽象类的唯一原因是要坚持某些方法被“填充”,而其他方法则在抽象类中具有其行为。在鸭子式语言中,重点放在方法上,而不是在类/类型上,因此您应该将担忧转移到该级别。

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


3
@克里斯托弗·佩里:为什么?
2013年

10
@ChristopherPerry我还是不明白。如果父母和兄弟姐妹毕竟关联的并且我希望这种关系是显式的,为什么我不希望这种依赖性?同样,要在某个其他类中组成某个类的对象,您也需要知道其定义。继承通常以合成的形式实现,它只是使组成对象的接口成为嵌入它的类的接口的一部分。因此,您仍然需要定义嵌入式或继承的对象。或者,也许您在谈论其他事情?您能详细说明一下吗?
2013年

2
@SasQ,您不需要知道父类的实现细节即可组成它,只需知道其API。但是,如果继承,则依赖于父母的实现。如果实现更改,您的代码可能会以意外方式中断。此处有
Christopher Perry

16
抱歉,但是“偏重继承而不是继承”并没有说“总是用户构成”。尽管通常应该避免继承,但是有一些用例更适合它们。不要盲目跟随这本书。
Nowaker

1
@Nowaker非常重要的一点。因此,我们常常会被阅读或听到的东西弄糊涂,而不是思考“在这种情况下什么是务实的方法”。它很少是完全黑色或白色的。
Per Lundberg

44

试试这个:

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

class B < A
  def initialize
  end
end

38
如果您希望能够使用#initializeB的super in ,则实际上可以在A#initialize中引发任何事情if self.class == A
2010年

17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end

7
另外,可以使用父类的继承钩子使构造函数方法在所有子类中自动可见:def A.inherited(subclass); subclass.instance_eval {public_class_method:new}; 结束
t6d 2010年

1
很不错的t6d。作为注释,只需确保将其记录在案,因为这是令人惊讶的行为(消除了最少的惊讶)。
bluehavana 2010年

16

对于Rails世界中的任何人,都可以通过在模型文件中使用以下声明来将ActiveRecord模型实现为抽象类:

self.abstract_class = true

12

我的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

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


1
编辑:好的措施,因为此方法使用define_method,所以可能要确保回溯保持不变,例如:err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre

我发现这种方法非常优雅。感谢您对这个问题的贡献。
wes.hysell

12

在Ruby的过去6 1/2年中,我不需要一次抽象类。

如果您认为需要一个抽象类,那么您在使用提供/要求它们的语言中考虑过多,而不是在Ruby中。

正如其他人所建议的,mixin更适合于应该是接口的事物(如Java定义的那样),而重新思考设计则更适合于“需要”来自其他语言(例如C ++)的抽象类的事物。

2019年更新:在使用16½年的时间里,我不需要Ruby中的抽象类。通过实际学习Ruby并使用适当的工具(例如模块(甚至为您提供通用的实现))可以解决所有在我的回应中发表评论的人们所回答的所有问题。我管理的团队中有一些人创建的基类实现失败的类(例如抽象类),但是这些大多数都是在浪费代码,因为它们NoMethodError会产生与生产中完全相同的结果AbstractClassError


24
您也不需要Java中的抽象类。这是一种证明它是基类的方法,不应为扩展您的类的人员实例化。
fijiaaron

3
IMO,很少有语言会限制您的面向对象编程概念。除非存在与性能相关的原因(或更令人信服的原因),否则在给定情况下合适的内容不应取决于语言。
thekingoftruth 2013年

10
@fijiaaron:如果您这样认为,那么您绝对不理解什么是抽象基类。对于“文档化”不应实例化的类,它们并不多(这是将其抽象化的副作用)。它更多地是为一堆派生类声明一个通用接口,然后保证将实现该接口(如果没有,派生类也将保持抽象)。它的目标是为实例化意义不大的类支持Liskov的替代原理。
2013年

1
(续)当然,只有一个类可以创建几个具有某些常用方法和属性的类,但是编译器/解释器将不知道这些类之间是否存在任何关联。在每个类中,方法和属性的名称可以相同,但是仅此名称并不意味着它们表示相同的功能(名称对应可能只是偶然的)。告诉编译器这种关系的唯一方法是使用基类,但是该基类本身的实例并不总是有意义。
SasQ 2013年


4

我个人在抽象类的方法中提出NotImplementedError。但是出于您提到的原因,您可能希望将其保留在“新”方法之外。


但是,如何阻止它实例化呢?
Chirantan

我个人只是刚开始使用Ruby,但是在Python中,具有声明的__init ___()方法的子类不会自动调用其超类的__init __()方法。我希望Ruby中会有类似的概念,但是就像我说的那样,我才刚刚开始。
扎克

在ruby中,initialize除非使用super显式调用,否则不会自动调用parent的方法。
2010年

4

如果要使用无法实例化的类,请在A.new方法中,在引发错误之前检查self ==A。

但是实际上,模块似乎更像您在这里想要的-例如,Enumerable是可能是其他语言中的抽象类的一种东西。从技术上讲,您不能将它们归类,但是调用include SomeModule可以达到大致相同的目标。是否出于某些原因对您不起作用?


4

您试图为抽象类提供服务的目的是什么?用红宝石可能有更好的方法,但是您没有提供任何细节。

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


确实。混合模块等同于使用AbstractClass:wiki.c2.com/?AbstractClass PS:让我们称它们为模块而不是mixins,因为模块就是它们,混合它们就是您对它们的处理。
Magne

3

另一个答案:

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>

就像其他张贴者所说的那样,您可能应该使用mixin而不是抽象类。


3

我是这样做的,所以它在子类上重新定义了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

还有一个小abstract_type瑰宝,允许以不显眼的方式声明抽象类和模块。

示例(来自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

1

您的方法没有错。只要您的所有子类都覆盖了初始化,那么引发初始化错误似乎就可以了。但是您不想这样定义self.new。这就是我要做的。

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

另一种方法是将所有这些功能放在一个模块中,就像您提到的那样,它永远不会被激活。然后将模块包含在您的类中,而不是从另一个类继承。但是,这会破坏诸如super之类的东西。

因此,这取决于您要如何构造它。尽管模块似乎是解决“如何编写一些供其他类使用的东西”问题的更干净的解决方案。


我也不想这样做。孩子们不能称其为“超级”。
奥斯汀·齐格勒


0

尽管这听起来不像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>
>>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.