2.1 Keep It Simple and Stupid

Allzu oft habe ich erlebt, dass in guter Absicht überflüssige Komplexität gebaut wurde. Pattern werden eingesetzt, ohne dass sie notwendig gewesen wären, einfach weil jemand meinte zeigen zu müssen was er kann. Beim Erstellen einer Architektur sollten Sie immer darauf achten, gerade so viel Komplexität wie nötig zu haben, um die Anforderungen zu erfüllen. Allerdings bedeutet das nicht, auf eine angemessene Architekturarbeit zu verzichten. Bauen Sie also eine Architektur, passend zu den aktuell bereits feststehenden Anforderungen, aber nehmen Sie keine zukünftigen Anforderungen dabei vorweg, was mich zum nächsten Punkt bringt.

YAGNI - You Ain´t Gonna Need It

Die Verlockung ist groß, Anforderungen mit denen man als Architekt bereits rechnet, welche aber noch nicht klar auf dem Tisch liegen, in der Architekturplanung vorwegzunehmen. In der Praxis hat sich aber, nach ausführlichem Requirements Engineering, gezeigt, dass die tatsächlichen Anforderungen selten den Erwartungen entsprachen. Wenn Sie nun eine dafür unpassende Architektur gebaut haben, so sind Sie unter Umständen danach weniger flexibel in der Umsetzung der echten Anforderungen, als wenn Sie auf diese Vorwegnahme verzichtet hätten.

Unnötige Abstraktionen

Eine typische Verletzung des YAGNI Prinzips stellen überflüssige Abstraktionen dar. Hier ein Beispiel für ein strings.xml Ressource Datei einer Anrdoid App:

<resources>
   <string name="welcome">Welcome to our App!</string>
   <string name="register">Register new account</string>
</resources>
					

Die Idee hinter einer solchen Abstraktion ist im Grunde die folgende: Durch die zusätzlichen Aufwände die bei der Entwicklung der App bei der Auslagerung der Texte in diese Datei entstehen, sparen wir uns später Aufwände beim Übersetzen der App in eine andere Sprache. Wenn dem so ist, so ist einer solchen Abstraktion natürlich nichts entgegenzusetzen. Man muss aber folgende Formel dabei immer beachten:

Nutzen der Abstraktion = (Aufwand der späteren Erstellung der Abstraktion * Wahrscheinlichkeit dass man diese später auch benötigt) – Aufwände diese Abstraktion gleich von Anfang an zu bauen

Im konkreten Fall könnte man den Aufwand für die spätere Auslagerung der Strings aus dem Code in die Ressource Datei mit 100 Stunden berechnen. Die Wahrscheinlichkeit, dass man später die App übersetzen müsste nehmen wir mit 25% an. Wenn nun der zusätzliche Aufwand bei der Entwicklung für die Pflege der strings.xml 30 Stunden beträgt, so ist von dieser Abstraktion eher abzuraten, weil der errechnete Nutzen dabei < 0 ist.

Das Prinzip des letzten, noch vernünftigen Moments für Entscheidungen

Dieses Spiel können Sie sogar noch weiter treiben. Stellen Sie sich bitte folgende Situation vor: Die Anforderungen sind klar genug um mit der Umsetzung zu starten, und als Architekt treffen Sie die Entscheidung noch nicht! Wenn Sie als Architekt oder Designer iterativ vorgehen dürfen, so ist es durchaus empfehlenswert mit der Entscheidung so lange zu warten, bis die Anforderungen klarer sind, oder Sie die verschiedenen möglichen Entscheidungsvarianten besser verstehen. So könnte erstmal das User-Interface entwickelt, während für die Persistenz verschiedene Varianten implementiert werden. Für das UI muss das erstmal keinen Unterschied machen. Wenn der Kunde die ersten Prototypen sieht, und Sie die beiden Datenbanken welche Sie dabei parallel ausprobieren besser verstehen, können Sie sich später immer noch für eine von den beiden entscheiden! Nähere Informationen dazu im Buch Vorgehensmuster für Softwarearchitektur von Stefan Toth[1]

Je einfacher Ihr System gebaut ist, desto flexibler werden Sie in Zukunft sein. Suchen Sie immer nach der angemessenen Komplexität für die Anforderungen. Vorweggenommene Lösungen bringen eine Komplexität mit sich, die später Ihre Möglichkeit die tatsächlichen Anforderungen umzusetzen vielleicht sogar einschränkt!

