Як реалізувати абстрактний клас у рубіні?


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
Модулі можна змішувати, але я вважаю, що вам потрібна класична спадщина з якоїсь іншої причини?
Зак

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виняток.


Це насправді більше схоже на інтерфейс, але я розумію. Дякую.
Чирантан

6
Це не є дійсним випадком використання, NotImplementedErrorякий по суті означає "залежне від платформи, недоступне для вашої". Див. Документи .
skalee

6
Ви замінюєте одно рядковий метод мета-програмуванням, тепер потрібно включити mixin та викликати метод. Я не думаю, що це взагалі декларативніше.
Паскаль

1
Я відредагував цю відповідь, виправив деякі помилки коду та оновив його, тому він зараз працює. Також трохи спростили код, щоб використовувати exte
Magne

1
@ManishShrivastava: Перегляньте цей код зараз, щоб прокоментувати важливість використання END тут проти кінця
Magne

113

Тільки, щоби запізнитися пізно тут, я думаю, що немає причин заважати комусь інстанціювати абстрактний клас, тим більше, що вони можуть додавати методи до нього на ходу .

Мови набору тексту, як-от Рубі, використовують присутність / відсутність чи поведінку методів під час виконання, щоб визначити, чи слід їх викликати чи ні. Тому ваше запитання, як це стосується абстрактного методу , має сенс

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

і це повинно бути про кінець історії. Єдина причина використовувати абстрактні класи на Java - наполягати на тому, щоб певні методи "заповнилися", а інші мали свою поведінку в абстрактному класі. У мові набору качки фокус робиться на методах, а не на класах / типах, тому вам слід перенести свої турботи на цей рівень.

У своєму запитанні ви в основному намагаєтесь відтворити abstractключове слово з Java, яке є кодовим запахом для роботи з Java в Ruby.


3
@Christopher Perry: Будь-які причини?
SasQ

10
@ChristopherPerry Я досі не розумію. Чому я не повинен хотіти цю залежність , якщо батьки і брат будуть пов'язані в кінці кінців , і я хочу , щоб це відношення бути явним? Крім того, щоб скласти об’єкт якогось класу всередині якогось іншого класу, ви також повинні знати його визначення. Спадкування зазвичай реалізується як композиція, воно просто робить інтерфейс складеного об'єкта частиною інтерфейсу класу, який його вбудовує. Тож вам все-таки потрібно визначення вбудованого або успадкованого об'єкта. А може, ти говориш про щось інше? Чи можете ви детальніше розповісти про це?
SasQ

2
@SasQ, Вам не потрібно знати деталі реалізації батьківського класу, щоб скласти його, вам потрібно знати лише його API. Однак якщо ви успадковуєте, ви покладаєтесь на здійснення батьків. Якщо зміна впровадження вашого коду може зламатися несподівано. Детальніше тут
Крістофер Перрі

16
Вибачте, але "Улюблена композиція над успадкуванням" не говорить "Завжди користувацький склад". Хоча в цілому слід уникати спадкування, є кілька випадків використання, коли вони просто краще підходять. Не слідкуйте сліпо за книгою.
Nowaker

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
Якщо ви хочете використовувати супер в #initializeB, ви можете просто збільшити що-небудь в ініціалізації A # if self.class == A.
mk12

17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end

7
Крім того, можна використовувати успадкований гак батьківського класу, щоб зробити метод конструктора автоматично видимим у всіх підкласах: def A.inherited (підклас); subclass.instan_eval {public_class_method: new}; кінець
t6d

1
Дуже приємний t6d. Як коментар, просто переконайтеся, що це задокументовано, оскільки це дивна поведінка (порушує найменший сюрприз).
синяхавана

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

В останні 6 1/2 років програмування Ruby мені не потрібен був абстрактний клас жодного разу.

Якщо ви думаєте, що вам потрібен абстрактний клас, ви занадто багато думаєте мовою, яка їх забезпечує / вимагає, а не в Ruby як такому.

