Rubinvererbung gegen Mixins

127

Da Sie in Ruby mehrere Mixins einschließen können, aber nur eine Klasse erweitern, scheint es, als würden Mixins der Vererbung vorgezogen.

Meine Frage: Wenn Sie Code schreiben, der erweitert / eingefügt werden muss, um nützlich zu sein, warum würden Sie ihn jemals zu einer Klasse machen? Oder anders ausgedrückt, warum würden Sie es nicht immer zu einem Modul machen?

Ich kann mir nur einen Grund vorstellen, warum Sie eine Klasse wollen, und das ist, wenn Sie die Klasse instanziieren müssen. Im Fall von ActiveRecord :: Base instanziieren Sie es jedoch nie direkt. Sollte es nicht stattdessen ein Modul gewesen sein?

Brad Cupit
quelle

Antworten:

176

Ich habe gerade in The Well-Grounded Rubyist ( übrigens ein großartiges Buch) über dieses Thema gelesen . Der Autor erklärt es besser als ich, also zitiere ich ihn:


Keine einzelne Regel oder Formel führt immer zum richtigen Design. Es ist jedoch hilfreich, einige Überlegungen zu berücksichtigen, wenn Sie Entscheidungen zwischen Klasse und Modul treffen:

  • Module haben keine Instanzen. Daraus folgt, dass Entitäten oder Dinge im Allgemeinen am besten in Klassen modelliert werden und Merkmale oder Eigenschaften von Entitäten oder Dingen am besten in Modulen gekapselt werden. Entsprechend sind Klassennamen, wie in Abschnitt 4.1.1 erwähnt, in der Regel Substantive, während Modulnamen häufig Adjektive sind (Stack versus Stacklike).

  • Eine Klasse kann nur eine Oberklasse haben, aber sie kann so viele Module einmischen, wie sie möchte. Wenn Sie die Vererbung verwenden, sollten Sie vorrangig eine sinnvolle Beziehung zwischen Oberklasse und Unterklasse erstellen. Verwenden Sie nicht die einzige Superklassenbeziehung einer Klasse, um der Klasse etwas zu verleihen, das sich als nur eines von mehreren Merkmalen herausstellen könnte.

Wenn Sie diese Regeln in einem Beispiel zusammenfassen, sollten Sie Folgendes nicht tun:

module Vehicle 
... 
class SelfPropelling 
... 
class Truck < SelfPropelling 
  include Vehicle 
... 

Sie sollten dies vielmehr tun:

module SelfPropelling 
... 
class Vehicle 
  include SelfPropelling 
... 
class Truck < Vehicle 
... 

Die zweite Version modelliert die Entitäten und Eigenschaften viel übersichtlicher. Lkw stammt vom Fahrzeug ab (was sinnvoll ist), während SelfPropelling ein Merkmal von Fahrzeugen ist (zumindest alle, die uns in diesem Modell der Welt interessieren) - ein Merkmal, das an Lkw weitergegeben wird, weil Truck ein Nachkomme ist. oder spezielle Form des Fahrzeugs.

Andy Gaskell
quelle
1
Das Beispiel zeigt es ordentlich - LKW IST ein Fahrzeug - es gibt keinen LKW, der kein Fahrzeug wäre.
PL J
1
Das Beispiel zeigt es ordentlich - TruckIS A Vehicle- es gibt keine Truck, die keine wäre Vehicle. Allerdings würde ich Modul vielleicht nennen SelfPropelable(:?) Hmm SelfPropeledklingt richtig, aber es ist fast das gleiche: D. Jedenfalls würde ich es nicht in, Vehiclesondern in aufnehmen Truck- da es Fahrzeuge gibt, die NICHT sind SelfPropeled. Auch ein guter Hinweis ist zu fragen - gibt es andere Dinge, NICHT Fahrzeuge, die sind SelfPropeled? - Na ja, aber ich wäre schwerer zu finden. So Vehiclekonnte von Klasse SelfPropelling erbt (als Klasse wäre es nicht so passen SelfPropeled- wie das ist eher eine Rolle)
PL J
39

Ich denke, Mixins sind eine großartige Idee, aber hier gibt es ein anderes Problem, das niemand erwähnt hat: Namespace-Kollisionen. Erwägen:

module A
  HELLO = "hi"
  def sayhi
    puts HELLO
  end
end

module B
  HELLO = "you stink"
  def sayhi
    puts HELLO
  end
end

class C
  include A
  include B
end

c = C.new
c.sayhi

Welches gewinnt? In Ruby stellt sich heraus, dass es sich um Letzteres handelt module B, da Sie es danach eingefügt haben module A. Jetzt ist es einfach, dieses Problem zu vermeiden: Stellen Sie sicher, dass sich alle Konstanten und Methoden von module Aund module Bin unwahrscheinlichen Namespaces befinden. Das Problem ist, dass der Compiler Sie bei Kollisionen überhaupt nicht warnt.

Ich behaupte, dass dieses Verhalten nicht auf große Teams von Programmierern skaliert werden kann - Sie sollten nicht davon ausgehen, dass die implementierende Person class Cjeden Namen im Umfang kennt. Mit Ruby können Sie sogar eine Konstante oder Methode eines anderen Typs überschreiben . Ich bin mir nicht sicher, ob dies jemals als korrektes Verhalten angesehen werden kann.