Relevant für: Design und Architektur

2.2 Information Hiding

Das Information Hiding Prinzip besagt, dass ein Benutzer einer Komponente möglichst wenig über diese Wissen sollte. Merkmale, welche zur Verwendung einer Komponente nicht nötig sind, sollten auch wirklich versteckt sein. Diese Erkenntnis stammt von David Parnas und ist als Information Hiding Principle[2] bekannt. Der Grund dafür ist einfach: Alles was Sie von Ihrer Komponente verstecken bleibt auch in Zukunft einfach änderbar. Wenn es für die Außenwelt nämlich unsichtbar ist, dann haben Sie die Garantie, dass diese keinerlei Abhängigkeiten zu diesen ihr unbekannten Aspekten entwickelt.

Das darf wirklich niemals unterschätzt werden! Nehmen wir mal an, Sie schreiben Code, welcher zu einem x-beliebigen Zeitpunkt über die Position eines Planeten Auskunft geben kann, und das zur Zeit von Sir Isaac Newton. Sie entwickeln einige JAVA Klassen und geben diese in ein und das selbe Package. Das sieht dann in etwa so aus wie in der folgenden Abbildung 2.1:

Physik Package

Abbildung 2.1

Der viele Code wurde zwecks Wartbarkeit und den sehr empfehlenswerten Clean Code Regeln von Robert Martin[3] auf mehrere Klassen aufgeteilt. Siehe folgendes Code Snippet:

package info.swa.physics;
					
public class Physics {

	public Position calculatePositionOfPlanet(Planet input) {
	
		Mechanics nm = new Mechanics();
		// ...
	}
	
	// further Methods...
}

package info.swa.physics;
					
public class Mechanics () {

	// Classical Newtonian Mechanics Code...
}

// further Classes...
					

Was Sie hier nun sehen, ist sicher der am meisten verbreitete Design Fehler in JAVA: Alle Klassen werden prinzipiell als public definiert, auch wenn dies gar nicht nötig ist. Nehmen wir jetzt an, ein gewisser Albert Einstein behauptet plözlich, dass die gesamte klassische Mechanik falsch ist, und ersetzt diese durch die Relativitätstheorie. Wenn Sie nun all Ihre Klassen bis auf die "Hauptklasse" Physics wegschmeissen müssen, wird das potentiell problematisch. Da die anderen Klassen außerhalb des Packages sichtbar sind, könnte es sein, dass es unzählige externe Abhängikgeiten außerhalb des Packages zu diesen Klassen gibt. Mit anderen Worten: Sie werden Probleme haben die Klassische Mechanik loszuwerden um sie auf die neuesten Erkentnisse umzuschreiben.

Die Lösung ist einfach. Im Falle von JAVA gibt es die Möglichkeit, einzelne Klassen als package protected zu definieren:

package info.swa.physics;
					
class Mechanics () {

	// Fancy Relativistic Mechanic Code...
}
					

Physics kann dann immer noch auf Mechanics zugreifen, Klassen außerhalb des Packages aber nicht mehr. Ihr Package sollte also eher so aussehen wie in Abb. 2.2, wobei die gelben Klassen jetzt package protected sind:

Physik Package, jetzt mit Information Hiding

Abbildung 2.2

Versuchen Sie immer so viele Aspekte Ihrer Komponenten wie möglich vor der Außenwelt zu verbergen. Je besser Sie das hinbekommen, desto flexibler werden Sie in Zukunft bei Änderungen sein!

Nach wie vor hat JAVA die Einschränkung, dass man keine Sichtbarkeit über ein Package hinaus definieren kann. Was also wenn man größere Strukturen bauen möchte? Sie können dann auf ein Tool wie das sehr empfehlenswerte SonarGraph von hello2morrow zurückgreifen, oder auf JAVA 9 warten, wo es dann das Modulkonzept geben wird. Es werden dann Definitionen wie die folgende möglich sein:

module com.foo.bar {
    requires com.foo.baz;
    exports com.foo.bar.alpha;
    exports com.foo.bar.beta;
}					

Sie können dann definieren, welche Packages außerhalb des Moduls sichtbar sind und auf welche anderen Module überhaupt zugegriffen werden kann.

