Wie implementiere ich eine abstrakte Klasse in Ruby?

121

Ich weiß, dass es in Ruby kein Konzept für abstrakte Klasse gibt. Aber wenn es überhaupt implementiert werden muss, wie geht man vor? Ich habe so etwas wie versucht ...

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

class B < A
  ...
  ...
end

Aber wenn ich versuche, B zu instanziieren, wird es intern aufrufen, A.newwas die Ausnahme auslösen wird.

Module können auch nicht instanziiert, aber auch nicht vererbt werden. Die neue Methode privat zu machen, funktioniert ebenfalls nicht. Irgendwelche Hinweise?

Chirantan
quelle
1
Module können eingemischt werden, aber ich nahm an, dass Sie aus einem anderen Grund klassische Vererbung benötigen?
Zach
6
Es ist nicht , dass ich brauche eine abstrakte Klasse zu implementieren. Ich habe mich gefragt, wie ich es machen soll, wenn man es überhaupt machen muss. Ein Programmierproblem. Das ist es.
Chirantan
127
raise "Doh! You are trying to write Java in Ruby".
Andrew Grimm

Antworten:

61

Ich mag es nicht, abstrakte Klassen in Ruby zu verwenden (es gibt fast immer einen besseren Weg). Wenn Sie wirklich der Meinung sind, dass dies die beste Technik für die Situation ist, können Sie das folgende Snippet verwenden, um aussagekräftiger zu sein, welche Methoden abstrakt sind:

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

Grundsätzlich rufen Sie nur abstract_methodsmit der Liste der abstrakten Methoden auf, und wenn sie von einer Instanz der abstrakten Klasse aufgerufen werden, wird eine NotImplementedErrorAusnahme ausgelöst.

Nakajima
quelle
Das ist eigentlich eher eine Schnittstelle, aber ich habe die Idee. Vielen Dank.
Chirantan
6
Dies klingt nicht nach einem gültigen Anwendungsfall, der im NotImplementedErrorWesentlichen "plattformabhängig, bei Ihnen nicht verfügbar" bedeutet. Siehe Dokumente .
Skalee
6
Sie ersetzen eine einzeilige Methode durch Metaprogrammierung, müssen jetzt ein Mixin einfügen und eine Methode aufrufen. Ich denke nicht, dass dies deklarativer ist.
Pascal
1
Ich habe diese Antwort bearbeitet, einige Fehler im Code behoben und aktualisiert, sodass sie jetzt ausgeführt wird. Außerdem wurde der Code ein wenig vereinfacht, um Extend anstelle von Include zu verwenden, aufgrund von: yehudakatz.com/2009/11/12/better-ruby-idioms
Magne
1
@ ManishShrivastava: Bitte lesen Sie diesen Code jetzt für den Kommentar über die Wichtigkeit der Verwendung von END hier gegen Ende
Magne
113

Um hier spät dran zu sein, gibt es meines Erachtens keinen Grund, jemanden davon abzuhalten, die abstrakte Klasse zu instanziieren, insbesondere weil er ihr im laufenden Betrieb Methoden hinzufügen kann .

Entensprachsprachen wie Ruby verwenden das Vorhandensein / Fehlen oder Verhalten von Methoden zur Laufzeit, um zu bestimmen, ob sie aufgerufen werden sollen oder nicht. Daher ist Ihre Frage, wie sie für eine abstrakte Methode gilt , sinnvoll

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

und das sollte ungefähr das Ende der Geschichte sein. Der einzige Grund, abstrakte Klassen in Java zu verwenden, besteht darin, darauf zu bestehen, dass bestimmte Methoden "ausgefüllt" werden, während andere ihr Verhalten in der abstrakten Klasse haben. In einer Ententypisierungssprache liegt der Fokus auf Methoden, nicht auf Klassen / Typen, daher sollten Sie Ihre Sorgen auf diese Ebene verschieben.

