Follow my new blog

Montag, 6. Dezember 2010

Was ist Softwarearchitektur – Teil 2

Softwarearchitektur ist eine der beiden Entwurfsdisziplinen der Softwareentwicklung. Ihr Ergebnis ist die fundamentale Struktur einer Software im Hinblick auf die nicht-funktionalen Anforderungen.

Das ist für mich schon eine recht präzise Definition – allerdings eine allgemeine. Sie beantwortet noch nicht alle Fragen in der Praxis. Gehört die Entscheidung für IIS und gegen NT Service eine Architekturentscheidung? Ist die Entscheidung für eine Aufteilung der Rechtschreibprüfung in Textzerlegung und Wortprüfung eine Architekturentscheidung? Oder ist die Entscheidung für die Zusammenfassung von Funktionalität zu einer Komponente eine Architekturentscheidung?

Um diese und andere Fragen zu beantworten, müssen Sie erstens mein allgemeines Verständnis von Struktur kennenlernen und zweitens meine Vision davon, wie Architektur Strukturen konkretisiert.

Was ist eine Struktur?

Als Struktur verstehe ich “etwas”, das aus Elementen besteht, die in Beziehung zu einander stehen.

image

Strukturen mit vielen Elementen und/oder Beziehungen sind unübersichtlich. Deshalb gehört zu Struktur auch noch die Abstraktion. Ein System wie im Bild beschreibt eine Struktur also nur auf einer Abstraktionsebene, andere können darunter/darinnen liegen…

image

oder darüber/darum…

image

Wer eine Struktur entwirft, muss sich also drei Fragen stellen:

  1. Welche Abstraktionsebenen hat die Struktur?
  2. Welche Elemente enthält eine Abstraktionsebene (innerhalb eines womöglich umschließenden Elements einer höheren Abstraktionsebene)?
  3. Wie sind die Elemente in einer Abstraktionsebene verbunden?

Die Abstraktionsebenen und Strukturelemente der Softwarearchitektur

Mit einem allgemeinen Strukturverständnis bewaffnet können Sie nun verstehen, dass Softwarearchitektur als Entwurf auf verschiedenen Ebenen betrieben werden sollte. Ohne diese Ebenen ist die Kompliziertheit eines Systems nicht in den Griff zu kriegen.

imageDie Abstraktionsebenen der Softwarearchitektur sind für mich diese:

  1. Das gesamte Anwendungssystem
  2. Bounded Contexts
  3. Partitionen/Apps
  4. (Virtuelle) Maschinen
  5. Betriebssystemprozesse
  6. Belange/Concerns

Ein paar Worte zu den Ebenen:

Anwendungssystem

Ein Entwicklungsauftrag wird gewöhnlich auf der Ebene des Anwendungssystems erteilt: “Schreiben Sie mir eine Software, die dies und jenes kann.” Das kann eine Warenwirtschaft sein, ein TicTacToe-Spiel oder eine Textverarbeitung. Wenn implementiert, erfüllt das Anwendungssystem alle Anforderungen.

Bounded Contexts

Das Gesamtsystem zerlegt der Architekt in Bounded Contexts, d.h. funktionale Untermengen, die unterschiedliche Datenmodelle haben. Sie gehören innerhalb des Gesamtsystems, der Anwendungsdomäne verschiedenen Sub-Domänen an. Bei einem Faktura-Anwendungssystem könnten das Rechnungslegung und Rechnungsverfolgung und Stammdatenverwaltung sein.

In jedem Bounded Context können Daten anders persistiert werden. Das drückt die kleine Tonne im Bild aus.

Nicht-funktionale Anforderungen, die auf dieser Ebene vor allem bedient werden: Evolvierbarkeit, Skalierbarkeit, Sicherheit

Apps

Innerhalb eines Bounded Context kann die Funktionalität weiter aufgeteilt werden auf Partitionen oder – neuerdings von mir so genannt – Apps. Jede App steht für eine klar umrissene Funktionalität, die durch ein spezifisches Userinterface bedient wird. iPhone/iPad Apps spiegeln diesen Gedanken sehr schön wider, finde ich. Oder die vielen kleinen Tools unter Unix.

Alle Apps in einem Bounded Context benutzen dieselbe Datenbasis.

Nicht-funktionale Anforderungen, die auf dieser Ebene vor allem bedient werden: Usability, Evolvierbarkeit

Maschinen

