Programmierung in Ruby

Der Leitfaden der Pragmatischen Programmierer

Klassen, Objekte und Variablen



Bei den bisher gezeigten Beispielen könnte man sich über unsere Behauptung wundern, Ruby sei eine objekt-orientierte Sprache. Nun ja , in diesem Kapitel werden wir das zeigen. Wir sehen uns an, wie man Klassen und Objekte in Ruby erzeugt sowie einige der Sachen, die Ruby besser als andere objekt-orientierte Sprachen kann. Dabei werden wir unser nächstes Eine-Milliarde-Dollar-Produkt mit entwerfen, die internet-fähige Jazz-und-Blue-Grass-Jukebox.

Nach Monaten harter Arbeit hat unsere hochbezahlte Forschungs- und Entwicklungsabteilung entschieden, dass unsere Jukebox songs braucht. Es wäre also sinnvoll, erstmal eine Ruby-Klasse zu bestimmen, die so etwas wie einen Song repräsentiert. Wir wissen, dass ein richtiger Song einen Namen hat, einen Sänger und eine Spieldauer. Unser Song-Objekt sollte das also auch haben.

Wir erzeugen also als erstes eine Basis-Klasse Song, [Wie wir schon auf Seite 9 sagten, fangen Klassen-Namen mit einem Großbuchstaben an und Methodennamen mit einem Kleinbuchstaben..] die eine einzige Methode enthält, nämlich initialize.

class Song
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
  end
end

initialize ist eine spezielle Methode in Ruby-Programmen. Wenn man Song.new aufruft, um ein neues Song-Objekt zu erzeugen, so erzeugt Ruby erst ein uninitialisiertes Objekt und ruft dann die Methode initialize auf, zusammen mit den Parametern, die wir der new-Methode mitgegeben haben. Dies gibt einem die Möglichkeit, den Anfangszustand des Objekts im Code zu bestimmen.

Bei der Klasse Song benötigt die Methode initialize drei Parameter. Diese Parameter funktionieren innerhalb der Methode genau wie lokale Variablen, so dass sie genau wie diese auch mit einem Kleinbuchstaben anfangen.

Jedes Objekt repräsentiert einen eigenen Song, deshalb muss jedes unserer Song-Objekte seinen eigenen Namen, Künstler und Dauer mit sich tragen. Das heißt, dass wir diese Variablen in Instanz-Variablen innerhalb des Objekts speichern müssen. In Ruby ist eine Instanz-Variable einfach ein Name, der mit einem ``at''-Zeichen (``@'') beginnt. In unserem Beispiel ist der Parameter name verknüpft mit der Instanz-Variablen @name, artist ist verknüpft mit @artist, und duration (die Länge des Songs in Sekunden) ist verknüpft mit @duration.

Testen wir also nun unsere schicke neue Klasse.

aSong = Song.new("Bicylops", "Fleck", 260)
aSong.inspect » "#<Song:0x4018bfc4  @duration=260, @artist=\"Fleck\", @name=\"Bicylops\">"

Das scheint ja nun zu funktionieren. Im Default-Fall gibt die Meldung inspect, die an jedes mögliche Objekt gesendet werden kann, die Id und die Instanz-Variablen aus. Es sieht so aus, als wär das alles ganz korrekt.

Unser Erfahrung sagt uns, dass wir während der Entwicklung den Inhalt eines SongObjektes öfter ausgeben werden, und inspects Default-Formatierung lässt etwas zu wünschen übrig. Glücklicherweise gibt es in Ruby eine Standard-Nachricht, to_s, die an alle Objekte geschickt wird, die man als String wiedergeben will.

aSong = Song.new("Bicylops", "Fleck", 260)
aSong.to_s » "#<Song:0x4018c1b8>"

Das war wohl nichts --- raus kam nur die Id des Objekts. Also werden wir einfach to_s in unserer Klasse überschreiben. Bevor wir das machen, sollten wir kurz darüber sprechen, wie wir Klassendefinitionen in diesem Buch darstellen.

In Ruby sind Klassen niemals geschlossen: man kann jederzeit Methoden zu einer existierenden Klasse hinzufügen. Das gilt für die Klassen die man selber schreibt genauso wie für die Standard-, die eingebauten Klassen. Wenn man neuen Inhalt spezifiziert, wird dieser zu dem vorhandenen hinzugefügt.