In Ihrer Frage versuchen Sie im Grunde, das abstractSchlüsselwort aus Java neu zu erstellen. Dies ist ein Code-Geruch für Java in Ruby.

Dan Rosenstark
quelle
3
@ Christopher Perry: Irgendwelche Gründe warum?
SasQ
10
@ChristopherPerry Ich verstehe es immer noch nicht. Warum sollte ich diese Abhängigkeit nicht wollen, wenn Eltern und Geschwister doch verwandt sind und ich möchte, dass diese Beziehung explizit ist? Um ein Objekt einer Klasse innerhalb einer anderen Klasse zu erstellen, müssen Sie auch dessen Definition kennen. Vererbung wird normalerweise als Komposition implementiert. Sie macht lediglich die Schnittstelle des zusammengesetzten Objekts zu einem Teil der Schnittstelle der Klasse, in die es eingebettet ist. Sie benötigen also trotzdem die Definition des eingebetteten oder geerbten Objekts. Oder redest du vielleicht über etwas anderes? Können Sie das näher erläutern?
SasQ
2
@SasQ, Sie müssen die Implementierungsdetails der übergeordneten Klasse nicht kennen, um sie zu erstellen. Sie müssen nur die API kennen. Wenn Sie jedoch erben, sind Sie auf die Implementierung der Eltern angewiesen. Sollte sich die Implementierung ändern, kann Ihr Code auf unerwartete Weise beschädigt werden. Weitere Details hier
Christopher Perry
16
Entschuldigung, aber "Komposition gegenüber Vererbung bevorzugen" sagt nicht "Immer Benutzerkomposition". Obwohl Vererbung im Allgemeinen vermieden werden sollte, gibt es einige Anwendungsfälle, in denen sie einfach besser passen. Folge dem Buch nicht blind.
Nowaker
1
@ Nowaker Sehr wichtiger Punkt. So oft neigen wir dazu, durch Dinge, die wir lesen oder hören, blind zu werden, anstatt zu überlegen, "was in diesem Fall der pragmatische Ansatz ist". Es ist selten ganz schwarz oder weiß.
Per Lundberg
44

Versuche dies:

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

class B < A
  def initialize
  end
end
Andrew Peters
quelle
38
Wenn Sie in der Lage sein möchten, Super-In #initializevon B zu verwenden, können Sie einfach alles in A # -Initialisierung erhöhen if self.class == A.
mk12
17
class A
  private_class_method :new
end

class B < A
  public_class_method :new
end
Bluehavana
quelle
7
Zusätzlich könnte man den geerbten Hook der übergeordneten Klasse verwenden, um die Konstruktormethode automatisch in allen Unterklassen sichtbar zu machen: def A.inherited (Unterklasse); subclass.instance_eval {public_class_method: new}; Ende
t6d
1
Sehr schöner t6d. Stellen Sie als Kommentar sicher, dass es dokumentiert ist, da dies ein überraschendes Verhalten ist (das gegen die geringste Überraschung verstößt).
Bluehavana
16

Für alle in der Rails-Welt erfolgt die Implementierung eines ActiveRecord-Modells als abstrakte Klasse mit dieser Deklaration in der Modelldatei:

self.abstract_class = true
Fred Willmore
quelle
12

Meine 2 ¢: Ich entscheide mich für ein einfaches, leichtes DSL-Mixin:

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

In diesem Fall wäre es natürlich trivial, einen weiteren Fehler zum Initialisieren der Basisklasse hinzuzufügen.

Anthony Navarre
quelle
1
EDIT: Da dieser Ansatz define_method verwendet, möchten Sie möglicherweise sicherstellen, dass die Rückverfolgung intakt bleibt, z. B.: err = NotImplementedError.new(message); err.set_backtrace caller()YMMV
Anthony Navarre
Ich finde diesen Ansatz ziemlich elegant. Vielen Dank für Ihren Beitrag zu dieser Frage.
wes.hysell
12

