(14.02.05 21:45)

Guter Code kommuniziert

Guter Code kommuniziert die dahinter stehenden Absichten klar und deutlich. Guter Code basiert auf klaren, leicht verständlichen und einfach erklärbaren Konzepten. Guter Code ist so einfach wie möglich, aber nicht einfacher. Guter Code löst ein Problem angemessen, er fügt dem Problem nichts Unnötiges hinzu und lässt nichts Wesentliches weg.

Sprachen wie Smalltalk oder Ruby unterstützen das Schreiben von ausdrucksstarkem Code besonders gut, da sich mit ihnen auf einfache Art und Weise Code schreiben lässt, der sich fast wie Prosa liest. Aber auch in anderen Sprachen lässt sich mit einfachen Mitteln die Klarheit und Verständlichkeit von Code wesentlich verbessern wie das folgende Java-Beispiel zeigt.

Ein Beispiel - RDF-Schnellbleiche

Die Klasse, um die es im Beispiel geht, stammt aus einem auf RDF-basierten Kernsystem, an dem ich momentan arbeite. In RDF gibt es im Wesentlichen drei wichtige Konzepte: 1) Alles ist eine sogenannte Ressource. 2) Über solche Ressourcen können Dinge ausgesagt werden. Hierzu werden entsprechende Statements formuliert, die sich stets auf eine bestimmte Ressource als Subjekt beziehen und dabei auf eine andere Ressource zeigen, die für diese Aussage das Objekt ist. Die Bedeutung einer Aussage ist durch ihr Prädikat definiert. 3) Das Prädikat einer Aussage ist stets eine spezielle Art von Ressource, genannt Property. Jedes Statement ist somit ein Tripel aus Subjekt, Prädikat und Objekt. Hier ein paar Beispiele für Aussagen:

James Bond ist Geheimagent.

James Bond trinkt Martini.

James Bond ist Romanfigur


Einfache Verwendung - schwieriges Aufsetzen

Die von der Klasse StatementTemplates gebotene Funktionalität ist im Kern sehr einfach: Sie erlaubt es, eine Sammlung von sogenannten StatementTemplates zu hinterlegen. Dabei handelt es sich um Beschreibungen von Aussagen, die zu wohldefinierten Zeitpunkten für bestimmte Ressourcen formuliert werden sollen, z.B. wenn eine Ressource neu erzeugt wird oder eine bestimmte Aussage formuliert wird; Letzteres erlaubt es auf einfache Weise, inferierte Aussagen explizit zu hinterlegen.

Die eigentliche Algorithmik der Klasse StatementTemplates steckt in der Methode apply. Diese sorgt dafür, dass auf der Grundlage der hinterlegten StatementTemplates entsprechende Statements formuliert werden. Die Verwendung ist - wie gesagt - nicht schwierig und tut hier auch nichts weiter zur Sache. Uns interessiert das Aufsetzen (Initialisierung) eines StatementTemplates-Objekts. Denn da werden die Dinge schon anspruchsvoller, zumindest bei herkömmlicher Umsetzung mit Hilfe von Konstruktorargumenten. Bei der Definition eines StatementTemplate-Objektes müssen nämlich mehrere Dinge festgehalten werden:

Es muss definiert werden, ob ein StatementTemplate in jedem Fall angewendet werden (always) soll oder nur, wenn eine entsprechende Aussage noch fehlt (ifMissing).

Für das Subjekt der Aussage muss entweder die gewünschte Ressource hinterlegt werden, oder aber man entscheidet sich dafür, die Ressource, welche an apply übergeben wird (sameSubject). Es handelt sich dabei um die Ressource, die entweder neu erzeugt worden ist oder für die eine Aussage formuliert worden ist; für beide Fälle kann man ja ein StatementTemplates-Objekt definieren.

Schritt 2 gilt sinngemäss für Prädikat und Objekt der Aussage.


Die Lösung

Der Versuch, die Deklaration eines StatementTemplate-Objekts mit Hilfe eines Konstruktors oder einer einfachen Initialisierungsmethode zu lösen, macht die Verwendung der Klasse StatementTemplates unnötig kompliziert. Mit einem kleinen Trick können wir dagegen das Aufsetzen eines StatementTemplate-Objekts als einfach verständliche, ausdrucksstarke, selbsterklärende und damit praktisch fehlerfrei verwendbare Lösung umsetzen.