Як вважають інші, міксин більше підходить для речей, які повинні бути інтерфейсами (як визначає Java), а переосмислення вашого дизайну є більш доречним для речей, які "потребують" абстрактних класів з інших мов, таких як C ++.

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


24
Вам також не потрібен / не потрібний / абстрактний клас на Java. Це спосіб задокументувати, що це базовий клас, і його не слід призначати людям, що розширюють ваш клас.
fijiaaron

3
ІМО, кілька мов повинні обмежувати ваші уявлення про об'єктно-орієнтоване програмування. Те, що підходить для даної ситуації, не повинно залежати від мови, якщо тільки немає причини, що стосується виступу (або чогось більш вагомого).
thekingoftruth

10
@fijiaaron: Якщо ви так думаєте, то ви точно не розумієте, що таке абстрактні базові класи. Вони не так сильно «документують», що клас не повинен бути примірником (скоріше побічним ефектом є абстрактність). Йдеться більше про те, щоб вказати загальний інтерфейс для групи похідних класів, який потім гарантує, що він буде реалізований (якщо ні, то і похідний клас також залишиться абстрактним). Його мета - підтримка Принципу заміни Ліскова для класів, для яких обґрунтування не має особливого сенсу.
SasQ

1
Зрозуміло, що можна зробити декілька класів із деякими загальними методами та властивостями, але компілятор / інтерпретатор тоді не знатиме, що ці класи пов'язані між собою. Назви методів та властивостей можуть бути однаковими у кожному з цих класів, але один лише це ще не означає, що вони представляють однакову функціональність (відповідність імен може бути просто випадковою). Єдиний спосіб повідомити компілятору про це відношення - це використовувати базовий клас, але це не завжди має сенс для існування примірників цього базового класу.
SasQ


4

Особисто я піднімаю NotImplementedError в методах абстрактних класів. Але ви, можливо, захочете вийти з цього «нового» методу з вказаних вами причин.


Але тоді як не допустити його примірника?
Чирантан

Особисто я тільки починаю з Ruby, але в Python підкласи з декларованими методами __init ___ () автоматично не називають їх суперкласи '__init __ () методами. Я би сподівався, що у Рубі буде подібне поняття, але, як я сказав, я тільки починаю.
Зак

У initializeметодах батьківського рубіну не дзвонять автоматично, якщо явно не викликаються супер.
mk12

4

Якщо ви хочете перейти з ненавмисним класом, у своєму методі A.new перевірте, чи self == A перед тим, як викинути помилку.

Але насправді модуль здається більше схожим на те, що ви хочете тут - наприклад, Enumerable - це те, що може бути абстрактним класом іншими мовами. Ви технічно не можете їх підкласирувати, але виклик include SomeModuleдосягає приблизно тієї самої мети. Чи є якась причина, що це не працює для вас?


4

З якою метою ви намагаєтесь послужити з абстрактним класом? Напевно, є кращий спосіб зробити це в рубіні, але ви нічого не давали.

Мій вказівник такий; використовувати міксин не у спадок.


Справді. Змішування модуля було б рівнозначним використанню абстрактного класу : wiki.c2.com/?Ab абстрактClass PS: Давайте назвемо їх модулями, а не міксінами, оскільки модулі - це те, що вони є, і змішати їх - це те, що ви робите з ними.
Магне

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

Я зробив це таким чином, тож він переосмислює нове на дочірньому класі, щоб знайти новий на не абстрактному класі. Я досі не бачу нічого практичного у використанні абстрактних класів у рубіні.

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

Іншим підходом було б поставити всю цю функціональність у модуль, який, як ви вже згадували, ніколи не може бути викликаний Потім включіть модуль у свої класи, а не успадковуючи його від іншого класу. Однак це зламає речі на кшталт супер.

Отже, це залежить від того, як ви хочете її структурувати. Хоча модулі здаються більш чистим рішенням для вирішення проблеми "Як написати деякі речі, призначені для використання в інших класах"


Я також не хотів би цього робити. Тоді діти не можуть назвати "супер".
Остін Циглер


0

Хоча це не схоже на Рубі, ви можете зробити це:

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>
>>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.