Jede App läuft auf mindestens einer Maschine – vom Server über den Desktop bis zum Smartphone –, kann aber auch mehrere Maschinen überspannen wie bei einer Web-Apps, die sowohl im Browser wie im IIS wie in einem Application Server wie in der Datenbank Codeteile hat.

Nicht-funktionale Anforderungen, die auf dieser Ebene vor allem bedient werden: Skalierbarkeit, Performance, Sicherheit, Verfügbarkeit, Robustheit

Prozesse

Auf jeder Maschine einer App läuft deren Code mindestens in einem Prozess, es können aber auch mehrere sein. Zum Beispiel könnten ein Application Server und ein Reporting Server auf derselben Maschine betrieben werden.

Nicht-funktionale Anforderungen, die auf dieser Ebene vor allem bedient werden: Skalierbarkeit, Performance, Evolvierbarkeit, Flexibilität

Concerns

Die Zerlegung des Anwendungssystems bis in Prozesse führt zu autonomen Funktionseinheiten. Die arbeiten jede für sich vor sich hin. Jede läuft auf mindestens einem eigenen Thread. Die Kommunikation ist mithin ganz fundamental nur asynchron zwischen ihnen möglich.

Diese autonomen Funktionseinheiten zu identifizieren, ist die vornehmste Aufgabe des Architekten.

Als Vorbereitung für die Modellierung und Implementierung ist das allerdings noch nicht genug, finde ich. Einen Schritt weiter sollte der Architekt noch gehen. Er sollte innerhalb der Prozesse auch noch die grundsätzlichen Concerns herausarbeiten, d.h. Bereiche, die gezielt nicht-funktionale Anforderungen realisieren. Das sind z.B. Frontend, Persistenz, Kommunikation, Sicherheit, Domänenlogik.

Die Modellierung hat es dann einfacher, zu einem Entwurf zu kommen, weil die Concerns eine Grundeinteilung für die Funktionseinheiten des Modells vorgeben. Wenn das Modell sich um Funktionalität kümmert, dann muss es wissen, um welche Funktionalität es geht. Domänenfunktionalität ist nicht die einzig zu modellierende.

Beziehungen in Abstraktionsebenen

Auf welchen Abstraktionsebenen der Architekturentwurf stattfindet, ist geklärt. Die wesentlichen Strukturelemente sind Bounded Context, App, Maschine und Prozess. Aber wie werden die mit Beziehungen innerhalb einer Ebene zu einem System verwoben? Ich denke, das sollte auch definiert sein, denn dann lassen sich weitere Fragen leichter beantworten.

Bounded Context

Bounded Contexts sind durch Datensynchronisationen miteinander verbunden. Sie teilen weder Datenbanken noch Datenmodelle, also müssen die Daten explizit zwischen Bounded Contexts synchronisiert werden. Das dauert natürlich seine Zeit. Deshalb ist über Bounded Contexts hinweg nur Eventual Consistency zu erwarten.

Fragen für die Kommunikation: Wie soll die Datensynchronisation implementiert werden? Gibt es Tools (Stichwort ETL)? Wie groß das die maximale Verzögerung sein? Kann es Konflikte geben, wie werden die behandelt?

Apps

Apps müssen nicht direkt miteinander kommunizieren, sondern teilen sich eine Datenbasis. Sie ist das Bindeglied. Zentral für die Apps eines Bounded Context ist daher das eine Datenmodell.

Fragen für die Kommunikation: Welche Persistenztechnologie soll verwendet werden (RDBMS, Dokumentendatenbank, Graphendatenbank, Dateisystem usw.)? Wie sieht das Schema aus?

Maschinen

Maschinen sind über (virtuelle) Leitungen miteinander verbunden; sie teilen keinen physischen Adressraum. Die Kommunikation kann daher nur nachrichtenorientiert und asynchron ablaufen. Und ich würde noch hinzufügen: Die Kommunikation sollte zwischen Maschinen auch immer als asynchron in der Implementierung zu sehen sein.

Fragen für die Kommunikation: Welche Kommunikationstechnologie soll zum Einsatz kommen, insb. wenn die Maschinen mit unterschiedlichen Betriebssystemen betrieben werden? Wie sollen Leitungsunterbrechungen behandelt werden?

Prozesse

