Ruby Tutorial: Eine Einladung in Ruby

Winfried Mueller, www.reintechnisch.de, Start: 28.11.2003, Stand: 05.09.2006

Übersicht

1. Einleitung

Was ist Ruby? Ruby ist eine recht neue Programmiersprache, die zur Zeit die Herzen vieler Programmierer erobert. Warum denn schon wieder eine neue Programmiersprache? Haben wir mit C++, Java, PHP, Perl, Python, TCL/TK, Basic, Pascal, Smalltalk nicht schon genügend Auswahl?

Seit mehr als 10 Jahren breitet sich die objektorientierte Sofwareentwicklung aus. Dies ist nicht nur eine weitere Technik sondern eine völlig neue Weise, auf die Welt zu schauen. Objektorientierte Entwickler denken in anderen Kategorien. Neue und andersartige Denkmuster entstehen, die in vielen Situationen zu besserem und eleganterem Code führen. Objektorientiertes Denken ist natürlicher, weil dem gewohnten Denken im Alltag näher. Man kann die Realität direkter abbilden. Gerade Programmier-Anfänger können sich in objektorientiertes Denken oft schneller einfinden, als in herkömmliche strukturierte Programmierung.

Wer objektorientiert programmieren möchte, kann sich bisher vorhandener Programmiersprachen widmen - die meisten können das mittlerweile. Sie sind in dieser Hinsicht erweitert worden. Wer aber wirklich Spaß am objektorientierten Programmieren erleben möchte, der braucht eine echte objektorientierte Sprache. Eine Sprache, die vom ersten Atemzug objektorientiert designt wurde.

Und wenn man jetzt schaut, was es da gibt, so wird es schon etwas enger. Da gibt es z.B. Smalltalk, Eiffel und ... ja eben Ruby.

Ruby bietet die Vorteile einer Skriptsprache wie Perl. Überhaupt ist Ruby Perl sehr ähnlich. Manche sagen, Ruby wäre das objektorientierte Perl. Ruby läuft genauso wie Perl, auf fast jeder Maschine und ist schnell installiert. Mit Ruby kann man all das tun, was man auch mit Perl, Python & Co. machen kann.

Wo jedoch Perl Objektorientierung zulässt, lädt Ruby in jeder Zeile Code dazu ein. Wer also den Genuss objektorientierter Programmierung in vollen Zügen genießen will, sollte Ruby verwenden.

Ruby hat eine interessante Historie und ist gar nicht so neu. Die Entwicklung begann bereits 1993 durch den Japaner Yukihiro Matsumoto (Spitzname Matz). 1995 veröffentlichte er die erste Version. Ruby breitete sich in Japan sehr schnell aus. Und es wuchs auch ziemlich getrennt vom Rest der Welt in Japan weiter. Erst zur Jahrtausendwende konzentrierte sich Matz darauf, die Sprache außerhalb von Japan bekannt zu machen. Zu dieser Zeit gab es in Japan schon über 20 Bücher zu Ruby und es war dort mittlerweile mehr verbreitet als Python oder Perl.

Ein großes Problem bei der Verbreitung war vor allem die Sprachbarriere - viele Dokumentationen waren nur in japanisch verfügbar. In den letzten Jahren erschienen jedoch mehrere Bücher zu Ruby in englisch und auch in deutsch. Artikel in iX, CT und einigen Linux-Magazinen machten die Sprache auch in Deutschland bekannt.

Das Besondere war, dass plötzlich eine ziemlich ausgewachsene und stabile Sprache verfügbar war, die zuvor nur in Japan genutzt wurde. Sonst wachsen Projekte international und man kann ihre Entwicklung beobachten. Hier wurde die OpenSource-Welt mit einem reifen Produkt überrascht. Sehr schnell fanden sich viele begeisterte Programmierer auf der ganzen Welt, die diese Sprache lieben lernten. Mittlerweile arbeiten etwa 40 Programmierer weltweit direkt an der Sprache. Und viele tausende schreiben an Bibliotheken oder Anwendungen, die sie als freie Software ins Netz stellen.

Mit diesem Ruby-Tutorial möchte ich vor allem Lust auf die Sprache machen. Ich werde viele Beispiele verwenden, um praxisnah zu zeigen, wie einfach Aufgaben umzusetzen sind. Es zeigt auch, wie wohlstrukturiert die Sprache Ruby aufgebaut ist.

Es gibt relativ wenig echte Profi-Programmierer, aber jede Menge Menschen, die öfters mal ein paar Zeilen Code schreiben müssen, um eine Aufgabe zu automatisieren. Ich möchte Ihnen zeigen, dass Ruby hierfür eine ideale Sprache ist.

Ruby eignet sich besonders für Aufgaben der Textmanipulation, für Webentwicklung, für Netzwerk-Werkzeuge, Systemadministration, Software-Prototypen, Client-Server Systeme, Datenbankanbindungen, XML-Verarbeitung und Werkzeuge aller Art. Anwendungen im Bereich WWW machen derzeit wohl den größten Anteil aus, zumindest was die veröffentlichten Projekte angeht. Aber selbst für so spezielle Sachen, wie die Steuerung von Spielautomaten, wurde Ruby erfolgreich eingesetzt. Ruby fühlt sich auf jeden Fall dort zu Hause, wo sich auch Perl und Python tummeln. Auch für umfangreiche Projekte ist es gut geeignet.

Auch wenn Ruby auf vielen Plattformen läuft, beschränke ich mich hier auf Linux. Dies deshalb, weil ich sonst zu viele plattformabhängige Dinge besprechen müsste, die eher verwirren. Unter Linux kann sich Ruby voll entfalten, unter anderen Systemen gibt es evtl. kleinere Einschränkungen. Trotzdem werden wohl viele Beispiele auch unter Windows laufen, z.T. habe ich diese auch unter Windows getestet. Der Windows Ruby-Port ist nämlich wirklich exzellent und voll praxistauglich.

2. Voraussetzungen

Der Artikel wendet sich an Menschen, die noch wenig Programmier-Erfahrung haben. Von Vorteil ist es, wenn man schon mal das eine oder andere Shell- oder Perl-Skript geschrieben hat. Oder wenn man überhaupt schon mal mit einer Programmiersprache gearbeitet hat.

Auf jeden Fall sollte man auf seinem Linux-System ein wenig mit der Kommando-Shell vertraut sein (typischerweise die bash). Man sollte auch wissen, wie man in seiner Distribution Pakete neu installiert, wie man einen Texteditor bedient und wie man sich im Dateisystem bewegt.

Ich werde Beispiele nicht bis ins kleinste Detail erklären. Wer sich hier oder da genauer informieren möchte, wie es funktioniert, dem empfehle ich die wunderbare deutsche Übersetzung "Programmierung in Ruby" von Jürgen Katins, siehe [1].

3. Installation

Wer unter Linux arbeitet, dürfte mit der Installation neuer Pakete vertraut sein. Jede neuere Distribution beinhaltet Ruby. Wähle Sie es einfach aus und installieren es. Dem Power-User steht natürlich auch nichts im Wege, sich die Quellen direkt von http://www.ruby-lang.org zu holen und selber zu compilieren.

Für die, die auch unter Windows nicht auf Ruby verzichten wollen: Es gibt, wie bei OpenSource oft typisch, mehrere Wahlmöglichkeiten. Einige Leute fühlten sich berufen, Pakete für Windows anzubieten und jedes Paket ist in anderer Hinsicht optimiert. Eine Auswahl findet man ebenfalls unter http://www.ruby-lang.org. Ich empfehle zu Anfang, auf das Paket aus [8] zurückzugreifen, weil das eine Rundum-Sorglos-Lösung ist, welche genauso leicht zu installieren ist, wie viele andere Windows-Programme.

Die Beispiele habe ich alle mit der Version 1.6.8 getestet, die sehr stabil läuft. Mittlerweile (2003) ist die stark erweiterte Version 1.8 verfügbar, auf der auch alle Beispiele laufen sollten.

4. Ein erster Test

Ruby ist ein Interpreter. Man schreibt sein Programm in eine Textdatei und übergibt diese Ruby zum ausführen. Das Verfahren ist genauso, als hätte man ein Shell-, oder Perl-Skript geschrieben.

Handwerkszeug ist also ein Texteditor und die Shell.

Es gibt viele nette Texteditoren, die auch Syntax-Highlighting für Ruby unterstützen. Zu Anfang reicht der immer und überall verfügbare vi.

Unser erstes Programm sieht so aus:

 
puts "Hello World!"

Dies speichern wir in hello.rb und führen es mittels des Ruby-Interpreters auf der Shell aus:

 
wm@leo:~$ ruby hello.rb
Hello World!

Unser Programm gibt also "Hello World!" auf dem Bildschirm aus. Ein großer Schritt, zeigt das doch, dass unser System bereit ist, auch tausende von Anweisungen entgegenzunehmen. Der Ruby Interpreter funktioniert. Jetzt können wir uns größeren Aufgaben widmen.

5. Projekt mp3 Encoder Batch

Lernen am Beispiel ist eine gute Idee: Nehmen wir an, wir haben mit cdparanoia eine CD eingelesen und nun eine Anzahl von .wav-Dateien. Diese sollen nun alle ins mp3-Format gewandelt werden.

Dies kann man mit lame machen. Die Aufrufsyntax ist

 
lame [options] <infile> <outfile>

Man kann also nicht direkt in einem Rutsch sämtliche .wav-Dateien in .mp3 umwandeln sondern müsste lame für jede .wav Datei aufrufen. Nichtswissend von anderen Möglichkeiten wollen wir dies nun mit einem Ruby-Skript automatisieren.

Das erklärte Ziel ist also: Nimm jede .wav-Datei in einem Verzeichnis und wandle es in eine .mp3 Datei um.

Und hier die Umsetzung:

 
#! /usr/bin/ruby

dir = "."

puts "ripall Verzeichnis: [#{dir}]"
Dir.foreach( dir ) do |file|
  next if file !~ /\.wav$/
  src = File.join( dir, file )
  dst = file.sub( /\.wav$/, ".mp3" )

  dst = File.join( dir, dst )
  puts "Ripping #{dst}..."
  puts `lame "#{src}" "#{dst}"`
  if $? == 0
    File.delete( src )
  end
end