Für unser Vorhaben ist das prima. Während wir dieses Kapitel abhandeln und dabei Eigenschaften zu unseren Klassen hinzufügen, werden wir nur die Klassendefinitionen für die neuen Methoden zeigen; die alten sind aber immer noch da. Wir brauchen dann nicht so viel redundantes Zeug in jedem Beispiel zu wiederholen. Wenn man natürlich diesen Code mal eben schnell aus dem Ärmel schüttelt, wird man wahrscheinlich alles zusammen in eine einzelne Klassendefinition packen.

Genug der Einzelheiten, gehen wir zurück zum Hinzufügen der to_s-Methode unserer Song-Klasse.

class Song
  def to_s
    "Song: [email protected][email protected]} ([email protected]})"
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.to_s » "Song: Bicylops--Fleck (260)"

Prima, wir machen Fortschritte. Allerdings sind wir in etwas Kniffliges hineingeraten. Wir sagten, dass Ruby die to_s-Methode bei allen Objekten unterstützt, aber wir sagten nicht wie. Die Antwort hat etwas mit Vererbung, Unterklassen und der Art zu tun, mit der Ruby feststellt, welche Methode denn nun laufen soll, wenn man eine Meldung zu einem Objekt schickt. Die ist das Thema eines neuen Abschnitts, also ...

Vererbung und Meldungen

Vererbung erlaubt die Erzeugung einer Klasse, die eine Verfeinerung oder Spezialisierung einer anderen Klasse ist. Zum Beispiel besitzt unsere Jukebox das Konzept von Songs, die wir einkapseln in der Klasse Song. Dann kommt die Marketingabteilung und erzählt uns, dass wir auch noch Karaoke unterstützen müssen. Ein Karaoke-Song ist genau wie ein normaler auch (es gibt da natürlich keinen Gesang, aber das braucht uns nicht zu interessieren). Allerdings gibt es dazu einen Satz von Textzeilen (lyrics), zusammen mit Zeitangaben zur Synchronisation. Wenn unsere Jukebox einen Karaoke-Song spielt, sollte der Zeilentext über den Bildschirm an der Vorderseite der Jukebox flimmern und das auch noch synchron zur Musik.

Eine Herangehensweise an dieses Problem ist die Definition einer neuen Klasse, KaraokeSong, die genauso ist wie die Song-Klasse, nur mit einem zusätzlichen Lauftext.

class KaraokeSong < Song
  def initialize(name, artist, duration, lyrics)
    super(name, artist, duration)
    @lyrics = lyrics
  end
end

Das ``< Song'' in der Zeile mit der Klassendefinition dagt Ruby, dass ein KaraokeSong eine Unterklasse von Song ist (Das heißt auch, dass Song eine Oberklasse von KaraokeSong ist, wer hätte das gedacht. Man spricht auch von Eltern-Kind Beziehungen, so dass KaraokeSongs Elter dann Song wäre.) Fürs Erste stören wir uns nicht weiter an der initialize-Methode, über diesen super-Aufruf sprechen wir später.

Jetzt erzeugen wir einen KaraokeSong und prüfen, ob unser Code funktioniert. (In dem endgülitngen System wird der Text innerhalb eines Objekts stecken, das den Text und die Synchronisationsinformationen enthält. Um unsere Klasse zu testen, benutzen wir erstmal einen String. Das ist einer der Vorteile einer typ-losen Sprache -- man braucht nicht alles zu definieren, bevor man den Code laufen lässt.)

aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "Song: My Way--Sinatra (225)"

Schön, das läuft, aber warum zeigt die to_s-Methode den Text nicht an?