Prozesse laufen zwar im selben physischen Adressraum, werden vom Betriebssystem darin jedoch sauber getrennt. Die Kommunikation kann zwischen ihnen kann daher auch nur nachrichtenorientiert sein. Allerdings ist zu erwarten, dass Prozesse in der selben Maschine verlässlicher vorhanden/erreichbar sind als solche in anderen Maschinen. Deshalb ist zu überlegen, ob die Kommunikation zwischen Prozesse in derselben Maschine die Kommunikation immer als asynchron in der Implementierung zu sehen sein muss. Im Augenblick tendiere ich dahin, meine Position, dass das immer so sein muss, zu lockern. Das bedeutet jedoch: Wenn ein Architekt sich für eine synchrone Kommunikation zwischen Prozessen entscheidet, weil er sie auf derselben Maschine laufen sieht, dann können diese Prozesse später nicht auf verschiedenen Maschinen deployt werden. Denn die sollen asynchron kommunizieren (s.o.). Soviel Grundsatz sollte sein, finde ich.

Fragen für die Kommunikation: Welche Kommunikationstechnologie soll zum Einsatz kommen? Welche Hosts für den Code sollen benutzt werden (z.B. IIS, Desktopanwendung, NT Service)?

Concerns

Concerns sind funktionale Bereiche innerhalb von Prozessen. Sie kommunizieren also im selben Adressraum synchron via Stack oder globale Speicherbereiche.

Fragen an die Architektur

Mit diesen Konkretisierungen lassen sich die obigen Fragen und andere beantworten. Ist die Entscheidung für IIS oder NT Service eine Architekturentscheidung? Das ist natürlich eine Architekturentscheidung auf Prozessebene.

Wer sich über WCF vs NServiceBus unterhält, unterhält sich ebenfalls über Architektur. REST vs SOAP? Auch ein Architekturthema – mindestens auf Maschinenebene. Was ist mit OAuth? Ein Architekturthema. Was ist mit NoSql? Ein Architekturthema. Was ist mit WPF? Nur insofern ein Architekturthema als dass WPF auf bestimmten Devices (nicht) möglich sein könnte. Was ist mit Bubblesort vs Quicksort? Kein Architekturthema, auf wenn es um die nicht-funktionale Anforerung Performance gehen mag; denn die Unterscheidung ist keiner Abstraktionsebene der Architektur zuzuordnen. Code im Application Server oder im Datenbankserver laufen lassen? Ein Architekturthema.

Architektur hat mit der Verteilung von Funktionalität zu tun, um nicht funktionale Anforderungen zu erfüllen. Wenn Funktionalität verteilt wird, dann stellen sich Fragen dazu, wie die Funktionalität an ihren Orten gehostet werden soll und wie die verteilten Teile miteinander kommunizieren sollen.

Verteilt wird Funktionalität in zwei Weisen:

imageArchitektur macht zunächst auf den Abstraktionsebenen Bounded Context und App Längsschnitte durch das Anwendungssystem. Architektur schneidet es in dünne Scheiben. Die sind vollständig insofern, als dass sie vom Frontend bis zum Backend reichen.

Der Entwurf beginnt also damit zu überlegen, wie die gesamten Anforderungen in überschaubare Happen zerlegt werden können. Welche für den Anwender sinnvollen Subsysteme lassen sich finden, um nicht sofort und immer alles umsetzen zu müssen? Bounded Contexts und Apps sind daher auch fundamental für die Entkopplung von Code. Sie dienen unmittelbar der Evolvierbarkeit.

imageApp, Maschine und Prozess zerlegt die Architektur dann transversal weiter. Die funktionalen Scheiben werden “gewürfelt”, um insbesondere Skalierbarkeit, Performance und Verfügbarkeit zu erreichen. Jedes Teil stellt dann nicht mehr für sich einen Anwendernutzen her, sondern trägt nur dazu bei. (Hier ist die klassische Schichtenarchitektur anzusiedeln.)

Zusammenschau

Meine Erfahrung mit der Definition von Architektur in dieser Weise ist, dass sie sich leicht erklären lässt und Fragen beantwortet, bei denen zumindest ich früher “rumgeeiert bin”. Ich behaupte nicht, dass diese Definition vollständig ist; ich empfinde sie lediglich als pragmatisch-praktisch-gut ;-) Nützlichkeit sowie Kommunikationsfähigkeit stehen für mich hier höher als formale Reinheit.

Kommentare:

Frank Striegel hat gesagt…

Sie definieren oben Bounded Context als "... Bounded Contexts, d.h. funktionale Untermengen, die unterschiedliche Datenmodelle haben."

Was genau verstehen Sie hierbei unter "unterschiedliche Datenmodelle"? Als Beispiel führen Sie (evtl. etwas altmodisch ausgedrückt) Programmbereiche eines Faktura-Anwendungssystems an. Bedeutet dies nun, dass Bounded Contexts Aggregate des Domain Model abbilden?

