Warum Clojure?
18.01.2013 Permalink Die letzten sechs Monate habe ich mich in meiner spärlichen, abendlichen Freizeit mit der JVM basierten Programmiersprache Clojure beschäftigt. Wer nicht lange suchen will, um einen vernünftigen Abriss zu erhalten, kann die Einführung von Mark Volkmann lesen.Ich hatte mich in den letzten paar Jahren schon mit Groovy und Scala beschäftigt, und mir anhand der Bücher von Dierk König bzw. Martin Odersky die Sprach-Features angesehen, alles allerdings ohne Versuche zu unternehmen, irgendetwas anderes als Beispiele zu probieren. Im letzten Jahr habe dann ich von zwei völlig unterschiedlichen Personen Berichte über vielversprechende Features von Clojure erhalten. Die Fremdartigkeit von Clojure Programmen verglichen mit C-Syntax-basierten OO-Sprachen wie Java, C# oder Scala gab dann den Ausschlag: das will ich richtig lernen.
Inzwischen habe ich nicht-triviale Teile eines Rich Client Frameworks implementiert (darunter eine GUI DSL, Databinding, Validierung, unittestbare Controller), eine Domäne also, die für Clojure auf den ersten Blick ungeeignet erscheint, da ein Rich Client Unmengen veränderlichen Zustands enthält. Doch es geht ohne besondere Kniffe und mit weit weniger Gehirnschmalz als die vergleichbare Java Implementierung, die ich in den letzten 18 Monaten für einen unserer Kunden geschrieben habe.
Das hervorragende Buch Clojure Programming versucht zu Beginn, den Leser vorzubereiten: 'Clojure demands that you raise your game, and pays you back for doing so.' Das trifft den Nagel auf den Kopf. Es ist eine Herausforderung wie der Marsch auf einen Gipfel. Und dann sieht man die Welt von 'da oben' auf eine andere Weise. Es verändert die Perspektive. Und lässt mich vieles, was ich in den letzten Jahren für 'Okay' hielt, in Zweifel ziehen.
OOP als Hinderniss
Ich sehe OOP mittlerweile kritisch und halte es nicht mehr für einen Teil der
Lösung... sondern einen Teil des Problems:
Wir erschaffen in OO Programmen am laufenden Band neue Datenstrukturen mit dazugehörigem Verhalten. Die Instanzen der resultierenden Klassen sind i.a. nicht in dem Sinne einheitlich, dass ich generelle Funktionen auf sie anwenden kann. Um Probleme in Java generell zu lösen, muss ich z.B. folgendes tun:
- Ausgiebig Reflection verwenden, womit ich die Sicherheit des statischen Typsystems aushebele. Interessanterweise funktionieren alle fortschrittlichen Java Frameworks wie das Spring DI Framework, Tapestry 5, Hibernate, Dozer usw. nur mittels Reflection.
- Eine gewisse Gleichartigkeit mittels Interfaces einziehen, und dort, wo ich nicht Herr der Klassen bin, Adapter erschaffen, die ich an geeigneter Stelle instanziieren muss.
Klassen enthalten umfangreiche Mengen veränderlicher, meist privater Attribute, die allerdings überwiegend durch entsprechende Setter wieder beschreibbar werden. Es gibt hier keine Garantien bzgl. des Zustands eines Objekts. Jeder Systemteil kann prinziell den Zustand ändern und das, zum Leidwesen nebenläufiger Programme, zu jedem Zeitpunkt. Die Konzepte von Identität und Zustand sind vermischt. Es ist nicht umsonst guter Programmierstil in Java und Scala, unveränderliche Objekte zu verwenden, wenn es irgendwie geht.
In Backends typischer Enterprise-Systeme findet man überwiegend keine OO Programme im Sinne der Erfinder, sondern zustandslose Funktionen, die auf Datenstrukturen operieren, welche in einer OO Sprache geschrieben wurden. OOP ist hier im wesentlichen nutzlos.
Heutige Unternehmensanwendungen lassen sich nur noch wirtschaftlich erstellen, indem möglichst viel aus dem Ökosystem einer Programmiersprache wiederverwendet wird. Mit anderen Worten: niemand würde heute ein Enterprise-Java-System bauen, das nicht mindestens ein Dutzend Fremdbibliotheken verwendet. OOP geht allerdings davon aus, dass eine Klasse eine nützliche, abgeschlossene Implementierung einer Verantwortlichkeit ist. Wenn ich nicht Eigner der Klasse bin, z.B. weil sie aus einer Fremdbibliothek stammt, dann kann ich heute in Java Vererbung oder das Decorator-Muster nutzen, um eine erweiterte Variante der Klasse zu bekommen. Beides erfordert, dass ich kontrollieren kann, wie Instanzen entstehen, was nicht immer gegeben ist. Natürlich kann ich alternativ statische Methoden oder 'Service'-Klassen erstellen, die auf den öffentlich zugänglichen Teilen eine fremden Klasse zusätzliche Funktionalität schaffen. Um dann den syntaktischen Bruch bei der Benutzung zu eliminieren, bieten Sprachen wie Xtend oder C# Extensions an. Extensions erscheinen mir wie ein implizites Eingeständnis, dass Klassen i.a. vom Autor nicht abschliessend mit genügend Funktionalität versehen werden können. Und ich schließe daraus: Klassen als wichtigstes Sprachmittel zur Erreichung des hohen Guts 'Wiederverwendung' sind offensichtlich nicht ausreichend, sondern erzwingen nicht selten Umwege, die man dann als Design-Patterns bezeichnet.
Macht Clojure alles richtig?
Weiß ich nicht. Aber viele Features der Sprache beseitigen gezielt die
prinzipiellen Schwächen von OOP. Das äußert sich dann dadurch, das
Clojure Programme nur einen Bruchteil der Zeilenzahl von
funktionsgleichen Java Programmen besitzen. Viele Probleme, die wir in
OO Sprachen mithilfe von Dependency-Injection, AOP, Reflection, Annotationen und
anonymen inneren Klassen lösen, verschwinden fast vollständig.
Wo für die sichere Umsetzung von Enterprise-Architekturen heute mit wachsender Begeisterung MDSD Verfahren zum Einsatz kommen, kann mit vertretbaren Abstrichen in Komfort und Tooling das Clojure (und allen Lisp-Sprachen) eigene Makro-System verwendet werden. Da Makros Teil von Clojure sind, ist keine Anpassung des Builds, kein expliziter Generatorbau mit fremdartigen Werkzeugen, keine Konfiguration von Compiler-Plugins und keine Bindung an eine IDE wie Eclipse nötig. Das wirkt sich vor allem auf den Kopf aus: Mini-Softwaregeneratoren, die das Schreiben von Boilerplate-Code vermeiden helfen, sind nur fünf bis zehn Zeilen entfernt. Als Entwickler denke ich daher nicht über 'Generatorbau' mit allen Konsequenzen nach, sondern erschaffe mir die erforderliche Abkürzung sofort.
Ich könnte mit einem Dutzend von Features (Destructuring, persistente Datenstrukturen, Pre- und Post-Conditions, Software-Transactional-Memory, Funktionen höherer Ordnung, bestehende Typen um neue Protokolle erweitern, Multi-Methoden, zyklenfreie Namespaces, REPL und interaktive Programmierung, Homoiconicity, Metadaten, Keywords) weiterschwärmen, doch ich will die mir ersichtlichen Nachteile, die heute bei Clojure bestehen, auch aufzählen:
- Alles, was heute auf der JVM läuft, ist von Clojure aus relativ bequem erreichbar. Insofern steht das vollständige Java Ökosystem an Fremdbibliotheken bereit. Allein: manche dieser Bibliotheken entsprechen nicht der Clojure Philosophie. Man wird sie nur dort einsetzen, wo es kein ausreichendes Clojure Pendant gibt. Diese Pendants entstehen gerade, aber die Feature-Menge ist noch nicht vergleichbar.
- IDEs können mit dem Code statisch getypter Sprachen wie Java, C# oder Scala eine deutlich bessere Code-Completion anbieten und die Navigation durch eine Code-Basis extrem erleichtern. Durch Typ-Inferenz ist es in Scala oder Xtend nicht mal nötig, immer alle Typen hinzuschreiben. Das geht Clojure natürlich ab, hier schreibe ich Typen (außer aus Performanzgründen) gar nicht hin. Die Werkzeuge (Plugins für Emacs, Eclipse oder IntelliJ) bieten heute nicht denselben Komfort der in Java verfügbar ist. Clojure macht durch seine starke Vereinheitlichung im Code, den REPL Prozess und die mächtigen Funktionen manches wett. Man gewinnt so Zeit, die man aufgrund des schwachen Toolings aber auch hier und da wieder verliert.
- Bildlich gesprochen komme ich in Java ohne Unittests vielleicht einen Kilometer weit, bevor ich Probleme spüre. In einer dynamisch getypten Sprache tut's schon nach 150 Metern weh. Um robuste, veränderbare Software zu entwickeln, muss ich in beiden Welten Unittests schreiben, insofern steht Clojure hier nicht wirklich schlechter da, auch wenn sich das Programmieren ohne statische Typen wie Radfahren ohne Stützräder anfühlt: erst ist man unsicher, aber letztlich wird man sicher und viel schneller.
- Clojure ist eine junge Programmiersprache, und funktionale Programmierung ist bei weitem nicht so weit verbreitet wie imperative Programmierung (mit oder ohne OO). Natürlich ist das ein Henne-Ei-Problem, vor dem auch Java stand. Doch der kognitive Weg von OOP zu FP scheint mir weiter zu sein als von C++ zu Java. Ich glaube, dass viele Entwickler mit Clojure produktiver wären als mit Java, wenn sie die ewigen Umwege in Java als solche erkennen würden und die Alternativen in Clojure wüssten. Daher habe ich große Hoffnung. Doch Trends im Software-Engineering sind eher wie Moden, sie entstehen selten durch das Streben nach besseren Methoden und Technologien.
- Es gibt heute noch 'Experten', die die Langsamkeit von Java beklagen. Sie haben bedingt Recht, aber in der Enterprise-IT interessiert das niemanden mehr. Performance wird an anderen Stellen 1000-fach verplempert. Der Clojure-Compiler produziert Bytecode, insofern gelten die gleichen Bedingungen. Allerdings kosten persistente Datenstrukturen, Multi-Methoden und die großzügige Verwendung von Maps sicherlich einige Taktzyklen mehr. Ich gehe davon aus, dass wieder die gleichen 'Experten' stöhnen, und das dieser Umstand nach einem Jahr wieder genauso wenig interessiert.
Ich halte Clojure für einen Produktivitäts-Booster in Projekt-Teams von erfahrenen, qualitätsbewussten Entwicklern, wenn die ersten Hürden erstmal genommen sind.
Entwickler, die nicht in der Lage sind, im Kern zu verstehen, was sie gerade beackern, werden mit Clojure allerdings wenig Glück haben. Frameworks wie Hibernate oder JSF, die die tatsächliche Welt (DB Relationen bzw. HTTP/HTML/CSS) zu verbergen versuchen, passen nicht so recht in das Clojure Weltbild. Es geht also nicht ohne profundes Know-How über das Einsatzgebiet. Clojure wird vermutlich nie eine Programmiersprache sein, die irgendwie der Klickibunti-Programmierung in die Hände spielt.
Clojure ist auf jeden Fall eine sichere Wette, wenn man die eigenen Fähigkeiten verbessern und eine wesentlich weitere Perspektive auf Programmierung gewinnen will.