Dan Barowy
quelle
2
Dies ist ein weises Wort der Vorsicht. Erinnert an die Fallstricke bei der Mehrfachvererbung in C ++.
Chris Tonkinson
1
Gibt es dafür eine gute Abschwächung? Dies scheint ein Grund zu sein, warum die Mehrfachvererbung von Python eine überlegene Lösung ist (nicht versuchen, eine Sprach-P * ssing-Übereinstimmung zu starten, sondern nur diese spezielle Funktion vergleichen).
Marcin
1
@bazz Das ist großartig und alles, aber die Komposition in den meisten Sprachen ist umständlich. Es ist auch hauptsächlich in ententypisierten Sprachen relevant. Es garantiert auch nicht, dass Sie keine seltsamen Zustände bekommen.
Marcin
Alter Beitrag, ich weiß, aber immer noch bei der Suche heraus. Die Antwort ist teilweise falsch - C#sayhiAusgaben B::HELLOnicht, weil Ruby die Konstanten verwechselt, sondern weil Ruby Konstanten von näher zu fern auflöst - also würde in immer HELLOverwiesen in aufgelöst . Dies gilt auch dann, wenn die Klasse C auch eine eigene definiert hat . BB::HELLOC::HELLO
Laas
13

Mein Standpunkt: Module dienen zum Teilen von Verhalten, während Klassen zum Modellieren von Beziehungen zwischen Objekten dienen. Sie könnten technisch einfach alles zu einer Instanz von Object machen und die gewünschten Module einmischen, um die gewünschten Verhaltensweisen zu erzielen. Dies wäre jedoch ein schlechtes, zufälliges und eher unlesbares Design.

Futter
quelle
2
Dies beantwortet die Frage direkt: Die Vererbung erzwingt eine bestimmte Organisationsstruktur, die Ihr Projekt lesbarer macht.
Schmirgel
10

Die Antwort auf Ihre Frage ist weitgehend kontextbezogen. Nach der Beobachtung von Pubb wird die Wahl in erster Linie von der betrachteten Domäne bestimmt.

Und ja, ActiveRecord sollte enthalten sein und nicht um eine Unterklasse erweitert werden. Ein anderer ORM - Datamapper - erreicht genau das!

nareshb
quelle
4

Die Antwort von Andy Gaskell gefällt mir sehr gut - ich wollte nur hinzufügen, dass ActiveRecord keine Vererbung verwenden sollte, sondern ein Modul zum Hinzufügen des Verhaltens (meistens Persistenz) zu einem Modell / einer Klasse. ActiveRecord verwendet einfach das falsche Paradigma.

Aus dem gleichen Grund mag ich MongoId gegenüber MongoMapper sehr, da der Entwickler die Möglichkeit hat, die Vererbung als Modellierungsmethode für etwas zu verwenden, das in der Problemdomäne von Bedeutung ist.

Es ist traurig, dass so gut wie niemand in der Rails-Community "Ruby-Vererbung" so verwendet, wie es verwendet werden soll - um Klassenhierarchien zu definieren, nicht nur um Verhalten hinzuzufügen.

Tilo
quelle
1

Ich verstehe Mixins am besten als virtuelle Klassen. Mixins sind "virtuelle Klassen", die in die Ahnenkette einer Klasse oder eines Moduls eingefügt wurden.

Wenn wir "include" verwenden und ihm ein Modul übergeben, wird das Modul der Ahnenkette direkt vor der Klasse hinzugefügt, von der wir erben:

class Parent
end 

module M
end

class Child < Parent
  include M
end

Child.ancestors
 => [Child, M, Parent, Object ...

Jedes Objekt in Ruby hat auch eine Singleton-Klasse. Zu dieser Singleton-Klasse hinzugefügte Methoden können direkt für das Objekt aufgerufen werden und fungieren daher als "Klassen" -Methoden. Wenn wir für ein Objekt "verlängern" verwenden und dem Objekt ein Modul übergeben, fügen wir die Methoden des Moduls der Singleton-Klasse des Objekts hinzu:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

Wir können mit der Methode singleton_class auf die Singleton-Klasse zugreifen:

Test.singleton_class.ancestors
 => [#<Class:Test>, M, #<Class:Object>, ...

Ruby bietet einige Hooks für Module, wenn diese in Klassen / Module gemischt werden. includedist eine von Ruby bereitgestellte Hook-Methode, die aufgerufen wird, wenn Sie ein Modul in ein Modul oder eine Klasse aufnehmen. Genau wie im Lieferumfang enthalten ist ein extendedHaken zum Ausfahren. Es wird aufgerufen, wenn ein Modul um ein anderes Modul oder eine andere Klasse erweitert wird.

module M
  def self.included(target)
    puts "included into #{target}"
  end

  def self.extended(target)
    puts "extended into #{target}"
  end
end

class MyClass
  include M
end

class MyClass2
  extend M
end

Dies schafft ein interessantes Muster, das Entwickler verwenden könnten:

module M
  def self.included(target)
    target.send(:include, InstanceMethods)
    target.extend ClassMethods
    target.class_eval do
      a_class_method
    end
  end

  module InstanceMethods
    def an_instance_method
    end
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

class MyClass
  include M
  # a_class_method called
end

Wie Sie sehen können, fügt dieses einzelne Modul Instanzmethoden, "Klassen" -Methoden hinzu und wirkt direkt auf die Zielklasse (in diesem Fall Aufruf von a_class_method ()).

ActiveSupport :: Concern kapselt dieses Muster. Hier ist das gleiche Modul, das für die Verwendung von ActiveSupport :: Concern umgeschrieben wurde:

module M
  extend ActiveSupport::Concern

  included do
    a_class_method
  end

  def an_instance_method
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end
Donato
quelle
-1

Im Moment denke ich über das templateDesignmuster nach. Mit einem Modul würde es sich einfach nicht richtig anfühlen.

Geo
quelle