Gruss
Frank Striegel

Ralf Westphal - One Man Think Tank hat gesagt…

@Frank: In einer Faktura spielen Rechnungen die zentrale Rolle. Darüber sind sich alle einige.

Aber fragt man die Rechnungslegung/Verkauf, dann sieht eine Rechnung anders aus als bei der Buchhaltung.

Die Rechnungslegung sieht eine Rechnung als Dokument mit Adressat, Positionen, die sich auf Produkte beziehen, Zahlungsziel, führenden und schließenden Texten. Für die Persistenz kann sich eine Dokumentendatenbank oder gar das Dateisystem empfehlen, weil darin die hierarchischen Daten ganz einfach gespeichert und von dort als Ganzes wieder abgerufen werden können.

Die Buchhaltung hingegen interessiert sich nur für eine Kundennummer, das Rechnungsdatum, Nettobetrag und MwSt und das Zahlungsziel. Damit kann sie Zahlungseingänge verbuchen und Mahnungen schreiben. Als Persistenzmedium empfiehlt sich hier vielleicht ein RDBMS, weil es schlicht um tabellarische Daten geht.

Rechnungslegung und Buchhaltung werden daher am besten als verschiedene Bounded Contexts angesehen. So kann die Datenhaltung optimiert werden für den jeweiligen Zweck. Das verringert die Kopplung.

Das Gegenteil wäre ein universelles Datenmodell und ein einziges Persistenzmedium. Das geht natürlich; so wird es meist getan. Aber die Probleme sind vielfältig. Der impact von Änderungen ist kaum abschätzbar, weil die Beziehungen so kompliziert sind; Denormalisierungen sind an der Tagesordnung und verwirren zusätzlich.

DRY und SRP und KISS sind allemal verletzt.

Frank Striegel hat gesagt…

Dann würde man die Bounded Contexts Rechnungslegung und Buchaltung auch dermassen getrennt implementieren, selbst wenn es zwei Programmbereiche in derselben Applikation (exe) sind?

Klar ich sehe die Vorteile. Aber, was ist, wenn beide Bounded Contexts in Teilen einer Rechnung dasselbe verstehen? DRY würde doch nun erfordern, dass es in einem gemeinsamen Basis-Modell zusammengeführt ist (s. Core-Domain/Shared Kernel by Evans DDD). Natürlich kann man dann argumentieren, dass die Schnittmenge der beiden Datenmodelle kleiner ist als wenn beide ein universelles Datenmodell verwenden. Aber der Aufwand um beide Datenmodelle der BCs auf das Basis-Modell zu synchronisieren ist auch nicht zu unterschätzen.

Ralf Westphal - One Man Think Tank hat gesagt…

@Frank: Nein, es wären keine getrennten "Programmbereiche" in derselben EXE. Bounded Contexts bestehen mindestens je aus einer App. Und App kannst du vereinfacht als EXE ansehen.

DRY ist hier bewusst nicht berücksichtigt! Das ist ganz wichtig zu verstehen. DRY steckt im "Normalisierungszwang" von RDBMS. Aber Normalisierung führt zu Kopplung. Und die soll möglichst niedrig sein. Das ist der Sinn von Bounded Contexts.

Datenduplikation ist hier eine Tugend!

Frank Striegel hat gesagt…

Ok, so weit so gut.

Ich überlege mir nur gerade, ob es nicht auch sinnvoll ist, innerhalb einer App so etwas wie Bounded Contexts einzuführen, wenn die einzelne Programmbereiche einer App sehr unterschiedliche Sichten auf die Domäne benötigen.

Der Gedanke, einzelne Programmbereiche dermassen stark voneinander zu trennen, finde ich nicht ohne, denn manchmal (habe dies schon bei mind. zwei Problemdomänen erlebt) sind selbst innerhalb einer App die Sichten unterschiedlicher Programmbereiche auf Domänenobjekte sehr verschieden, und man hat wirklich Mühe diese unterschiedlichen Belange in einem universellen Modell zu vereinen.

Ralf Westphal - One Man Think Tank hat gesagt…

@Frank: Wenn du an weitere Entkopplung innerhalb einer App denkst, dann ist das natürlich schön. Das Mittel der Wahl sind dann Komponenten.

Bounded Contexts können aber nicht gehen, weil sie per definitionem in der Zerlegungshierarchie oberhalb von Apps stehen.

Bounded Contexts bestehen aus Apps. Deshalb können Apps nicht aus Bounded Contexts bestehen.