In den letzten 6 1/2 Jahren der Programmierung von Ruby habe ich kein einziges Mal eine abstrakte Klasse benötigt .

Wenn Sie denken, Sie brauchen eine abstrakte Klasse, denken Sie zu viel in einer Sprache, die sie bereitstellt / erfordert, nicht in Ruby als solcher.

Wie andere vorgeschlagen haben, ist ein Mixin besser für Dinge geeignet, die Schnittstellen sein sollen (wie Java sie definiert), und ein Überdenken Ihres Designs ist besser für Dinge geeignet, die abstrakte Klassen aus anderen Sprachen wie C ++ "benötigen".

Update 2019: Ich habe in 16½ Jahren keine abstrakten Klassen in Ruby benötigt. Alles, was alle Leute, die meine Antwort kommentieren, sagen, wird dadurch angegangen, dass sie Ruby lernen und die entsprechenden Tools wie Module verwenden (die Ihnen sogar allgemeine Implementierungen bieten). Es gibt Leute in Teams, die ich geleitet habe und die Klassen erstellt haben, deren Basisimplementierung fehlschlägt (wie eine abstrakte Klasse), aber diese sind meistens eine Verschwendung von Codierung, da NoMethodErrorsie genau das gleiche Ergebnis wie AbstractClassErrorin der Produktion liefern würden .

Austin Ziegler
quelle
24
Sie brauchen / brauchen / auch keine abstrakte Klasse in Java. Auf diese Weise können Sie dokumentieren, dass es sich um eine Basisklasse handelt, die nicht für Personen instanziiert werden sollte, die Ihre Klasse erweitern.
Fijiaaron
3
IMO, wenige Sprachen sollten Ihre Vorstellungen von objektorientierter Programmierung einschränken. Was in einer bestimmten Situation angemessen ist, sollte nicht von der Sprache abhängen, es sei denn, es gibt einen leistungsbezogenen Grund (oder etwas überzeugenderes).
thekingoftruth
10
@fijiaaron: Wenn du so denkst, dann verstehst du definitiv nicht, was abstrakte Basisklassen sind. Sie sind nicht viel zu "dokumentieren", dass eine Klasse nicht instanziiert werden sollte (es ist eher ein Nebeneffekt davon, dass sie abstrakt ist). Es geht mehr darum, eine gemeinsame Schnittstelle für eine Reihe abgeleiteter Klassen anzugeben, die dann garantiert, dass sie implementiert wird (wenn nicht, bleibt die abgeleitete Klasse auch abstrakt). Ziel ist es, das Liskov-Substitutionsprinzip für Klassen zu unterstützen, für die Instanziierung wenig sinnvoll ist.
SasQ
1
(Forts.) Natürlich kann man nur mehrere Klassen mit einigen gemeinsamen Methoden und Eigenschaften erstellen, aber der Compiler / Interpreter wird dann nicht wissen, dass diese Klassen in irgendeiner Weise zusammenhängen. Die Namen von Methoden und Eigenschaften können in jeder dieser Klassen gleich sein, aber diese allein bedeutet noch nicht, dass sie dieselbe Funktionalität darstellen (die Namenskorrespondenz kann nur zufällig sein). Die einzige Möglichkeit, den Compiler über diese Beziehung zu informieren, besteht in der Verwendung einer Basisklasse. Es ist jedoch nicht immer sinnvoll, dass die Instanzen dieser Basisklasse selbst vorhanden sind.
SasQ
4

Persönlich löse ich NotImplementedError in Methoden abstrakter Klassen aus. Aber vielleicht möchten Sie es aus den von Ihnen genannten Gründen aus der "neuen" Methode herauslassen.

