Objektorientierter Rubin

Metaprogrammierung mit Ruby
Kommentare

Kodierer und Compiler leben seit Anbeginn der Zeit nach einem strengen Konzept der Arbeitsteilung: Der Programmierer schreibt Code, der von seiner Entwicklungsumgebung ausgeführt wird. Wie dieses Konzept mit der spannenden Historie von Ruby zusammenhängt, erläutert dieser Artikel.

Das Konzept der Metaprogrammierung versucht, diese althergebrachte Harmonie zu stören. Die Idee dahinter ist, dass Code – zumindest theoretisch – Code generieren könnte. An sich ist diese Idee nichts Neues; Windows 1.0 nutzte zwecks Performancesteigerung eine Blitter-Routine, die sich vor der Anwendung „selbst kompilierte“ und so unnötige Sprünge und Selektionen beseitigte.

Was ist Ruby?

Ein japanischer Student namens Yukihiro Matsumoto – Ruby-Nutzer bezeichnen ihn normalerweise als „Matz“ – war mit den im Jahre 1993 existierenden Skriptsprachen unzufrieden. Er empfand Perl als nicht ausreichend mächtig, während Python seiner Ansicht nach nicht ausreichend objektorientiert war. Als oberste Maxime avisierte er, dass die Nutzung der Programmiersprache dem Entwickler Freude bereiten müsse. Dies wirkt sich auch auf die Community aus: Dem Autor sind nur wenige Gemeinschaften von Entwicklern bekannt, die einen ähnlich angenehmen Umgangston pflegen.

Das Resultat seiner Bemühungen war eine streng objektorientierte Sprache, die nicht statisch typisiert ist. Stattdessen kommt das aus JavaScript bekannte Konzept des Duck Typings zum Einsatz: Wenn ein Objekt die notwendigen Methoden implementiert, so gilt es als zur Klasse zugehörig. Dies ist für die Metaprogrammierung wichtig. Ruby erlaubt Entwicklern das Einpflegen von neuen Funktionen und Variablen zur Laufzeit: Es ist ohne Probleme möglich, eine bestehende Klasse als Reaktion auf ein Ereignis um eine neue Funktion zu erweitern. Der Aufbau dieser Methode darf von den vorliegenden Parametern beeinflusst werden, wodurch die Sprache eine immense Flexibilität erreicht. Von C++ oder Java umsteigende Entwickler stolpern zudem über zwei kleine Besonderheiten des Sprachstandards: Erstens sind die Regeln für boolesche Typen wesentlich strikter, zweitens liefert alles – also auch eine Selektion – einen Wert zurück.

Mine auf!

Nach diesen einführenden Überlegungen ist es an der Zeit, erste Schritte in die Welt von Ruby zu setzen. Wer, wie der Autor dieser Zeilen, unter Unix arbeitet, muss normalerweise nur den Befehl irb eingeben, um einen Ruby-Interpreter zu aktivieren. Unter Windows lebende Entwickler finden hier ein fertiges Installationspaket. Unser erstes Programm mag auf den ersten Blick banal erscheinen, illustriert aber eine für spätere Versuche immens wichtige Vorgehensweise. Den Quellcode zeigt Listing 1:

Listing 1

tamhan@ubuntu:~$ irb 
irb(main):001:0> def sagWas(iwas) 
irb(main):002:1> puts "Ich sage #{iwas}" 
irb(main):003:1> end 
=> nil 
irb(main):004:0> sagWas("oioioi") 
Ich sage oioioi 
=> nil

irb führt die eingegebenen Befehle „interaktiv“ aus. Das bedeutet, dass jedes eingegebene Kommando sofort in die Ruby Engine wandert und dort ausgewertet wird. Wir beginnen mit der Definition einer sagWas genannten Funktion, die einen Parameter namens iwas entgegennimmt. Kenner von JavaScript und anderen dynamisch typisierten Sprachen wundern sich nicht darüber, wo die Deklaration des Datentyps bleibt – er wird dynamisch festgelegt.