Das Skript nennen wir ripall und legen es in das Verzeichnis ab, wo auch die .wav-Dateien liegen. Nun starten wir es:

 
wm@leo:~/test$ ./ripall
ripall Verzeichnis: [.]
Ripping ./track01.cdda.mp3...
LAME version 3.93 MMX  (http://www.mp3dev.org/)
Using polyphase lowpass  filter, transition band: 15115 Hz - 15648 Hz
Encoding ./track01.cdda.wav to ./track01.cdda.mp3
Encoding as 44.1 kHz 128 kbps j-stereo MPEG-1 Layer III (11x) qval=2
    Frame          |  CPU time/estim | REAL time/estim | play/CPU |    ETA
  8150/10649  (77%)|    4:07/    5:23|    4:12/    5:30|   0.8609x|    1:17

Ein Ruby-Skript kann man auf mehrere Arten starten. Was immer geht, ist der Aufruf von ruby Scriptname, also hätten wir auch ruby ripall schreiben können.

Ein Skript kann man aber auch direkt starten, in dem man es mit seinem Namen aufruft. Das geht dann, wenn das entsprechende x-flag der Datei gesetzt wurde, z.B. so:

 
wm@leo:~/test$ chmod 777 ripall

Jetzt können wir es mit ripall oder ./ripall starten, je nachdem, ob das aktuelle Verzeichnis auch im Shell-Suchpfad eingebunden ist.

Damit dies funktioniert, ist jedoch eine weitere Angabe im Skript selber nötig. In der ersten Zeile des Skripts muss #! /usr/bin/ruby stehen. Dies ist ein allgemeiner Linux-Mechanismus: Beim Aufruf eines Skripts wird nachgeschaut, ob in der ersten Zeile der Befehlsinterpreter angegeben ist, mit dem das Skript bearbeitet werden soll. In unserem Fall ist das ruby. Bei Shell-Skripten liest man oft #! /bin/bash.

Gehen wir nun das Skript Zeile für Zeile durch.

 
dir = "." 

Hier wird eine lokale Variable angelegt und ein Wert zugewiesen. Der "." steht für das aktuelle Verzeichnis. Genauso könnte man hier absolute Pfadangaben machen, wo sich das Verzeichnis befindet, in welchem die .wav Dateien liegen. Lokale Variablen beginnen bei Ruby immer mit einem kleinen Buchstaben. Ein Name, der mit einem Großbuchstaben beginnt, ist eine Konstante, der man nur einmal einen Wert zuweisen kann. Später kann dieser Wert nicht mehr verändert werden. In diesem Fall hier hätten wir auch eine Konstante verwenden können. Wir ändern das Verzeichnis später ja nicht.

 
puts "ripall Verzeichnis: [#{dir}]"}

Der Befehl puts dient zur Ausgabe eines Strings auf der Konsole (stdout). Puts bewirkt eine automatische Zeilenschaltung nach Ausgabe. Möchte man dies nicht, so verwendet man print. In dem String wird vor der Ausgaben eine sogenannte Variablen-Substitution ausgeführt. Man kann nämlich über #{variable} Ruby mitteilen, dass es den Inhalt der Variable dort einsetzt.

 
Dir.foreach( dir ) do |file| ... end

Hier haben wir eine echte Ruby-Spezialität, die uns noch oft begegnen wird. Dir ist hierbei eine Klasse, die eine Klassenmethode foreach hat. Was Klassen sind, dazu kommen wir später noch. Der Methode foreach übergibt man ein Verzeichnisnamen. Sie wird dann für jede Datei, die es in dem Verzeichnis findet, den Codeblock zwischen do und end ausführen, wobei über die Variable file jeweils der aktuelle Dateiname abrufbar ist.

Diese Form der Abarbeitung ist sehr typisch für Ruby-Programme. Immer dann, wenn man mit einer Menge von Dingen etwas tun muss, findet man dieses Konstrukt in ähnlicher Form. Dieser Mechanismus ist wesentlich leistungsfähiger als normale Schleifen, wie man sie aus anderen Sprachen kennt.

Konkret bedeutet es hier, dass für jede .wav-Datei der folgende Codeblock ausgeführt wird.

 
next if file !~ /\.wav$/

Hier finden wir eine weitere Spezialität aus dem Unix-Bereich: reguläre Ausdrücke. Es ist eine sehr leistungsfähige Möglichkeit, in Strings zu suchen. Das Programm grep und viele weitere Unix Tools machen Gebrauch davon. Wer unter Linux arbeitet, sollte sich mit regulären Ausdrücken auskennen.

Konkret versuchen wir mit dieser Zeile, Dateien, die nicht mit ".wav" enden, nicht zu verarbeiten, denn diese sollen ja nicht konvertiert werden. Es könnten also in dem Verzeichnis auch beliebige andere Dateien liegen, die alle nicht bearbeitet würden. Aber auch dann, wenn wir keinerlei andere Dateien hier liegen haben, brauchen wir diese Zeile, weil nämlich jedes Verzeichnis die Spezialdateien "." und ".." beinhaltet, die wir ausschließen müssen.

Next bedeutet hier, dass der Codeblock nicht weiterverarbeitet werden soll. Es soll vielmehr sofort der nächste Durchlauf starten. Next wird aber nur dann ausgeführt, wenn die Bedingung hinter if wahr ist. Der Operator !~ steht für String nicht gleich dem Muster. Es ist hier wie in Perl. Überhaupt ähnelt Ruby in vielem der Programmiersprache Perl.

 
src = File.join( dir, file )

Der Variable src wird hier der vollständige Name der Quelldatei mit Pfad übergeben. Hierzu müssen dir und file miteinander verbunden werden. Folgende Zeile hätte es auch getan:

 
src = dir + "/" + file

Will man jedoch plattformunabhängig programmieren, was generell eine gute Idee ist, so nutzt man besser die Methode join der Klasse File hierzu. Unter Windows würde dann nämlich automatisch ein "\" anstatt eines "/" verwendet.

 
dst = file.sub( /\.wav$/, ".mp3" )

Die Zieldatei soll ja genauso heißen, wie die Quelldatei, lediglich die Endung .wav soll durch .mp3 ausgetauscht werden. Die Variable file ist vom Typ String bzw. gehört der Klasse String an. Dieses Objekt kennt die Methode sub, mit der man ein Teil im String substituieren, also austauschen kann. Wir suchen hierzu mit einem regulären Ausdruck nach der Endung .wav und tauschen die dann durch .mp3 aus. Die Methode sub gibt dann den kompletten String zurück, hier also den Dateinamen.

 
dst = File.join( dir, dst )

Auch bei der Zieldatei müssen wir mit join den kompletten Dateinamen mit Pfadangabe erzeugen. Jetzt steht also in src z.B. ./MeinSong.wav und in dst ./MeinSong.mp3.

 
puts "Ripping #{dst}..."

Bevor wir mit dem rippen beginnen, machen wir noch eine Ausgabe auf der Konsole, welche Datei wir jetzt bearbeiten. Auch hier wird #{dst} wieder durch den Inhalt der Variablen ersetzt.

 
puts `lame "#{src}" "#{dst}"`

Hier wird jetzt lame aufgerufen. In Ruby kann man externe Programme über sogenannte Backticks "`" aufrufen. Alles, was man zwischen dies Backticks schreibt, wird als Shell-Befehl ausgeführt, so als hätte man es per Hand in die Shell eingetippt. Aber auch hier wird vorher noch eine Variablen-Substitution durchgeführt. Es wird also #{src} und #{dst} durch den Inhalt der Variablen getauscht. Wozu aber die doppelten Anführungsstriche, in die die Variablen gesetzt sind? Es könnte sein, dass der Dateiname auch Leerzeichen enthält. Und dann würde der Befehl nicht korrekt abgearbeitet. Sie werden also der Shell weitergereicht, haben nichts mit Ruby zu tun.

Das puts vor dem Befehlsaufruf braucht es deshalb, weil Ruby alle Ausgaben, die ein Befehl auf die Konsole schreiben würde, umleitet. Es ist sozusagen der Rückgabewert des Aufrufs. Damit die Ausgabe also trotzdem auf die Konsole geht, wird sie mit puts ausgegeben. Genauso gut hätte man sie in Ruby einer Variablen zuweisen können, um dann damit weitere Aktionen durchzuführen. So könnte man z.B. eine html-Seite aus der Kommando-Rückgabe basteln, um dies dann an einen Webserver auszuliefern.

 
if $? == 0
  File.delete( src )
end

Bisher haben wir nur lokale Variablen benutzt. Diese leben nur im Kontext, sobald dieser verlassen wird, sind sie nicht mehr existent. Im Beispiel existieren src und dst nicht mehr, sobald der Codeblock zwischen do und end verlassen wird. Die Variable dir lebt dagegen das ganze Programm hinweg, weil sie im äußersten Kontext angelegt ist.

Daneben gibt es globale Variablen, die generell für die ganze Programmlaufzeit verfügbar sind. Sie sind auch immer und überall erreichbar, was bei lokalen Variablen nicht der Fall ist.

Globale Variablen beginnen immer mit einem $-Zeichen. Eine vordefinierte globale Variable ist $?. Das sieht für eine Variable etwas komisch aus, aber auch hier hat Ruby sich bei Perl bedient. In ihr steht jeweils der Rückgabewert des letzten extern ausgeführten Kommandos. Jedes Kommando, was sich in der Shell ausführen lässt, gibt einen sogenannten Return-Code zurück, der etwas darüber aussagt, ob das Kommando ordnungsgemäß ausgeführt werden konnte. 0 bedeutet, dass das Kommando korrekt ausgeführt wurde.

Und nur dann, wenn also das korrekte .mp3 File vorhanden ist, löschen wir auch die Quelldatei. Vorsichtige Programmierung könnte man das nennen. Hierzu benutzen wir ein if, ähnlich wie es in anderen Sprachen üblich ist. Nur wenn die Bedingung zutrifft, werden alle Befehle bis zum end ausgeführt. Wir rufen hier die Methode delete der Klasse File auf, um die Quelldatei zu löschen. File ist dabei eine Klasse der Ruby Standard-Bibliothek. Übrigens, man hätte es auch so schreiben können:

 
File.delete( src ) if $? == 0

Soweit ein erstes Beispiel einer kleinen praktischen Anwendung von Ruby. Einige Fragen bin ich schuldig geblieben, weil sie ein tieferes Verständnis benötigen. Dem möchte ich mich als nächstes zuwenden. Es geht um ein paar ganz grundlegende objektorientierte Konzepte. Und weil Ruby eine von Grund auf objektorientierte Sprache ist, durchziehen diese Ideen fast jede Zeile Ruby-Code. Es ist also wichtig, darüber ein gewisses Verständnis zu entwickeln.

6. Objektorientierung

