Ruby/Mixin

Aus Scientia
Wechseln zu: Navigation, Suche

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.

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