Programmierung in Ruby

Der Leitfaden der Pragmatischen Programmierer

Module



Module sind eine Art und Weise, Methoden, Klassen und Konstanten zu gruppieren. Module haben zwei große Vorteile
  1. Module bieten einen Namensraum und verhindern Namenskollisionen.
  2. Module implementieren Mixins.

Namensräume

Sobald Ihre Programme in Ruby größer und größer werden, werden Sie normalerweise bemerken, dass Sie wiederverwertbaren Code produzieren -- Bibliotheken mit ähnlichen Routinen, die allgemein anwendbar sind. Sie werden diesen Code in separaten Dateien abspeichern möchten, damit ihn auch andere Ruby-Programme verwenden können.

Dieser Code wird oft in Klassen organisiert sein, also werden Sie vermutlich eine Klasse (oder eine Reihe von Klassen, die in Beziehung zueinander stehen) in eine Datei stecken.

Manchmal kommt es jedoch vor, dass man Dinge gruppieren möchte, die normalerweise keine Klasse bilden.

Ein erster Ansatz könnte sein, all diese Dinge in eine Datei zu schreiben und diese dann von jedem Programm, in dem die sie benötigt werden, laden zu lassen. Die Programmiersprache C funktioniert auf diese Weise. Sagen wir, sie schreiben ein Reihe von Trigonometriefunktionen wie sin, cos usw. Sie stopfen sie alle in die Datei trig.rb, damit auch spätere Generationen Spaß daran haben können. In der Zwischenzeit arbeitet Sally an einer Simulation von Gut und Böse und schreibt eine Reihe von nützlichen Routinen wie beGood und sin (Sünde) und steckt sie in die Datei action.rb. Joe, der ein Programm schreiben möchte, um herauszufinden, wie viele Engel auf dem Kopf einer Stecknadel tanzen können, muss sowohl trig.rb als auch action.rb in sein Programm laden. Beide definieren aber eine Methode namens sin. Schlechte Neuigkeiten.

Die Antwort ist der Modulmechanismus. Module können einen Namensraum definieren, einen Sandkasten, in dem Ihre Methoden und Konstanten spielen können, ohne befürchten zu müssen, dass sie von anderen Methoden und Konstanten getreten werden. Die Trigonometriefunktionen wandern in ein Modul:

module Trig
  PI = 3.141592654
  def Trig.sin(x)
   # ..
  end
  def Trig.cos(x)
   # ..
  end
end

und die guten und schlechten Taten in ein anderes:

module Action
  VERY_BAD = 0
  BAD      = 1
  def Action.sin(badness)
    # ...
  end
end

Modulkonstanten werden, wie Klassenkonstanten, mit großem Anfangsbuchstaben geschrieben. Auch die Definitionen von Methoden schauen gleich aus: Modulmethoden werden so wie Klassenmethoden definiert.

Wenn ein drittes Programm diese Module verwenden will, kann es einfach die beiden Dateien laden (mittels der Anweisung require, die wir auf Seite 105 besprechen) und auf die dort bestimmten Namen verweisen.

require "trig"
require "action"

y = Trig.sin(Trig::PI/4) wrongdoing = Action.sin(Action::VERY_BAD)

Wie auch Klassenmethoden, ruft man Modulmethoden auf, indem man ihnen den Modulnamen und einen Punkt voranstellt. Für Konstanten verwendet man den Modulnamen und zwei Doppelpunkte.

Mixins

Module haben einen anderen wunderbaren Verwendungszweck. Auf einen Schlag eliminieren sie den Bedarf nach Mehrfachvererbung, indem sie Mixins bereitstellen.