Bei Computerprogrammen gibt es vor allem 2 Dinge: Es gibt Daten und es gibt Programmcode, der mit Daten irgend etwas anstellt. Daten werden in Form von Variablen gespeichert bzw. bei größeren Mengen und dauerhaft in Datenbanken oder Dateien. Durch den Programmcode werden Variablen mit Inhalten gefüllt, Berechnungen durchgeführt, Daten ausgegeben, transformiert oder nach irgendwelchen Regeln verändert.

Bei der herkömmlichen Art, der prozeduralen Programmierung, wurden Daten und Programmcode getrennt gehalten. Es gab Daten auf der einen Seite und Prozeduren bzw. Funktionen, die einen bestimmten Zweck erfüllten auf der anderen Seite. Funktionen wurden mit Daten gespeist und gaben Daten zurück, es gab aber keinen strukturellen Zusammenhang zwischen Daten und Programmcode.

Bei der objektorientierten Programmierung (OOP) stehen die Daten im Mittelpunkt. Eine Menge von Daten ergeben etwas funktional Ganzes, etwas Zusammengehöriges. Genauso wie eine Anzahl von Menschen eine Gruppe bilden können. Die Gruppe ist mehr als eine Ansammlung von Menschen, sie ist etwas neues Ganzes. Ein Auto besteht aus vielen Einzelteilen und ergibt zusammengesetzt etwas neues Ganzes, was man eben Auto nennt.

Mit diesem Ganzen, also der Menge an Daten oder Variablen, verbindet man nun noch Programmcode, der dieses Ganze mit Fähigkeiten ausstattet. So entsteht also etwas, was Eigenschaften hat (Variablen) und mit Fähigkeiten ausgestattet ist (Programmcode oder konkreter Funktionen bzw. Methoden).

Man kann das gut vergleichen mit einem CD-Player. Auf der CD befinden sich Informationen, also Daten. Das Format, wie die Daten gespeichert sind, ist festgelegt. Dadurch ist der Player in der Lage, etwas mit den Daten anzufangen, er stellt Fähigkeiten zur Verfügung, mit der CD umgehen zu können: Er kann sie abspielen, er kann von Titel zu Titel springen, er kann starten und stoppen, kann die Lautstärke einstellen usw. CD und Player sind so ein gekapselte autonome Einheit. Der Player weiß nichts davon, wie eine Waschmaschine funktioniert und eine CD kann nicht in einer Waschmaschine abgespielt werden. Player und CD gehören zusammen, sind füreinander geschaffen. Auf der CD befinden sich die Daten, der Player stellt Methoden zur Verfügung, mit den Daten was anzufangen und diese Ergebnisse an die Umwelt zu kommunizieren. Oder umgedreht - von der Umwelt (dem Benutzer) etwas entgegenzunehmen und dadurch Funktionen auszuführen.

Zurück zur Programmierung. Ein objektorientiertes Gesamtprogramm ist im Idealfalls nichts weiter, als eine Zusammenfügung einer Anzahl solcher Ganzen, die jetzt zu Teilen des neuen Ganzen werden.

Es ist etwas Grundlegendes, dass ein objektorientiertes Programm aus Ganzen besteht, die Teil eines neuen Ganzen sind, die wiederum Teil eines übergeordneten Ganzen werden. Und jeder dieser Teile ist relativ autonom und hat die Intelligenz, sich um sich selbst zu kümmern und mit anderen Teilen zu interagieren. Dies deshalb, weil jedes Teil neben den Daten auch Programmcode enthält, der weiß, wie es mit diesen Daten interagieren muss.

Und um nicht weiter so allgemein von Ganzen sprechen zu müssen, verrate ich jetzt die schon längst fälligen Begriffe: In der objektorientierten Programmierung sprechen wir von Klassen und Objekten und meinen diese recht autonom fungierenden Teile, die für sich etwas Ganzes sind.

Solch ein Design hat viele Vorteile. Es ist gut wiederverwendbar, weil relativ autonome und allgemeingültige Bausteine entstehen. Aus diesem Grund sind auch die meisten Bibliotheksfunktionen in fast allen modernen Sprachen mittlerweile objektorientiert. Solch ein Design ist gut erweiterbar und veränderbar. Es ist auch gut überschaubar, weil die Komplexität begrenzt wird. Weil jeder Teil recht autonom fungiert, kann die Funktionsweise eines Teiles recht schnell überblickt werden, ohne das Gesamtprogramm verstanden zu haben.

Objektorientierung ist ein allgemeines Design-Paradigma. Um objektorientiert programmieren zu können, muss eine Sprache bestimmte Konstrukte zur Verfügung stellen. Im Idealfall sollte es so sein, dass man ein objektorientiertes Design eines Programmes komplett losgelöst von der eingesetzten Programmiersprache machen kann. Und dieses Design muss dann in jeder OO-Sprache umsetzbar sein. Es gibt also Objektorientierung allgemein und eine objektorientierte Umsetzung des Problems in Ruby.

Wer schon in mehreren prozeduralen Sprachen Probleme gelöst hat, weiß ja, dass man auch hier ganz losgelöst von der Sprache ein Problem prozedural beschreiben kann und es dann in unterschiedlichsten Sprachen meist sehr ähnlich implementieren könnte.

Für das allgemeine objektorientierte Design gibt es mittlerweile eine einheitliche Modellierungssprache, mit der man objektorientiertes Design losgelöst von der konkreten Sprache darstellen kann. Sie heißt UML (Unified Modelling Language). Bei großen Projekten sollte man immer mit UML-Designwerkzeugen ein Modell entwickeln, bevor man mit der konkreten Umsetzung beginnt. Bei kleinen Projekten wird oft gänzlich darauf verzichtet. Es wird vielmehr drauf los programmiert und Design entsteht aus dem Moment heraus. Wer eine gute Vorstellungsgabe und viel Erfahrung hat, kann auch so gut strukturierte Programme erstellen. Oft mangelt es Programmen jedoch am Design, an innerer Struktur. Es ist ungefähr so, als würde man ein Haus bauen, ohne jeglichen Plan, einfach so nach Gefühl und aus dem Moment heraus. Manche Dinge sind jedoch nicht wieder auszubügeln, wenn sie erst einmal entstanden sind. Ein Keller kann nur schwer ein Meter tiefer gemacht werden, wenn schon das ganze Haus drauf sitzt. Und so muss man dann vielleicht lange mit Undurchdachtem leben. Je mehr Programmiererfahrung man hat, um so mehr wird man in der Regel auch dem Design Bedeutung zumessen, weil man schon so manches Unausgegorene produziert und durchlitten hat.

Viele Sprachen sind vom Ursprung prozedural. Eingebauten Funktionen werden über eine prozedurale Schnittstelle angeboten. Zusätzlich bekamen sie dann irgendwann objektorientierte Erweiterungen. Bei manchen Sprachen wurden diese sehr konsistent eingebettet, bei anderen wirkt es eher etwas umständlich. Im Grunde ist es jedoch immer ein gewisser Bruch, wenn man einerseits objektorientiert entwickeln möchte, die Sprache vieles aber nur prozedural anbietet. Man könnte OO programmieren, aber keiner tut es, weil prozedural von der Sprache her besser unterstützt ist...

Ein Grund, warum ich bei Ruby gelandet bin, ist die doch recht umständliche Umsetzung des objektorientierten Paradigmas in Perl. Ich lernte 1994 Perl auf einer Microsoft-Umgebung kennen und war ziemlich begeistert. Jahrelang programmierte ich mit der Version 4 von Perl, die noch keine objektorientierte Programmierung zuließ. Etwa zeitgleich lernte ich die objektorientierte Programmierung kennen. Damals war für viele eine echte Aufbruchstimmung hin zu objektorientiertem Design. Und ich lernte Objektorientierung immer mehr lieben. So fing ich an, in Ansi-C Handstände zu machen, um dort auch OO zu programmieren. Mit Perl 4 ging das eher nicht. Als dann Perl 5 rauskam, konnte ich nun auch hier objektorientiert programmieren. So richtig glücklich wurde ich damit aber nie. Es ging zwar alles irgendwie, aber als jemand, der die Schönheit von OO-Programmen liebt, überzeugte mich das nicht. Zwischenzeitlich programmierte ich viel in C++, was mich aber in Sachen Objektorientierung auch nicht zufrieden stimmte. C++ ist allerdings in vielen Bereichen nicht wegzudenken und wird auch noch lange überleben. Als mich dann 2002 ein Freund auf Ruby aufmerksam machte, wusste ich ziemlich schnell, nun endlich meine OO-Sprache gefunden zu haben. Hiermit kann ich wirklich schöne OO-Programme schreiben und habe die gleichen Funktionalitäten wie unter Perl. Seither programmiere ich Skripte nur noch in Ruby.

Ruby ist eine reine objektorientierte Sprache. Außer Klassen und den zugehörigen Objekten gibt es nicht viel. Dadurch entstehen klare und wohl geformte objektorientierte Programme. Hier muss man sich verbiegen, um nicht objektorientiert zu programmieren, während es in anderen Sprachen oft umgekehrt ist. Objektorientierung und prozedurale Programmierung haben jedoch auch vieles gemeinsam: Es gibt strukturierende Elemente, wie Schleifen, bedingte Ausführungen oder Operatoren, die in beiden Paradigmen ganz ähnlich eingesetzt werden.

In der Praxis ist es oft so, dass objektorientierte Erweiterungen von prozeduralen Sprachen kaum genutzt werden. Sie werden nur dazu genutzt, um Bibliotheksfunktionen anzusprechen. Die eigene Programmlogik wird hingegen weiter prozedural geschrieben. Oder es wird versucht, prozedurales Denken mit objektorientierten Mitteln zu realisieren. Das liegt oft an der Gewohnheit der Programmierer. Ein neues Programmier-Paradigma, was alles Gewohnte auf den Kopf stellt, braucht viel Zeit, bis es angenommen wird, auch wenn es noch so gut ist. Meist braucht es mindestens eine Programmierer-Generation. Man muss darin laufen lernen, man muss den Mut haben, sein gewohntes Denken erstmal beiseite zu legen um sich auf gänzlich Neues einzulassen.

Das Objektorientierung die Zukunft gehört und das es sich weiter ausbreitet, ist ziemlich gewiss. Objektorientierung ist jedoch kein Allheilmittel und auch die prozedurale Programmierung hat ihre Bereiche, in der sie eine gute Wahl ist. Die hochgesteckten Erwartungen der Anfangs-Euphorie haben sich nicht erfüllt. OO-Programme bauen sich nicht von selbst. Richtig angegangen jedoch, entstehen durch Objektorientierung wohlgeformte, robuste, flexible und gut wiederverwendbare Softwaresysteme.

7. Von Klassen und Objekten

