Ruby/Mixin
Mixins ermöglichen es, Methoden und Konstanten von Modulen in Klassen zu vererben. Sie stellen eine Möglichkeit dar, Mehrfachvererbung in Ruby umzusetzen. Das Einbinden von Mixins in Klassen geschieht über die Module#include Methode.
Inhaltsverzeichnis
Motivation
Mixins sind ein Konzept in Ruby um Mehrfachvererbung umzusetzen. Jede Klasse in Ruby darf nur eine Superklasse haben und entsprechend nur von einer anderen Klasse erben. Daher bilden alle Klassen in Ruby einen Vererbungsbaum. Will man eine Methode an mehrere Klassen vererben, ist es eigentlich notwendig diesen Klassen eine gemeinsame Superklasse zu geben. Beispielsweise sind sowohl Integer als auch Strings vergleichbar (d.h. sie beherrschen die Vergleichsmethoden <, >, <=, >=, <=>) und müssten daher eine gemeinsame Superklasse haben. Hashs sind hingegen nicht vergleichbar, da es keine sinnvolle Ordnung für Hashs gibt. Gleichzeitig sind Strings und Hashs aber iterierbar, müssten also ebenfalls eine gemeinsame Superklasse haben. Mit Einfachvererbung ist so etwas nicht umsetzbar, daher benötigt eine Sprache entweder Mehrfachvererbung (eine Klasse darf mehrere Superklassen haben) oder ein alternatives Konzept.
Mehrfachvererbung hat diverse Nachteile. Wenn eine Klasse zwei Superklassen hat, die eine gemeinsame Methode definieren, so gibt benötigt man eine klare Festlegung welche von beiden Methoden vererbt werden soll. Ruft man eine Superklassenmethode mit dem super Schlüsselwort auf, so braucht es eine Festlegung von welcher Superklasse man die Methode aufrufen will. Außerdem sind Klassen auch immer eng mit dem internen Datentyp und dessen Speicherallokation verbunden. Ein Array wird z.B. intern anders im Speicher abgelegt als ein String. Die Klasse (oder Superklasse) eines Objekts definiert daher eine Singleton-Methode #allocate, die festlegt wie die Instanzen der Klasse im Speicher angelegt werden. Für ein Objekt, das gleichzeitig String und Array als Superklasse hätte, wäre nicht mehr eindeutig festgelegt auf welche Weise das Objekt im Speicher abgelegt werden soll.
Fazit: Mehrfachvererbung geht mit einer ganzen Reihe an Problemen einher. Ruby verwendet ein Mixin-Konzept, welches die selben Vorteile wie Mehrfachvererbung bietet, ohne aber deren Nachteile in Kauf zu nehmen.
Konzept
Ruby unterscheidet zwischen Klassen und Modulen. Beide Konstrukte dürfen Konstanten und Variablen beinhalten sowie Instanzmethoden definieren. Doch nur Klassen dürfen Instanzen erzeugen und Superklassen haben. Jede Klasse darf nur eine Superklasse haben und besitzt eine Liste von Vorfahren (angefangen bei der Superklasse, endend in Object). Diese Liste lässt sich mit der Class#ancestors Methode abfragen. Mixins sind eine Möglichkeit, diese Vorfahrenliste nachträglich zu ändern. Dies geschieht über die Methode Module#include bzw. Class#include. Die include Methode nimmt ein Modul als Parameter und erzeugt eine sogenannte Mixin-Klasse, die sich zwischen der Klasse und ihrer Superklasse in die Vererbungslinie "einmischt" und welche die selben Eigenschaften (Variablen, Methoden) besitzt wie das Modul. Die Klasse erbt nun alle Methoden von dem Mixin. Das Mixin selbst enthält dieselben Methoden und Variablen von dem Modul und erbt alle Methoden von der Superklasse. Das einmischen von Mixins kann jederzeit stattfinden. Auch lange nach dem Erstellen der Klasse. Die Vererbungslinie der Klasse lässt sich so also nachträglich ändern. Mixins, die einmal erzeugt wurden, lassen sich aber nicht mehr entfernen.
Anwendung
Mit Module#include lassen sich Module als Mixins in Klassen einbinden. Die Methode Object#extend bindet ein Modul als Mixin in die Metaklasse eines Objekts ein, womit das Objekt selbst die Methoden des Mixins erbt.
module A def eine_methode print "A" end end module B def andere_methode print "B" end end module C def weitere_methode print "C" end end class EineKlasse include A include B end objekt = EineKlasse.new objekt.eine_methode #=> A objekt.andere_methode #=> B EineKlasse.ancestors #=> [EineKlasse, B, A, Object, Kernel] objekt.extend C objekt.weitere_methode #=> C
Wenn eine Klasse mehr als ein Mixin einbindet, erbt es immer vom zuletzt eingebundenen Mixin. Dies lässt sich leicht an der Vererbungsliste sehen, die mit der Methode Class#ancestors ausgegeben werden kann.
Mixins vererben nicht nur Methoden, sondern auch Konstanten. Daher ermöglichen es Mixins, Module wie Namespaces zu behandeln.
module Vocabulary GOLD = "Goldmünzen" BATTLE_BEGIN = "Ein neuer Kampf beginnt" end class Battle include Vocabulary #=> alle Konstanten sind nun auch in dieser Klasse direkt verfügbar def initialize print BATTLE_BEGIN #=> Ein neuer Kampf beginnt end end
Singleton-Methoden (Klassenmethoden) von Modulen werden hingegen nicht über das Mixin vererbt!
Modul-Mixins
Module dürfen auch selbst Mixins einbinden und auf diese Weise Methoden erben. Wie auch bei Klassen, dürfen Module nur andere Module als Mixins einbinden, keine Klassen. Wird ein Mixin per include in eine Klasse oder Modul eingebunden, so werden auch alle seine Mixins mit in die Vererbungslinie übernommen. Mixins, die nachträglich hinzugefügt werden, bleiben davon jedoch unbeeinflusst! Dies lässt sich mit folgendem Beispiel zeigen:
module A def eine_methode print "A" end end module B include A end class EineKlasse include B # erbt nun von B und A end module C def weitere_methode puts "C" end end module B include C # fügt C als Mixin in B ein end objekt = EineKlasse.new objekt.eine_methode #=> A objekt.weitere_methode #=> NoMethodError! EineKlasse.ancestors #=> EineKlasse, B, A, Object, Kernel class AndereKlasse include B end objekt2 = AndereKlasse.new objekt2.eine_methode #=> A objekt2.weitere_methode #=> C AndereKlasse.ancestors #=> EineKlasse, B, C, A, Object, Kernel
Beispiel
class Fahrzeug def starte print "los gehts" end end module Motorisiert def starte print "starte Motor" super() end end class LandFahrzeug < Fahrzeug def starte ziel super() print "auf den Landweg nach #{ziel}" end end class LuftFahrzeug < Fahrzeug def starte ziel super() print "auf den Luftweg nach #{ziel}" end end class MotorFlugzeug < LuftFahrzeug include Motorisiert end flugzeug = MotorFlugzeug.new flugzeug.starte "Berlin" #=> starte Motor #=> los gehts #=> auf den Luftweg nach Berlin segelflugzeug = LuftFahrzeug.new segelflugzeug.starte "Berlin" #=> los gehts #=> auf den Luftweg nach Berlin auto = LandFahrzeug.new auto.starte "Berlin" #=> los gehts #=> auf den Landweg nach Berlin auto.extend Motorisiert auto.starte "Berlin" #=> starte Motor #=> los gehts #=> auf den Landweg nach Berlin
Module Functions
Module besitzen eine Methode Module#module_function, welche eine Methode sowohl als Klassen- als auch als private Instanzmethode zur Verfügung stellt. Auf diese Weise lassen sich Namespaces definieren, also Module die Konstanzen und Funktionen beinhalten, die nicht überall im Programm verfügbar sind, die aber dennoch in bestimmten Bereichen (Klassen oder Modulen) aufgerufen werden sollen ohne dabei jedes Mal den Modulnamen davor zu schreiben. Genau wie Module#public, Module#private etc. bekommt die Methode Module#module_function entweder als Parameter den Namen der Methode, oder gar keinen Parameter wodurch alle folgenden Methoden automatisch zu Modulmethoden werden.
module Savegame module_function QUICK_SAVE = 0 def load id # lade Spiel mit gegebener ID end def save id # speichere Spiel mit gegebener ID end end Savegame.load 1 #=> lädt Spiel mit ID 1 Savegame.load Savegame::QUICK_SAVE #=> lädt Schnellspeicherstand class Scene_Title include Savegame def main load QUICK_SAVE #=> Methode ist direkt verfügbar # ... end end
Wichtige Mixins der Core-Library
Kernel
Das Kernel-Modul ist als Mixin in die Object-Klasse eingebunden und steht daher ganz am Ende der Vererbungslinie aller Objekte. Es enthält Methoden die global verfügbar sein sollen, aber dennoch nicht auf den Objekten selbst operieren. Die print Methode beispielsweise soll global verfügbar sein und liegt daher in dem Kernel-Modul. Die Object#class Methode ist ebenfalls global verfügbar, liegt aber in der Object Klasse, weil sie einen Zustand (die Klasse) des Objektes selbst ausliest. Die Methoden in Kernel sind Modul-Funktionen. Daher kann man sowohl Kernel.print als auch direkt print schreiben.
Comparable
Das Comparable-Modul beinhaltet alle Vergleichsmethoden ==, <, >, <= und >=. Um zu funktionieren, benötigt es aber eine Methode die angibt, wie Objekte verglichen werden sollen. Hierfür dient die Methode <=>, die 0 zurückgibt, wenn zwei Objekte gleich sind, 1 wenn das erste größer als das zweite ist und -1 wenn das erste kleiner als das zweite ist sowie nil wenn zwei Objekte nicht vergleichbar sind.
class Waehrung include Comparable attr_reader :value, :type def self.umrechnung von, zu # konvertiere Währung von zu Währung zu end def initialize value, type @value = value @type = type end def <=> other return nil unless other.kind_of? Waehrung umgerechnet = umrechnung(other, @type) @value <=> umgerechnet.value end end euro = Waehrung.new 125, :euro dollar = Waehrung.new 200, :dollar print(euro > dollar) #=> ...
Enumerable
Das Enumerable Mixin enthält alle Methoden, die sich auf Objekte anwenden lassen, die aufzählbar bzw. iterierbar sind. Darunter gehören Methoden wie select, map, sort, min, max usw. Um zu funktionieren, muss die Klasse, die dieses Mixin einbindet, eine Methode each besitzen, die angibt wie das Objekt iteriert wird.
Held = Struct.new :name, :level class HeldenGruppe include Enumerable def initialize @gruppe = [] end def fuege_hinzu held @gruppe << held end def entferne held @gruppe.delete held end def each @gruppe.each do |held| yield held end end end gruppe = HeldenGruppe.new gruppe.fuege_hinzu Held.new("Alex", 13) gruppe.fuege_hinzu Held.new("Valnar", 3) gruppe.fuege_hinzu Held.new("Libra", 17) # gib Held mit höchstem Level aus p gruppe.max {|held1, held2| held1.level <=> held2.level }.name #=> Libra