Relevant für: Design und Architektur

2.3 Separation Of Concerns / Single Responsibility Principle

Bei SOC / SRP geht es um folgendes: Jede Komponente hat genau eine Aufgabe, und jede Aufgabe ist möglichst in genau einer Komponente gekapselt. Ist aber ein solcher Concern fachlicher oder technischer Natur? Schauen wir uns mal ein System an welches mit dem weit verbeiteten Paradigma des Layering gebaut wurde, wie in Abb. 2.3:

Typische Layering Architekur

Abbildung 2.3

Nehmen wir an, Sie fügen bei einer der Entitäten von Feature A ein Feld hinzu. Ich bin mir sicher, dass Sie in der Datenbank beginnen werden, und diese Änderung eine Kaskade an Änderungen in den Layern darüber nach sich ziehen wird. Während andere fachliche vertikale Schnitte (Feature B, C, etc.) davon unberührt bleiben. Daran erkennt man, dass es zwischen den Layern eine hohe Kopplung (oder Kohäsion) gibt, während sie zwischen den fachlichen Schnitten eher gering ist. Daraus folgere Ich, dass man zu erst nach fachlichen Kriterien strukturieren sollte, um erst innerhalb dieser Fachlichkeiten die technischen Strukturen zu bilden, wie in Abbildung 2.4:

Layering besser mit fachlichen Schnitten

Abbildung 2.4

Wenn Sie bei Änderungen eines Features immer mehrere Komponenten gleichzeitig angreifen müssen, so kann das ein Indiz für eine schlechte Architektur sein, wo die groben Strukturschnitte an den falschen Stellen getätigt wurden.

Relevant für: Design und Architektur

2.4 Zyklenfreie Strukturierung

Um Gernot Starke´s Buch Effektive Software Architekturen[4] zu zitieren:

Vermeiden Sie Strukturzyklen wie der Teufel das Weihwasser.

Um das näher zu erläutern möchte ich Sie bitten, einen Blick auf Abbildung 2.5 zu werfen:

Struktur ohne Zyklen

Abbildung 2.5

Wir sehen hier ein sauber aufgebautes System, welches ohne Strukturzyklen auskommt. Die Pfeile geben die Abhängigkeiten jeder einzelnen Komponente an, und bedeuten im Grunde, dass beispielsweise dass Package A die Packages C und D verwendet. Die Zahlen neben den Schrägstrichen geben wiederum jeweils an, von wievielen anderen Packages das Packake in Summe abhängig ist, also inklusive indirekter Abhängigkeiten. Diese Zahl bedeutet also, wieviele Packages es gibt, wo sich Änderungen direkt oder indirekt auf dieses Package auswirken können. Je geringer diese Zahl ist, desto geringer demnach auch das Risiko solcher Seiteneffekte auf ein Package. So könnte eine Änderung in Package C zu Problemen in Package A führen, aber niemals Auswirkungen auf Package D haben.

Diesen Kopplungsgrad kann man auch wunderbar mit einer Kennzahl wiedergeben. In dieser Grafik hängt jede Komponente im Schnitt von 2,33 anderen Komponenten bzw. sich selbst ab. Da wir in Summe 6 Komponenten bzw. Packages haben, macht das einen Prozentsatz von 39%, was für ein System dieser Größe durchaus einen guten Wert darstellt. Schauen wir uns aber nun an, was passiert wenn wir nur eine einzige, dafür zyklische Abhängigkeit hinzufügen, und werfen einen Blick auf die hier folgende Abbildung 2.6:

Struktur mit Zyklen

Abbildung 2.6

Wir sehen, dass es durch den Zyklus zu einem sprunghaften Anstieg der Abhängigkeiten kommt, nämlich von 39% auf 67%. Das bedeutet auch, dass es dadurch zu mehr möglichen Problemen kommt, die bei Änderungen an den Komponenten indirekt in anderen Komponenten ausgelöst werden können. In den meisten Fällen ist es ganz einfach solche Strukturzyklen aufzulösen, z.B. so wie in Abbildung 2.7:

Auflösen eines Zyklus

Abbildung 2.7

Komponente B braucht hier offenbar Funktionalität von Komponente A. Wir lösen das auf, indem wir eine neue Komponente C definieren, der die Funktionalität von A erhält, die auch von B benötigt wird. Somit sind wir frei von Zyklen!