Wenn man von Objektorientierung spricht, so sind die nächsten beiden Worte Klasse und Objekt. Klassen sind die grundlegenden Bausteine objektorientierten Designs.

Der Grundgedanke eines solchen Designs ist: Wir suchen nach etwas, was strukturell zusammengehört, benennen es und gießen es in eine Klasse. Dieses Ding statten wird dann mit Funktionen aus. Diese Funktionen sind die Intelligenz der Klasse: Sie können mit den Daten der Klasse umgehen, stellen Schnittstellen nach außen zur Verfügung und statten die Klasse mit unterschiedlichsten Fähigkeiten aus. Ein Programm besteht dann aus einer Menge solcher Dinge, die recht autonom sind, die miteinander in Beziehung stehen und miteinander kommunizieren. Das ist ziemlich abgedreht für jemanden, der prozedurale Programmierung gewohnt ist.

Fangen wir mit einem einfachen Beispiel an. Ein nettes Ding, was fast jedes Männerherz begehrt, ist ein Auto. Also bauen wir eben mal eine Auto-Klasse:

 
class Automobil
  def initialize( hersteller, typ, baujahr, farbe, tankvolumen, verbrauch )
    @hersteller  = hersteller
    @typ         = typ
    @baujahr     = baujahr
    @farbe       = farbe
    @tankvolumen = tankvolumen
    @tank_inhalt = 0.0
    @verbrauch   = verbrauch
  end
  def print
    puts "Hersteller: " + @hersteller
    puts "Typ:        " + @typ
    puts "Baujahr:    " + @baujahr
    puts "Farbe:      " + @farbe
    puts "Tank:       " + @tank_inhalt.to_s + " Liter"
  end
  def tanken( menge )
    @tank_inhalt += menge
    if @tank_inhalt > @tankvolumen
      @tank_inhalt = @tankvolumen
    end
  end
  def fahren( entfernung )
    verbrauch = entfernung * @verbrauch / 100
    @tank_inhalt -= verbrauch
    if @tank_inhalt < 0
      @tank_inhalt = 0
    end
  end
end

meinAuto  = Automobil.new( "Opel", "Corsa 1.1", "1997", "blau", 45, 6.3 )
chefsAuto = Automobil.new( "Porsche", "911", "2001", "rot", 90, 12.7 )

puts "----- Autos nach Init"
meinAuto.print
chefsAuto.print
puts

meinAuto.tanken( 35 )
chefsAuto.tanken( 35 )

puts "----- Autos nach Tanken"
meinAuto.print
chefsAuto.print
puts

meinAuto.fahren( 250 )
chefsAuto.fahren( 250 )

puts "----- Autos nach Fahrt"
meinAuto.print
chefsAuto.print
puts

Um die Übersicht zu bewahren, stelle ich die Klasse Automobil in einem vereinfachten Modell dar:

 
class Automobil
  Attribute:
    hersteller
    typ
    baujahr
    farbe
    tankvolumen
    tank_inhalt
    verbrauch
  Methoden:
    initialize( hersteller, typ, baujahr, farbe, tankvolumen, verbrauch )
    print
    tanken( menge )
    fahren( entfernung )
end

Ein Klasse besteht also aus Attributen und Methoden. Attribute sind Variablen, denen Werte zugewiesen werden können. In der Ruby Syntax wird solchen Variablen ein @-Symbol vorangestellt. Das unterscheidet sie von lokalen Variablen, die ja nur solange gültig sind, bis man einen Block oder eine Methode verlässt. Attribute sind über die ganze Lebenszeit des Objekts verfügbar, sie sind ja ein Teil des Objekts.

Methoden sind Funktionen, mit denen man diese Variablen manipulieren oder abfragen kann. Solche Methoden sind dazu gedacht, um Objekte dieser Klasse zu manipulieren. Sie werkeln nirgendwo sonst herum (für das Grundverständnis ist dies wichtig, später werden wir die Sicht noch erweitern).

Eine Klasse ist lediglich der Bauplan oder die Struktur eines Objekts. Man kann sich das so vorstellen, wie wenn man eine Tabelle anlegt: Durch die Definition der Spalten legt man eine Struktur, eine Form fest, was so eine Tabelle an Daten aufnehmen kann. Eine Klassendefinition ist auch sowas. Die einzelnen Datensätze, mit der man die Tabelle dann füllt, sind hier die Objekte. Gegenüber dem Tabellenbeispiel hat eine Klasse zusätzlich noch Methoden, die mit den Daten des jeweiligen Objektes etwas anfangen können.

Wird jetzt ein Objekt vom Typ bzw. von der Klasse Automobil angelegt, so kann man diesem konkrete Werte zuweisen. Ein Objekt nennt man auch eine Instanz der Klasse. In der Klasse wird z.B. festgelegt, dass es das Attribut @hersteller gibt, dem Objekt wird dann z.B. hierfür "Porsche" zugewiesen. Ein Objekt verfügt also über alle Variablen und Methoden, wie sie in der Klassen-Definition festgelegt wurden. Legt man mehrere Objekte einer Klasse an, sind die Variablen auch mehrfach im Hauptspeicher.

Genau das tun wir, sobald wir im obigen Beispiel 2 Objekte vom Typ Automobil anlegen: meinAuto und chefsAuto. Neue Objekte einer Klasse werden immer mit der Methode new dieser Klasse angelegt. Diese ruft nach Erstellung des Objekts automatisch die Methode initialize auf, die wir oben definiert haben. Hierüber weisen wir fast allen Attributen Werte zu. Danach existieren also zwei Objekte, die über meinAuto und chefsAuto ansprechbar sind.

Im obigen Beispiel haben wir weitere Methoden angelegt: tanken, print, fahren. Solche Methoden können dann für ein Objekt dieser Klasse aufgerufen werden, wie wir dies auch z.B. mit meinAuto.fahren( 250 ) tun. Die Methode fahren bewirkt, dass der @tank_inhalt sich nach einer bestimmten Vorgabe verändert. Über object.print erfahren wir dann u.a., wieviel Benzin noch im Tank ist. Und da zeigt sich, dass Autos mit kleinerem Protzfaktor im Vorteil sind:

 
[snip...]
----- Autos nach Fahrt
Hersteller: Opel
Typ:        Corsa 1.1
Baujahr:    1997
Farbe:      blau
Tank:       19.25 Liter
Hersteller: Porsche
Typ:        911
Baujahr:    2001
Farbe:      rot
Tank:       3.25 Liter

Bei den Methoden Automobil#tanken und Automobil#fahren (diese Klasse#Methode Schreibweise hat sich in Ruby zur Dokumentation eingebürgert) sehen wir eine gute OO-Angewohnheit: Eine Methode sollte sein Objekt immer in einem konsistenten Zustand belassen. Negative Tankinhalte gibt es nicht und in den Tank kann auch nicht mehr getankt werden, als er groß ist. Solche Überprüfungen begegnen uns ständig in der realen Programmierwelt. Ein Objekt einer Klasse kann sich nicht darauf verlassen, immer korrekt benutzt zu werden. Es muss sich selber darum kümmern, seine Integrität zu bewahren. Eine Möglichkeit ist, sinnvolle Werte zu setzen. Das führt aber mitunter dazu, dass Fehler und Fehlnutzung nicht auffallen. Zu was das führt, kann man sich bei einem weit verbreiteten Betriebssystem anschauen. Es ist oft besser, Fehler offen zu kommunizieren. Wie das geht, dazu später.

8. Alles Objekte

Es sieht so aus, als hätte Ruby auch normale Variablen wie Integer, String und Float, wie es andere Sprachen bieten. Dem ist jedoch nicht so. In Ruby ist alles ein Objekt irgendeiner Klasse. Strings sind z.B. Objekte der Klasse String und diese hat alle möglichen Methoden. Hier ein paar Beispiele, was man mit Strings so anstellen kann: (Alles hinter einem "#" ist Kommentar.)

 
# 20 Leerzeichen vorne und hinten um das Wort setzen
s = "Test"
s.center(20)  # >> "        Test        "

# so gehts auch, weil auch "Test" ein Stringobjekt ist, 
# jedoch ein Konstantes
"Test".center(20)

# Zwei Teilstrings zusammensetzen
s = "Hello "
s.concat( "World" ) # >> "Hello World"

# oder so...
s = "Hello "
s << "World"  # >> "Hello World" 
              # -> Auch hinter Operatoren verbergen sich Methoden

# Alle Buchstaben in Großbuchstaben umwandeln
s = "hello"
s.upcase!     # >> "HELLO"


# Einen String in einen Integer umwandeln
s = "99.3"
s.to_i        # >> 99

# in ein float
s = "99.3"
s.to_f        # >> 99.3

# Aufspliten an Wortgrenze, Rückgabe eines Array von Strings
s = "Das ist ein Test"
s.split       # >> [ "Das", "ist", "ein", "Test" ]

# Länge eines Strings
s = "Hello"
puts s.length # >> 5

# String zuerst in Integer und dann n mal Ausgabe des Strings
s = "5"
s.to_i.times do
  puts "Hello!"  # wird 5 mal ausgegeben, weil der Codeblock von times
                 # so oft ausgeführt wird, wie der Integer groß ist
end

# Tausche ein "." durch ein Komma aus
# bei dem ersten Argument von sub handelt es sich um einen regulären 
# Ausdruck, der immer mit /regulärerAusdruck/ eingefasst ist.
s = "23.3"
s.sub!( /\./, ',') # >> "23,3"


In Ruby gibt es also nicht die Trennung zwischen normalen, fest eingebauten Standard-Datentypen (Basis-Datentypen) und Objekten. Und so sind auch die Attribute einer Klasse immer Objekte, die ihrerseits wiederum aus Objekten bestehen können.

9. Ein praktischer Anwendungsfall

Wenden wir uns einem konkreten Anwendungsfall zu, damit wir einen besseren Bezug zu echten Problemlösungen bekommen.

Wir stehen vor der Aufgabe, eine Reihe alter Dokumentationen in Euro umzustellen. Es handelt sich dabei um Textdateien, die überall DM Beträge enthalten. Hierzu ein Beispieldokument:

 
Das Geschäftsergebnis 1999 betrug 2472934,89 DM

Dieses setzt sich zusammen aus:

Verkauf Waren:                    1503949,47 DM
Dienstleistungen:                  209899,34 DM
Mieteinnahmen:                     759086,08 DM     

Solche Dokumente sollen nun durch ein Skript in Euro umgewandelt werden.

 
#! /usr/bin/ruby