In den Beispielen des vorigen Abschnitts haben wir Modulmethoden definiert, indem wir den Namen des Moduls den Namen der Methoden vorangestellt haben. Wenn Sie dabei an Klassenmethoden gedacht haben, wird ihr nächster Gedanke wohl sein: ``Was passiert, wenn ich eine Instanzmethoden innerhalb eines Moduls definiere?'' Gute Frage. Ein Modul kann keine Instanzen haben, denn ein Modul ist keine Klasse. Man kann jedoch innerhalb einer Klassendefinition ein Modul inkludieren. Wenn das geschieht, sind auf einmal alle Instanzmethoden des Moduls auch für die Klassen zugänglich. Sind wurden beigemischt (mixed in). In der Tat verhalten sich ``mixed-in'' Module wie Superklassen.

module Debug
  def whoAmI?
    "#{self.type.name} (\##{self.id}): #{self.to_s}"
  end
end
class Phonograph
  include Debug
  # ...
end
class EightTrack
  include Debug
  # ...
end
ph = Phonograph.new("West End Blues")
et = EightTrack.new("Surrealistic Pillow")
ph.whoAmI? » "Phonograph (#537683810): West End Blues"
et.whoAmI? » "EightTrack (#537683790): Surrealistic Pillow"

Durch das Inkludieren des Moduls Debug erlangen sowohl Phonograph als auch EightTrack Zugriff zur Instanzmethode whoAmI?.

Bevor wir weitermachen, noch einige Punkte über die Anweisung include: Erstens hat sie nichts mit Dateien zu tun. C-Programmierer verwenden eine Präprozessor-Anweisung namens #include, um während des Compilierens den Inhalt einer Datei in eine andere einzufügen. In Ruby erstellt include aber einfach eine Referenz auf ein benanntes Modul. Wenn dieses Modul in einer separaten Datei steht, muss diese zuerst mit require geladen werden, bevor include benutzt werden kann. Zweitens kopiert include nicht einfach die Instanzmethoden eines Moduls in die Klasse. Stattdessen erstellt Ruby eine Referenz von der Klasse zum inkludierten Modul. Wenn mehrere Klassen dieses Modul inkludieren, zeigen sie alle auf dasselbe Ding. Wenn Sie die Definition einer Methode innerhalb eines Moduls ändern, auch während das Programm läuft, zeigen alle Klassen das neue Verhalten.[Natürlich sprechen wir hier nur von Methoden. Instanzvariablen zum Beispiel gelten immer jeweils für ein Objekt.]

Mixins stellen einen wunderbar kontrollierten Weg bereit, um Funktionalität zu Klassen hinzuzufügen. Die wahre Kraft von Mixins kommt aber erst zur Geltung, wenn der Code im Mixins mit dem in der Klasse, das das Mixin benutzt, zu interagieren beginnt. Nehmen wir als Beispiel das Standardmixin Comparable. Dieses Mixin kann benutzt werden, um die Vergleichsoperatoren (<, <=, ==, >= und >) sowie die Methode between? zu einer Klasse hinzuzufügen. Damit das funktioniert, setzt Comparable voraus, dass der Benutzer den Operator <=> definiert. Als Klassenschreiber definieren Sie also die eine Methode <=>, inkludieren Comparable und erhalten sechs Vergleichsmethoden gratis. Probieren wir das mit unserer Klasse Song aus, indem wir die Lieder anhand ihrer Dauer vergleichen. Alles, was wir tun müssen, ist das Modul Comparable zu inkludieren und den Vergleichsoperator <=> zu implementieren.

class Song
  include Comparable
  def <=>(other)
    self.duration <=> other.duration
  end
end

Mit einigen Testsongs können wir überprüfen, ob die Ergebnisse vernünftig sind:

song1 = Song.new("My Way",  "Sinatra", 225)
song2 = Song.new("Bicylops", "Fleck",  260)
song1 <=> song2 » -1
song1  <  song2 » true
song1 ==  song1 » true
song1  >  song2 » false

Schließlich haben wir auf Seite 45 eine Implementation der Funktion inject aus Smalltalk gezeigt, die wir in die Klasse Array implementierten. Wir versprachen, dass wir diese Funktion allgemeiner anwendbar machen würden. Gibt es einen besseren Weg, also daraus ein Mixin Modul zu machen?

