Ruby Tutorial: Eine Einladung in Ruby
Winfried Mueller, www.reintechnisch.de, Start: 28.11.2003, Stand: 05.09.2006
Übersicht
- 1. Einleitung
- 2. Voraussetzungen
- 3. Installation
- 4. Ein erster Test
- 5. Projekt mp3 Encoder Batch
- 6. Objektorientierung
- 7. Von Klassen und Objekten
- 8. Alles Objekte
- 9. Ein praktischer Anwendungsfall
- 10. Globale Funktionen
- 11. Vererbung
- 12. Ausnahmebehandlung
- 13. Groß- und Kleinschreibung
- 14. Alles nil, oder was?
- 15. Interaktives Ruby
- 16. Ausblick
- 17. Fehler, Ergänzungen?
- 18. Changelog
- 19. Referenzen
- 20. Copyright und Hinweise
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:
Bezeichner | Groß/Klein |
lokale Variable | Klein |
globale Variable | Klein |
Klassen-Name | Groß erster Buchstabe |
Konstante | Groß durchweg |
Modul-Name | Groß erster Buchstabe |
Objekt-Attribut | Klein erster Buchstabe |
Methode | Klein erster Buchstabe |
Klassen-Attribut | Klein 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
- [1] Programmierung in Ruby
- [2] Englische Originalausgabe von [1]: Programming Ruby
- [3] selfhtml: Reguläre Ausdrücke
- [4] Buch: Röhrl, Schmiedl, Wyss; Programmierung mit Ruby; dpunkt Verlag 2002
- [5] Rubys Homepage
- [6] Ruby Weblog
- [7] Deutsches Ruby Referenzwerk (scheint inaktiv)
- [8] Ruby Rundum-Sorglos-Paket für MS-Windows
- [9] Deutsches Ruby-Forum
- [10] Ruby wie php einbetten und über apache benutzen.
- [11] Ruby Projekte-Plattform, ähnlich Sourceforge
- [12] Ruby Portal
- [13] Ruby Documentation Project
- [14] Buch: Meyer, Bertrand; Objektorientierte Softwareentwicklung; Hanser 1990
- [15] Buch: Engel, Spreckelsen; Das Einsteigerseminar Ruby; vmi-Buch.de 2002
- [16] Programmierung mit Ruby Online Version
- [17] RubyChannel - Rubyportal mit einem Online-Ruby-Interpreter
- [18] regular-expressions.info: Alles über reguläre Ausdrücke (regexp)
- [19] Buch: Jeffrey E. F. Friedl; Reguläre Ausdrücke; O'Reilly; ISBN: 3897213494; ("Die Regexp-Bibel")
- [20] Buch: Tony Stubblebine; Reguläre Ausdrücke kurz&gut; O'Reilly; ISBN: 3897212641
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.