Nehmen wir noch ein anderes Beispiel her: Sie haben eine Komponente welche sich um Kunden-/Personendaten kümmert, und eine welche für Bestellungen verantwortlich ist. Personendaten können auch ohne Bestellungen existieren, eine Bestellung macht aber ohne einen Kunden wenig Sinn. Von daher werden Sie die Abhängigkeit von den Bestellungen zum Kunden zeigen lassen. Schön und gut, aber was ist wenn der Auftraggeber ein Feature verlangt, wo zu einem Kundendatensatz auch die Bestellungen angezeigt werden sollen? Haben wir da schon den ersten Zyklus? Nein, denn dieses Feature kann einfach als Teil der Komponente gebaut werden, welche sich um die Bestellung kümmert, und die Lösung wäre frei von Zyklen!

Während Zyklenfreiheit in einer monolithischen Architektur fast immer empfehlenswert ist, stellt sich für verteilte Systeme, wie einer Microservice Architektur, die Frage nach der Sinnhaftigkeit. Jedes Microservice ist ja automatisch hinter seiner eigenen REST-API quasi versteckt, und Probleme in einem Service wirken sich auf andere nicht direkt aus. Auch wenn Sie einen modularen Monolithen bauen, wo sie es schaffen, den Großteil der Modullogik hinter schmalen Fassaden bzw. Modul-APIs zu verstecken, kann man auf Zyklenfreiheit verzichten.

Relevant für: Design und mit Einschränkung auch für Architektur

2.5 Open Closed Principle

Das Open Closed Prinzip besagt, dass ein System immer offen für Erweiterungen sein soll, aber für Änderungen geschlossen. Vereinfacht gesagt bedeutet das, dass es möglichst einfach sein soll, einem System neue Funktionalität hinzuzufügen. Dass Sie für ein neues Feature eine neue JAVA Klasse schreiben müssen ist ja nichts ungewöhnliches, wenn Sie aber viele bestehende JAVA Klassen dafür auch angreifen müssen, dann wäre das ein Beispiel für ein System welches eben nicht Open Closed ist.