module Inject
  def inject(n)
     each do |value|
       n = yield(n, value)
     end
     n
  end
  def sum(initial = 0)
    inject(initial) { |n, value| n + value }
  end
  def product(initial = 1)
    inject(initial) { |n, value| n * value }
  end
end

Wir können das nun testen, indem wir das Mixin zu einigen eingebauten Klassen hinzufügen:

class Array
  include Inject
end
[ 1, 2, 3, 4, 5 ].sum » 15
[ 1, 2, 3, 4, 5 ].product » 120

class Range
  include Inject
end
(1..5).sum » 15
(1..5).product » 120
('a'..'m').sum("Letters: ") » "Letters: abcdefghijklm"

Für ein umfangreicheres Beispiel eines Mixins schauen Sie sich die Dokumentation des Moduls Enumerable an, die auf Seite 407 beginnt.

Instanzvariablen in Mixins

Leute, die von C++ zu Ruby kommen, fragen uns oft: ``Was passiert in einem Mixin mit Instanzvariablen? In C++ muss ich einige Hürden überwinden, um kontrollieren zu können, wie Variablen in einer Hierarchie von Mehrfachvererbung gemeinsam benutzt werden. Wie geht Ruby damit um?''

Das ist für Anfänger nicht wirklich eine faire Frage, sagen wir ihnen dann. Erinnern Sie sich, wie Instanzvariablen in Ruby funktionieren: Die erste Erwähnung einer Variable mit einem ``@''-Präfix erzeugt die Instanzvariable im aktuellen Objekt, self.

Für Mixins heißt das, dass das Modul, das Sie Ihrer Unterklasse beimischen, Instanzvariablen im Unterobjekt erzeugen kann und attr und Ähnliches verwenden kann, um Zugriffsmethoden für diese Instanzvariablen zu definieren. Ein Beispiel:

module Notes
  attr  :concertA
  def tuning(amt)
    @concertA = 440.0 + amt
  end
end

class Trumpet   include Notes   def initialize(tune)     tuning(tune)     puts "Instance method returns #{concertA}"     puts "Instance variable is [email protected]}"   end end

# Das Klavier ist ein bisschen tief: Trumpet.new(-5.3)
erzeugt:
Instance method returns 434.7
Instance variable is 434.7

Wir haben nicht nur Zugriff auf die definierten Methoden, sondern können auch auf die notwendigen Instanzvariablen zugreifen. Natürlich gibt es hier das Risiko, dass verschiedene Mixins Instanzvariablen mit denselben Namen verwenden und es zur Kollision kommt:

module MajorScales
  def majorNum
    @numNotes = 7 if @numNotes.nil?
    @numNotes # Gib 7 zurück
  end
end

module PentatonicScales   def pentaNum     @numNotes = 5 if @numNotes.nil?     @numNotes # Gib 5 zurück?   end end

class ScaleDemo   include MajorScales   include PentatonicScales   def initialize     puts majorNum # Sollte 7 sein     puts pentaNum # Sollte 5 sein   end end

ScaleDemo.new
erzeugt:
7
7

Die zwei Stückchen Code, die wir beimischen, verwenden beide eine Instanzvariable namens @numNotes. Unglücklicherweise entspricht das Ergebnis nun wahrscheinlich nicht der Absicht des Autors.

Zum Großteil versuchen Mixin-Module nicht, eigene Instanzdaten mitzuführen -- sie verwenden Zugriffsmethoden, um Daten von der Unterklasse zu erhalten. Wenn Sie aber ein Mixin erstellen, das seinen eigenen Status haben muss, vergewissern Sie sich, dass die Instanzvariablen eindeutige Namen tragen, damit sie von jedem anderen Mixin im System unterschieden werden können (vielleicht, indem Sie den Namen des Moduls als Teil des Variablennamens verwenden).

Iteratoren und das Modul Enumerable

