Erhältlich ab: Mai 2014
Vor vielen Jahren hatte ich mit einem System zu tun, das bei bestimmten Eingabewerten Daten verfälscht in der Datenbank ablegte. Das Phänomen gab dem ganzen Team Rätsel auf, zumal das unser erstes Projekt mit einem O/R Mapper war und wir also keine Intuition für mögliche Fehlerursachen hatten.
Zur Analyse habe ich einen JDBC-Treiber implementiert, der alle Aufrufe protokolliert und dann an den eigentlichen Datenbanktreiber weiterreicht. Und ich war überrascht, wie einfach und schnell das mit dynamischen Proxies geht.
Ein dynamischer Proxy ist eine synthetische, zur Laufzeit des Programms erzeugte Klasse, die ein oder mehrere Interfaces implementiert und Aufrufe an einen InvocationHandler delegiert. Dieser Mechanismus ist u. a. nützlich, wenn man Interfaces generisch implementieren will, z. B. um Aufrufe zu protokollieren und dann weiterzuleiten. Das ist eine einfache, leichtgewichtige Form von aspektorientierter Programmierung (AOP) [1]. Listing 1 zeigt eine generische Implementierung eines solchen loggenden InvocationHandlers.
Listing 1
public class LoggingInvocationHandler implements InvocationHandler {
private final Object inner;
public LoggingInvocationHandler(Object inner) {
this.inner = inner;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
final Object result = method.invoke(inner, args);
System.out.println("invoking " + method + ": " +
(args != null ? Arrays.asList(args) : "") +
" -> " + result);
return result;
}
catch(InvocationTargetException exc) {
throw exc.getCause();
}
}
}
Die Klasse erhält im Konstruktor das „innere“ Objekt, das die eigentliche Arbeit leistet und an das die Aufrufe weitergeleitet werden sollen. Die Arbeit passiert in der Methode invoke(), die vom Interface InvocationHandler vorgegeben ist. Jeder Methodenaufruf auf dem dynamischen Proxy löst einen Aufruf dieser Methode aus. Sie hat drei Parameter. Der erste davon ist das Proxy-Objekt, das den Aufruf an diesen InvocationHandler delegiert hat. Er kann hilfreich sein, wenn mehrere dynamische Proxies sich einen InvocationHandler teilen, meist kann man ihn aber getrost ignorieren. Die restlichen beiden Parameter enthalten die aufgerufene Methode als Method-Objekt sowie die Parameter des Aufrufs als Objekt-Array.
Unsere Implementierung ruft als Erstes per Reflection die Methode mit ihren Parametern auf. Anschließend gibt sie die Details des Aufrufs nach System.out aus. Sie könnte beliebige andere Dinge tun – die Aufrufdauer messen, Wertebereiche von Parametern überprüfen, Ergebnisse teurer Aufrufe cachen oder auch Berechtigungsprüfungen auf Basis von Annotationen an der Zielmethode durchführen. Spring AOP funktioniert intern zum Beispiel so.
Dieser gesamte Code steckt noch in einem try-Block mit einem Handler für InvocationTargetException. Das ist der Mechanik von Reflection in Java geschuldet: Ein Aufruf von Method.invoke() wickelt alle Exceptions in InvocationTargetExceptions ein, und wir packen sie hier wieder aus.
Als einfaches Beispiel können wir mit unserem InvocationHandler alle Aufrufe auf einer ArrayList protokollieren (Listing 2). Das Erzeugen einer Proxy-Instanz erfolgt dabei durch einen Aufruf von Proxy.newProxyInstance().
Listing 2
List<String> inner = new ArrayList<>();
List<String> l = (List<String>) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[]{List.class},
new LoggingInvocationHandler(inner));
l.add("a");
Der Aufruf hat drei Parameter, von denen der erste der Classloader ist, mit dem der Proxy erzeugt wird. Hier ist der Kontext-Classloader fast immer die richtige Wahl [2]. Der zweite Parameter enthält die Liste aller Interfaces, die der Proxy implementieren soll. In diesem Fall ist das nur eines, nämlich List.
Der dritte Parameter ist schließlich der InvocationHandler. Dort erzeugen wir einen LoggingInvocationHandler und übergeben ihm die eigentliche Liste, an die er delegieren soll. Das anschließende Hinzufügen eines Elements zur Liste wird jetzt tatsächlich protokolliert.
Um daraus einen JDBC-Treiber zu machen, brauchen wir als Startpunkt eine Klasse, die java.sql.Driver implementiert (Listing 3).
Listing 3
public class LoggingJdbcDriver implements Driver {
public static final String PREFIX = "jdbc:logger:";
public boolean acceptsURL(String url) throws SQLException {
return url.startsWith(PREFIX);
}
public Connection connect(String url, Properties info) throws SQLException {
if (acceptsURL(url)) {
final Driver innerDriver = DriverManager.getDriver(
url.substring(PREFIX.length()));
final Connection innerConn = innerDriver.connect(
url.substring(PREFIX.length()), info);
return (Connection) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[] {Connection.class},
new LoggingInvocationHandler(innerConn));
}
else {
return null;
}
}
... // weitere Methoden, die hier egal sind
}
Der Treiber soll auf alle URLs reagieren, die mit "jdbc:logger:" beginnen. Danach soll dann der „eigentliche“ URL kommen, an den der Treiber delegiert. Die Überprüfung, ob unser Treiber für einen URL zuständig ist, erfolgt in der Methode acceptsURL(). Die prüft einfach, ob der URL mit unserem Präfix beginnt.
Die eigentliche „Implementierung“ unseres Treibers steckt in der Methode connect(). Die holt sich anhand der Connection-URL die „eigentliche“ Datenbankverbindung vom „richtigen“ Treiber und verpackt die in einen dynamischen Proxy mit LoggingInvocationHandler.
Das Driver-Interface enthält noch eine Reihe weiterer Methoden (Version des Treibers etc.), deren Implementierung für unseren Treiber aber durchweg egal ist.
Dieser Treiber loggt alle Aufrufe auf Connections, die er erzeugt. Wenn die Connection aber z. B. ein PreparedStatement erzeugt und zurückliefert, passiert da noch kein Logging.
Listing 4 zeigt eine Erweiterung der invoke-Methode, die generisch auch Statements, ResultsSets und andere Objekte in dynamische Proxies verpackt, die als Rückgabewert einen unserer Proxies verlassen.
Listing 4
// Dieses Listing ersetzt das return-Statement aus Listing 1
if (result == null) {
return null;
}
if (method.getReturnType().isInterface()) {
return Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[] {method.getReturnType()},
new LoggingInvocationHandler(result));
}
return result;
Und weil das Ergebnis auch wieder einen LoggingInvocationHandler hat, funktioniert das auch mehrstufig, also z. B. wenn eine Connection ein Statement erzeugt, das seinerseits ein ResultSet liefert.
Listing 5 zeigt den Treiber schließlich „in der Praxis“. Die erste Zeile startet einen H2-Datenbankserver [3], und die zweite Zeile registriert den Treiber. „Richtige“ Treiber registrieren sich über den ServiceLoader automatisch [4], sodass dieser Aufruf überflüssig wäre. Das ist zwar nicht wirklich kompliziert, sprengt aber den Rahmen dieser Kolumne.
Listing 5
org.h2.tools.Server.main(new String[] {"-tcp", "-web"});
DriverManager.registerDriver(new LoggingJdbcDriver());
final Connection conn = DriverManager.getConnection(
"jdbc:logger:jdbc:h2:tcp://localhost/abc",
"sa",
"");
final PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM INFORMATION_SCHEMA.TABLES where table_name like ?");
stmt.setString(1, "I%");
final ResultSet rs = stmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("TABLE_NAME"));
}
conn.close();
Der eigentliche Code erzeugt eine Connection und liest alle Tabellennamen der Datenbank aus, die mit dem Buchstaben „I“ beginnen. Das ist inhaltlich nicht besonders spannend, aber es zeigt, wie unser Treiber alle Aufrufe protokolliert.
Mit dynamischen Proxies kann man mit wenig Aufwand synthetische Klassen mit generischen Implementierungen erzeugen. Ihre wichtigste Beschränkung ist, dass sie nur auf Interfaces funktionieren. Man kann mit ihnen keine Subklassen bestehender Klassen erzeugen. Außerdem verstecken sie den eigentlichen Implementierungstyp. Wenn wir z. B. in Listing 2 überprüfen wollten, ob die Liste eine ArrayList ist, oder die Referenz sogar auf den Typ ArrayList casten wollten, würde das fehlschlagen. Trotzdem sind dynamische Proxies ein extrem nützliches Werkzeug, um querschnittliche Funktionalität generisch zu implementieren.
Und der Bug aus der Einleitung? Mithilfe des loggenden Wrappers fanden wir heraus, dass der kommerzielle Datenbanktreiber einen Bug hatte, der beim Zusammenspiel von Batch-Updates und BigDecimal als Spaltentyp mit bestimmten Werten reproduzierbar die Zahlen verfälschte. Wir haben daraufhin auf Batch-Updates verzichtet.
Arno Haase ist freiberuflicher Softwareentwickler. Er programmiert Java aus Leidenschaft, arbeitet aber auch als Architekt, Coach und Berater. Seine Schwerpunkte sind modellgetriebene Softwareentwicklung, Persistenzlösungen mit oder ohne relationale Datenbank, und nebenläufige und verteilte Systeme. Arno spricht regelmäßig auf Konferenzen und ist Autor von Fachartikeln und Büchern. Er lebt mit seiner Frau und seinen drei Kindern in Braunschweig.
[1] http://de.wikipedia.org/wiki/Aspektorientierte_Programmierung
Der Heartbleed-Bug in OpenSSL, einer Software, die das Open-Source-Modell in ihrem Namen trägt, ist für Verfechter quelloffener Software ein Stich ins Herz. Ob sich die gesamte Open-Source-Welt deswegen in der „prekären Lage“ befindet, die etwa Patrick Beuth von ZEIT ONLINE (http://bit.ly/1esuGEp) wahrnimmt, darf allerdings bezweifelt werden. Open Source hat Konjunktur. Weshalb sonst würden Unternehmen wie SAP auf quelloffene Technologien wie Eclipse Virgo oder Eclipse Orion setzen? Und weshalb sonst würden Hazelcast oder Hortonworks, die sich ganz auf Open-Source-Technologien spezialisiert haben, großzügige Finanzspritzen erhalten? Allerdings zeigen Venture-Kapital-Summen von 100 Millionen US-Dollar, wie im Fall von Hortonworks: Open-Soure-Software entsteht keineswegs aus dem Nichts. Und Autoren offener Quelltexte leben nicht nur von Luft und Liebe zum Beruf.
Steve Marquess, einer der Gründer der OpenSSL Software Foundation (OSF) legte in einem Blogpost den finanziellen Status quo der Stiftung dar – und der ist in der Tat „prekär“: 2 000 US-Dollar Spenden erhalte sie durchschnittlich pro Jahr. Seit Heartbleed bekannt wurde, seien zusätzlich etwa 9 000 Dollar auf das Konto der OSF eingegangen. Marquess kommentiert die gut gemeinten Zuwendungen nüchtern: Selbst wenn die Spenden weiterhin in derselben Häufigkeit einträfen, würde der finanzielle Aufwand, der nötig ist, um eine so wichtige und komplexe Software wie OpenSSL instand zu halten, bei Weitem nicht erreicht. Gerade einmal elf Entwickler, ein Team der Größe einer Fußballmannschaft, sind an der Entwicklung von OpenSSL beteiligt – genauso viele bzw. genauso wenige wie an der Eclipse-Plattform. Hier wie dort schlägt sich der finanzielle Engpass in personeller Knappheit nieder. Doch von nichts kommt nichts. „There ain’t no such thing as free software“, könnte man das Bonmot „There ain’t no such thing as a free lunch“ des Science-Fiction-Autors Robert A. Heinlein umformulieren.
Zu leicht vergessen Unternehmen, dass sie auch Stakeholder der quelloffenen Software sind, die sie verwenden – und somit niemand außer ihnen die Verantwortung für den Code trägt. Wer ein Geschäftsmodell allein auf dem Altruismus Unbekannter begründet, dem ist nicht zu helfen.
Open-Source-Ökosysteme werden immer größer, anonymer und verflochtener. Und wo es keine eindeutige Zugehörigkeit gibt, ist es umso schwieriger, Zuständigkeiten zu definieren. Die Tragik der Allmende (Tragedy of Commons) lässt grüßen. Blind wird auf Committer-Idealisten vertraut, die aus purer Leidenschaft am Handwerk den Brunnen graben, aus dem alle anderen schöpfen. Ein verfehlter Glaube an eine abstrakte Instanz, die, einem übermenschlichen Produktmanager gleich, das große Ganze im Blick hat.
Findet allerdings ein Umdenken statt, ist die Tragedy of Commons zwar ein ernstes, aber kein existenzgefährdendes Problem für das Open-Source-Modell, sondern eher wie Heartbleed selbst eine kritische Schwachstelle, die sich beheben lässt. An den Grundfesten der Open-Source-Kultur wird der Bug nicht rütteln. Allerdings ist zu hoffen, dass viele Betroffene ihn als Weckruf verstehen und sich personell und finanziell stärker in die Open-Source-Communitys einbringen, von denen ihre Produkte abhängen. Eine neue Maxime von Unternehmen, die die Vorteile von Open-Source-Software nutzen, sollte lauten: Ask not what your software can do for you. Ask what you can do for your software.
Ob sich die Investition in JavaFX mittlerweile lohnt, wollen wir derweil im großen Heftschwerpunkt dieser Ausgabe ermitteln: Mit der Integration in Java 8 hat die UI-Technologie einen großen Schritt nach vorne gemacht. Laut ist indes immer noch die Kritik an der Enterprise-Fähigkeit von JavaFX. Das wird in dieser Ausgabe ebenfalls thematisiert. Verschaffen Sie sich einen Überblick, viel Spaß bei der Lektüre!
Bei aller Diskussion um Softwarequalität, die beste technische und fachliche Schichtung von Anwendungscode und die optimale Frameworkauwahl wird oftmals ein wichtiger Bestandteil des Softwareentwicklungszyklus komplett vernachlässigt: das professionelle Set-up der Entwicklungsumgebung. Mit kleinen Optimierungen lassen sich hier nicht selten große Effekte erzielen. Ein Überblick über eine Reihe von Verbesserungen, die sich schnell und einfach in den eigenen Arbeitsprozess integrieren lassen und das Entwicklerleben deutlich angenehmer machen.
Eine gute Entwicklungsumgebung ist weit mehr als eine IDE. Vielmehr setzt sie sich aus einer ganzen Reihe von Werkzeugen, aber auch Prozessen und Architekturentscheidungen zusammen, die alle das Ziel verfolgen, den einzelnen Entwickler bzw. das Entwicklungsteam in die Lage zu versetzen, schnell und einfach neue Features auf einer Codebasis zu entwickeln.
In der Praxis ist häufig ein wiederkehrendes Verhalten beim Aufsetzen von Entwicklungsprojekten zu beobachten: Nach den ersten Kundengesprächen passiert eine Weile zunächst relativ wenig, da zuerst die finanziellen und organisatorischen Rahmenbedingungen abgesteckt werden müssen. Außer etwas Prototyping und dem einen oder anderen Proof of Concept geschieht kaum etwas. Kommt dann der Startschuss, muss es von heute auf morgen losgehen – dem Kunden ist ja das erste Release schon innerhalb kürzester Zeit versprochen worden. Nicht selten ist hier schon der Punkt erreicht, an dem für eine ordentliche Anwendungsarchitektur wenig Zeit bleibt – das Drumherum, also eine gut eingerichtete Umgebung, wird da fast automatisch aus den Augen verloren.
Die Folgen hiervon sind nicht immer sofort erkennbar, aber auf Dauer extrem teuer. Im günstigsten Fall dauert die eigentliche Entwicklungsarbeit nur länger als nötig, im schlimmsten Fall entstehen enorme Reibungsverluste, und das Ergebnis ist durchsetzt von Bugs und sonstigen Fehlern. Häufig kommt man nach einiger Zeit zu der Erkenntnis „Eigentlich müssten wir uns da einmal richtig Gedanken zu machen – wenn da nur nicht dieser Zeitdruck wäre, die nächste Version zu liefern.“ Im Projektalltag bleibt daher kaum noch Zeit übrig. Es ist ein wenig wie in der Holzfällerparabel (siehe gleichnamiger Kasten).
Ob wir uns also nun die Zeit vor dem Projektstart nehmen oder erst im Nachhinein – sich über das eigene Set-up Gedanken zu machen und dort Verbesserungen einzuführen, wird sich auf jeden Fall auszahlen.
Als ein Mann im Wald spazieren geht, kommt er an einer Lichtung vorbei, wo ein Waldarbeiter gerade Holz hackt. Er sieht ihm eine Weile zu und bemerkt dabei, dass der Arme sich recht abrackert, müht und plagt, nur weil seine Axt recht stumpf zu sein scheint. Schließlich gibt er sich einen Ruck und spricht ihn an: „Hallo! Warum schärft Ihr denn Eure Axt nicht? Die ist ja total stumpf.“ – Der Holzfäller sieht kurz auf und antwortet außer Atem: „Was? Die Axt schärfen? Nein – ausgeschlossen, dazu habe ich keine Zeit – ich muss doch noch soviel Holz hacken!“
Sehen wir uns hierzu zunächst ein Beispiel aus der Praxis an, das der Autor selbst vor nicht allzulanger Zeit erlebt hat. Zur Entwicklung einer Java-Webanwendung mussten eine Reihe von unterschiedlichen Projekten mit Maven gebaut und vom Entwickler jeweils auf einem lokalen Entwicklungsserver deployt werden, damit diese – ebenfalls lokal – getestet werden konnten. Für den einzelnen Entwickler bedeutet dies die immer wiederkehrende Abfolge von monotonen Einzelschritten, nachdem er die eigentlichen Änderungen umgesetzt hat: den Maven-Build- und Deploy-Prozess starten, den lokalen Tomcat-Webserver durchstarten und auf das Redeployment der Anwendung warten.
Alles in allem ein Prozess von bestenfalls etwa fünf Minuten. Auf den ersten Blick kein allzu großer Zusatzaufwand. Auf den zweiten Blick jedoch ein enormer Zeitfresser. Der Build musste schließlich nicht nur einmalig, sondern regelmäßig den gesamten Tag über durchgeführt werden – an einem normalen Arbeitstag durchschnittlich fünfzehn bis zwanzig Mal. Zusammengefasst hat daher jeder einzelne Entwickler mit diesem Prozedere zwischen einer und anderthalb Stunden pro Tag verbracht – immerhin gute 15 Prozent der Arbeitszeit bei einem Acht-Stunden-Tag.
Aufsummiert geht bei einer Teamgröße von fünf Entwicklern dabei fast ein gesamter Manntag nur für Wartezeit verloren. Oder anders ausgedrückt: Eigentlich ist das Fünferteam nur so produktiv wie ein Viererteam. Ein extrem hoher Preis.
Die Verbesserung dieser speziellen Situation war relativ einfach: Um den skizzierten Prozess zu optimieren, wurde ein kleines Unterprojekt erstellt. Dieses Unterprojekt stellte eine Launcheranwendung zur Verfügung, deren einzige Aufgabe darin bestand, einen embedded Tomcat-Webserver zu starten und die in der bestehenden Quellcodebasis vorhandenen Projekte entsprechend zur Laufzeit in diesen zu integrieren und lokal direkt aus der IDE heraus verfügbar zu machen.
Anstatt den kompletten Build- und Deploymentzyklus zu durchlaufen, konnte nun nach einer Änderung innerhalb der IDE einfach der Launcher neu gestartet werden. Die Turnaround-Zeit, also die Zeit, die von der Umsetzung der Änderung bis zum Aufruf der Webseite im lokalen Webserver benötigt wird, wurde auf unter eine Minute gesenkt. Das gesamte Entwicklungsteam war daraufhin auf einen Schlag um 20 Prozent produktiver. Zusätzlich konnten – sozusagen als „Abfallprodukt“ – direkt die normalen Debugging- und sonstigen Werkzeuge der IDE besser eingesetzt werden.
Natürlich ließ sich dies nicht ganz ohne zusätzlichen Aufwand erreichen. Die Erstellung des Launcherprojekts sowie die Integration in die Codebasis dauerte – alles in allem – etwa drei Manntage. Verglichen jedoch mit der bereits nach einer Woche gewonnenen Zeit eine mehr als lohnende Investition.
Zusätzlich zu den sofort messbaren zeitlichen Ergebnissen sind allerdings noch zwei weitere Punkte erwähnenswert: Durch die Verwendung des embedded Tomcat-Webservers entfällt für zukünftige Entwickler die Notwendigkeit, initial eine lokale Tomcat-Installation durchzuführen und diese auf dem aktuellen Stand zu halten. Last but not least kann sich der einzelne Entwickler wieder auf seine eigentliche Aufgabe konzentrieren: die Implementierung neuer Features. Kaum etwas trägt so stark zu schlechter Stimmung unter Entwicklern bei wie unproduktive Zeit, in der auf das Beenden eines Skripts oder das Starten eines Application Servers gewartet wird. Sehen wir uns nun einige Ansatzpunkte an, in denen wir die Entwicklungsumgebung optimieren können.
Als Ramp-up wird die initiale Einrichtung der Entwicklungsumgebung bzw. der projektspezifischen Einstellungen bezeichnet. Letzten Endes alles das, was ein Entwickler zu tun hat, um in einem Projekt produktiv einsteigen und an der Implementierung teilnehmen zu können.
Typische Aufgaben, die hierbei erledigt werden müssen, sind die Installation und Konfiguration von IDE, lokalen Application Servern oder Datenbanken.
Häufig existieren hierzu innerhalb einer Organisation oder eines Projektteams vorgefertigte Checklisten, die von neuen Teammitgliedern abgearbeitet werden müssen. Nicht selten ist allerdings dieses Wissen auch nur in den Köpfen der bisherigen Entwickler verfügbar, was einen Spaziergang von Pontius zu Pilatus und wieder zurück erfordert.
Beide Vorgehensweisen (das Befolgen einer Anleitung sowie das Durchfragen) kränkeln häufig daran, dass selbst diese Schritte nicht immer konsistent, geschweige denn aktuell, gehalten werden. Wahrscheinlich jeder hat schon einmal einen Satz gehört wie: „Ach ja, das haben wir ja vor zwei Monaten umgestellt. Stimmt. Also dann müssen wir nochmal anders anfangen.“
Letzten Endes führt auch das wieder zu erhöhtem Aufwand (und damit erhöhtem Zeitbedarf), immer wieder zu Frust beim einzelnen Entwickler oder sogar offenen Konflikten im Team. Doch auch hier gibt es gute und gangbare Alternativen. Projekte wie XAMPP [1] verkürzen die Installationsdauer für den Stack von Webserver, Datenbank und weiteren Entwicklungstools enorm, indem sie vorgefertigte Distributionen zur Verfügung stellen. Zwar bringen einen diese Distrubutionen schon einen großen Teil des Wegs vorwärts, doch haben die meisten Projekte immer noch zusätzliche und projektspezifische Anforderungen. Es empfiehlt sich daher, aufbauend auf bestehenden und vorgefertigten Distributionen, eine eigene Projektdistribution für den Entwicklungstoolstack zu erstellen und vor allem ständig aktuell zu halten.
Da die Distributionsanbieter ständig aktuelle Versionen liefern, wird ein Mechanismus benötigt, der basierend auf der Ausgangsdistribution eine eigene Projektdistribution erstellt. Idealerweise besteht hierzu ein Skript (oder kleines Hilfsprogramm), das aus der Quelldistribution und einem Satz von projektspezifischen Einstellungen und Zusatzinhalten die entsprechende Zieldistribution erstellt. Das Ziel hierbei sollte immer sein, diese Zieldistribution so komplett und angepasst wie möglich bereitzustellen.
Es wird jedoch selten gelingen, einem neuem Teammitglied ein einziges großes Archiv (oder einen einzigen Installer) zur Verfügung zu stellen, in dem alle benötigten Ressourcen und Einstellungen vorhanden sind. Wenn allerding – ganz im Sinne der 80/20-Regel – bereits 80 Prozent der benötigten Ressourcen mit relativ geringem Aufwand bereitgestellt werden können, ist auch hier schon eine Menge gewonnen. Für diejenigen Werkzeuge und Einstellungen, die nicht automatisch bereitgestellt werden können, sollte eine klare Auflistung für alle Teammitglieder (idealerweise in einem Projektwiki) bereitgestellt werden.
Der Schwerpunkt hierbei sollte darauf gelegt werden, lediglich die Abweichungen vom Standard zu erläutern und dem Entwickler zu erklären, welche zusätzlichen Schritte von ihm durchgeführt werden müssen, um an der Codebasis mitarbeiten zu können.
Selbst bei einer ideal vorgefertigten Distribution wird es immer noch Einstellungen und Funktionsschalter geben, die spezifisch für einen einzelnen Entwicklern oder bestimmte Szenarien gehalten werden müssen. Ein Paradebeispiel hierfür ist die Angabe von Datenbankzugangsdaten. So muss in der Regel eine Anwendung auf verschiedenen Stufen und verschiedenen Systemen mit unterschiedlichen Inhalten und Konfigurationen verfügbar sein (z. B. Entwicklung, Test, Abnahme und Produktion). Alle diese unerschiedlichen Systeme verwenden typischerweise ihre eigene Datenbasis und benötigen daher eine eigene Datenbank. Die Zugangsdaten hierzu müssen auf jedem System unterschiedlich gehalten werden können.
Auch jeder einzelne Entwickler stellt in dieser Betrachtungsweise ein eigenes System dar, das (mindestens) eine eigene Konfiguration benötigt. Unterschiedliche Anforderungen können sogar bedingen, dass der einzelne Entwickler eine Vielzahl an wechselbaren Einstellungen vorrätig hält. Bei der Entwicklung einer Anwendung mit Schnittstellen an externe Systeme kann es für einen Entwickler notwendig sein, während seiner Entwicklung sowohl gegen das Test-, als auch das Produktivsystem einer Schnittstelle Tests auszuführen.
Die Anwendung muss daher von vornherein nicht nur in der Lage sein, unterschiedliche Systemkonfigurationen zu unterstützen, sondern es explizit erlauben (wenn nicht sogar fördern), die für ein System abweichenden Konfigurationsparameter anzupassen. Eine Möglichkeit hierzu ist die Verwendung eines dedizierten Konfigurationsprojekts. Aufgabe dieses Projekts ist es, auf der einen Seite alle verfügbaren Konfigurationen an zentraler Stelle zu sammeln und bereitzustellen, aber auch den Wechsel der Konfigurationen selbst schnell und einfach zu ermöglichen.
Angenommen eine Anwendung benötigt zur Konfiguration eine XML-Datei mit dem Namen config.xml, so könnte die Verzeichnisstruktur des Konfigurationsprojekts in etwa wie in Listing 1 aussehen.
Listing 1: Verzeichnisstruktur des Konfigurationsprojekts
project
+ config
+ developers
+ crobert
- config.xml
+ adent
- config.xml
+ systems
+ prod. test.com
- config.xml
+ test1.test.com
- config.xml
+ test2.test.com
- config.xml
...
Aufgabe der Anwendung ist es nun, anhand der aktuellen Umgebung herauszufinden, welche der vorhandenen Einstellungen für die lokal zu startende Anwendung verwendet werden soll. Idealerweise sollte diese Auswahl für den Entwickler möglichst transparent ablaufen und keine zusätzlichen Einstellungen beim Start erfordern, da dieses Setzen wiederum anfällig für Fehler ist.
Eine gute Möglichkeit in Projekten, die Maven als Build-Werkzeug verwenden, ist die Option eines dynamisch definierten Classpaths, der automatisch auf den aktuell angemeldeten Benutzer reagiert (Listing 2).
Listing 2: Maven-Konfiguration
<project>
...
<build>
<resources>
<resource>
<directory>src/main/resources/</directory>
</resource>
<resource>
<directory>src/developers/${user.name}/resources/</directory>
</resource>
</resources>
</build>
...
Zusätzlich zum Standard für Ressourcen, die mit einer JAR-Datei eingebunden werden (src/main/resources), wird hier ein zusätzliches für jeden Benutzer verschiedenes Verzeichnis ausgewählt. Der große Vorteil an dieser Lösung ist, dass der Benutzer sich nicht um die Auswahl (s)eines Verzechnisses kümmern muss – das System konfiguriert dieses automatisch – alleine durch die Tatsache, dass er mit seinem Benutzernamen am entsprechenden Rechner angemeldet ist.
Ein weiterer, nicht zu vernachlässigender Vorteil des Vorhandenseins aller verfügbarer Konfigurationen als Dateien innerhalb eines Projekts ist die Tatsache, dass nun Änderungen deutlich besser eingepflegt werden können. Es gibt immer wieder Situationen – gerade bei sich schnell verändernden Projekten –, die es erfordern, bestimmte Konfigurationsparameter und -einstellungen nachträglich komplett zu ändern. So ist es dem Entwickler, der genau diese Änderung durchführt, schnell und einfach möglich, die notwendigen Änderungen auch in die Konfigurationsdateien sowohl aller Umgebungen als auch aller Mitentwickler zu integrieren und sich damit eine Verteilernachricht wie z. B. „Bitte alle Änderungen Y nach dem nächsten Update durchführen“ zu ersparen.
Allgemeines Ziel bezüglich der Konfiguration sollte auch hier sein, eine möglichst einheitliche Konfiguration auf allen Systemen zu erreichen und die tatsächlich abweichenden Einstellungen schnell und einfach änderbar zu machen.
In nahezu jedem Projekt entsteht eine Reihe von Hilfstools und Skripten, die benötigt werden, um bestimmte Entwicklungsartefakte oder Zwischenressourcen zu erzeugen. Ein Beispiel hierfür wäre die Erstellung von Lokaliserungs-Excel-Tabellen. Diese werden von der Entwicklungsabteilung durch ein Skript automatisch aus dem aktuellen Entwicklungsstand erzeugt und an den Kunden gesendet. Dieser füllt die entsprechenden Spalten mit lokalisierten Werten für alle angebotenen Sprachen, leitet die ausgefüllte Excel-Datei zurück an die Entwicklungsabteilung, die mit einem weiteren Skript die lokalisierten Werte in die Anwendung übernimmt. Oftmals sind solche Skripte Ad-hoc-Projekte, die Inselwissen bei einzelnen Entwicklern konzentrieren und diese zu Flaschenhälsen bei Änderungen machen.
Um dieses Szenario zu verbessern, sollte zusätzlich zu den restlichen Modulen der Codebasis ein weiteres Modul, das explizit nur für solche Entwicklungsartefakte zuständig ist, erstellt werden. In diesem Projekt werden dann alle Logiken, Skripte und sonstigen Ressourcen gesammelt, die zwar für die Anwendung bzw. das Entwicklungsprojekt notwendig sind, jedoch bei Auslieferung nicht mit in die eigentliche Anwendung integriert werden. In einer Maven-Struktur könnte dies im Parent-Projekt wie in Listing 3 definiert werden.
Listing 3: Einbindung des Entwicklungsprojekts
<project>
<groupId>com.sapient.example</groupId>
<artifactId>example-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>example-backend</module>
<module>example-frontend</module>
<module>example-persistence</module>
</modules>
<profiles>
<profile>
<id>development</id>
<modules>
<module>example-development</module>
</modules>
</profile>
</profiles>
...
Das Entwicklungsprojekt gehört somit, wie die eigentlichen Projektmodule auch, zur Codebasis, wird jedoch nur aktiviert, wenn explizit das Profil development aktiv ist. Bei einem regulären Deployment ist dieses Profil nicht aktiv, das heißt, das Entwicklungsprojekt wird auch nicht mit in ein Release übernommen.
Durch die Konzentration aller dieser Bestandteile in einem Projekt unterliegen sie den gleichen Richtlinien wie auch die restliche Anwendung, was Versionierung, Architektur oder Konfiguration angeht. Ziel eines solchen Entwicklungsprojekts ist es daher, die Behandlung von nebenbei entstehenden Entwicklungsartefakten als normalen Teil bzw. Ergebnis des Entwicklungsprozesses zu sehen und entsprechend in allen Entwicklungsstufen verfügbar zu haben.
Moderne IDEs sind weit mehr als erweiterte Texteditoren mit Syntax-Highlighting. Sie bieten eine große Anzahl an Integrationen mit diversen Systemen und in diverse Prozesse. Durch Plug-ins (die von allen großen IDEs angeboten werden) lassen sich auch ausgefallenere Systeme oder Prozesse, die nicht bereits von Haus aus unterstützt werden, gut einbinden. Alle Entwickler im Team sollten daher eine gute Vorstellung davon haben, welche Möglichkeiten die eingesetzte IDE bietet und wie diese im aktuellen Projekt eingesetzt werden.
Zusätzlich bietet z. B. Eclipse die Möglichkeit, Konfiguration und allgemeine Einstellungen bzgl. der Konfiguration des lokalen Workspace zu vereinheitlichen. Durch das Plug-in Eclipse Workspace Mechanic [2] lassen sich für Eclipse beispielsweise nahezu alle Einstellungen des lokalen Workspace von einem gemeinsamen Entwicklungsserver beziehen und aktualisieren. Es erlaubt daher dem gesamten Team, einen globalen Satz von Einstellungen und Konfigurationsparametern zu nutzen. Einerseits führt dies zu einem konsistenten Entwicklungsprozess, der unter anderem Fehlerquellen wie unterschiedlich eingestellte Dateikodierungen unterbindet, andererseits zu einer enormen Zeitersparnis bei der Änderung von globalen Einstellungen wie z. B. Kodierungsrichtlinien. Richtig konfiguriert, werden so neue Einstellungen vollkommen unbemerkt und ohne aktives Eingreifen direkt aktualisiert und verwendet.
Es sind oftmals die kleinen Dinge bzw. deren Summe, die den Entwicklungsprozess effizienter gestalten können. Wichtig ist auch hier der erste Schritt: sich über Optimierungspotenzial bewusst zu werden und entsprechend zu handeln. Ganz egal, ob bereits vor dem eigentlichen Projektstart oder im laufenden Projekt – in diese Metaorganisation investierte Ressourcen zahlen sich oftmals bereits nach kürzester Zeit aus und sorgen nicht zuletzt für ein angenehmeres und entspannteres Entwickeln. Schließlich gibt es auf technischer und fachlicher Ebene immer noch genügend Probleme, die gelöst werden wollen – das Drumherum sollte dabei möglichst nicht im Wege stehen.
Christian Robert ist Senior Developer für Mobile-Lösungen bei SapientNitro in Köln. Seit über zehn Jahren beschäftigt er sich mit der Konzeption und Entwicklung von Individualsoftware im Java-Umfeld. Seine aktuellen Schwerpunkte liegen in der Entwicklung von pragmatischen und dennoch (oder gerade deswegen) effizienten Softwarelösungen im mobilen Umfeld. Außerdem interessiert er sich intensiv für die Ideen der Software-Craftsmanship-Bewegung.
[1] http://www.apachefriends.org/de/xampp.html
[2] https://code.google.com/a/eclipselabs.org/p/workspacemechanic/
Im ersten Teil der Serie ging es um Praktiken, die beim Umgang mit begrenztem Geld- oder Zeitbudget auf Architekturseite wichtig sind. In diesem Teil schwenkt die Aufmerksamkeit auf den zweiten wichtigen Punkt zeitgemäßer Softwarearchitektur: Überraschungen. Erfolgreiche Projekte setzen auf häufige Überraschung aus der Umsetzung und scheitern immer wieder in kleinem Umfang. Der Weg führt über priorisierte Architekturanforderungen, mit der Entwicklung verzahnte Architekturarbeit und das Konzept der technischen Schulden.
Ein Freund von mir lädt öfter mal Filme aus dem Netz (ich kenne ihn eigentlich kaum). Liegen die Kinoneuheiten nicht im AVI- oder MP4-Format vor, kann sein am Fernseher angeschlossener Mediaplayer sie nicht abspielen – er muss nach dem Download rekodieren, die Datei dann auf seine externe Festplatte kopieren und kann schließlich von dieser streamen. Der Prozess kann einige Stunden in Anspruch nehmen. Natürlich kann man diese Kette optimieren und modernisieren, aber mein Punkt ist ein anderer: Wann würden Sie herausfinden wollen, ob die Video- und Audioqualität annehmbar ist, ob die Sprache passt und ob die A/V-Synchronisierung stimmt? Am Ende der Verarbeitung, am Fernseher? Vor dem zeitaufwändigen rekodieren am Computer? Am besten doch noch vor dem Download, also sehr früh. Eine kleine Streamingpreview hilft bei der Wahl der richtigen Filmversion und spart Stunden (sagt der Freund).
Auch wenn Sie (so wie ich) nichts mit illegalen Filmdownloads zu tun haben: Früh nach Erkenntnis und Wahrheit zu suchen, ist intuitiv und normal. Es spart Zeit. Sie wollen nicht lange in die falsche Richtung laufen, vermeidbare Irrwege früh erkennen. Warum sollten wir in der Arbeit an Softwarearchitektur anders handeln? Eine Architekturvision, konzeptionelle Entscheidungen oder entworfene Schnittstellen und Anwendungsstrukturen sind zuallererst nur eine Idee. Ob diese Ideen gut und tragfähig sind, kann erst auf Codeebene und mit entsprechenden Tests abschließend beantwortet werden. Irgendwann kommt jedes Projekt zu diesem Punkt: „Jetzt wissen wir, wie wir es hätten machen sollen“. Architekturentscheidungen schlagen mit der Zeit jedoch immer tiefere Wurzeln in einer Applikation. Wir sollten also danach streben, möglichst bald belastbares Feedback zu Architekturarbeit zu bekommen – und uns davon in kleinen Häppchen überraschen lassen.
Werfen Sie alle Architekturfragen am Anfang des Projekts auf und entwerfen sogleich Lösungen auf diese Fragen, dauert es tendenziell länger, bis einzelne Lösungsideen in der Implementierung umgesetzt sind. Arbeiten Sie kleinteiliger, zeitlich gestreckt, können Sie schnelleres Feedback garantieren und profitieren bei der Bearbeitung des nächsten Architekturthemas von Ihren Erkenntnissen beim vorherigen.
Ja, das ist „einfach nur“ iteratives Vorgehen, jedoch gehört zu iterativer Architekturarbeit mehr, als üblicherweise in der Praxis gelebt wird. Zunächst müssen Sie Ihre Architekturarbeit „stückeln“ können, also in eine Serie von relativ unabhängigen Architekturfragestellungen und -entscheidungen verwandeln. Das wiederum erfordert explizite Arbeit mit Architekturanforderungen und eine Idee, wie diese Anforderungen priorisiert werden können. Auch Architektureigenschaften im Code der Implementierung zu überprüfen, ist wichtig und, damit das stetig und möglichst früh erfolgt: häufig lauffähige Software von Beginn an. Schließlich ist ein Konzept hilfreich, das aufgedeckte Probleme und als falsch erkannte Entscheidungen bearbeitbar macht. Technische Schulden sind auch auf Architekturebene ein gutes Denkkonstrukt. Sie tragen der Tatsache Rechnung, dass nicht jedes Problem projektgefährdend ist und sofort oder vollständig beseitigt werden muss. Tabelle 1 zeigt diese Themen noch einmal strukturiert.
Aufgabe |
Konzepte |
---|---|
Förderung früher Überraschungen |
Iteratives Vorgehen, keine „Disziplinensilos“, Continuous Integration |
Priorisierte Architekturanforderungen |
Szenarien, Risikobewertung, der letzte vernünftige Moment (LVM) |
Überraschungen und Probleme explizit bearbeiten |
Technische Schulden |
Tabelle 1: Aufgaben zum Umgang mit Überraschungen (Ausschnitt der Übersichtstabelle in Teil 1 der Artikelserie)
Zwischen einer Architekturentscheidung und der Rückmeldung aus der Umsetzung sollte wenig Zeit verstreichen. Das gelingt am besten, wenn Sie wenige Architekturfragen gleichzeitig bearbeiten. Starten Sie mit einer schlanken Architekturvision, die Ihr Entwicklungsteam grundsätzlich arbeitsfähig macht. Abbildung 1 zeigt, was eine typische Architekturvision beinhalten kann. Erstellen Sie anschließend schon lauffähigen Code, der Rückschlüsse auf die ersten Architekturideen erlaubt.
In jedem Projekt werden Architektur- und Entwicklungsarbeit zusammenwirken. Beschränken Sie sich auf wenig Vorabarbeit, ist das noch mehr der Fall. Dynamische Projekte verrichten konzeptionelle Arbeit und Entwicklungsaufgaben verzahnt. Abbildung 2 illustriert diesen Sachverhalt grob und fokussiert auf die Berührungspunkte (In den letzten Jahren hat sich der Name „Architekturbrezel“ etabliert [1] [2].).
Rechts ist der Umsetzungszyklus zu sehen, in dem der eigentliche Programmcode entsteht, Tests geschrieben und ausgeführt werden, sowie Softwareteile integriert und ausgeliefert werden. In diesem Zyklus entsteht Wert für den Kunden und wir bekommen hier auch unverrückbare Tatsachen zurückgespiegelt, die uns vielleicht überraschen, in jedem Fall aber lernen lassen. Sie wollen so viel Zeit wie möglich in diesem rechten Teil der Abbildung verbringen.
Im linken Teil von Abbildung 2 befindet sich der Architekturzyklus. Grundsätzliche fundamentale Fragestellungen wandern vor der Implementierung durch den Architekturzyklus, um das Risiko einer teuren Fehlentscheidung zu minimieren. Dort werden Prototypen und Modelle erstellt, technische Problemstellungen analysiert, Technologien evaluiert oder Schnittstellen entworfen (im Kasten „Kleine Überraschungen >= breite Analyse“ wird der Eventmechanismus in einem Architekturzyklus entworfen). Brauchbare Möglichkeiten und Architekturentscheidungen bieten die Grundlage für Implementierungstätigkeiten, sie stellen eine Vorgabe dar (Abb. 2, oben links). Es handelt sich um den ersten wichtigen Berührungspunkt zwischen Architektur- und Umsetzungsarbeit. Der zweite ist die Rückmeldung aus der Implementierung samt den Erkenntnissen aus Integration und Test (Abb. 2, oben rechts und Teil 1 dieser Serie). So prüfen Sie Architekturentscheidungen und minimieren den Raum für Annahmen und Spekulationen. Insgesamt entsteht eine gelebte Softwarearchitektur, die durch die Implementierung nicht verwässert, sondern bereichert wird.
Die beiden Berührungspunkte (Vorgabe und Rückmeldung) sollten möglichst eng beieinander liegen, um gut auf Überraschungen und Probleme reagieren zu können. Die erfolgreichsten Softwareprojekte, die ich begleitet habe, arbeiten in einem kontrollierten Trial-and-Error-Prozess. Sie haben eine technische Herausforderung zu lösen, wählen eine vielversprechende Lösungsoption und testen in einem fokussierten Umsetzungszyklus, ob sie hält, was sie verspricht. Ist der Test erfolgreich, bearbeiten sie die nächste Fragestellung und achten kontinuierlich darauf, dass das Gesamtkonzept noch trägt. Diese erfolgreichen Projekte erkennen oft, dass sie falsch liegen – sie werden oft überrascht. Im Gegensatz dazu stehen Projekte, die jahrelang „gut laufen“, beim ersten Auslieferungsversuch oft vor einem unüberwindbaren Berg an Problemen. Sie drehen mehrere, langsame Runden im Architekturzyklus und werden spät überrascht – dann aber richtig. Gerade bei Architekturarbeit ist die späte Überraschung teuer und projektgefährdend.
Es gibt einige Praktiken, die helfen, Architektur- und Implementierungszyklus dynamisch in Austausch zu bringen. Früh ein lauffähiges System zu haben, ist in jedem Fall hilfreich. Hier kommt Continuous Integration ins Spiel: die zwei zentralen Konzepte sind (angelehnt an [3]):
Häufiges Commit: Änderungen erfolgen kleinteilig und werden häufig in die Versionsverwaltung übertragen. Als Daumenregel gilt: mindestens ein Commit pro Tag.
Sofortiger Build: Änderungen in der Versionsverwaltung lösen einen Build aus, der das Gesamtsystem neu baut, automatisierte Tests ausführt, Qualitätsindikatoren und Architekturregeln prüft und entsprechende Reports generiert.
Die Ausführung von Tests, Qualitäts- und Regelprüfungen geschieht also oft und automatisiert. Neben Continuous Integration hat sich auch der Begriff Continuous Delivery etabliert, der zusätzlich zu Integration und Test auch die Auslieferung von Software in unterschiedliche Umgebungen inklusive der Produktivumgebung automatisiert [4]. Wenn man so möchte, denkt Continuous Delivery die Idee von Continuous Integration konsequent zu Ende. Oft bestimmt Ihre Domäne oder Basistechnologie, wie weit Sie gehen können. Im Embedded-Bereich oder bei teuren und außergewöhnlichen Produktivumgebungen ist Continuous Delivery schwieriger umzusetzen als bei Webapplikationen oder Cloud-Anwendungen. Die wichtige, frühe Rückmeldung aus entsprechenden Prüfungen gelingt meist mit Continuous Integration oder, mit Abstrichen, sogar mit Nightly Builds und halbautomatisierten Verfahren recht gut.
Abbildung 3 zeigt den Auslieferungsprozess etwas detaillierter. Im linken Bereich finden Sie den Kern von Continuous Intergration. Continuous Delivery automatisiert alles, was Sie auf der Abbildung sehen, wodurch eine so genannte Deployment Pipeline entsteht.
Der Einstieg in Abbildung 3 ist links oben. Als Entwickler schreiben Sie Programmcode und Tests. Bevor Sie einchecken, führen Sie einen lokalen Build aus und testen die Anwendung auf Unit-Ebene. Sobald Sie Ihren Code in die Versionsverwaltung einchecken, beginnt ein zentraler Build- und Integrationsprozess. Continuous-Integration-Server übernehmen hier die Orchestrierung von Build, Tests, Analyse und Reporting. In dieser Commit Stage liegt der Fokus auf funktionalen Tests, Integrationstests und statischer Analyse. Erst nach erfolgreichem Durchlaufen dieser Phase wird der Commit als geglückt angesehen. Architekturell spannend sind dabei Metrikanalysen für Qualitätsindikatoren und Umsetzungsprüfungen aus Architekturregelwerkzeugen. Beides steht schnell als Feedback zur Verfügung.
Nach dieser ersten schnellen Phase (oder „Stage“) wird der weitere Prozess in Schritte unterteilt, die aufwändiger oder längerlaufend sind. Aus dem Repository werden Binaries an Umgebungen ausgeliefert, die möglichst genau der späteren Produktivumgebung entsprechen. Dort werden üblicherweise zunächst Akzeptanztests durchlaufen, bevor manuelle Tests und Testsuiten für Performanz, Last, Sicherheit etc. zum Einsatz kommen. Die Aufteilung dieser Aktivitäten in Stages ermöglicht zeitnahe Rückmeldungen, auch wenn nachfolgende Stages länger dauern, und bedeutet Kostenersparnis, wenn frühere Stages bereits fehlschlagen und damit aufwändigere Tests erspart bleiben. Moderne Continuous-Integration-Server machen staged builds relativ leicht möglich und lassen sich mit automatischen Deployment-Werkzeugen für Konfiguration und Auslieferung kombinieren. Reports werden automatisch aggregiert und machen die Verfehlung (architektonischer) Ziele analysierbar. Architekturregelwerkzeuge wie Sonar, Structure101 oder Sonargraph lassen sich ebenfalls gut integrieren und unterstützen zusätzlich.
Auch organisatorische Aspekte fördern eine Verzahnung von Architektur- und Umsetzungsarbeit. Ein Beispiel dafür ist die Auflösung so genannter Disziplinensilos, in denen sich Projektmitarbeiter auf genau eine Disziplin der Softwareentwicklung spezialisieren (etwa Anforderungsanalyse, Architektur, Entwicklung oder Test). Teamgrenzen zwischen einzelnen Disziplinen werden, befeuert durch Konzepte wie Stories und Backlogs oder Bewegungen wie DevOps, in den letzten Jahren weniger. Auch Architektur und Entwicklung werden auf Personalebene immer verwobener betrachtet. Das sorgt für besseres gegenseitiges Verständnis, weniger Kommunikationsschwierigkeiten (und Missverständnisse) sowie davon abgeleitet höhere Entwicklungsgeschwindigkeit. Es sind weniger Übergaben nötig und Architekturfragen wandern früh in die Hoheit derer, die sie letztendlich auch entscheidend beeinflussen: Entwickler. Überlegen Sie also gut, ob Sie getrennte Architekturabteilungen oder -teams brauchen. Viele moderne Architekturpraktiken, wie auch die in Teil 1 beschriebene Architekturbewertung oder die weiter unten beschriebene Arbeit mit Qualitätsszenarien, machen gemeinsame Architekturarbeit im Entwicklerkreis einfacher. Weitere Ideen liefert [1].
Auf Architekturebene sind Qualitätsanforderungen sehr oft ein Thema. Die Wahl zwischen verschiedenen technologischen Plattformen, Frameworks oder Schnittstellenmodellen kann nur unzureichend mit funktionalen Unterschieden erklärt werden. Eher sind Unterschiede in der Stabilität, der Wartbarkeit oder im Laufzeitverhalten zu beobachten. Diese Anforderungen bzw. Probleme bei der Erreichung dieser Anforderungen sollten Architekturarbeit treiben. Voraussetzungen dafür sind:
Konkrete Aussagen zu nicht funktionalen Aspekten
Eine Möglichkeit, diese Aussagen zu priorisieren
Ein Konzept, um architektonische Probleme auf Anforderungen zurückzuführen
Der letzte Punkt ist mit dem Konzept der technischen Schulden realisierbar (mehr dazu weiter unten). Die ersten beiden Punkte können mit den in Teil 1 vorgestellten Qualitätsszenarien erreicht werden. Sie stellen konkrete Beispiele für Qualitätsanforderungen dar und können auch in agilen Backlogs verwendet werden. Ein Szenariobeispiel für Wartbarkeit wäre: „Der Algorithmus zur Berechnung der Artikelbeliebtheit soll leicht anpassbar und austauschbar sein“.
Um Szenarien in einer priorisierten Anforderungsliste wie einem Backlog zu verankern, gibt es zwei Möglichkeiten: Sie können die Qualitätsszenarien wie Stories unabhängig in den Backlog legen oder sie einzelnen funktionalen Anforderungen zuordnen. In der Praxis ist beides sinnvoll: Obiges Szenariobeispiel ist am besten bei der funktionalen Anforderung zur Berechnung der Artikelbeliebtheit aufgehoben und wird gemeinsam mit dieser Anforderung priorisiert. Andere Szenarien, die Sicherheit oder Zuverlässigkeit zum Thema haben, beziehen sich oft nicht auf einzelne Funktionalitäten, sondern betreffen das gesamte System oder weite Teile desselben. Diese Szenarien sind die aus architektonischer Sicht wichtigeren und werden mit der „normalen“ Suche nach Akzeptanzkriterien zu funktionalen Anforderungen schwer gefunden. Fragen Sie sich deshalb:
Für Benutzbarkeit und Effizienz: Was soll bei normaler Nutzung des Systems spürbar sein?
Für Skalierbarkeit, Erweiterbarkeit und Übertragbarkeit: Wo und wie entwickelt sich das System weiter?
Für Zuverlässigkeit, Sicherheit und Lastverhalten: Wie reagiert das System auf unvorhergesehene Ereignisse?
Gefundene Szenarien, die unabhängig in die Anforderungsliste aufgenommen werden können, müssen priorisiert werden. In neu gestarteten Projekten sollte möglichst schnell ein „walking skeleton“ entstehen – ein Gerüst der Anwendung, das alle kritischen Schichten und Technologien berührt, ohne viel „Fleisch“ (im Sinne von Funktionalität) zu bieten. Technologisch herausfordernde oder neue Anforderungen und Szenarien werden folglich höher priorisiert als Anforderungen, die architektonisch mehr vom selben sind. In späteren Projektphasen ist vor allem das architektonische Risiko entscheidend. Folgende Fragen liefern Hinweise auf die Höhe des Risikos:
Wie teuer oder zeitaufwändig wäre eine Fehlentscheidung zu revidieren?
Wie viele Projektmitglieder sind von der Entscheidung betroffen?
Sind zentrale Ziele des Projekts mit der Fragestellung verknüpft?
Szenarien mit hohem architektonischen Risiko wandern weiter oben in die Anforderungsliste. Die verwobene Liste kann anschließend mit der Architekturbrezel (Abb. 2) verarbeitet werden.
Der letzte vernünftige Moment stellt eine weitere Priorisierungshilfe dar [5]. (Architektur-)Entscheidungen werden dabei möglichst spät getroffen, auch wenn das Problem (Szenario) selbst schon früher bekannt ist. Hören Sie dieses Konzept zum ersten Mal, klingt es vielleicht eigenartig oder gewöhnungsbedürftig, die Grundidee ist jedoch einleuchtend: Wann wissen Sie das Meiste über ein Softwareprojekt? Wahrscheinlich wenn es abgeschlossen ist. Mit dem Wissen am Ende eines Projekts könnten Sie das gleiche Problem wohl in der Hälfte der Zeit lösen. Sie würden weniger Mitentwickler benötigen und manche Entscheidungen anders treffen. „Hinterher ist man immer klüger“ hört man Leute sagen. Die Klugheit kommt allerdings nicht auf einen Schlag zu Projektende. Sie wächst über den gesamten Projektverlauf.
Die meisten Entscheidungen lassen sich nicht beliebig weit nach hinten schieben. Irgendwann gibt es einen Punkt, an dem eine Entscheidung spätestens getroffen werden muss, um negative Effekte zu vermeiden. Diesen virtuellen Moment nennen wir den „letzten vernünftigen“. Zwischen dem Erkennen der Fragestellung und dem letzten vernünftigen Moment, öffnet sich ein Lernfenster. In diesem Fenster steigt das Wissen über das Problem und wichtige Rahmenbedingungen stetig. Sie sollten versuchen, diesen Lernzeitraum so groß wie möglich zu machen und aktiv zu nutzen. Lernen Sie durch Ausprobieren, Testen, Klärung und eventuell auch Umsetzung von Alternativen. Sie treffen so informierte und tendenziell bessere Entscheidungen.
Vielleicht fragen Sie sich gerade, warum das Verschieben von Architekturentscheidungen frühe Überraschungen fördern soll. Konkret gibt es zwei Gründe dafür:
Durch die Verschiebung bleibt mehr Zeit für jene Architekturfragen, die schon früh im Projekt am letzten vernünftigen Moment sind. Diese können dann konzentriert angegangen und mit Rückmeldungen aus der Entwicklung versorgt werden, bevor andere Fragen ablenken.
Frühe Überraschung bedeutet auch, dass zwischen der Entscheidung und der Rückmeldung wenig Zeit verstreicht. Genau genommen optimiert der letzte vernünftige Moment diese Zeiträume. Sie warten mit der Entscheidung möglichst lange, setzen dann aber zeitnah um.
Ich habe nun grob umrissen, wie Anforderungen detailliert und priorisiert werden können, um eine echt iterative Arbeit an Softwarearchitektur zu ermöglichen und möglichst zeitnah Rückmeldung zu architektonischen Konzepten zu bekommen. Wie gehen Sie jedoch mit dieser Rückmeldung um? Kann die laufende Applikation Ihre Vermutungen bestätigen und liefern Tests positive Erkenntnisse, können Sie die dahinter stehenden Architekturentscheidungen annehmen und breit umsetzen. Komplizierter wird es, wenn sich Probleme offenbaren. Hier kommt das Konzept der technischen Schulden ins Spiel.
Technische Schulden werden oft auf Design- und Entwicklungsebene betrachtet, sind aber als Konzept viel breiter und gut auf Architekturversäumnisse anwendbar. Tatsächlich sind sich technische Schulden und Architekturanforderungen nicht unähnlich. Wie Architekturanforderungen können technische Schulden einen Bedarf an Architekturarbeit aufdecken. Das passiert allerdings zu einem Zeitpunkt, an dem bereits Umsetzungsarbeit geleistet wurde und die nötigen (Architektur-)entscheidungen, bewusst oder unbewusst, verpasst oder falsch getroffen wurden.
Decken Sie in der Umsetzung nicht optimal getroffene Architekturentscheidungen, fehlende Prinzipien, nicht eingehaltene und verwässerte Architektur auf, haben Sie eine Schuldenlast zu tragen. Einiges davon wird, genau wie Codeschulden, stetig bezahlt (verwässerte Architektur und Inkonsistenzen), andere Architekturschulden zeigen sich durch eine immer teurer werdende „wirkliche“ Lösung aus. Arbeiten Sie momentan etwa auf einer Systemkonfiguration, die unpassend für den Produktionsbetrieb ist, können Sie weiterhin Funktionalität entwickeln oder auch nicht funktionale Tests durchführen – je später Sie aber die tatsächlich nötige Umgebung wechseln, desto teurer wird der Umstieg (alleine weil die Menge an zu übertragenden Artefakten höher ist). Die Schulden steigen also „im Stillen“. Ähnlich verhält es sich mit halbherzig getroffenen Entscheidungen wie der Idee, Portierbarkeitsanforderungen alleine durch den Einsatz von Java vollständig gelöst zu haben. Allgemein können die folgenden Probleme technische Schulden auf Architekturebene anzeigen:
Inkonsistenzen
Redundanzen
Unrealistische Lösungen (unpassend zu aktuellen Qualitätsmerkmalen oder Rahmenbedingungen)
Trivialaufwände (Stellen, die viel triviale Arbeit erfordern)
Sonderlösungen
Fehlende Richtlinien (leitende Prinzipien oder rahmengebende Entscheidungen)
Fehlender Überbau (allgemeine Lösungen, Frameworks, Kommunikationsinfrastruktur etc.)
Nicht alle gefundenen technischen Schulden verdienen eine Behandlung. Sie haben drei Handlungsoptionen [6]:
Schuldenfortzahlung: Sie bearbeiten das Problem nicht weiter, sondern akzeptieren es (Null-Entscheidung).
Schuldenrückzahlung: Sie beseitigen die Schuld durch Umstrukturierungen, Migration, Portierung etc.
Umschuldung: Sie ersetzen die momentane Lösung mit einer guten, aber nicht perfekten Lösung, die eine geringere Schuldenlast verursacht (aber billiger ist als die komplette Schuldenrückzahlung).
Um zu entscheiden, welche dieser Taktiken die passendste ist, sollten Sie die Schuld fachlich ausdrücken. Definieren Sie Qualitätsszenarien, die durch die Beseitigung der Schuld erreicht werden könnten. Diesem (auch durch Nichtentwickler nachvollziehbaren) Nutzen stehen die Kosten der Schuldenrückzahlung oder Umschuldung gegenüber. Die Qualitätsszenarien sind priorisierbar und erkannte Probleme werden zu einem sekundären Architekturtreiber, der mit „normalen“ Anforderungen gleichsetzbar ist.
In der vorliegenden Artikelserie habe ich Praktiken vorgestellt, die eine zeitgemäße Architekturdisziplin ausmachen, eine Disziplin, die sowohl auf begrenztes Geld- oder Zeitbudget reagiert als auch mit Überraschungen umgehen kann. Die Komplexität heutiger Softwareprojekte macht Überraschungen in der Umsetzung und Auslieferung zum Standard, nicht zur Ausnahme. Die in diesem Teil besprochenen Praktiken zeigen, wie Sie diese Überraschungen kleinteilig und häufig fördern können und sie so handhabbar machen. Das Konzept des letzten vernünftigen Moments verschafft Ihnen dabei den Freiraum, den Sie brauchen.
Teilweise konnte ich im Rahmen der Artikel besprochene Praktiken nur anschneiden und habe darauf fokussiert, ein Gesamtbild zum Zusammenspiel zu vermitteln. Detailliertere Informationen finden Sie im Web [7] und in Buchform [1].
Stefan Toth (embarc GmbH) unterstützt Projekte und Unternehmen als Softwareentwickler, -architekt und Berater. Er ist regelmäßig Sprecher auf Fachkonferenzen und Autor des Buchs „Vorgehensmuster für Softwarearchitektur“, Carl Hanser Verlag, 2014.
[1] Toth, S.: „Vorgehensmuster für Softwarearchitektur – Kombinierbare Praktiken in Zeiten von Agile und Lean“, Carl Hanser Verlag, 2014
[2] Zörner, S.: „Softwarearchitekturen dokumentieren und kommunizieren“, Carl Hanser Verlag, 2012
[3] http://www.martinfowler.com/articles/continuousIntegration.html
[4] Humble, J.; Farley, D.: „Continuous Delivery: Reliable Software Releases Through Build, Test“, and Deployment Automation, Addison-Wesley Longman, 2010
[5] Poppendieck, M.; Poppendieck, T.: „Lean Software Development: An Agile Toolkit for Software Development Managers“, Addison-Wesley Longman, 2003
[6] Buschmann, F.: „To Pay or Not to Pay Technical Debt“, IEEE Software, 2011