# Klasse Geld: Kann mit Euro und DM umgehen. 
# z.B. set("10,39 DM" ); set( "9,89 EUR" )
# intern wird Betrag in Euro gespeichert mit float-Genauigkeit
# Ausgabe erfolgt mit 2 Nachkommastellen fest
class Geld
  EUR       = 1.9558
  TRENNER   = ','
  NACHKOMMA = 2

  def initialize( betrag="0 DM" )
    set( betrag ) 
  end

  def Geld.factor
    return EUR
  end

  def set ( betrag )
    rxpIsDM  = /([\d,]+)\s+DM/
    rxpIsEUR = /([\d,]+)\s+EUR/
    if betrag =~ rxpIsDM
      @betrag = $1.sub(/,/, ".").to_f / EUR
    elsif betrag =~ rxpIsEUR
      @betrag = $1.sub(/,/, ".").to_f
    else
      # andere Formate nicht erlaubt.
      raise
    end
    return self
  end

  def to_EUR
    return sprintf( "%.#{NACHKOMMA}f EUR", @betrag ).sub( /\./, TRENNER )
  end

  def to_DM
    return sprintf( "%.#{NACHKOMMA}f DM", @betrag * EUR ).sub( /\./, TRENNER )
  end
end

geld = Geld.new
rxpFindeGeld = /([\d,]+\s+(DM|EUR))/

File.foreach( $*[0] ) do |line|
  line.gsub!( rxpFindeGeld ) do |match| 
    geld.set(match).to_EUR
  end
  puts line
end

Speichern Sie das Programm nach dm2eur.rb ab und das Beispieldokument nach test.dat. Nun kann man das Programm wie folgt starten:

 
wm@leo:~$ ruby dm2eur.rb test.dat
Das Geschäftsergebnis 1999 betrug 1264410,93 EUR

Dieses setzt sich zusammen aus:

Verkauf Waren:                    768968,95 EUR
Dienstleistungen:                  107321,47 EUR
Mieteinnahmen:                     388120,50 EUR

Zur Lösung eines solchen Problems versucht man erstmal, Dinge aufzuspüren, die man gut in Klassen abbilden kann. Hier habe ich den Weg gewählt, eine allgemeine Geld-Klasse einzuführen. Nach meinem Geschmack fand ich es sinnvoll, dass so eine Geld-Klasse als eine Art Datentyp existiert. Dieser Datentyp kann mit einer Geldkonstanten gefüttert werden, z.B. "10 DM" oder "15 EUR". Und man kann von diesem Datentyp auch erfahren, wie groß der Wert in Euro oder aber in DM ist.

Es ist immer eine gute Idee, allgemeingültige Klassen zu definieren, die nicht so stark auf das konkrete Problem zugeschnitten sind. Man muss sozusagen ein Gefühl dafür bekommen, was man aus dem konkreten Problem für allgemeingültige Klassen erschaffen kann. Denn wenn man dies tut, kann man Klassen sehr oft wieder einsetzen - Code-Wiederverwendbarkeit ist hier das Stichwort. Eines der großen Vorteile von OOP ist die Möglichkeit und Unterstützung, gut wiederverwendbaren Code zu schreiben. Das erspart eine Menge Mühe für künftige Projekte. Und es schafft gut getestete und robuste Komponenten, auf die man sich verlassen kann.

Die Klasse Geld ist in diesem Sinne generisch und wiederverwendbar. Immer wenn ich das Problem habe, sowohl mit Euro wie mit DM umgehen zu müssen, kann mir diese Klasse behilflich sein. Und mit der Zeit wird sie wahrscheinlich auch wachsen, zusätzliche Funktionalitäten erhalten. Aber selbst, wenn sie noch weitere 100 Methoden hinzu bekommt, ist sie in unserem Beispiel weiterhin einsetzbar.

Wir sehen auch, dass diese Klasse schon Funktionalität erhalten hat, die wir hier nicht brauchen: Geld#to_DM wird nirgendwo eingesetzt. Es ist ganz typisch bei objektorientiertem Design, dass man Klassen schon weitere Methoden hinzufügt, die aus der generalisierten Sicht sinnvoll sind. Man investiert in die Zukunft. Man denkt weitsichtig. Man denkt nicht mehr in den Kategorien: "Wie löse ich möglichst schnell mein konkretes Problem." sondern "Wie schaffe ich möglichst sinnvolle und universelle Bausteine, die ich gut wiederverwenden kann." OOP ist auch immer Investition in die Zukunft.

Auch ist es möglich, erstmal nur die Methoden zu definieren, ohne sie zu implementieren. Man legtdamit schon die Schnittstelle fest und dokumentiert gleichzeitig, wie man sich die Klasse vorstellt, welche Struktur sie haben soll.

Das Programm macht mehrfach Gebrauch von regulären Ausdrücken. Reguläre Ausdrücke bieten sehr mächtige Möglichkeiten. Man kann mit wenig Tippaufwand komplexe Suchmuster erstellen. Das bedeutet allerdings auch, dass man sich schon etwas Zeit nehmen muss, um so konzentrierten Code auch wirklich zu verstehen.

Es ist eine gute Angewohnheit, reguläre Ausdrücke einer Variablen oder Konstanten zuzuweisen und sie erst dann zu benutzen. Dadurch benennt man nämlich den Ausdruck und dokumentiert damit, was er tut. Das hilft, den Code schnell zu verstehen. Andere werden es Ihnen danken. Und wenn man seinen eigenen Code nach ein paar Monaten wieder mal anfässt, kommt man auch schneller wieder durch. Es ist generell gut, Programme so einfach und leichtverständlich wie möglich zu schreiben. Das spart Zeit bei der Wartung und Weiterentwicklung. Diesen Aspekt sollte man immer im Hinterkopf behalten.

Reguläre Ausdrücke (regexp oder regular expressions) wurden schon sehr oft erläutert und dokumentiert, weshalb ich dies hier nicht noch einmal tun möchte. Eine Dokumentation findet man z.B. unter [1] und [3].

Es gibt verschiedene Dialekte von regulären Ausdrücken - leider. Das hängt hauptsächlich damit zusammen, dass die Ur-Regexp's für spezielle Dinge nicht ausreichten und dann einige anfingen, sie zu erweitern. Ruby orientiert sich jedoch weitestgehend an Perl.

Reguläre Ausdrücke werden in Ruby immer zwischen zwei Slash-Zeichen als Begrenzer geschrieben (/RegulärerAusdruck/), genauso wie man für Strings die Anführungs-Striche benutzt. Damit weiß Ruby, dass es sich bei der Zuweisung um ein Regexp-Literal handelt. Folglich wird damit ein neues Objekt vom Typ Regexp angelegt. Deshalb sind diese Zeilen im Grunde identisch:

 
Form1:
rxpFindeGeld = /([\d,]+\s+(DM|EUR))/

Form2:
rxpFindeGeld = Regexp.new( '[\d,]+\s+(DM|EUR))' )

Form3:
rxpFindeGeld = Regexp.new( /[\d,]+\s+(DM|EUR))/ )

Form1 weist ein Regexp-Literal zu. Dadurch wird automatisch ein neues Objekt vom Typ Regexp angelegt. Form2 erzeugt ein neues Regexp-Objekt und weist ihm ein String zu, der den Ausdruck enthält. Die Methode new von Regexp kann nämlich sowohl mit String-Zuweisungen wie auch mit Regexp-Zuweisungen umgehen, wie Form3 dann zeigt.

In obigen Beispiel benutzen wir für die Ausgabe die Funktion sprintf(). Auch sie ist eine enorm leistungsfähige Funktion, mit der man sich gut und gerne mal einen Nachmittag beschäftigen kann. Perl-, Shell- und C-Programmierer kennen sie bereits.

So, wie reguläre Ausdrücke bestimmte Muster finden, formatiert sprintf eine Ausgabe nach festgelegten Formatvorgaben. Hier brauchen wir sie hauptsächlich, um die Nachkommastellen festzulegen. Der Aufbau von sprintf ist generell so, dass zuerst ein Formatstring festgelegt wird und dann mehrere Variablen folgen. Hierzu ein Beispiel:

 
puts sprintf( "Test: %s %s %s", "Hans", "Wurst", "0231-111112" )
puts sprintf( "Test: %s %s %s", "Peter", "Strauf", "0231-777112" )

Der Formatstring wird so ausgegeben wie er ist, allerdings werden einige Ersetzungen durchgeführt. Das zentrale Ersetzungsmerkmal ist das Prozentzeichen; %s bedeutet, ersetze dort mit einem String; %d bedeutet, ersetze mit einem Integer usw. Und dann werden alle folgenden Variablen in richtiger Reihenfolge dort eingesetzt.

Jetzt wollen wir das Beispiel so verbessern, dass spaltenkonform ausgegeben wird:

 
puts sprintf( "Test: %-20s %-20s %-20s", "Hans", "Wurst", "0231-111112" )
puts sprintf( "Test: %-20s %-20s %-20s", "Peter", "Strauf", "0231-777112" )

Das Minuszeichen hinter dem % bedeutet, das linksbündig formatiert werden soll, die 20 besagt, dass das Textfeld 20 Zeichen lang ist. Es gibt viele weitere Formatieroptionen, die jede denkbare Ausgabe ermöglichen. Früher hat man über solche Anweisungen ganze Formulare für den Drucker aufbereitet.

Für das Durchwandern der Daten-Datei benutzen wir wieder die Methode foreach, die uns im Beispiel weiter oben schonmal begegnet ist. Nur von welchem Objekt ist diese Methode? Wir rufen ja mit File.foreach auf, wo kommt aber jetzt das Objekt File auf einmal her? File ist gar kein Objekt sondern eine Klasse. Die Klasse File stellt eine Schnittstelle zu Dateien her. Wir hätten auch folgendes schreiben können:

 
datfile = File.new( "test.dat" )
datfile.each do |line|
  # weiterer Code...
end
datfile.close

Hier wird also zuerst ein neues File-Objekt angelegt und hierbei mit der Datei test.dat verbunden. Dann wird die Methode each dieses Fileobjekts aufgerufen, welche Zeile für Zeile durch die Datei läuft und den Code im Block ausführt. Zum Schluss wird die Methode close aufgerufen, die die Datei dann schließt.

Worüber wir noch nicht gesprochen haben, sind sogenannte Klassen-Methoden. Methoden sind gewöhnlich an die Objekte gebunden, sie tun etwas mit dem Objekt. Klassen-Methoden wirken nicht auf ein spezielles Objekt sondern stehen der Klasse generell zur Verfügung. Im Grunde ist es nichts weiter, als eine Funktion, die in einer Klasse gekapselt ist. Hierzu ein Beispiel:

 
class EinTest
  def EinTest.eineFunktion
    puts "Hello World."
  end