Zack
quelle
Aber wie kann man dann verhindern, dass es instanziiert wird?
Chirantan
Persönlich fange ich gerade erst mit Ruby an, aber in Python rufen Unterklassen mit deklarierten __init ___ () -Methoden die __init __ () -Methoden ihrer Superklassen nicht automatisch auf. Ich würde hoffen, dass es in Ruby ein ähnliches Konzept gibt, aber wie gesagt, ich fange gerade erst an.
Zack
In Ruby werden die initializeMethoden der Eltern nicht automatisch aufgerufen, es sei denn, sie werden explizit mit super aufgerufen.
mk12
4

Wenn Sie mit einer nicht instabilisierbaren Klasse arbeiten möchten, überprüfen Sie in Ihrer A.new-Methode, ob self == A ist, bevor Sie den Fehler auslösen.

Aber wirklich, ein Modul scheint eher das zu sein, was Sie hier wollen - zum Beispiel ist Enumerable die Art von Dingen, die eine abstrakte Klasse in anderen Sprachen sein könnten. Sie können sie technisch nicht unterordnen, aber das Anrufen include SomeModuleerreicht ungefähr das gleiche Ziel. Gibt es einen Grund, warum dies bei Ihnen nicht funktioniert?

Futter
quelle
4

Welchen Zweck möchten Sie mit einer abstrakten Klasse erfüllen? Es gibt wahrscheinlich einen besseren Weg, dies in Rubin zu tun, aber Sie haben keine Details angegeben.

Mein Zeiger ist dies; Verwenden Sie ein Mixin ohne Vererbung.

jshen
quelle
Tatsächlich. Das Mischen eines Moduls entspricht der Verwendung einer AbstractClass: wiki.c2.com/?AbstractClass PS : Nennen wir sie Module und keine Mixins, da Module das sind, was sie sind, und das Mischen in sie das ist, was Sie mit ihnen tun.
Magne
3

Eine andere Antwort:

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

Dies hängt von der normalen #method_missing ab, um nicht implementierte Methoden zu melden, verhindert jedoch, dass abstrakte Klassen implementiert werden (selbst wenn sie eine Initialisierungsmethode haben).

class A
  include Abstract
end
class B < A
end

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

Wie die anderen Poster bereits sagten, sollten Sie wahrscheinlich eher ein Mixin als eine abstrakte Klasse verwenden.

Rampion
quelle
3

Ich habe es so gemacht, also definiert es neu in der Kinderklasse neu, um eine neue in der nicht abstrakten Klasse zu finden. Ich sehe immer noch kein Praktikum darin, abstrakte Klassen in Ruby zu verwenden.

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
quelle
3

Es gibt auch dieses kleine abstract_typeJuwel, mit dem abstrakte Klassen und Module unauffällig deklariert werden können.

Beispiel (aus der Datei 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
シ リ ル
quelle
1

An Ihrem Ansatz ist nichts auszusetzen. Das Auslösen eines Fehlers bei der Initialisierung scheint in Ordnung zu sein, solange natürlich alle Ihre Unterklassen die Initialisierung überschreiben. Aber du willst dich nicht so neu definieren. Folgendes würde ich tun.

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

Ein anderer Ansatz wäre, all diese Funktionen in ein Modul zu integrieren, das, wie Sie bereits erwähnt haben, niemals initiiert werden kann. Nehmen Sie dann das Modul in Ihre Klassen auf, anstatt es von einer anderen Klasse zu erben. Dies würde jedoch Dinge wie super brechen.

Es kommt also darauf an, wie Sie es strukturieren möchten. Obwohl Module wie eine sauberere Lösung zur Lösung des Problems "Wie schreibe ich einige Dinge, die für andere Klassen verwendet werden sollen" erscheinen.

Alex Wayne
quelle
Das würde ich auch nicht wollen. Die Kinder können dann nicht "super" nennen.
Austin Ziegler
0

Obwohl sich das nicht nach Ruby anfühlt, können Sie dies tun:

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

    puts 'initialized'
  end
end

class B < A
end

Die Ergebnisse:

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