Es gibt sowohl auf Mikro-Architektur bzw. Design Ebene Pattern (wie die Abstract oder Method Factory, aber auch auf Ebene der Makro-Architektur (Choreografie), welche eine spätere leichte Erweiterbarkeit unterstützen, wie sie im Open Closed Prinzip gemeint ist.

Relevant für: Design und Architektur

2.6 High Cohesion und Low Coupling

Nehmen wir an, wir haben ein System wo die einzlenen Komponenten strukturiert sind, wie in der hier folgenden Abbildung 2.8 dargestellt:

Schlechte Kohäsion

Abbildung 2.8

Man sieht auf den ersten Blick, dass diese Struktur wenig Sinn macht. Es besteht eine Kopplung von Paket A zu einem Element des Paketes B, ohne dass dieses dort verwendet werden würde. Weiters gibt es 2 Abschnitte in Paket A, die wiederum nichts miteinander zu tun haben. Eine für diesen Fall sinnvollere Struktur wäre die, welche hier in Abbildung 2.9 dargestellt ist:

Gute Kohäsion

Abbildung 2.9

In diesem sehr theoretischen Optimalfall haben diese 3 Pakete dann nichts mehr miteinander zu tun, und können völlig unabhängig voneinander weiterentwickelt werden. Seiteneffekte durch Probleme eines anderen Paketes sind nicht möglich. Ein hohes Grad an Abhängigkeiten gibt es jeweils nur innerhalb der Komponenten.

Das gibt es im wirklichen Leben natürlich so gut wie nie. Trotzdem sollte man immer einen so geringen Kopplungsgrad wie möglich zwischen Komponenten anstreben. Das Wort "gering" bezieht sich dabei nicht nur auf die Anzahl der Kopplungen, sondern auch auf die "Stärke" der jeweiligen Verbindung. Je "schwächer" also eine Kopplung, desto besser. Eine Kopplung ist dabei umso schwächer, je weniger Annahmen dabei die eine Komponente von der anderen macht.

Suchen Sie als Architekt oder Designer immer nach Komponenten mit hoher innerer Kohäsion, sodass es dazwischen nur eine geringe Kopplung braucht!

Relevant für: Design und Architektur

2.6.1 Annahmen

Im Zuge jeder Verbindung bzw. Kopplung trifft also jeder Consumer einer Schnittstelle Annahmen über die jeweils andere Komponente bzw. den Service Provider. Hier eine Liste der Annahmen die typischerweise getroffen werden, ohne Anspruch auf Vollständigkeit:

Laufzeit / Ausführungsort

Die andere Komponente muss auf der selben Maschine laufen.

Technologie

Einschränkungen bei der Technologiewahl der angebundenen Komponente.

Zeit

Die andere Komponente kann beispielsweise zu gewissen Zeitpunkten nicht angesprochen werden, bietet aber keine asynchrone Variante der Schnittstelle an.

Daten und Formate

Bei der Kommunikation gelten gewisse Einschränkungen ob der möglichen Datenformate die geparsed und verstanden werden müssen.

2.7 Kopplung und Integration

Werfen wir jetzt einen Blick auf ein paar Beispiele möglicher Kopplungen, und bewerten diese auf Grund ihres Kopplungsgrades:

Wenn Sie 2 Komponenten integrieren, so tun Sie das immer auf die Art und Weise, die die geringst mögliche Kopplung bedeutet! Bei hohem Abstraktionsgrad wie der Makro-Architektur ist dies umso wichtiger, als auf der Mikro-Architektur oder Designebene wo auch Kopplung auf Code Ebene akzeptabel sein kann!

2.7.1 Code Reuse

Davon sprechen wir, wenn wir z.B. in JAVA direkt eine andere Klasse aufrufen, und die anderen Komponente als .jar File wiederverwenden. Das ist die stärkste Form der Kopplung und sollte nur in der Makro-Architektur-Ebene verwendet werden, bzw. beim Bau von Monolithen.

Grad der Kopplung: sehr groß

2.7.2 Datenbank-Integration

Hier greifen verschiedene Systeme direkt auf die selbe Datenbank zu. Wir sind dabei zwar zeitlich voneinander entkoppelt, treffen aber sonst alle möglichen Annahmen. So müssen alle beteiligten Systeme mit dieser Datenbank-Technologie kommunizieren können, und haben auch Abhängigkeiten zum konkreten Datenmodell.

Datenbank Integration

Abbildung 2.10

Grad der Kopplung: groß

2.7.3 Synchrone, remote Kommunikation (RPC) wie SOAP, oder auch REST Level 0

Eine Komponente kommuniziert mit eomer anderen über eine mehr (SOAP) oder weniger (REST) standardisierte Schnittstelle über das Netzwerk. Hier muss der andere nach wie vor zur selben Zeit verfügbar sein (Annahme: Zeit), dafür ist es uns egal mit welcher Technologie das Gegenüber gebaut wurde.

Grad der Kopplung: mittel

2.7.4 Datenreplikation

Daten werden nicht per Remote Schnittstelle geholt, sondern asynchron repliziert. Damit wäre die zeitliche Koppelung entfernt. Der Aufwand lohnt sich aber meiner Meinung nach nur, wenn es eine Begründung gibt die zeitliche Kopplung loszuwerden.

ETL

ETL steht für Extract, Transform und Load. Es gibt verschiedene Tools am Markt, mit welchem solche Prozesse umsetzbar sind, um damit Daten aus einer oder mehreren Quellen zu extrahieren, umzuwandeln, und in eine oder mehrere Ziele zu importieren. Das passiert dabei üblicherweise im Batch. NoSQL Datenbanken Viele Datenbanken aus dem NoSQL Bereich haben Replikationsmechanismen bereits eingebaut.

Polling

Der Consumer welcher Daten von einem Provider replizieren möchte, kann in gewissen Zeitabständen ein Query an diesen senden. Dabei wird der Timestamp der letzten erfolgten Replikation mitgegeben. Der Provider antwortet mit allen Datensätzen die einen Änderungstimestamp nach diesem haben. Dabei ist man gut beraten, eine Obergrenze für die Menge der pro Request gelieferten Datensätze zu definieren. Der Consumer holt sich die jeweils geänderten Daten also häppchenweise vom Provider ab.

Messaging / Events

Nach jeder Änderung eines Geschäftsfalles stellt der Provider einen Event auf den Message Bus, welcher über die erfolgte Änderung informiert. Der Consumer der Daten abonniert dabei diese Nachrichten und holt sich bei Bedarf die neue Version des Datensatzes über RPC vom Provider ab.

Grad der Kopplung: gering bis mittel

2.7.5 Messaging

Wo Remote Procedure Calls darüber definiert sind, dass sie synchron erfolgen, so geht es beim Messaging um asynchrone Kommunikation. Man reduziert die Abhängigkeit zwischen Sender und Empfänger also um die zeitliche Annahme. Das bedeutet, dass Sender und Empfänger nicht gleichzeitig online sein müssen. Man unterscheidet dabei 2 Arten von Messages:

  • Hinter Commands steht der Wunsch des Absenders, eine bestimmte Aktion auszuführen. Eine Messaging Infrastruktur ist dabei als dezidierte Indirektion dafür zuständig, diesen an den dafür zuständigen Empfänger zur Umsetzung weiterzuleiten. Dem Empfänger ist es dabei üblicherweise auch möglich zu antworten.
  • Bei Events informiert ein Absender darüber, dass ein bestimmtes Ereignis stattgefunden hat. Interessierte Empfänger können solche Nachrichten abonnieren, und werden dann bei Auftreten eines der abonnierten Ereignisse informiert. Der Absender ist dabei agnostisch darüber, wer und wie viele Empfänger seine Nachrichten abonniert haben.

Grad der Kopplung: gering

2.7.6 Composite UI

Die einfachste Art und Weise für die Integration bietet Ihnen das User-Interface. Wenn es für den jeweiligen Anwendungsfall Sinn macht, sollten Sie immer bestrebt sein die Kopplung in der UI Ebene herzustellen. Wobei das prinzipiell auf dem Server, oder am Client und somit meist im Browser erfolgen kann. Am Server war die Portal / Portlet JSR 286 früher einmal für eine Zeit lang populär. Ein großer Nachteil dieser Technologie war aus meiner Sicht, dass es sich dabei um einen JAVA Standard handelt, und man im Falle einer Verwendung dadurch eine technologische Kopplung zwischen den UI Elementen herstellt. Als bessere Alternative zur serverseitigen UI Integration seien daher die W3C Standards Edge Side Include und Server Side Include genannt. Ich empfehle Ihnen aber wenn möglich auf eine Integration im Client zu setzen. Im Falle einer Webanwendung wäre das im einfachsten Fall ein simpler Link, welcher den User zu einer anderen Seite navigiert. Jedes Feature wird dann über sein UI angesprochen, und bietet jeweils verständliche URLs im REST Stil an, unter denen es erreichbar ist. Dieser Ansatz wird auch ROCA Style [ROCA] genannt, was dabei für Resource Oriented Client Architecture steht. Wenn Sie Glück haben so ist die Systemintegration in Ihrem Fall erledigt, indem ein Menü gerendert wird, welches Links zu den einzelnen Features anzeigt. Genügt das nicht, können mit simplen Javascript Befehlen Teile des DOMs aus unterschiedlichen Quellen geladen werden. Auf diese Art und Weise kommt es also zu einer Komposition einzelner Seiten aus unterschiedlichen Quellen. Wenn Sie die Einschränkung der Single Origin Policy dabei beachten, dann geht das zum Beispiel so:

$('a.replace-link').each(function() {
 var link = $(this);
 var content =
$('
').load(link.attr('href'), function() { link.replaceWith(content); }); });

Dabei werden bestimmte Links jeweils durch den Inhalt den der URL des Href´s liefert ersetzt. In der folgenden Abbildung 2.11 sehen Sie zwei mögliche Varianten zur UI Integration. Bei der links im Bild wird die komplette Seite am Server gerendert, z.B. unter Verwendung eines Template Engine wie Velocity von Apache. Dabei werden bei Bedarf UI Teile von anderen Services zur Komposition später an Client nachgeladen, oder sie werden bereits am Server durch ESI oder SSI eingebunden. Im rechten Beispiel bietet jeder Service eine Art Plugin an, die von einem Rahmenwerk im Browser (dem „Portal“) angezeigt werden kann:

ROCA vs. Portal

Abbildung 2.11

Grad der Kopplung: sehr gering