end

EinTest.eineFunktion

Die Methode eineFunktion ist Teil der Klasse EinTest, sie ist also in dieser Klasse gekapselt. Man kann sie nicht erreichen, in dem man einfach eineFunktion aufruft, man muss vielmehr spezifizieren: Rufe die Methode eineFunktion in der Klasse EinTest auf. Dies tun wir durch den Aufruf EinTest.eineFunktion.

Genau so funktioniert auch File.foreach. Es ist eine Klassen-Methode der Klasse File. Sie bewirkt, dass die übergebene Datei geöffnet und Zeile für Zeile durchlaufen wird. Nach Beendigung des Code-Blocks wird die Datei automatisch geschlossen. Das ist eine praktische Sache.

Neben Klassen-Methoden gibt es auch noch Klassen-Attribute. Dies sind Variablen, die nicht für jedes Objekt einer Klasse existieren sondern für alle Objekte nur einmal.

Beispiel:

 
class AttributTest
  @@klassenAttribut=0
  def initialize
    @@klassenAttribut +=1
  end
  def wievieleObjekte?
    return @@klassenAttribut
  end
  def AttributTest.wievieleObjekte?
    return @@klassenAttribut
  end
end

aTest = AttributTest.new
aTest1 = AttributTest.new
aTest2 = AttributTest.new

puts AttributTest.wievieleObjekte?
puts aTest.wievieleObjekte?

Interessant bei diesem Beispiel: Eine Methode kann auch ein Fragezeichen am Ende enthalten, welches keine Sonderbedeutung hat. Viele Methoden werden jedoch verständlicher, wenn ein Fragezeichen hinten anhängt.

Hier wird bei jedem Neuanlegen eines Objektes über die initialize-Methode ein Zähler der Klasse, ein Klassen-Attribut, hochgezählt. Man kann diesen Zähler sowohl mit einer Objekt-, wie mit einer Klassenmethode abfragen.

Die anfängliche Initialisierung dieser Klassen-Variable geschieht durch die direkte Zuweisung bei der Definition.

Zurück zum Geld-Beispiel. Was bedeutet die Angabe von $*[0] als Dateiname? Es gibt ein paar vordefinierte globale Variablen. Wir erinnern uns, globale Variablen beginnen immer mit einem $-Zeichen. $* ist ein Array von Werten, die durch die Kommandozeile übergeben werden. Hierzu folgendes Beispiel:

 
puts "1. Parameter:" + $*[0] if $*[0] 
puts "2. Parameter:" + $*[1] if $*[1] 
puts "3. Parameter:" + $*[2] if $*[2] 
puts "4. Parameter:" + $*[3] if $*[3] 

Dieses Programm kann man unter partest.rb abspeichern und so aufrufen:

 
wm@leo:~$ ruby Ein kleiner Rubytest
1. Parameter:Ein
2. Parameter:kleiner
3. Parameter:Rubytest

Alles, was also über die Kommandozeile übergeben wird, wird in $* gespeichert, wobei Leerzeichen die Parameter trennen. Wir haben hier also 3 Parameter übergeben. Hätte man "Ein kleiner Rubytest" in Anführungszeichen geschrieben, wäre es nur ein Parameter.

Und hier ist noch eine Verbesserung dieses kleinen Programms:

 
x = 1
$*.each do |parameter|
  puts "#{x}. Parameter: #{parameter}"
  x = x + 1
end

Sie sehen schon, die Methoden each und foreach sind echte Wunderwaffen und begegnen uns bei sehr vielen Klassen. Immer dann, wenn über alle Elemente iteriert werden soll, kommen sie zum Einsatz. Es ist ein abstraktes Konzept, egal ob es nun um Zeilen einer Datei, alle Dateien in einem Verzeichnis, Einträge eines Arrays, eines Hashs usw. geht.

Machen wir weiter mit dem Geldbeispiel. In der foreach-Schleife wird mit der Methode gsub gearbeitet. Sie ist eine Methode der Klasse String. Es wird das Vorkommen des regulären Ausdrucks geprüft und wenn gefunden, wird dieser Teil des Strings durch das letzte Ergebnis des Codeblocks ersetzt. In unserem Fall weisen wir den Teilstring dem Objekt geld zu und fragen es dann in der gleichen Anweisung mit to_EUR ab. Folgende Ausdrücke sind also identisch:

 
#so...  
geld.set(match).to_EUR

#oder so...
geld.set(match)
geld.to_EUR

Was man hier auch entdecken kann: Hat man intelligente Klassen entwickelt, wird die eigentliche Programmieraufgabe recht simpel. Denn hätten wir die Geldklasse aus einem vorherigen Projekt schon gehabt, dann würde unser Programm fast nur aus der foreach-Schleife bestehen. Das sind gerade mal 10 Zeilen! Hier zeigt sich, wie leistungsfähig objektorientierte Programmierung ist.

Allgemeingültige Klassen lagert man gewöhnlich in eine extra Datei aus, die man dann in beliebige Projekte einbinden kann. Auch dies ist keine große Sache. Wir trennen das Programm einfach in zwei Dateien auf:

 
# Datei geld.rb

# Klasse Geld: Kann mit Euro und DM umgehen. 
# z.B. set("10,39 DM" ); set( "9,89 EUR" )
# intern wird Betrag in Euro gespeichert mit float-Genauigkeit
# Ausgabe erfolgt mit 2 Nachkommastellen fest
class Geld
  EUR       = 1.9558
  TRENNER   = ','
  NACHKOMMA = 2

  def initialize( betrag="0 DM" )
    set( betrag ) 
  end

  def Geld.factor
    return EUR
  end

  def set ( betrag )
    rxpIsDM  = /([\d,]+)\s+DM/
    rxpIsEUR = /([\d,]+)\s+EUR/
    if betrag =~ rxpIsDM
      @betrag = $1.sub(/,/, ".").to_f / EUR
    elsif betrag =~ rxpIsEUR
      @betrag = $1.sub(/,/, ".").to_f
    else
      # andere Formate nicht erlaubt.
      raise
    end
    return self
  end

  def to_EUR
    return sprintf( "%.#{NACHKOMMA}f EUR", @betrag ).sub( /\./, TRENNER )
  end

  def to_DM
    return sprintf( "%.#{NACHKOMMA}f DM", @betrag * EUR ).sub( /\./, TRENNER )
  end
end


 
#! /usr/bin/ruby

# Datei: dm2eur.rb

require 'geld.rb'

geld = Geld.new
rxpFindeGeld = /([\d,]+\s+(DM|EUR))/

File.foreach( $*[0] ) do |line|
  line.gsub!( rxpFindeGeld ) do |match| 
    geld.set(match).to_EUR
  end
  puts line
end

Die Datei geld.rb wird mit require eingebunden. Hierzu muss sie entweder im selben Verzeichnis liegen oder aber typischerweise im lib-Verzeichnis. Wo sich auf Ihrer Maschine die Ruby lib-Verzeichnisse befinden, kann man mit folgendem Befehl herausbekommen:

 
ruby -e "puts $:"

Auf meiner Debian-Woody Maschine sind es:

 
/usr/local/lib/site_ruby/1.6
/usr/local/lib/site_ruby/1.6/i386-linux
/usr/local/lib/site_ruby
/usr/lib/ruby/1.6
/usr/lib/ruby/1.6/i386-linux
.

Eigene Skripte könnte man hier unter /usr/local/lib/site_ruby speichern.

Ruby mit einem -e aufgerufen, kann einzelne Kommandos abarbeiten. Das haben wir gerade mit "puts $:" gemacht. $: ist nämlich auch eine vordefinierte globale Variable, in der alle Bibliotheks-Suchpfade enthalten sind.

Hier ein paar Einzeiler-Beispiele:

 
#nur Verzeichnisse auflisten
ruby -e '`ls -l`.each do |l| puts l if l =~ /^d/ end'

#oder auch so...
ruby -e 'puts `ls -l`.gsub( /^[^d].*\n/, "")'

#ls-Ausgabe sortiert nach Filetyp
ruby -e 'puts `ls -l`.sort'

#zeige alle apache Prozesse an
ruby -e '`ps -Af`.each do |l| puts l if l =~ /apache\n/ end'

10. Globale Funktionen

In Ruby gibt es wie in andere Programmiersprachen auch globale Funktionen:

 
puts "Hello"
sleep( 1 )
puts( "Hello" )
sleep 1

Ich habe hier mal bewusst beide Schreibweisen verwendet. Die Klammern um die Argumente von Funktionen/Methoden kann man nämlich auch weglassen. Sowohl sleep wie puts sind globale Funktionen.

Man kann globale Funktionen genauso wie Methoden definieren:

 
def globaleFunktion
  puts "Hello World!"
end

def myputs( ausgabestring )
  puts "  #{ausgabestring}"
end

globaleFunktion
myputs( "Hello World" )
myputs "Hello World"

Hier werden also zwei globale Funktionen oder Methoden definiert, eine ohne Parameter, die andere mit. Hierbei gibt myputs den übergebenen String aus, jedoch etwas eingerückt. Hier habe ich auch wieder beide Schreibweisen benutzt, einmal mit und einmal ohne Klammerung der Argumente. Mehrere Argumente werden übrigens mit Komma getrennt.

Dadurch, dass man globale Funktionen hat, kann man grundsätzlich auch ganz normale prozedurale Programme schreiben. Es gibt Fälle, wo das auch Sinn macht. Wenn man z.B. mit einem 20 Zeiler irgendeine Aufgabe lösen will, ist es einfacher, Funktionalität in eine globale Methode auszulagern. Wird so ein Programm dann größer, ist es meist sinnvoller, in Klassen zu kapseln.

Genaugenommen sind globale Methoden nicht wirklich global. Sie sind auch wieder Teil der Klasse Object, die Basisklasse aller Objekte. Von daher können globale Methoden auch mit Object.methode_name aufgerufen werden. Möchte man innerhalb einer Klasse auf eine gleichnamige globale Methode zugreifen, kann man so vorgehen. Aufgrund des andersartigen Konzeptes gibt es in Ruby keinen Scope-Operator für Methoden, wie man es aus anderen Sprachen kennt, ::globale_methode funktioniert also nicht. Lediglich für den Zugriff auf Konstanten und Modulverschachtelungen benötigt man den ::-Scope Operator.

11. Vererbung

Was wäre Objektorientierung ohne Vererbung? Auch wenn es ein wichtiges objektorientiertes Konzept darstellt, kann man in Ruby zu Anfang sehr oft ohne Vererbung auskommen. Ich möchte das Thema deshalb nur ganz kurz anreißen. Und zwar deshalb, damit man überhaupt die Standard-Klassenbibliothek versteht, die zu Ruby mitgeliefert wird.