Exotischer ist in dieser Hinsicht der Aufbau der Funktion. Er beginnt mit def und endet mit end – geschwungene Klammern finden Sie in Ruby nur höchst selten. Das gilt auch für das in C und Java beliebte Semikolon: Ruby-Kommandos enden normalerweise mit dem Zeilentrennzeichen. Der Aufruf von sagWas mit dem Parameter oioioi ist dann wieder komplett konventionell.

Befehle und Fälschungen

Wer eine Funktion aufruft, erwartet normalerweise eine Antwort bzw. einen Rückgabewert. irb zeigt „unbehandelte“ Rückgabewerte in Form einer mit einem ASCII-Pfeil beginnenden Zeile an. In unserem Fall führen sowohl die Definition der Funktion als auch ihr Aufruf zu einem Rückgabewert vom Typ nil. Ruby-erfahrene Entwickler nutzen diese Besonderheit der Programmiersprache gerne zum Abkürzen von Quellcode. Ein klassisches Beispiel dafür ist eine Selektion, die einen Wert wie in Listing 2 zurückgibt:

Listing 2

tamhan@ubuntu:~$ irb 
irb(main):001:0> def sagWas(iwas) 
irb(main):002:1> puts "Ich sage #{iwas}" 
irb(main):003:1> end 
=> nil 
irb(main):004:0> sagWas("oioioi") 
Ich sage oioioi 
=> nil

Damit ist nur mehr die Frage nach der Bedeutung von nil offen. C, Java und Python sind bei der Auslegung von Boolean-Werten vergleichsweise gnädig. Eine mit dem Wert 0 befüllte Integer-Variable geht in den meisten Programmiersprachen problemlos als false durch. Ruby tickt an dieser Stelle etwas anders, was wir anhand des kleinen Beispiels in Listing 3 demonstrieren wollen. Es findet sich in leicht abgeänderter Form in der offiziellen FAQ für Umsteiger – anscheinend haben viele Entwickler mit dieser Besonderheit ihre liebe Not.

Listing 3

irb(main):014:0> if 0 
irb(main):015:1> puts "0 ist wahr" 
irb(main):016:1> else 
irb(main):017:1* puts "0 ist falsch" 
irb(main):018:1> end 
0 ist wahr 
=> nil 
irb(main):019:0> if false 
irb(main):020:1> else 
irb(main):021:1* puts "false ist falsch" 
irb(main):022:1> end 
false ist falsch 
=> nil 

Die Erklärung dieses Zusammenhangs ist ähnlich philosophisch wie die alte Frage nach der Einheit eines einheitenlosen Werts: Elektroniker können sich stundenlang darüber streiten, ob das korrekte Einheitensymbol [1] oder [-] lautet. Erfreulicherweise gibt es im Fall von Ruby eine „einfachere“ Erklärung. Betrachten wir – wie so oft – die Nachbarin des Autors. Sie liebt slowakische Kekse abgöttisch und bewahrt ihren Vorrat in einer seltsam anmutenden Holzkiste auf. In Ruby ließe sich dieses Element nach folgendem Schema modellieren:

irb(main):006:0> keksBoxInhalt=1 
=> 1 

Wenn der Lieferdienst der Supermarktkette Tesco eine Packung weiterer Kekse liefert, so erhöht sich der Vorrat – zumindest dann, wenn man den in Ruby nicht implementierten ++-Operator nicht versehentlich einsetzt:

irb(main):007:0> keksBoxInhalt=keksBoxInhalt+1 
=> 2 

Nun steht am Abend die Party einer Bekannten an. Die Nachbarin weiß, dass es auf dieser Party keine Kekse geben wird. Weil sie aber nicht auf ihre geliebten Naschereien verzichten möchte, nimmt sie den Inhalt der Holzkiste mit zu ihrer Bekannten. In Ruby sieht dies so aus:

irb(main):009:0> keksBoxInhalt=keksBoxInhalt-keksBoxInhalt 
=> 0 