Bevor wir uns die Details der Implementierung ansehen, hier zuerst ein Verwendungsbeispiel:

assignSlot
  .onCreateStatement()
  .always().addStatementWith()
    .sameSubject()
    .predicate(fScopeModel.scope)
    .sameObject();

Die obige Deklaration lässt sich sehr gut lesen und erklärt sich eigentlich von selbst, wenn man weiss, dass assignSlot eine Property-Ressource ist, deren Methode onCreateStatement ein StatementTemplates-Objekt liefert, das dann dazu verwendet werden kann, um die gewünschten StatementTemplate-Objekte zu definieren. Die Bedeutung der einzelnen Methodenaufrufe ist unschwer zu erkennen und deckt sich mit den oben beschriebenen Anforderungen. Im Beispiel wird also immer dann, wenn eine Aussage mit einer assignSlot-Property als Prädikat formuliert wird, für die gleiche Ressource eine weitere Aussage mit dem Prädikat scope und dem gleichen Objekt wie in der ursprünglichen Aussage formuliert. Soll eine Aussage mit dem Prädikat scope nur dann formuliert werden, wenn eine solche für das Subjekt noch nicht existiert, dann sieht die Deklaration so aus:

assignSlot
  .onCreateStatement()
  .ifMissing().addStatementWith()
    .sameSubject()
    .predicate(fScopeModel.scope)
    .sameObject();

Wer jetzt „Code-Smell„ ruft, hat eigentlich Recht, denn diese Form des Method-Chaining ist ein klassischer Code-Smell, den wir normalerweise vermeiden sollten. Doch im vorliegenden Fall ist diese Verkettung von Methodenaufrufen (Daisy Chaining) ausdrücklich gewünscht. Wir erhalten dadurch nämlich so etwas wie eine Minisprache in Java, deren Verwendung wesentlich leichter ist als eine herkömmliche Implementierung. Und in einer modernen IDE wie Eclipse oder IntelliJ hilft uns die Code Completion zusätzlich mit sinnvollen Vorschlägen beim Schreiben des Codes für StatementTemplates.

Die Implementierung ist denkbar einfach. Sie besteht im Wesentlichen aus einer Reihe von Schnittstellen:

public interface IStatementAddable {
  public IStatementTemplate ifMissing();
  public IStatementTemplate always();
}

public interface IStatementTemplate {
  public ISubject addStatementWith();
}

public interface ISubject {
  public IPredicate sameSubject();
  public IPredicate subject(Resource subject);
}

public interface IPredicate {
  public IObject samePredicate();
  public IObject predicate(Resource predicate);
}

public interface IObject {
  public IStatementAddable sameObject();
  public IStatementAddable object(Resource object);
}

Und einer Klasse, welche diese Schnittstellen unterstützt und die so implementiert ist, dass ihre Instanzen sich jeweils selber als Objekt der geforderten Schnittstelle zurück geben. Das sieht dann für die Methode ISubject.sameSubject() zum Beispiel so aus:

public IPredicate sameSubject() {
  fSameSubject = true;
  return this;
}

Der Trick, sofern man überhaupt von Trick reden will, besteht darin, dass wir das StatementTemplate-Objekt einfach verschiedene Rollen spielen lassen, die es während seiner Konfiguration durchlaufen muss. Das Objekt verwandelt sich von einem Typ in den anderen und sorgt so dafür, dass seine Verwender die jeweils nötigen Informationen einfach und verständlich festhalten können.

Aus Platzgründen veröffentliche ich hier nicht den ganzen Code. Wer es aber genau wissen will, der ist herzlich eingeladen, einen Blick auf den Quelltext der Klasse StatementTemplates zu werfen.

Diese Form schnittstellenbasierter Minisprachen eignet sich immer dann, wenn wir es mit komplexem Initialisierungscode zu tun haben. Deklarativer Setup-Code trägt wesentlich dazu bei, unseren Code verständlich und wartbar zu halten.

-nemo :-)

Über diesen Artikel diskutieren.

Zugriffe heute: 1 - gesamt: 3916.




Guter Code stirbt schnell Talent und Begeisterung

Druckbare Version