Vererbung ist die Möglichkeit, das neue Klassen die Eigenschaften einer anderen Klasse erben können. Die neue Klasse ist dann alles, was die bisherige Klasse ist und normal erweitert man sie um weitere Eigenschaften.

Ein Beispiel:

 
class Fahrzeug
  def initialize( hersteller, preis )
    @hersteller = hersteller
    @preis = preis
  end
end

class Automobil < Fahrzeug
  def initialize( hersteller, preis, typ, km_stand )
    super( hersteller, preis )
    @typ = typ
    @km_stand = km_stand
  end
  def report
    puts "Hersteller: " + @hersteller
    puts "Preis     : " + @preis.to_s
    puts "Typ       : " + @typ
    puts "Km-Stand  : " + @km_stand.to_s
  end
end

class Fahrrad < Fahrzeug
  def initialize( hersteller, preis, art, alter )
    super( hersteller, preis )
    @art = art
    @alter = alter
  end
  def report
    puts "Hersteller: " + @hersteller
    puts "Preis     : " + @preis.to_s
    puts "Art       : " + @art
    puts "Alter     : " + @alter.to_s
  end
end

aAuto    = Automobil.new( "BMW", 22499.00, "525i", 49000 )
aFahrrad = Fahrrad.new( "Diamant", 150.99, "Stadtrad", 2 ) 

aAuto.report
puts "-----------------------"
aFahrrad.report


Hier wird zuerst eine abstrakte Basisklasse definiert, die Fahrzeug heißt und Eigenschaften implementiert, die alle unterschiedlichsten Fahrzeuge auch mitbringen. Sowohl Automobil wie auch Fahrrad übernehmen diese Klasse dann, erben sozusagen alle Eigenschaften und Methoden.

In beiden abgeleiteten Klassen wird dann initialize neu definiert. Dies deshalb, weil hier jetzt speziellere Argumente übergeben werden, je nach konkreter Implementierung. Ein Automobil bekommt andere Werte übergeben, als ein Fahrrad. Und auch die Methode report ist jeweils anders implementiert. Was überall gleich ist, sind die Attribute @hersteller und @preis.

Weil man die Methode initialize in der abgeleiteten Klasse überschreibt, muss man auch irgendwie die Möglichkeit haben, die gleiche Methode in der Basisklasse aufzurufen. Dies geschieht durch die Methode super. Hiermit wird die gleiche Methode aufgerufen, nur halt die in der Basisklasse. Die weiß, wie man hersteller und preis behandelt, alles weitere machen dann die Ableitungen für sich selbst. Die Attribute der Basisklasse sind auch in den Ableitungen verfügbar, hier sind das @hersteller und @preis. Der Zugriff ist genauso, als wären sie in dieser Klasse definiert worden.

Von Klassenhierarchie spricht man deshalb, weil abgeleitete Klassen wiederum zu einer Basisklasse für weitere Ableitungen dienen können. Dies kann man beliebig fortsetzen, wobei man in der Praxis typischerweise Tiefen von 2-8 antrifft. Die Standard-Bibliothek von Ruby bewegt sich auch in diesem Rahmen. Durch Module, die als Mixins in Klassen eingefügt werden, spart man sich in Ruby oft Hierarchie. Deshalb sind viele Ruby-Bibliotheken tendenziell flach strukturiert.

Module werden überwiegend für sogenannte Mixins verwendet, von denen die Ruby Standard-Klassenbibliothek gebrauch macht. Hierbei werden über ein Module mehrere Methoden oder Attribute zusammengefasst, die dann in beliebige Klassen eingefügt werden können. Dieser Mechanismus ist sehr leistungfähig und es wird oft Gebrauch davon gemacht. Bei der Standard-Bibliothek sollte man immer darauf achten, welche Mixins eine Klasse verwendet, weil auch alle Methoden dieses Mixins verfügbar sind.

Die Oberklasse aller Klassen in Ruby ist übrigens die Klasse Object. Das bedeutet, dass alle Funktionalität, die dort festgelegt ist, in allen Klassen verfügbar ist. Einige Methoden sind z.B. class, clone, equal?, methods, instance_of?. Es lohnt sich, mal ein wenig damit zu spielen, weil man von jedem Objekt darauf zurückgreifen kann.

12. Ausnahmebehandlung

Viele Programme wären wohl nur halb so groß, wenn es keine Ausnahmebehandlung gäbe. In der realen Computer-Welt können leider an vielen Ecken Dinge schief laufen und das Programm muss sich darum kümmern, Fehler abzufangen. Dies wird auch als Exception-Handling bezeichnet.

In älteren Sprachen gab es keine besondere Unterstützung für Ausnahme-Behandlung. Sowohl in C wie auch in der Shell wird es oft so gemacht, dass ein Kommando bzw. eine Funktion einen Wert zurückgibt, der einen evtl. aufgetretenen Fehler anzeigt. So können z.B. alle Werte außer 0 anzeigen, dass ein Fehler aufgetreten ist. Das Problem, was bei einem solchen Design entsteht, ist der große Aufwand, den man treiben muss, um wirklich alle Fehlerzustände auszuwerten.

Weil dadurch schnell mehr Aufwand für die Fehlerbehandlung entsteht, als für die eigentliche Programmlogik, wurde und wird hier oft geschlampt. Fehlerzustände werden nicht abgefragt und das Programm läuft so weiter, als hätte alles funktioniert.

Dies kann jedoch fatale Folgen haben. Aktionen können aufeinander aufbauen und wenn eine nicht tut, dürfen auch alle weiteren nicht ausgeführt werden. Unsere Quelldatei aus dem mp3-Beispiel darf erst dann gelöscht werden, wenn das Ziel korrekt erzeugt wurde, sonst gibt es Datenverlust.

Moderne Sprachen wie C++, Java, Python oder eben Ruby haben deshalb spezielle Sprachelemente eingeführt, die eine einfache Ausnahme-Behandlung ermöglichen. Das grundsätzliche Design ist hierbei immer gleich. Hierzu ein Beispiel:

 
def my_function( a, b )
  begin
    r = a / b
  rescue
    puts "Division durch Null"
    raise
  end
  return r
end

def start_application
  begin
    a = my_function( 10, 2 ) # Ok
    a = my_function( 10, 0 ) # loest Exception aus
  rescue
    puts "Ein Fehler in der Applikation, beende deshalb."
  ensure
    puts "Bis bald."
  end
end

start_application

Die eingebauten Ruby-Klassen nutzen Exception-Handling bereits ausgiebig. Und so wird bei einer Division durch Null normal sofort das Programm beendet. Dies funktioniert so, dass die aufgerufene Methode eine Exception mit raise auslöst. Ruby bricht dann sofort den normalen Programlauf ab und führt den rescue-Block aus, sofern vorhanden. Wenn kein rescue-Block vorhanden ist, wird die Exception an die nächste Aufrufebene weitergegeben und dort kann wiederum ein rescue-Block die Ausnahme auffangen. Ist dort auch kein rescue-Block vorhanden, hangelt sich Ruby weiter durch die verschachtelten Aufrufebenen und wenn es überhaupt keinen rescue-Block findet, bricht es die Programmausführung mit einer Fehlermeldung ab.

In unserem Beispiel fangen wir jedoch die Ausnahme ab und machen zuerst einmal die Ausgabe "Division durch Null". Damit wäre die Ausnahme behandelt, zumindest mitgeteilt. Wir wollen aber auch der übergeordneten Ebene, nämlich start_application mitteilen, dass etwas nicht funktioniert hat. In einem rescue-Block kann man hierfür die gleiche Exception erneut durch raise auslösen. Nun wird in start_application diese Exception durch rescue aufgefangen und nochmal eine allgemeinere Fehlermeldung ausgegeben.

Mitunter gibt es Programmcode, der trotz Exception in jedem Fall ausgeführt werden muss, z.B. um noch irgendwelche Aufräumarbeiten auszuführen. Dieser wird in den ensure Abschnitt eingefügt. Der ensure-Block wird in jedem Fall durchlaufen, egal ob es eine Exception gab oder nicht. Ein typisches Beispiel aus der Praxis wäre:

 
f = file.open( "meinfile.txt" )
begin
  file.each do |line|
    #mache irgendwas, wo auch Exceptions auftreten können
  end
rescue
  log( "Error beim bearbeiten von meinfile.txt" )
  raise
ensure
  f.close
end

Hier wird eine Datei geöffnet. Diese muss in jedem Fall wieder geschlossen werden. Durch den ensure-Abschnitt wird dies sichergestellt. Bei einem Fehler wird rescue ausgeführt, eine Fehlermeldung ins log geschrieben und dann erneut abgeworfen. Zuvor jedoch wird ensure durchlaufen.

Fehlerbehandlung ist oft keine einfache Sache, vor allem wenn es um die Verantwortlichkeits-Aufteilung geht. Eine tiefliegende Funktion in einer Software sollte z.B. nicht anfangen, auf dem Bildschirm Fehlerausgaben zu produzieren. Dies sollten höherliegende Schichten übernehmen.

Die Idee, der man bei objektorientiertem Design folgt, lautet etwa so: "Ich als Objekt achte darauf, ob bei mir was schief läuft. Wenn ja, dann kümmere ich mich darum, konsistent zu bleiben und die Aufräumarbeiten zu erledigen, die in meinem Verantwortungsbereich liegen. Ich kann auch entscheiden ob höherliegende Schichten über den Fehler informiert werden müssen. Wenn ja, dann reiche ich nach erfolgter eigener Fehlerbehandlung die Exception an meinen Aufrufer weiter."

Diese Form der Fehlerbehandlung führt zu sauberem Code mit klarer Aufgabenteilung. Durch die Sprachelemente der Ausnahme-Behandlung wird diese viel einfacher und übersichtlicher, als das früher der Fall war. Auch ist eine klare Trennung zwischen Programmstruktur und Ausnahme-Code gegeben, was die eigentlichen Algorithmen besser erkennen lässt. Code versumpft nicht durch tausende von Fehlerbehandlungen.

Ordentliche Fehlerbehandlung wird dann wichtig, wenn man über das Stadium einiger Quick & Dirty Wegwerfskripte hinaus will. Eigentlich sollte jedes Skript, was man längerfristig und produktiv einsetzen möchte, eine vernünftige Fehlerbehandlung aufweisen. Shell Skripte sind in dieser Beziehung oft unzureichend programmiert. Exception-Handling ist hier nicht vorgesehen und Rückgabewerte werden oft nicht überprüft.