Rein zufällig findet sich bei besagter Bekannten ein baugleiches Exemplar der Holzkiste. Die beiden Kisten – die zu Hause und die bei der Bekannten – sind nun insofern „identisch“, als sie beide im Moment keine Kekse beheimaten. Leider enden die Ähnlichkeiten an dieser Stelle. Die in der Wohnung meiner Nachbarin stehende Holzkiste enthält – im Grunde genommen – Kekse, ist im Moment aber leer. Ihr Wert ließe sich mit Einheit als 0 Stück [Kekse] ausdrücken.

Die bei der Bekannten vereinsamte Holzkiste ist in dieser Hinsicht schlechter aufgestellt. Da sich bisher noch niemand für sie interessiert hat, ist ihr Inhalt 0 Stück [nichts] – ein Zustand, der in Ruby durch nil dargestellt wird.

Variable auf Abwegen

Nach diesen zugegebenermaßen etwas ausgefallenen Überlegungen müssen wir uns fragen, ob die von sagWas ausgegebene Zeichenfolge Teil des Rückgabewerts ist. Dies ist nicht der Fall, da Puts ihren Inhalt direkt auf den Bildschirm ausgibt. Betrachten wir die relevante Zeile noch einmal:

irb(main):002:1> puts "Ich sage #{iwas}" 

Das Hash-Symbol weist Ruby dazu an, den String mit dem Inhalt des „angeschlossenen“ Terms zu versehen. Dies ist im Bereich der Metaprogrammierung auf vielerlei Weise sinnvoll – wir kommen darauf alsbald zurück.

Alles ist veränderlich

Bisher haben unsere Beispiele – so soll es auch sein – problemlos funktioniert. Wenn wir nach der Deklaration von sagWas stattdessen die unangemeldete Funktion sage aufrufen, so wirft irb eine auf den ersten Blick befremdliche Fehlermeldung (Listing 4).

Listing 4