Sie haben vermutlich bemerkt, dass Rubys Sammlungsklassen eine große Zahl von Operationen unterstützen, die eine Reihe von Dingen mit der Kollektion anstellen: sie durchlaufen sie, sortieren sie und so weiter. Sie könnten nun denken: ``Mann, es wäre sicher nett, wenn meine Klasse all diese megaaffentollen Fähigkeiten hätte!'' (wenn Sie das wirklich gedacht haben, wird es vermutlich Zeit, dass Sie aufhören, Wiederholungen von Fernsehsendungen aus den 60er Jahren anzuschauen.[Wenn Sie Österreicher sind und auch das gedacht haben, wäre es überhaupt an der Zeit aufzuhören, Fernsehserien aus Deutschland anzuschauen, Anm. d. Übers.])

Gut, unsere Klassen können, dank der Magie von Mixins und des Moduls Enumerable, all diese megaaffentollen Fähigkeiten unterstützen. Alles, was Sie tun müssen, ist einen Iterator namens each zu schreiben, der die Elemente Ihrer Sammlung zurückgibt. Mischen Sie Enumerable und plötzlich unterstützt Ihre Klasse Dinge wie map, include? und find_all?. Wenn sich die Objekte ihrer Sammlung mit der Methode <=> auch sinnvoll ordnen lassen, können Sie weiters min, max und sort verwenden.

Andere Dateien einfügen

Weil Ruby es einfach macht, guten, modularen Code zu schreiben, werden Sie öfters feststellen, dass Sie kleine Dateien mit eigenständiger Funktionalität produzieren -- ein Interface zu x, ein Algorithmus um y zu tun und so weiter. Normalerweise organisieren Sie diese Dateien als Klassen oder Modulbibliotheken.

Nachdem Sie diese Dateien erstellt haben, werden Sie sie in Ihr neues Programm aufnehmen wollen. Ruby hat zwei Anweisungen, um das zu tun.

load "filename.rb"

require "filename"

Die Methode load fügt die genannte Datei mit Ruby Sourcecode jedesmal, wenn Sie sie aufrufen, ein, während require jede gegebene Datei nur ein einziges Mal lädt. require hat zusätzliche Funktionalität: Es kann verteilte Binärbibliotheken laden. Beide Routinen akzeptieren relative und absolute Pfade. Wenn Sie einen relativen Pfad (oder nur einen einzelnen Dateinamen) angeben, durchsuchen sie jedes Verzeichnis im aktuellen Load-Pfad ($:, siehe Seite 142) nach der Datei.

Dateien, die mit load und require geladen wurden, können natürlich auch andere Dateien inkludieren, die wiederum Dateien laden usw. Nicht so offensichtlich ist, dass require eine ausführbare Anweisung ist -- require kann innerhalb einer if-Anweisung stehen oder einen String enthalten, der gerade erst zusammengesetzt wurde. Auch der Suchpfad kann während der Laufzeit verändert werden. Fügen Sie dazu einfach das Verzeichnis, das sie möchten, an den String $: hinzu.

Da load den Source ohne Abfrage lädt, kann man es benutzen, um eine Datei, die vielleicht seit dem Programmaufruf geändert wurde, erneut zu laden:

5.times do |i|
   File.open("temp.rb","w") { |f|
     f.puts "module Temp\ndef Temp.var() #{i}; end\nend"
   }
   load "temp.rb"
   puts Temp.var
 end
produces:
0
1
2
3
4


Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Übersetzung: Johannes Tanzler
Für das englische Original:
© 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0. That reference is available for download.
Diese Lizenz sowie das Original vom Herbst 2001 bilden die Grundlage der Übersetzung
Es wird darauf hingewiesen, dass sich die Lizenz des englischen Originals inzwischen geändert hat.
Für die deutsche Übersetzung:
© 2002 Jürgen Katins
Der Copyright-Eigner stellt folgende Lizenzen zur Verfügung:
Nicht-freie Lizenz:
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.
Freie Lizenz:
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".