Wenn ich Skripte schreibe, mache ich gern von zwei Methoden Gebrauch, die ich Eiffel und C++ entnommen habe.

 
class ExcAssert < StandardError; end

class ExcRequire < StandardError; end

def assert
  raise ExcAssert.new
end

def require_err
  raise ExcRequire.new
end

def my_function( a, b)
  require_err if a.class != String

  if b == 1
    puts "Hello #{a}"
  elsif b == 2
    puts "Hallo #{a}"
  else
    assert
  end
end

begin
  my_function( "World", 1 )
  my_function( "World", 2 )
  my_function( "World", 3 ) # assert Exception
  my_function( 1,1 )        # require_err Exception
rescue ExcAssert => exc
  puts "Ein Assertfehler ist aufgetreten:"
  puts exc.backtrace[1]
rescue ExcRequire => exc
  puts "Ein Requirefehler ist aufgetreten:"
  puts exc.backtrace[1]
rescue => exc
  puts "Ein unbekannter Fehler ist aufgetreten:"
  puts exc.backtrace[1]
ensure
  puts "Bis bald."
end

Es gibt zwei häufige Fehlerquellen in Programmen. Zum einen sind es falsch übergebene Aufrufparameter, z.B. vom falschen Typ. Ein String wird erwartet und ein Integer wird übergeben. Zum anderen sind es Zustände, die nicht vorkommen dürfen. Während der Programmierung wird einem klar, dass ein Zustand zwar theoretisch entstehen kann, aber eben nicht gewollt ist und auch nicht entstehen dürfte.

Immer, wenn ich auf solche Fälle stoße, sichere ich das Programm ab. Diese Absicherungen haben mich schon vor vielen Problemen bewahrt oder eine schnelle Fehlersuche ermöglicht. Auch sind sie eine gute Dokumentation des Quelltextes. Man sieht sofort, was erwartet wird bzw. nicht vorkommen darf.

Die Funktion assert wird immer dort genutzt, wo Zustände auftreten könnten, die nicht sein dürfen. In C++ ist es ein Makro, was man nur in der Debugversion des Programms aktiviert und später ausschaltet. Solange keine Laufzeitprobleme auftreten, ist es jedoch sinnvoll, es auch in der Produktiv-Version drin zu haben. Hier vielleicht mit einer etwas anderen Implementierung. Man könnte dann die Funktion assert durch eine andere Version ersetzen, die nicht abwirft sondern lediglich in eine Logfile schreibt.

Die Funktion require_err benutze ich immer dort, wo eine Methode bestimmten Input erwartet, der hier überprüft wird. Stimmen die Argumente nicht mit den Erwartungen überein, kann die Methode auch nicht korrekt abgearbeitet werden. Sie wird deshalb abgebrochen.

Das Beispiel zeigt auch, das man raise ein Objekt übergeben kann, welches dann unterschiedliche rescue-Pfade steuert. Auch kann man das Objekt abfragen und weiter Infos zum Problem bekommen. Ich habe zu diesem Zweck zwei neue Exception-Klassen angelegt, die beide von der StandardError Klasse erben. StandardError ist eine von Ruby definierte Exceptionklasse.

Das Tragisch-Komische ist, dass mich ausgerechnet ein Microsoft-Mitarbeiter vor ein paar Jahren zu dem exzessiven Einsatz von asserts verführte. Es gibt also auch dort Menschen, die sich Gedanken über saubere Fehlerbehandlung machen. Ich empfehle hier das wirklich lesenswerte Buch "Nie wieder Bugs!, Die Kunst der fehlerfreien C-Programmierung" von Steve Maguire, Microsoft Press, 1996.

13. Groß- und Kleinschreibung

Ruby verhält sich anders als viele sonstige Sprachen, wenn es um Groß- und Kleinschreibung von Bezeichnern geht. Es kommt nämlich in einigen Fällen darauf an, ob der erste Buchstabe eines Bezeichners klein oder groß geschrieben wird.

Ein Klassen-Name muss immer groß begonnen werden. Sonst gibt es einen Syntax-Fehler. Konstanten beginnen ebenfalls mit einem Großbuchstaben. Diesen kann man einmal einen Wert zuweisen, eine Veränderung ist dann nicht mehr möglich. Die Unterscheidung, ob ein Bezeichner eine Konstante oder eine lokale Variable ist, wird genau durch den ersten Buchstaben entschieden. Trotzdem wird man gewöhnlich Konstanten komplett groß schreiben, das ist guter Programmierstil und Gewohnheit in Sprachen wie C/C++.

Hier mal eine Auflistung, wie man es gewöhnlich machen sollte, auch wenn nicht immer eine Verpflichtung dahinter steht:

BezeichnerGroß/Klein
lokale VariableKlein
globale VariableKlein
Klassen-NameGroß erster Buchstabe
KonstanteGroß durchweg
Modul-NameGroß erster Buchstabe
Objekt-AttributKlein erster Buchstabe
MethodeKlein erster Buchstabe
Klassen-AttributKlein erster Buchstabe

14. Alles nil, oder was?

Der Wert nil (not in list) hat eine besondere Bedeutung. Man ist sich seit jeher nicht einig, ob es einen besonderen Wert geben soll, wenn eine Variable nichts enthält. Viele Sprachen definieren einen bestimmten Wert als Nichts, nämlich 0 für Integer oder einen Leerstring für Zeichenketten. Ruby geht den Weg, dass eine nicht initialisierte Variable den Wert nil hat. Das hat Vorteile und kann auch manchmal nerven. Es führt aber oft dazu, das potenzielle Fehler frühzeitig abgefangen werden.

Variablen kann man auch den Wert nil zuweisen. Damit hat sie den Zustand "nicht definiert" oder "nicht verfügbar". Es kommt im Programmieralltag ja oft vor, dass bestimmte Sachen gerade nicht verfügbar sind. Der Wert nil lässt sich durch boolesche Vergleiche gut abfragen, denn er gibt false zurück.

 
a = "Hello"
b = nil

puts a if a
puts b if b

Sobald eine Variable eine Referenz auf ein Objekt ist, gibt sie true zurück, ansonsten im Falle nil ist sie false. Hier wird also nur die erste puts Anweisung ausgeführt.

15. Interaktives Ruby

Zum testen möchte ich ein interessantes Programm Namens irb erwähnen, was ein interaktives Ruby darstellt. Sozusagen eine Ruby-Shell. Normal gehört irb zum Ruby Paket dazu, es kann aber auch sein, dass man es separat installieren muss. Hier eine Beispielsitzung:

 
wm@leo:~$ irb
irb(main):001:0> puts "Hello World!"
Hello World!
nil
irb(main):002:0> 5.times do
irb(main):003:1* puts "Hello World!"
irb(main):004:1> end
Hello World!
Hello World!
Hello World!
Hello World!
Hello World!
5
irb(main):005:0>

Hier kann man also einfache Ruby Kommandos absetzen oder auch Kommandos, die über mehrere Zeilen gehen. Sobald man dann das schließende end eingibt, wird die komplette Sequenz abgearbeitet und die Ergebnisse dargestellt. Neben den Ausgaben durch puts wird auch noch das Ergebnis des Ausdrucks zum Schluss ausgegeben. Im ersten Beispiel gibt puts nil zurück, weil puts keinen Wert zurückgibt. Im zweiten Beispiel ist es der Wert 5, genauso, als hätte man nur 5 eingetippt. Mit irb kann man wunderbar mit der Sprache Ruby experimentieren. Mit dem Kommando exit verlässt man die irb-Umgebung.

16. Ausblick

Wir sind am Ende unserer Reise durch die Sprache Ruby. Ich habe versucht, durch viele Beispiele ein Gefühl für die Sprache zu vermitteln. Ich bin auch durch alle wichtigen Grundbereiche gewandert, so dass Sie jetzt eine Menge Basiswissen haben, um mit eigenen Programmen zu beginnen.

Die Bücher Programmierung in Ruby [1] wie auch Programmierung mit Ruby [2] sind beide sehr empfehlenswert. [1] nutze ich als Referenzwerk bei der täglichen Programmierarbeit. Zu empfehlen ist hier vor allem das Kapitel "Die Sprache Ruby", welches die Syntax und Sprachelemente von Ruby erklärt. Und natürlich das Kapitel "Eingebaute Klassen und Methoden", was eine gute Klassenreferenz darstellt.

Ich hoffe, ich konnte ein Stück Lust auf die Sprache auslösen und Ihnen genügend Informationen geben, um bald mit eigenen Projekten zu beginnen.

Willkommen in der Ruby-Community!

17. Fehler, Ergänzungen?

Habe ich etwas vergessen? Kann etwas verbessert werden? Haben Sie eine Idee? Ich freue mich über Feedback zu diesem Text.

18. Changelog

  • 05.09.2006: Inhaltsübersicht, kleine Bereinigung
  • 13.07.2005: Bereinigung Scope-Operator: Gibt es nicht für Methoden.
  • 02.02.2005: Übernahme ins Wiki, kleine Bereinigungen
  • 11.12.2003 Rev 0.1.2: Titelbild, kleinere Korrekturen, Copyright unten
  • 28.11.2003 Rev 0.1: Erste Veröffentlichung.

19. Referenzen

20. Copyright und Hinweise

Copyright (c) 2003 Winfried Mueller, www.reintechnisch.de

Es wird die Erlaubnis gegeben dieses Dokument zu kopieren, zu verteilen und/oder zu verändern unter den Bedingungen der GNU Free Documentation License, Version 1.1 oder einer späteren, von der Free Software Foundation veröffentlichten Version; mit keinen unveränderlichen Abschnitten, mit keinen Vorderseitentexten, und keinen Rückseitentexten.

Eine Kopie dieser Lizenz finden Sie unter GNU Free Documentation License.

Eine inoffizielle Übersetzung finden Sie unter GNU Free Documention License, deutsch.

In diesem Artikel werden evtl. eingetragene Warenzeichen, Handelsnamen und Gebrauchsnamen verwendet. Auch wenn diese nicht als solche gekennzeichnet sind, gelten die entsprechenden Schutzbestimmungen.

Alle Informationen in diesem Artikel wurden mit Sorgfalt erarbeitet. Trotzdem können Inhalte fehlerhaft oder unvollständig sein. Ich übernehme keinerlei Haftung für eventuelle Schäden oder sonstige Nachteile, die sich durch die Nutzung der hier dargebotenen Informationen ergeben.

Sollten Teile dieser Hinweise der geltenden Rechtslage nicht entsprechen, bleiben die übrigen Teile davon unberührt.