irb(main):004:0> sage "oioioi" 
NoMethodError: undefined method 'sage' for main:Object 
  from (irb):4 
  from /usr/bin/irb:12:in `<main>' 

irb wirft einen Fehler vom Typ NoMethodError, um auf das Fehlen einer passenden Methode hinzuweisen. Dieses im Großen und Ganzen „normale“ Verhalten ist insofern seltsam, als die Suche in main:Object erfolgt.

Alle Ruby-Objekte haben die angenehme Eigenschaft, ihre enthaltenen Member-Funktionen auf Wunsch in Form eines Arrays auszugeben. Wenn Sie instance_methods aufrufen, reagiert das Mutterobjekt mit der einer dem Snippet ähnlichen Liste von Funktionen (Listing 5).

Listing 5

irb(main):028:0> Object.instance_methods 
=> [:sagWas, :nil?, :===, :=~, :!~, :eql?, :hash, :<=>, :class, :singleton_class, :clone, :dup, :initialize_dup, . . . 

JavaScript-Programmierer fühlen sich an dieser Stelle an das beliebig erweiterbare Mutterobjekt Object erinnert. Ruby geht in dieser Hinsicht noch einen Schritt weiter: Die Sprache kennt keine primitiven Datentypen. Das bedeutet, dass sie String, Integer und Co. mit beliebigen Hilfsfunktionen erweitern können. Als kleines Beispiel dafür wollen wir in Listing 6 den für Zahlen zuständigen Datentyp um eine Methode erweitern, die den Wert der im Element gespeicherten Zahl um eins inkrementiert und zurückgibt.

Listing 6

irb(main):036:0> class Numeric 
irb(main):037:1> def plusEins 
irb(main):038:2> self+1 
irb(main):039:2> end 
irb(main):040:1> end 
=> nil 
irb(main):041:0> 2.plusEins 
=> 3 

Da Ruby keine Unterteilung zwischen „Kompilationszeit“ und „Laufzeit“ kennt, lässt sich die Definition einer Klasse jederzeit öffnen und erweitern. Wir greifen uns die als Basistyp für alle Zahlen dienende Numeric und ergänzen sie um die Funktion plusEins. Falls Sie diese Schritte in einer Sitzung nach dem Anlegen der Keksbox-Variable abgearbeitet haben, so bietet sich die Gelegenheit für ein interessantes Gedankenexperiment. keksBoxInhalt ist ja vom Typ Numeric, wurde aber vor der Erweiterung deklariert. Folgender kleiner Test verschafft uns Klarheit:

irb(main):042:0> keksBoxInhalt.plusEins 
=> 1 
irb(main):043:0> 

Klassenerweiterungen wirken sich in Ruby – zumindest normalerweise – auf alle Elemente aus, die je von der betreffenden Klasse abgeleitet wurden. Das bedeutet, dass unser Numeric-Kiste von der soeben angelegten Methode plusEins profitiert. Ruby-Programmierer bezeichnen das Erweitern von „Basisobjekten“ mitunter als Monkey Patching. Dieser wenig respektvolle Begriff resultiert aus dem Risiko, dass derartige Veränderungen beim Weiterverwenden von Code zu diversen Problemen führen.

Eigenes Objekt erbeten

Obwohl die Erweiterung von vorgegebenen Elementen mit Sicherheit nicht uninteressant ist, setzt die seriöse Nutzung von Ruby die Möglichkeit zur Erstellung eigener Sprachelemente voraus. Als erstes Beispiel wollen wir in Listing 7 eine Klasse realisieren, die den Munitionsvorrat einer Maschinenkanone modelliert.

Listing 7

irb(main):043:0> class MyCannon1 
irb(main):044:1> def lade 
irb(main):045:2> @ammo=30 
irb(main):046:2> end 
irb(main):047:1> def burst 
irb(main):048:2> @ammo=@ammo-3 
irb(main):049:2> puts @ammo 
irb(main):050:2> end 
irb(main):051:1> end 
=> nil 

Klassendeklarationen entstehen durch das Schlüsselwort Class, dem der Name der zu bearbeitenden Klasse folgt. Wenn Ruby unter diesem Namen noch keine Definition findet, wird sie automatisch neu angelegt. Das gilt – amüsanterweise – auch für Member-Variablen. Ein mit @ beginnender Variablenname wird in einer Klassendeklaration stets als Deklaration eines Members gewertet – die Methode lade schreibt den Wert 30 in das ammo-Member der aktuellen Maschinenkanoneninstanz. Unser soeben besprochenes Paradigma der „Initialisierung bei Bedarf“ gilt natürlich auch hier. instance_variables erlaubt uns das Auslesen der Member-Variablen eines Objekts. Vor dem Aufruf von lade ist das Array leer (Listing 8).

Listing 8

irb(main):053:0> aCannon=MyCannon1.new() 
=> #<MyCannon1:0x9d1b600> 
irb(main):054:0> aCannon.instance_variables 
=> [] 
irb(main):055:0> aCannon.lade() 
=> 30 
irb(main):056:0> aCannon.instance_variables 
=> [:@ammo] 

Als kleinen Versuch deklarieren wir im nächsten Schritt eine neue Objektinstanz, die sofort nach der Erstellung mit einem Aufruf von burst() bearbeitet wird. Dies führt – wie eigentlich zu erwarten – zu einer Fehlermeldung, da @ammo zu diesem Zeitpunkt noch nicht deklariert war (Listing 9).

Listing 9

irb(main):058:0> bCannon=MyCannon1.new() 
=> #<MyCannon1:0x9d0e5cc> 
irb(main):059:0> bCannon.burst 
NoMethodError: undefined method '-' for nil:NilClass 
  from (irb):48:in `burst' 
  from (irb):59 
  from /usr/bin/irb:12:in `' 

In der Praxis ist diese Vorgehensweise oft nur wenig wünschenswert. Am Einfachsten lässt sich dieses Problem durch das Einführen eines Konstruktors umgehen. Ruby geht hier keinen „Sonderweg“ – die Methode verhält sich im Großen und Ganzen wie von anderen Programmiersprachen her erwartet (Listing 10).

Listing 10

irb(main):071:0> class MyCannon1 
irb(main):072:1> def initialize 
irb(main):073:2> @ammo=0 
irb(main):074:2> end 
irb(main):075:1> end 
=> nil 

Ab diesem Zeitpunkt durchläuft die Erstellung einer neuen MyCannon-Instanz automatisch den Konstruktor, in dem die Ammo-Variable angelegt wird. Da wir kein Mutterobjekt spezifiziert haben, wurde unsere Maschinenkanonenklasse – automatisch – von Object abgeleitet. Das bedeutet, dass die Kanone die Sprechfunktion sagWas mitbringt:

irb(main):057:0> aCannon.sagWas("Rat Tat Tat!") 
Ich sage Rat Tat Tat! 
=> nil 

Methoden lassen sich in Ruby auf Wunsch privat, protected oder public setzen. Leider gilt dies nicht für Variable: Wer auf den Wert eines Members eines Objekts von außen heraus zugreifen möchte, wird vom Interpreter in seine Schranken gewiesen. Dieses Problem lässt sich nur durch das Realisieren von Akzessorfunktionen umgehen. Dabei handelt es sich um das Äquivalent der von Java und C++ hinreichend bekannten Setter– und Getter-Methoden (Listing 11).

Listing 11

irb(main):076:0> class MyCannon1 
irb(main):077:1> def ammo 
irb(main):078:2> @ammo 
irb(main):079:2> end 
irb(main):080:1> def ammo=(howmuch) 
irb(main):081:2> @ammo=howmuch 
irb(main):082:2> end 
irb(main):083:1> end 
=> nil 

ammo ist für das Auslesen des Variablenwerts zuständig. Da jedes Statement in Ruby einen Wert hat, genügt es, den Inhalt der Variable „aufzurufen“ – er wird automatisch an den Aufrufer zurückgegeben.

Funktionen, autogeneriert

Unsere Maschinenkanone hat nur einen Munitionsvorrat, weshalb die Erstellung von Setter und Getter nicht allzu dramatisch ist. Leider gibt es in der Praxis oft Objekte, die mehrere Dutzend Eigenschaften mitbringen. Im ersten Schritt erweitern wir unsere MK-Klasse um eine Gruppe von Eigenschaften. Starten Sie irb dazu neu und geben danach den Code aus Listing 12 ein.

Listing 12

irb(main):001:0> class MyMK
irb(main):002:1> attris=["ammo", "leuchtspur", "schrot"]
irb(main):003:1> attris.each do |anattr| 
irb(main):004:2* define_method("#{anattr}_lese") do 
irb(main):005:3* instance_variable_get("@#{anattr}") 
irb(main):006:3> end 
irb(main):007:2> end 
irb(main):008:1> end 
=> ["ammo", "leuchtspur", "schrot"] 

Nach der Klassendeklaration erstellen wir ein Array, das die Namen der zu erstellenden Akzessoren enthält. Dieses wird durch die each-Methode mit einer als Block bezeichneten Codestruktur bearbeitet. Blöcke sind das Ruby-Äquivalent zur aus C# bekannten foreach-Schleife: Sie wenden den in ihnen enthaltenen Code auf alle Elemente des zu bearbeitenden Arrays an. Über die eigentliche Verarbeitung gibt es nur wenig zu berichten. Jedes Attribut dient als Eingabeparameter für die define_method-Funktion, die zur Realisierung neuer Member-Funktionen dient. instance_variable_get hat die Aufgabe, den resultierenden Wert an den Aufrufer zu retournieren. Zwecks Funktionsüberprüfung erstellen wir eine Instanz der Klasse. Diese wird sofort zum Aufruf einer der generierten Methoden eingespannt (Listing 13).

Listing 13

irb(main):009:0> anMP=MyMK.new()

=> #<MyMK:0x9df4b08>

irb(main):010:0> anMP.ammo_lese

=> nil

irb(main):011:0>

Ruby beantwortet unsere Anfrage – weisungsgemäß und korrekt – mit nil. Das liegt daran, dass die Lesemethode auf ein nicht initialisiertes Element zugreift – zur Behebung dieses Problems sollte die Klasse einen Konstruktor bekommen. Da nur wenige Objekte ohne Akzessoren auskommen, gibt es in Ruby mittlerweile ein auf die Erstellung von derartigen Methoden optimiertes Modul. Die Verwendung von attr_accessor erleichtert die Realisierung:

class MyCannonAuto
  attr_accessor: ammo
end

attr_accessor nimmt einen oder mehrere Parameter entgegen. Pro übergebenem String wird eine Gruppe von Methoden erstellt, die das Einlesen und Ausgeben der Werte nach dem soeben erstellten Schema ermöglichen. Falls Sie keinen vollwertigen Akzessor realisieren möchten, so sollten Sie stattdessen auf attr_reader und attr_writer zurückgreifen. Die beiden Attribute erstellen eine Lese- oder eine Schreibmethode nach dem Schema von attr_accessor.

Methode, aus der Luft gegriffen

Wer eine unbekannte Methode aufruft, wird von der Ruby-Laufzeitumgebung mit der weiter oben abgedruckten Fehlermeldung belohnt. Für von C++ oder Java umsteigende Entwickler ist das Verhalten logisch: Die Runtime erledigt die Arbeit des Compilers zur Laufzeit. Da das Member fehlt, wird eine Exception geworfen.

Für die folgenden Schritte sollten Sie die Maschinenkanonen-Klasse in der History von irb vorhalten. Deklarieren Sie danach folgende Funktion:

irb(main):084:0> def method_missing(method) 
irb(main):085:1> puts "AUTSCH: #{method} existiert nicht" 
irb(main):086:1> end 
=> nil 

Wenn Sie die nicht existierende Funktion oioioi einer Maschinenkanoneninstanz aufrufen, so kommt die im Mutterobjekt angelegte Methode method_missing zum Einsatz. Da wir die Default-Implementierung durch eine eigene Funktion überschrieben haben, erscheint unsere Fehlermeldung am Bildschirm:

(main):087:0> aCannon.oioioi() 
AUTSCH: to_ary existiert nicht 
AUTSCH: oioioi existiert nicht 
=> nil 

Aufmerksame Beobachter bemerken, dass undefined_method zweimal in Folge aufgerufen wird. Dieses nur in manchen Versionen von Ruby auftretende Verhalten liegt an puts. Die Funktion wandelt ihre Argumente unter Umständen über to_ary um. Leider ist diese in vielen Grundklassen nicht deklariert, was zum hier gezeigten Fehlerbild führt.

Verkettetes Nichtstun

Jedes Objekt bringt von Haus aus eine leere Implementierung von method_missing mit, die die Analyse des fehlenden Members an die in der Superklasse implementierte gleichnamige Funktion delegiert. Das Mutterobjekt BasicObject enthält dann den für das Werfen der Exception notwendigen Code. Unsere soeben demonstrierte Überschreibung der Muttermethode wird in der Praxis nur selten angewendet. Entwickler ersetzen die method_missing-Funktion des Objekts, das die undefinierten Aufrufe abbekommen soll. Die auf diese Art und Weise entstehende Logik wird in der Ruby-Community als Ghost Method bezeichnet.

Wir wollen dies anhand einer Beispielklasse testen, die eine Gruppe von Member-Variablen enthält. Wenn die aufgerufene Funktion dem Namen eines Members entspricht, reagiert die Klasse mit dem Ausgeben des Werts. Ist dies nicht der Fall, wandert der Methodenname in die Ausgabe (Listing 14).

Listing 14

irb(main):001:0> class MyMK2

irb(main):002:1> def initialize 

irb(main):003:2> @ammo=0 

irb(main):004:2> @leuchtspur=0

irb(main):005:2> end 

irb(main):006:1> def method_missing(method, *args) 

irb(main):007:2> puts args

irb(main):008:2> self.instance_variables.each do |whatsit|

irb(main):009:3* method2="@#{method}"

irb(main):010:3> if whatsit.to_s == method2.to_s

irb(main):011:4> puts "Member entdeckt"

irb(main):012:4> return self.instance_variable_get(whatsit)

irb(main):013:4> end

irb(main):014:3> end

irb(main):015:2> puts "Error!"

irb(main):016:2> return nil 

irb(main):017:2> end 

irb(main):018:1> end

=> nil

Unsere Klassendefinition beginnt nun mit der Deklaration einiger Member-Variablen. Wenn method_missing aufgerufen wird, beschafft sie ein Array aller im Objekt enthaltenen Member. Diese werden danach Schritt für Schritt mit dem angeforderten Element verglichen. Im Fall einer Übereinstimmung beenden wir die weitere Verarbeitung des Blocks. Wenn der Block ohne Treffer durchläuft, so folgt das Zurückgeben von nil. Die explizite Angabe des Rückgabewerts ist erforderlich, da Ruby sonst den Inhalt des Methoden-Arrays auswerfen würde.

Der Test der Methode beweist, dass unsere Ghost Methods wie vorgesehen funktionieren. Das Abfragen von ammo wird mit einem Wert quittiert, während los() einen Fehler auslöst (Listing 15).

Listing 15

irb(main):019:0> cCannon=MyMK2.new()

=> #<MyMK2:0x98293e0 @ammo=0, @leuchtspur=0>

irb(main):020:0> cCannon.los()

Error!

=> nil

irb(main):021:0> cCannon.ammo()

Member entdeckt

=> 0

BasicObject enthält eine Vielzahl weiterer Routinen, die bei der Überwachung des Zustands von Objekten hilfreich sind. Daran interessierte Entwickler finden hier weitere Informationen zum Thema.

Unit Tests sind Leben

Aufgrund der komplett unterschiedlichen Basiskonzepte präsentieren Lehrbücher Ruby-Beispiele meist ohne dazugehörende Unit Tests: Es ist aus didaktischen Gründen nicht sinnvoll, umsteigende Leser von den wichtigen Änderungen abzulenken. In der Praxis ist dies jedoch sogar höchst fahrlässig. Ruby-Code lässt sich über hier nicht weiter besprochene Gems erweitern. Dabei handelt es sich um von Drittanbietern realisierte Module, die nach der Inklusion nach Belieben „herumfuhrwerken“ dürfen. Wenn ein Gem-Entwickler in seinem Code rein zufällig eine von Ihrem Code in Object durchgeführte Änderung rückgängig macht, so ist dies der Runtime herzlich egal: Gems gelten als Teil ihres Hauptprogramms. Aus diesem Grund ist es von eminentester Bedeutung, das Programm mit einer Gruppe von Unit Tests auszustatten. Dazu müssen Sie nicht unbedingt auf die in Ruby integrierte Testsoftware zurückgreifen: Es ist in vielen Fällen ausreichend, während der Initialisierung die eine oder andere kleine Testroutine abzuarbeiten.

Ruby, statisch

irb und sein unter Windows lauffähiger Bruder fxri sind für erste Gehversuche mit der neuen Programmiersprache beinahe ideal geeignet. Ab einem gewissen Grad an Programmkomplexität ist es wünschenswert, den Interpreter mit klassischen Codedateien zu versorgen. Dies wird über eine Vielzahl verschiedener Laufzeitumgebungen ermöglicht. Eine von Matsumoto gewartete und zu seinen Ehren als MRI (Matz’s Ruby Intepreter) bezeichnete Runtime gilt als Standard – sie steht als native Applikation für diverse Betriebssysteme zur Verfügung.

Hier stechen einige Projekte besonders aus der Masse heraus, die einen Ruby-Interpreter in einer anderen Programmiersprache zu realisieren suchen. JRuby ist eine Runtime, die innerhalb einer JVM lebt. Auf diese Art und Weise lassen sich Ruby-Programme unter Android oder im Browser ausführen. Im .NET-Bereich ist die Lage weniger befriedigend. Das ehemals von Microsofts Dynamic Language Runtime Team entwickelte IronRuby wurde trotz anfänglicher Popularität und einiger Fachbücher zum Thema seit 2011 nicht mehr weiterentwickelt.

Eine Frage der Kompatibilität

Rubys Entwicklung wurde anfangs von Matsumotos MRI vorangetrieben: Die in der gerade aktuellen Version des Interpreters implementierte Sprache galt als „Goldstandard“. Daraus ergab sich ein nicht unerheblicher Grad an Inkompatibilität, der die Weiterverwendung von als Gem bezeichneten Programmmodulen erschwerte. Eine im Jahr 2006 gestartete Initiative namens RubySpec versucht, diesem Problem entgegenzuwirken. Als „Lösungsweg“ wurde die Definition von Spezifikationen ersonnen, eine Batterie von Unit Tests prüft die Einhaltung der Regeln. Im Moment dient eine auf der Basis von 1.8.x und 1.9 definierte Testbatterie als Standard. Aktuellere Versionen der Sprache erfüllen diese im Großen und Ganzen; je nach Portierung und Versionsstand sind mitunter Besonderheiten zu beachten.

Lerne mehr

Matsumotos konstant-freundliche Herangehensweise an Probleme führt dazu, dass die Ruby-Community – wie schon in der Einleitung besprochen – Quereinsteigern mit relativ offenen Armen begegnet. Dies wirkt sich auch auf das Verhalten von Buchautoren aus, die ihre Werke gerne mit Genehmigung des Verlages als E-Book kostenfrei anbieten.

Der kostenlos einsehbare „Why’s Poignant Guide to Ruby“ verdient eine „honorable mention“. Das als eine Art Mischung aus Roman, Comic und Programmierlehrbuch zusammengestellte und mit hauseigenem Hintergrundsound versehene Werk lässt sich nebenbei lesen – bei konkreten Fragen ist es eher ungeeignet.

An Metaprogrammierung interessierte Entwickler sollten sich das leider nur in englischer Sprache verfügbare „Metaprogramming Ruby: Program Liket he Ruby Pros“ von Paolo Perrotta ansehen. Das bei Amazon um rund 23 Euro erhältliche Werk ist zwar nicht mehr zu 100 Prozent auf dem aktuellsten Stand, bietet aber eine höchst lesenswerte und tiefgehende Einführung in die Thematik.

Fazit

Ruby ist eine andere Welt. Wer von Programmiersprachen wie C/C++ und Java umsteigt, muss an vielen Stellen radikal umdenken. Nach dem Überwinden dieser Hürden stellen Entwickler oft fest, dass die von Matsumoto gern zitierte Idee der „Sprache für den Programmierer“ so falsch nicht ist. Bei geschickter Nutzung der Möglichkeiten zur Metaprogrammierung erlaubt Ruby die Realisierung von immens kompakten Programmen, die trotz ihrer geringen Länge sehr leistungsfähig sind. Unsere hier gezeigten Beispiele kratzen an vielerlei Stelle nur an der Oberfläche dessen, was sich mit Ruby realisieren lässt.

Die Erfindung des Japaners unterscheidet sich beispielsweise von LISP insofern, als die verwendete Syntax vertrauter ist. Ruby ist somit eine Art Kompromiss: Die Sprache bietet zwar viele fortgeschrittene Features, verhält sich am Ende aber doch irgendwie „wie erwartet“. Nicht zuletzt aus diesem Grund konnte die Sprache im Laufe der letzten Jahre eine rasend wachsende Fangemeinde gewinnen: Es gibt eine Vielzahl von Unternehmen, deren Applikationen ausschließlich in Ruby entstehen.

Aufmacherbild: One red ruby crystal held by tweezers von Shutterstock / Urheberrecht: Imfoto

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -