Как реализовать абстрактный класс в Ruby?

121

Я знаю, что в 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который вызовет исключение.

Кроме того, модули не могут быть созданы, но и унаследованы. сделать новый метод закрытым также не будет. Есть указатели?

Chirantan
источник
1
Модули можно смешивать, но я полагаю, вам нужно классическое наследование по какой-то другой причине?
Зак,
6
Это не значит, что мне нужно реализовать абстрактный класс. Интересно, как это сделать, нужно ли вообще это делать. Проблема программирования. Это оно.
Чирантан,
127
raise "Doh! You are trying to write Java in Ruby",
Эндрю Гримм,

Ответы:

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исключение.

Nakajima
источник
На самом деле это больше похоже на интерфейс, но я понял. Спасибо.
Чирантан,
6
Это не звучит как допустимый вариант использования, NotImplementedErrorкоторый, по сути, означает «платформенно-зависимый, недоступный в вашей». См. Документы .
skalee
7
Вы заменяете однострочный метод метапрограммированием, теперь вам нужно включить миксин и вызвать метод. Я не думаю, что это более декларативно.
Паскаль
1
Я отредактировал этот ответ, исправил некоторые ошибки в коде и обновил его, поэтому теперь он работает. Также немного упрощен код, чтобы использовать расширение вместо включения, из-за: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne
1
@ManishShrivastava: Пожалуйста, посмотрите этот код сейчас, чтобы увидеть комментарий о важности использования END здесь и в конце
Магне
113

Я думаю, что нет причин мешать кому-либо создавать экземпляр абстрактного класса, особенно потому , что они могут добавлять к нему методы на лету .

Языки с утиной типизацией, такие как Ruby, используют наличие / отсутствие или поведение методов во время выполнения, чтобы определить, следует ли их вызывать или нет. Поэтому ваш вопрос, поскольку он применяется к абстрактному методу , имеет смысл

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

и это должно быть конец истории. Единственная причина использовать абстрактные классы в Java - настаивать на том, чтобы одни методы были «заполнены», в то время как другие имели свое поведение в абстрактном классе. В языке «утиного набора» основное внимание уделяется методам, а не классам / типам, поэтому вам следует переместить свои заботы на этот уровень.

В вашем вопросе вы в основном пытаетесь воссоздать abstractключевое слово из Java, которое является запахом кода для выполнения Java в Ruby.

Дэн Розенстарк
источник
3
@ Кристофер Перри: По каким причинам?
SasQ 03
10
@ChristopherPerry Я все еще не понимаю. Почему я не должен хотеть эту зависимость , если родители и брат будут связаны в конце концов , и я хочу , чтобы это отношение быть явным? Кроме того, чтобы составить объект одного класса внутри другого класса, вам также необходимо знать его определение. Наследование обычно реализуется как композиция, оно просто делает интерфейс составного объекта частью интерфейса класса, который его встраивает. Тем не менее, вам нужно определение встроенного или унаследованного объекта. А может ты о другом? Не могли бы вы подробнее рассказать об этом?
SasQ 03
2
@SasQ, вам не нужно знать детали реализации родительского класса, чтобы составить его, вам нужно знать только его API. Однако, если вы наследуете, вы полагаетесь на реализацию родителей. В случае изменения реализации ваш код может неожиданно сломаться. Более подробная информация здесь
Кристофер Перри
16
Извините, но «Предпочитать композицию над наследованием» не означает «Всегда использовать композицию». Хотя в целом следует избегать наследования, есть несколько случаев, когда они лучше подходят. Не следуйте слепо за книгой.
Новакер
1
@Nowaker Очень важный момент. Очень часто мы оказываемся ошеломленными тем, что читаем или слышим, вместо того, чтобы думать «каков прагматический подход в этом случае». Он редко бывает полностью черным или белым.
Пер Лундберг,
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
Если вы хотите иметь возможность использовать super в #initializeB, вы можете просто поднять все, что в A # initialize if self.class == A.
mk12 01
17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end
bluehavana
источник
7
Кроме того, можно использовать унаследованный хук родительского класса, чтобы сделать метод конструктора автоматически видимым во всех подклассах: def A.inherited (subclass); subclass.instance_eval {public_class_method: new}; конец
t6d
1
Очень красивый т6д. В качестве комментария просто убедитесь, что это задокументировано, так как это удивительное поведение (нарушает наименьшее удивление).
bluehavana 01
16

для всех в мире рельсов реализация модели 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
Энтони Наварре
Я считаю этот подход довольно элегантным. Спасибо за ваш вклад в этот вопрос.
wes.hysell
12

За последние шесть с половиной лет программирования на Ruby мне ни разу не понадобился абстрактный класс.

Если вы думаете, что вам нужен абстрактный класс, вы слишком много думаете о языке, который их предоставляет / требует, а не о Ruby как таковом.