Dei Antwort hat etwas damit zu tun, wie Ruby feststellt, welche Methode dran ist, wenn man eine Meldung an ein Objekt schickt. Zu dem Zeitpunkt, an dem Ruby den Methodenaufruf aSong.to_s compiliert, weiß es noch gar nicht, wo die Methode zu finden ist. Stattdessen verschiebt es diese Entscheidung auf später, wenn das Programm läuft. Es schaut sich dann die Klasse aSong an. Wenn in der Klasse eine Methode mit dem selben Namen wie die Meldung implementiert ist, so wird diese Methode benutzt. Andernfalls sucht Ruby nach eine Methode in der Elternklasse, dann in der Großelternklasse und so weiter die ganze Leiter der Vorfahren hinauf. Wenns keine Vorfahren mehr gibt und Ruby keine passende Methode gefunden hat, so läuft dann eine spezielle Aktion ab, in deren Verlauf normalerweise ein Fehler geraist wird. [Natürlich kann man einen solchen Fehler abfangen. Damit kann man dann auch Methoden zur Laufzeit simulieren. das wird beschrieben unter Object#method_missing auf Seite 360.]

Nun als zurück zu unserem Beispiel. Wir schickten die Meldung to_s an aSong, ein Objekt der Klasse KaraokeSong. Ruby schaut in KaraokeSong nach eine Methode mit dem Namen to_s, findet dort aber nichts. Der Interpreter sieht dann bei der Elternklasse von KaraokeSong nach, nämlich Song, und findet dort die to_s-Methode, die wir auf Seite 20 definiert haben. Deshalb beschränkt sich die Ausgabe auf die Einzelheiten des Songs und es gibt keinen Text --- die Klasse Song weiß nichts von irgendwelchen Texten.

Diese Problem beheben wir, indem wir KaraokeSong#to_s implementieren. Es gibt mehrere Möglichkeiten das zu tun. Fangen wir mit der schlechteren an. Wir kopieren die to_s-Methode von Song und fügen einfach die Textbehandlung hinzu.

class KaraokeSong
  # ...
  def to_s
    "KS: [email protected][email protected]} ([email protected]}) [[email protected]}]"
  end
end
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "KS: My Way--Sinatra (225) [And now, the...]"

Der Wert der @lyrics-Instanz-Variable wird richtig angezeigt. Dazu greift die Subklasse direkt auf die Instanz-Vatiable ihres Vorfahrens zu. Warum ist das also eine schlecht Art to_s zu implementieren?

Die Antwort hat etwas mit gutem Programmier-Stil zu tun (und mit etwas, das decoupling (Entkoppeln) heißt). Indem wir in dem internen Zustand unserer Elternklasse herumfuchteln, verbinden wir uns fest mit deren Implementation. Wir könnten uns etwa entscheiden, dass Song von jetzt an die Song-Dauer in Millisekunden speichern soll. Dann würde plötzlich KaraokeSong unschöne Werte zurückmelden. Der Gedanke einer Karaoke-Version von ``My Way'', die 3750 Minuten dauert, ist einfach zu schrecklich.

Wir umgehen dieses Problem, wenn jede Klasse ihren inneren Zustand selber handhabt. Wenn KaraokeSong#to_s aufgerufen wird, müssen wir die to_s-Methode des Elternteils aufrufen, um die Song-Details zu bekommen. Daran werden dann die Text-Informationen drangehängt und das ganze zurückgegeben. Der Trick dabei ist das Ruby-Schlüselwort ``super''. Wenn man super ohne Argumente aufruft, schickt Ruby eine Meldung zum Elternteil des aktuellen Objekts und bittet um den Aufruf einer Methode mit dem selben Namen wie die aktuelle Methode. Dabei kriegt die Elternmethode dieselben Parameter mit, die die aktuelle Methode bekommen hatte. Damit können wir unsere neue und verbesserte Methode to_s implementieren.

class KaraokeSong < Song
  # Format ourselves as a string by appending
  # our lyrics to our parent's #to_s value.
  def to_s
    super + " [[email protected]}]"
  end
end
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "Song: My Way--Sinatra (225) [And now, the...]"

Wir erzählen hier Ruby explizit, dass KaraokeSong eine Subklasse von Songist, aber wir haben keine Elternklasse für Song selber angegeben. Wenn man beim Definieren einer Klasse keine Elternklasse angibt, nimmt Ruby automatisch die Klasse Object als Elternklasse und dadurch werden die Instanz-Methoden von Object für jedes andere Objekt verfügbar. Das heißt dass alle Objekte Object als Vorfahren haben und dass die Instanz-Methoden von Object für alle Objekte verfügbar sind. Jetzt wissen wir die Antwort; to_s ist eine von mehr als 35 Instanz-Methoden der Klasse Object. Die komplette Liste fängt auf Seite 356 an.

Vererbung und Mixins

Einige objekt-orientierte Sprachen (insbesondere C++) unterstützen Mehrfach-Vererbung, wobei eine Klasse mehr als einen direkten Elternteil haben kann und Funktionalitäten von jedem erben kann. Obwohl das eine feine Sache ist, kann diese Technik sehr gefährlich sein, weil die Vererbungshierarchie äußerst undurchsichtig werden kann.

Andere Sprachen , wie Java, unterstützen nur Einfach-Vererbung. Dabei kann eine Klasse nur einen direkten Elternteil haben. Obwohl das sauberer (und einfacher zu implementieren) ist, hat die Einfach-Vererbung auch Nachteile --- In der Realen Welt erben die Sachen Eigenschaften aus vielerlei Quellen (ein Ball ist sowohl ein zurückspringendes Ding als auch ein rundes Ding).

Bei Ruby gibts einen interessanten und geeigneten Kompromiss, der die Einfachheit der Einfach-Vererbung besitzt und die Kraft der Mehrfach-Vererbung. Eine Ruby-Klasse kann nur einen direkten Vorfahr besitzen, Ruby ist also eine Sprache mit Einfach-Vererbung. Darüber hinaus können Ruby-Klassen eine beliebige Anzahl von Mixins einbinden (ein Mixin ist eine Art Teil-Klassen-Definition). Das ergibt eine kontrollierte mehrfach-vererbungs-ähnliche Fähigkeit ohne die Nachteile. Genauer werden wir Mixins ab Seite 100 untersuchen.

Bis jetzt haben wir uns in diesem Kapitel Klassen und ihre Methoden angesehen. Jetzt ist es Zeit, sich den Objekten zuzuwenden, wie den Instanzen der Klasse Song.

Objekte und Attribute

Die Song-Objekte, die wir bisher erzeugt haben, besitzen einen internen Zustand (wie der Song-Titel und Künstler). Dieser Zustand ist privat zu diesen Objekten, kein anderes Objekt kann auf diese Instanz-Variablen zugreifen. Normalerweise ist das eine gute Sache. Es bedeutet, dass das Objekt selber zuständig ist für die Aufrechterhaltung seiner eigenen Konsistenz.

Natürlich ist ein Objekt, das völlig eingekapselt ist, etwas nutzlos -- man kann es erzeugen, aber dann kann man nichts mehr damit anfangen. Normalerweise definiert man Methoden, um auf den Zustand eines Objekts zuzugreifen und ihn zu verändern, damit die Außenwelt mit dem Objekt interagieren kann. Diese von außen sichtbaren Eigenschaften eines Objekts nennt man Attribute.

Bei unserem Song-Objekt wäre das erste was wir bräuchten die Fähigkeit, den Titel und den Künstler herauszufinden (damit wir das anzeigen können, während der Song abgespielt wird), sowie die Dauer (damit wir eine Art Fortschrittsbalken anzeigen können).

class Song
  def name
    @name
  end
  def artist
    @artist
  end
  def duration
    @duration
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.artist » "Fleck"
aSong.name » "Bicylops"
aSong.duration » 260

Hier haben wir drei Zugriffs-Methoden definiert, um den Wert von drei Instanz-Attributen zurückzugeben. Weil dies ein sehr gebräuchliches Codefragment ist, gibt es bei Ruby dafür eine Abkürzung: attr_reader erzeugt die Zugriffs-Methode automatisch.

class Song
  attr_reader :name, :artist, :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.artist » "Fleck"
aSong.name » "Bicylops"
aSong.duration » 260

In diesem Beispiel haben wir etwas neues eingeführt. Das Konstrukt :artist ist ein Ausdruck, der ein mit artist verbundenes Symbol-Objekt zurückliefert. Man kann sich :artist als name der Variablen artist vorstellen, während das einfache artist der Wert der Variablen ist. In diesem Beispiel nannten wir die Zugriffs-Methoden name, artist und duration. Die dazugehörenden Instanz-Variablen @name, @artist und @duration werden automatisch erzeugt. Diese Zugriffs-Methoden sind identisch zu denen, die wir davor von Hand geschrieben haben.

Beschreibbare Attribute

Manchmal muss es möglich sein, auf ein Atribut von außen zu schreiben. Als Beispiel nehmen wir an, die Dauer eines Songs wird von außen vorgegeben (möglicherweise von CD gelesen oder aus einem MP3-Datenstrom). Wenn wir den Song das erste Mal spielen, müssen wir irgendwie die Dauer herausfinden und diesen neuen Wert dann in dem Song-Objekt speichern.

In Sprachen wie C++ und Java macht man dies mit Setter-Funktionen.

class JavaSong {                     // Java code
  private Duration myDuration;
  public void setDuration(Duration newDuration) {
    myDuration = newDuration;
  }
}
s = new Song(....)
s.setDuration(length)

In Ruby kann auf ein Attribut eines Objekts genauso wie auf andere Variablen zugegriffen werden. Wir haben das schon oben gesehen bei Ausdrücken wie aSong.name. Also scheint es nur natürlich, den Namen einer solchen Variablen zu benutzen, wenn man auf den Wert eines Attributs zugreifen will. Dem Prinzip der geringsten Überraschung folgend funktioniert das in Ruby genau so, wie uns das erhoffen.

class Song
  def duration=(newDuration)
    @duration = newDuration
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration » 260
aSong.duration = 257   # setzt in attribute den neuen Wert ein
aSong.duration » 257

Die Zuweisung ``aSong.duration = 257'' ruft die Methode duration= des aSong-Objekts auf und gibt 257 als Argument mit. Es ist also so: wenn ein Methodenname mit einem Gleichheitszeichen endet, dann kann dieser Name auf der linken Seite einer Zuweisung stehen.

Natürlich gibts bei ruby dafür wieder eine Abkürzung.

class Song
  attr_writer :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration = 257

Virtuelle Attribute

Die Zugriffs-Methoden auf Attribute müssen nicht nur einfache Kapseln um die Instanz-Variablen sein. Man möchte zum Beispiel die Dauer in Minuten und Bruchteilen von Minuten angeben statt in Sekunden, wie wir das bisher gemacht haben.

class Song
  def durationInMinutes
    @duration/60.0   # force floating point
  end
  def durationInMinutes=(value)
    @duration = (value*60).to_i
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.durationInMinutes » 4.333333333
aSong.durationInMinutes = 4.2
aSong.duration » 252

Hier haben wir Attribut-Methoden benutzt, um eine virtuelle Instanz-Variable zu erzeugen. Für die äußere Welt scheint durationInMinutes ein Attribut wie jedes andere zu sein. Intern gibt es dazu keine Instanz-Variable.

Das ist keinesfalls eine Seltsamkeit. In seinem wegweisenden Buch Object-Oriented Software Construction  nennt Bertrand Meyer dies das Uniform Access Principle (Prinzip des einheitlichen Zugriffs). Indem man den Unterschied zwischen Instanz-Variablen und berechneten Werten verbirgt, trennt man den Rest der Welt ab von der Implementation der Klasse. Dann hat man die Freiheit, interne Abläufe zu ändern, ohne dass die Millionen von Code-Zeilen, die diese Klasse benutzen, geändert werden müssen. Das ist ein großer Gewinn.

Klassen-Variablen und Methoden-Variablen

Bis jezt haben alle von uns erzeugten Klassen Instanz-Variablen und Instanz-Methoden besessen: Variablen, die mit jeweils einer Instanz dieser Klasse verbunden waren und Methoden, die mit diesen Variablen gearbeitet haben. Manchmal müssen Klassen selber einen Zustand besitzen. Damit kommen nun Klassen-Variablen ins Spiel.

Klassen-Variablen

Eine Klassen-Variable wird von allen Objekten dieser Klasse gemeinsam benutzt, und man kann auch mit Klassen-Methoden, die wir später beschreiben, darauf zugreifen. Es gibt immer nur ein Original und keine Kopie von jeder Klassen-Variablen pro Klasse. Klassen-Variablen fangen mit zwei ``at''-Zeichen an, wie ``@@count''. Anders als globale oder Instanz-Variablen müssen Klassen-Variablen vor Gebrauch initialisiert werden. Oftmals ist diese Initialisierung eine einfache Zuweisung im Rumpf der Klassendefinition.

Als Beispiel soll unsere Jukebox sich merken, wie oft jeder Tirel gespielt wurde. Diese Anzahl könnte man gut als Instanz-Variable des Song-Objektes realisieren. Wenn ein Song gespielt wird, wird der Wert in dieser Instanz um eins erhöht. Wenn wir nun aber wissen wollen, wieviele Songs überhaupt gespielt wurden? Wir könnten alle Song-Objekte durchsuchen und ihre Zähler aufaddieren, wir könnten auch die Exkommunizierung aus der Kirche des guten Designs riskieren und eine globale Variable benutzen. Stattdessen nehmen wir eine Klassenvariable.

class Song
  @@plays = 0
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
    @plays    = 0
  end
  def play
    @plays += 1
    @@plays += 1
    "This  song: [email protected] plays. Total #@@plays plays."
  end
end

Fürs Deguggen lassen wir Song#play einen String zurückgeben, der die Anzahl angibt, wie oft dieser Song gespielt wurde, zusammen mit der Gesamtanzahl für alle Songs. Wir können das ganz einfach testen.

s1 = Song.new("Song1", "Artist1", 234)  # test songs..
s2 = Song.new("Song2", "Artist2", 345)
s1.play » "This  song: 1 plays. Total 1 plays."
s2.play » "This  song: 1 plays. Total 2 plays."
s1.play » "This  song: 2 plays. Total 3 plays."
s1.play » "This  song: 3 plays. Total 4 plays."

Klassen-Variablen sind privat zu der Klasse und ihren Instanzen. Wenn man von außen auf sie zugreifen können soll, muss man für sie eine Zugriffs-Methode schreiben. Diese Methode kann entweder eine Instanz-Methode sein oder, das führt uns nahtlos zum nächsten Abschnitt, eine Klassen-Methode.

Klassen-Methoden

Manchmal muss eine Klasse Methoden unterstützen, die ohne Bindung an ein bestimmtes Objekt funktionieren.

Wir sind schon über eine solche Methode gestolpert. Die new-Methode erzeugt ein neues Song-Objekt, ist aber selbst nicht mit einem bestimmten Song verbunden.

aSong = Song.new(....)

Man findet Klassen-Methoden überall verteilt in den Ruby-Bibliotheken. Als Beispiel repräsentiert ein Objekt der Klasse File eine offene Datei des darunterliegenden Datei-Systems. Darüber hinaus unterstützt die Klasse File mehrere Methoden, um Dateien zu manipulieren, die nicht geöffnet sind und deshalb auch kein File-Objekt haben. Wenn man eine Datei löschen will, benutzt man die Klassen-Methode File.delete und gibt ihr den Namen mit.

File.delete("doomedFile")

Klassen-Methoden unterscheidet man von Instanz-Methoden durch ihre Definition. Klassen-Methoden werden definiert, indem man den Klassennamen und einen Punkt vor den Methodennamen setzt.

class Example

  def instMeth              # Instanz-Methode   end

  def Example.classMeth     # Klassen-Methode   end

end

Jukebox nehmen Geld pro gespieltem Song, nicht pro Minute. Das macht kurze Songs profitabler als lange. Deshalb wollen wir, dass zu lange Songs nicht auf unserer Song-Liste verfügbar sein sollen. Dazu könnten wir eine Klassen-Methode in SongList definieren, die prüft, ob ein bestimmter Song eine Grenzdauer überschreitet. Zum Setzen dieser Grenze benutzen wir eine Klassen-Konstante, das ist eine einfache Konstante (schon vergessen? Die fangen mit einem Großbuchstaben an), die im Klassen-Rumpf definiert wird.

class SongList
  MaxTime = 5*60           #  5 minutes
  def SongList.isTooLong(aSong)
    return aSong.duration > MaxTime
  end
end
song1 = Song.new("Bicylops", "Fleck", 260)
SongList.isTooLong(song1) » false
song2 = Song.new("The Calling", "Santana", 468)
SongList.isTooLong(song2) » true

Singletons und andere Konstruktoren

Manchmal möchte man die Standard-Methode, mit der Ruby Objekte erzeugt, überschreiben. Als Beispiel sehen wir uns mal wieder die Jukebox an. Weil wir viele Jukeboxes übers ganze Land verteilt haben, möchten wir die Wartung so einfach wie möglich machen. Ein Teil der Anforderungen ist es, alles was passiert in ein Log zu schreiben: Die gespielten Songs, das erhaltene Geld, die seltsamen Flüssigkeiten, die hineingegossen werden und so weiter. Weil wir aber die Netzwerk-Bandbreite für die Musik nutzen wollen, werden wir diese Dateien lokal speichern. Das bedeutet, dass wir eine Klasse brauchen, die dieses Loggen handhabt. Natürlich wollen wir nur ein Log-Objekt pro Jukebox und wir wollen, dass dieses Objekt von den anderen Objekten gemeinsam benutzt wird.

Füge das Singleton-Muster ein, dokumentiert in Design Patterns . (der Überstzer: Häh, was soll ich wo einfügen, wieso und warum. Ich glaube, hier stimmt was nicht im Original.) Wir werden das so machen, dass es nur eine Möglichkeit gibt, ein Log-Objekt zu erzeugen, nämlich der Aufruf von Logger.create. Und wir werden sicherstellen, das immer nur höchstens ein Objekt erzeugt wird.

class Logger
  private_class_method :new
  @@logger = nil
  def Logger.create
    @@logger = new unless @@logger
    @@logger
  end
end

Indem wir Loggers Methode new privat machen, verhindern wir, dass jemand mit dem üblichen Konstruktor ein Log-Objekt erzeugt. Anstelle dessen stellen wir eine neue Klassen-Methode zur Verfügung: Logger.create. Diese benutzt die Klassen-Variable @@logger, die eine Referenz auf eine einzelne (die einzige) Instanz von logger hält und diese Instanz jedesmal zurückgibt, wenn sie aufgerufen wird. [Achtung: Die Implemantation von Singeletons, die wir hier vorstellen, ist nicht thread-sicher; wenn mehrere Threads am laufen sind, ist es möglich, mehrere Log-Objekte zu erzeugen. Statt selber Thread-Sicherheit zu erzeugen, würden wir wenn nötig eher das Singleton-Mixin verwenden, das es zu Ruby dazugibt und das auf Seite 472 beschrieben ist..] Wir können dies prüfen, indem wir uns die von der Methode zurückgegebenen Objekt-Identifikatoren ansehen.

Logger.create.id » 537684700
Logger.create.id » 537684700

Das Benutzen von Methoden als Pseudo-Konstruktoren kann auch für die Benutzer dieser Klassen das Leben leichter machen. Als einfaches Beispiel möge hier die Klasse Shape dienen, die ein regelmäßiges Polygon repräsentiert. Instanzen von Shape werden erzeugt, indem man dem Konstruktor die benötigte Seiten-Anzahl und den Durchmesser mitgibt.

class Shape
  def initialize(numSides, perimeter)
    # ...
  end
end

Nun ja, nach einigen Jahren wird diese Klasse in einer anderen Applikation verwendet, wo die Programmierer normalerweise diese Formen erzeugen, indem sie den Namen des Polygons und eine Seitenlänge angeben, und nicht die Seitenzahl und den Durchmesser. In diesem Fall kann man einfach ein paar weitere Klassen-Methoden zu Shape hinzufügen.

class Shape
  def Shape.triangle(sideLength)
    Shape.new(3, sideLength*3)
  end
  def Shape.square(sideLength)
    Shape.new(4, sideLength*4)
  end
end

Es gibt viele interessante und mächtige Anwendungsarten für Klassen-Methoden, aber alle zu untersuchen würde unsere Jukebox kein bisschen fertiger machen, also fahren wir jetzt fort.

Zugriffs-Kontrolle

Wenn man ein Klassen-Interface (die Schnittstelle nach draußen) erstellt, ist es wichtig sich zu überlegen, wieviel Einflussmöglichkeiten man der Außenwelt auf diese Klasse gewähren will. Wenn man zuviel erlaubt, riskiert man, dass zu viele Kopplungen in der Applikation vorhanden sind --- Benutzer diesser Klasse sind versucht, sich auf Details der Innereien der Klasse zu verlassen, statt auf die Interface-Beschreibung. Die gute Nachricht ist, dass die einzige Methode, den Zustand eines Objekts zu ändern, im Aufruf einer seiner Methoden liegt. Wenn man den Zugriff auf die Methoden kontrolliert, kontrolliert man den Zugriff auf das Objekt selber. Im Regelfall sollte man niemals Methoden zur Verfügung stellen, die das Objekt in einen unsicheren Zustand bringen könnten. In Ruby gibt es drei Ebenen der Zugriffskontrolle.

Der Unterschied zwischen ``protected'' und ``private'' ist nur klein und ist in Ruby anders als den meisten gebräuchlichen OO-Sprachen. Wenn eine Methode protected ist, kann sie von jeder Instanz der definierenden Klasse oder ihrer Sub-Klassen aufgerufen werde. Wenn eine Klasse private ist, kann sie nur von innerhalb des eigenen Objekts aufgerufen werden --- es ist niemals möglich, die private-Methoden eines anderen Objekts direkt aufzurufen, selbst wenn das Objekt zu derselben Klasse gehört, wie der Aufrufer.

Ruby unterscheidet sich noch in einem anderen wichtigen Punkt von anderen OO-Sprachen. Zugriffskontrolle findet dynamisch statt, wenn das Programm läuft, nicht statisch. Man bekommt eine Zugriffsverletzung nur dann geliefert, wenn der Code versucht eine eingeschränkte Methode auszuführen.

Zugriffskontrolle setzen

Man setzt den Level der Zugriffskontrolle innerhalb der Definition der Klasse oder des Moduls, indem man eine oder mehrere dieser drei Funktionen benutzt: public, protected und private. Jede dieser Funktionen kann auf zwei verschiedene Arten benutzt werden.

Wenn man sie ohne Argumente benutzt, setzen diese Funktionen die Zugriffskontrolle auf danach definierten Methoden. Dies ist wahrscheinlich für C++ oder JavaProgrammierer ganz bekannt, wo man Schlüsselwörter wie public benutzt, um denselben Effekt zu erzielen.

class MyClass

      def method1    # default ist 'public'         #...       end

  protected          # die folgende Methode ist 'protected'

      def method2    # die ist 'protected'         #...       end

  private            # die folgende Methode ist 'private'

      def method3    # die ist 'private'         #...       end

  public             # die folgende Methode ist 'public'

      def method4    # und die ist wieder 'public'         #...       end end

Alternativ kann man die Zugriffskontrolle von benannten Methoden auch einstellen, indem man sie als Argumente den Zugriffs-Kontroll-Funktionen übergibt.

class MyClass

  def method1   end

  # ... und so weiter

  public    :method1, :method4   protected :method2   private   :method3 end

Die initialize-Methode einer Klasse ist automatisch private.

Jetzt ist es Zeit für ein paar Beispiele. Vielleicht sollten wir ein Kontensystem entwickeln, wo jeder Schuldner ein nur ihm zugeordnetes Guthaben hat. Weil wir diese Zuordnung schützen wollen, machen wir die Methoden zum Setzen der Guthaben und Schulden private, und das externe Interface definieren wir mit Mitteln der Transaktion.

class Accounts

  private

    def debit(account, amount)       account.balance -= amount     end     def credit(account, amount)       account.balance += amount     end

  public

    #...     def transferToSavings(amount)       debit(@checking, amount)       credit(@savings, amount)     end     #... end

Protected Zugriff wird benutzt, wenn Objekte Zugriff auf den internen Zustand von anderen Objekten der selben Klasse benötigen. Als Beispiel möchten wir den einzelnen Account-Objekten erlauben, ihr Guthaben zu vergleichen, aber gleichzeitig möchten wir das vor dem Rest der Welt geheimhalten (vielleicht wollen wir das dort in einer schöneren Form ausdrücken).

class Account
  attr_reader :balance       # Zugriffs- Methode 'balance'

  protected :balance         # wird hier protected gemacht

  def greaterBalanceThan(other)     return @balance > other.balance   end end

Weil das Attribut balance protected ist, ist es nur innerhalb von Account-Objekten verfügbar.

Variablen

Nachdem wir nun den ganzen Ärger mit dem Erzeugen dieser Objekte hatten, wollen wir doch sicher gehen, sie nicht wieder zu verlieren. Variablen werden benutzt, um die Objekte im Griff zu behalten; jede Variable enthält eine Referenz auf ein Objekt.

Variablen speichern Objektreferenzen

Figur 03.1 Variablen speichern Objektreferenzen

Wir werden das mit etwas Code zeigen.

person = "Tim"
person.id » 537684980
person.type » String
person » "Tim"

In der ersten Zeile erzeugt Ruby eine neues String-Objekt mit dem Wert ``Tim''. Eine Referenz dieses Objekts wird in die lokale Variable person geschoben. Ein kurzer Check zeigt, dass die Variable tatsächlich die Rolle eines Strings angenommen hat, mit einer Objekt-Id, einem Typ und einem Wert.

Also ist eine Variable ein Objekt?

In Ruby lautet die Antwort ``Nein''. Eine Variable ist einfach nur eine Referenz auf ein Objekt. Objekte schwimmen in einem großen Topf umher (meistens der Heap (der Übersetzer: der köchelt irgendwo im computer rum)) und die Variablen zeigen auf sie.

Jetzt machen wir das Beispiel etwas komplizierter.

person1 = "Tim"
person2 = person1
person1[0] = 'J'
person1 » "Jim"
person2 » "Jim"

Was passiert da? Wir änderten den ersten Buchstaben von person1, aber beide person1 und person2 änderten sich von ``Tim'' zu ``Jim.''

Das kommt alles daher, dass Variablen Referenzen auf Objekte halten, nicht die Objekte selber. Die Zuweisung von person1 zu person2 erzeugt gar kein neues Objekt; sie kopiert einfach nur person1s Objekt-Referenz auf person2, so dass beide person1 und person2 auf das selbe Objekt zeigen. Wir zeigen dies in Figur 3.1 auf Seite 33.

Die Zuweisung von Alias-Objekten gibt einem möglicherweise mehrere Variablen, die aufs selbe Objekt zeigen. Kann das nicht zu Problemen führen? Ja, es kann, aber nicht so oft, wie man denkt (Objekte in Java etwa funktionieren nach genau dem gleichen Schema). Im Speziellen kann man solch ein Aliasing vermeiden, indem man wie in dem Beispiel aus Fugur 3.1 die dup-Methode von String benutzt, die ein neues String-Objekt erzeugt mit dem identischen Inhalt.

person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1 » "Jim"
person2 » "Tim"

Man kann auch jeden daran hindern ein Objekt zu ändern, indem man es freezed (einfriert) (dazu mehr auf Seite 255). Versucht man ein eingefrorenes Objekt zu ändern, so wird Ruby eine TypeError-Exception auslösen.

person1 = "Tim"
person2 = person1
person1.freeze       # verhindert Modifikationen an dem Objekt
person2[0] = "J"
erzeugt:
prog.rb:4:in `=': can't modify frozen string (TypeError)
	from prog.rb:4


Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Übersetzung: Jürgen Katins
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".