Как предлагали другие, миксин больше подходит для вещей, которые должны быть интерфейсами (как их определяет Java), а переосмысление вашего дизайна больше подходит для вещей, которым «нужны» абстрактные классы из других языков, таких как C ++.

Обновление 2019: мне не нужны абстрактные классы в Ruby за 16,5 лет использования. Все, что говорят все, кто комментирует мой ответ, решается путем фактического изучения Ruby и использования соответствующих инструментов, таких как модули (которые даже дают вам общие реализации). В командах, которыми я управлял, есть люди, которые создали классы с базовой реализацией, которая терпит неудачу (например, абстрактный класс), но это в основном пустая трата кода, потому NoMethodErrorчто приведет к тому же результату, что и AbstractClassErrorв производстве.

Остин Зиглер
источник
24
Вам также не нужен / не нужен / абстрактный класс в Java. Это способ документально подтвердить, что это базовый класс, и его не следует создавать для людей, расширяющих ваш класс.
fijiaaron
3
ИМО, несколько языков должны ограничить ваши представления об объектно-ориентированном программировании. Что уместно в данной ситуации, не должно зависеть от языка, если нет причин, связанных с производительностью (или чего-то более убедительного).
thekingoftruth
10
@fijiaaron: Если вы так думаете, то вы точно не понимаете, что такое абстрактные базовые классы. Они не особо «документируют», что класс не должен создаваться (скорее, это побочный эффект абстрактности). Это больше о том, чтобы указать общий интерфейс для группы производных классов, который затем гарантирует, что он будет реализован (в противном случае производный класс также останется абстрактным). Его цель - поддержать принцип подстановки Лискова для классов, для которых создание экземпляров не имеет большого смысла.
SasQ 03
1
(продолжение) Конечно, можно создать всего несколько классов с некоторыми общими методами и свойствами в них, но компилятор / интерпретатор тогда не узнает, что эти классы каким-либо образом связаны. Имена методов и свойств могут быть одинаковыми в каждом из этих классов, но это еще не означает, что они представляют одну и ту же функциональность (соответствие имен могло быть просто случайным). Единственный способ сообщить компилятору об этой связи - использовать базовый класс, но не всегда имеет смысл существование экземпляров этого базового класса.
SasQ 03
4

Лично я возбуждаю NotImplementedError в методах абстрактных классов. Но вы можете оставить его вне «нового» метода по указанным вами причинам.

Zack
источник
Но тогда как остановить его создание?
Чирантан,
Лично я только начинаю работать с Ruby, но в Python подклассы с объявленными методами __init ___ () не вызывают автоматически методы __init __ () своих суперклассов. Я надеюсь, что в Ruby будет аналогичная концепция, но, как я уже сказал, я только начинаю.
Zack,
В ruby ​​родительские initializeметоды не вызываются автоматически, если явно не вызваны с помощью super.
mk12
4

Если вы хотите использовать неустановленный класс, в своем методе A.new проверьте, есть ли self == A, прежде чем выдавать ошибку.

Но на самом деле модуль больше похож на то, что вы хотите здесь - например, Enumerable - это то, что может быть абстрактным классом на других языках. Технически вы не можете разделить их на подклассы, но колл include SomeModuleпозволяет достичь примерно той же цели. Есть ли причина, по которой это не сработает для вас?

цыпленок
источник
4

Какой цели вы пытаетесь достичь с помощью абстрактного класса? Возможно, есть лучший способ сделать это на рубине, но вы не указали никаких подробностей.

Мой указатель таков; используйте миксин, а не наследование.

jshen
источник
На самом деле. Смешивание модуля было бы эквивалентно использованию AbstractClass: wiki.c2.com/?AbstractClass PS: Давайте называть их модулями, а не миксинами, поскольку модули - это то, чем они являются, и их смешивание - это то, что вы с ними делаете.
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 для сообщения о нереализованных методах, но предотвращает реализацию абстрактных классов (даже если у них есть метод инициализации)

class A
  include Abstract
end
class B < A
end

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

Как было сказано на других плакатах, вам, вероятно, следует использовать миксин, а не абстрактный класс.

колокольчик-рапунцель
источник
3

Я сделал это таким образом, поэтому он переопределяет новое в дочернем классе, чтобы найти новый в не абстрактном классе. Я до сих пор не вижу практического смысла в использовании абстрактных классов в 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
ZeusTheTrueGod
источник
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

Другой подход заключался бы в том, чтобы поместить всю эту функциональность в модуль, который, как вы упомянули, никогда нельзя внедрить. Затем включите модуль в свои классы вместо наследования от другого класса. Однако это сломало бы такие вещи, как супер.

Так что это зависит от того, как вы хотите его структурировать. Хотя модули кажутся более чистым решением для решения проблемы «Как мне написать что-то, что предназначено для использования другими классами»

Алекс Уэйн
источник
Я бы тоже не хотел этого делать. Тогда дети не могут называть «супер».
Остин Зиглер,
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>
>>
lulalala
источник