© DrHitch/Shutterstock.com
Agile Architektur mit .Net

3 Agile Architekturmuster


In Abschnitt 1.1 wurden bereits die Qualitätsattribute von Software und die damit einhergehenden Architekturziele betrachtet. In diesem Kapitel sollen Patterns und Vorgehensweisen vorgestellt werden, die insbesondere dazu geeignet sind, die Anforderungen an Softwarearchitekturen in agilen Projekten zu erfüllen.

Welche Anforderungen sind das ganz konkret? Agile Projekte sind in der Regel sehr stark von Veränderungen geprägt. Änderungen bei den Anforderungen ziehen Änderungen bei der Architektur nach sich, die wiederum Auswirkungen auf die Implementierung haben. Das Versprechen, das uns die agilen Vorgehensmodelle geben, ist es, jederzeit flexibel auf Änderungen reagieren zu können. Das stellt allerdings besondere Anforderungen an die zugrunde liegende Architektur des Systems und auch an das Vorgehen bei der Erstellung dieser Architektur. Einen Ansatz für das organisatorische Vorgehen wurde in Kapitel 2 vorgestellt. Entscheidend dabei ist es vor allem, wichtige Architekturentscheidungen so spät wie möglich zu treffen. Mit dem Sashimi-Ansatz in Abschnitt 3.1 wird ein mögliches technisches Vorgehen beschrieben, um beispielsweise Big Upfront Design zu vermeiden, aber dennoch vergleichsweise fundierte Entscheidungen bezüglich der Architektur treffen zu können.

Darüber hinaus gilt es, die folgenden, für agile Architekturen besonders entscheidenden Qualitätsattribute zu beachten:

  • Änderbarkeit

Die Architektur des Systems muss in der Lage sein, auf Änderungen zu reagieren. Das bedeutet, dass Änderungen ohne großen Aufwand in die Architektur einfließen können. Wie schon bei den Anforderungen an Architekten in agilen Projekten gesagt, heißt das aber nicht, dass man durch unnötige Komplexität versucht, jeden möglichen Fall zu berücksichtigen.

  • Erweiterbarkeit

Wenn neue Anforderungen auftauchen, muss auch das System entsprechend erweitert werden. Eine gute Architektur erlaubt das einfache Hinzufügen zusätzlicher Komponenten und Module.

  • Wartbarkeit

Wartbarkeit beschäftigt sich mit der Pflege der Software, nachdem diese in Produktion gegangen ist. Zumeist wird dieses Attribut während der Erstellung des Systems vernachlässigt. Allerdings macht die Wartungsphase in heutigen Projekten bis zu 90 % des Gesamtprojekts aus. Sicherlich lassen sich Fehler nicht vermeiden, aber eine gute Architektur ermöglicht das schnelle Finden und effiziente Beheben solcher Fehler.

Diese Attribute lassen sich im Wesentlichen durch ein möglichst hohes, aber zugleich auch angemessenes Maß an Entkopplung innerhalb des Systems wie auch gegenüber Drittsystemen erfüllen. Die Verantwortung für die Angemessenheit der dafür aufgewendeten Maßnahmen obliegt dem Softwarearchitekten bzw. dem Architecture Keeper.

Die Abschnitte 3.2 bis 3.4 stellen mögliche Architekturmuster vor, mit denen sich die angestrebte Entkopplung erreichen lässt.

3.1 Sashimi

Das Sashimi Pattern [3] ist kein Pattern im eigentlichen Sinne, sondern vielmehr eine Vorgehensweise, um Big Upfront Design zu vermeiden und dennoch fundierte Entscheidungen in Hinblick auf die Architektur treffen zu können.

Der Kerngedanke hierbei ist, sich zu Beginn des Projekts nicht mit dem detaillierten Design von Schichten, Modulen, Komponenten oder auch Cross-Cutting Concerns zu beschäftigen, sondern lediglich die minimal benötigte Menge an Code zu produzieren, um alle Teile des Systems miteinander zu verbinden und so ein Gesamtbild auf die Architektur des Systems zu bekommen. In den folgenden Phasen wird dann die eigentliche Funktionalität aufbauend auf dem vorhandenen Rahmen implementiert. Somit stellt dieses Vorgehen eine Art evolutionäres Prototyping dar, bei dem man sich je nach Phase auf unterschiedliche Teile der Anwendung konzentriert. Der Vorteil dabei ist, dass sich das System frühzeitig verschiedenen Tests unterziehen lässt, da alle Teile bereits miteinander verbunden sind.

An einem kleinen Beispiel soll exemplarisch das Vorgehen bei Sashimi verdeutlicht werden. Abbildung 3.1 zeigt eine typische Servicearchitektur.

Abb_3_1.png

Abbildung 3.1: Servicearchitektur

Die zu entwickelnde Serviceanwendung ist dabei zunächst als Blackbox mit ihren umgebenden Systemen dargestellt. Solch eine Darstellung erhält man typischerweise zu Beginn eines Projekts während der Analyse des Systemkontexts [2]. An dieser Stelle muss man sich also zunächst nur über die Schnittstellen des eigenen Systems und der angeschlossenen Systeme Gedanken machen. Das Innere des Systems spielt vorerst keine Rolle.

Im nächsten Schritt werden nun die einzelnen Verbindungen genauer betrachtet, es werden die zu verwendenden Protokolle und Nachrichten definiert und prototypisch die Anbindung der einzelnen Komponenten mit der dafür minimal notwendigen Menge an Code implementiert. Die Implementierung muss nicht zwangsläufig für jede Verbindung erfolgen, sofern diese unkritisch sind oder es bereits vorgefertigte Komponenten gibt, die wiederverwendet werden können. Hier gilt es abzuwägen, wie viel Aufwand zu diesem Zeitpunkt wirklich notwendig ist. Abbildung 3.2 zeigt die nähere Beschreibung der Verbindung zwischen Clients und Service. Dafür ist zunächst nur der Kommunikationskanal und die erste Serviceschicht des Systems von Belang.

Abb_3_2.png

Abbildung 3.2: Servicearchitektur – Services Layer

Implementieren läßt sich dies nun ebenfalls sehr leicht. Der Service soll über eine REST-Schnittstelle verfügbar sein und JSON als Nachrichtenformat nutzen. Um dies ohne größeren Aufwand umzusetzen, bietet sich die Verwendung des ASP.NET Web API an. Über die im Visual Studio enthaltene Projektvorlage lässt sich eine bereits lauffähige Version eines REST-basierten Service per Mausklick erzeugen. Leider werden aber auch für eine reine Web-API-Anwendung sehr viele Elemente generiert, die dafür eigentlich nicht notwendig sind. Der Übersichtlichkeit halber sollte man Elemente wie die Views oder auch Skripte entfernen. Auch die erzeugten Controller können gelöscht werden.

Nachdem das Projekt entsprechend aufgeräumt ist und nur noch die notwendigen Elemente enthält, kann der erste API Controller hinzugefügt werden. Der Service im Beispiel ermöglicht u. a. die Verwaltung von Kunden. Dementsprechend wird dafür eine passende Serviceschnittstelle benötigt. Im Sinne des Sashimi-Ansatzes geht es an dieser Stelle aber nicht darum, den gesamten Service Contract zu definieren, sondern lediglich die Anbindung des Service zu testen. Aus diesem Grund erhält der Controller vorerst nur eine Get-Methode zum Abfragen eines Kunden. Zuvor muss jedoch der Kunde selbst als Objekt definiert werden, damit der passende Rückgabetyp vorhanden ist. Auch hier gilt wieder: Nur so viel wie aktuell notwendig. Im ersten Schritt genügt es also, wenn der Kunde ein bis zwei Eigenschaften besitzt. Ein mögliches Beispiel für die zugehörige Klasse zeigt Listing 3.1.

public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

Listing 3.1: Einfache Customer-Klasse

Als Nächstes kann nun der API Controller angelegt werden. Mit der passenden Visual-Studio-Vorlage lässt sich ein leerer Controller erstellen, der dann um die benötigte Get-Methode erweitert wird. Den kompletten Controller zeigt Listing 3.2.

public class CustomerController : ApiController
{
// GET api/customer/5
public Customer Get(int id)
{
return new Customer();
}
}

Listing 3.2: Customer Controller mit Get-Methode

An diesem Punkt sind alle notwendigen Teile der Anwendung so weit implementiert, dass sich der Service ausführen und über einen passenden REST-Aufruf ansteuern lässt. Im einfachsten Fall genügt ein Aufruf aus dem Browser mit folgendem URL: http://localhost:49836/api/customer/5. Je nach Client liefert das Web-API entweder ein XML- oder ein JSON-Objekt (Listing 3.3) zurück, das man sich im Browser oder einem geeigneten Texteditor anschauen kann.

{
“Id”:0,
“FirstName”:null,
“LastName”:null,
}

Listing 3.3: JSON Darstellung Customer-Objekt

Im nächsten Schritt könnte man nun die Verbindung zum Business Layer herstellen und damit beispielsweise den verwendeten Dependency-Injection-Mechanismus oder auch andere Aspekte testen. Die Vorgehensweise von Sashimi sollte allerdings hinreichend erklärt sein, sodass an dieser Stelle auf eine Weiterführung verzichtet wird.

3.2 Ports and Adapters

Ziel der Ports-and-Adapters-Architektur ist das isolierte Entwickeln und Testen einer Anwendung unabhängig von umgebenden Systemen, Datenbanken oder anderen Akteuren. Damit dies erreicht werden kann, definiert die Anwendung spezielle Ports, über die Ereignisse und Daten von außen in die Anwendung gelangen. Die Daten werden innerhalb der Ports über entsprechende Adapter in Formate umgewandelt, mit denen die Anwendung umgehen kann. Das Adapter-Pattern wird ausführlich in Abschnitt 3.2.1 beschrieben.

Alistair Cockburn hat diesen Architekturstil bereits Mitte der 90ziger Jahre entwickelt und nannte ihn zunächst hexagonale Architektur [4], da er bei der ersten schematischen Darstellung ein Hexagon verwendete, um die unterschiedlichen Belange der Applikation voneinander zu trennen. Im Jahr 2005 benannte er schließlich die Architektur in Ports and Adapters um [5].

Heute gibt es viele Systeme, die in irgendeiner Form Ansätze aus der Ports-and-Adapters-Architektur nutzen. So können beispielsweise alle Systeme, die lediglich ein API zur Verfügung stellen und die eigentliche Erstellung einer Applikation samt User Interface anderen Instanzen überlassen, diesem Stil zugeordnet werden. Über das API stehen spezielle Schnittstellen zur Verfügung, die das Innere des Systems abgrenzen und eine definierte Kommunikation ermöglichen. Wird das API über Systemgrenzen hinweg angesprochen, sind zusätzlich in den meisten Fällen passende Adapter notwendig. Konkret kann so jeder Web Service in Richtung seiner Clients als Form der Ports-and-Adapter-Architektur angesehen werden. Es existieren Ports, die in diesem Fall sogar Ports im eigentlichen Sinne sind, und es gibt Adapter, die definierte Nachrichten im XML- oder JSON-Format in Objekte umwandeln und umgekehrt.

Abbildung 3.3 zeigt eine schematische Darstellung der (ursprünglichen) hexagonalen Architektur, wie sie von Alistair Cockburn proklamiert wurde.

Abb_3_3.png

Abbildung 3.3: Hexagonale Architektur

Die Abbildung verdeutlicht noch einmal, woher dieser Architekturstil zunächst seinen Namen hatte. Die sechs Kanten der Anwendung werden als Use Case Boundaries bezeichnet und repräsentieren zugleich die Ports des Systems. Die Ports selbst enthalten wiederum einen oder mehrere Adapter. Der Begriff Use Case ist hierbei sehr weit gefasst. So kann beispielsweise eine Kante die Abgrenzung zu allen clientseitigen Anbindungen darstellen. Eine weitere Kante grenzt dagegen alle Zugriffe auf Daten ab.

Die Architektur fordert nicht, dass eine Anwendung wirklich sechs Kanten, respektive Ports, haben muss. Sie gibt mit ihrem Design lediglich einen Hinweis auf eine maximal günstige Zahl der verfügbaren Ports. Im Grunde ist die Anzahl bzw. die Granularität der Ports allerdings nicht festgelegt. Prinzipiell könnte jeder Use Case einer Anwendung seinen eigenen Port besitzen, sodass die Applikation letztendlich Hunderte von Ports besitzt. Genauso gut könnten alle Use Cases in einem Port zusammengefasst werden, was wahrscheinlich zu einem Ball of Mud führen würde. Die sinnvolle Aufteilung der Funktionalitäten in Ports obliegt also dem Architekten.

3.2.1 Adapter-Pattern

Das Adapter-Pattern ermöglicht die Verbindung zweier Schnittstellen, die sonst nicht zueinander kompatibel wären. Am einfachsten kann man sich das verdeutlichen, wenn man sich das Problem der Reisestecker vor Augen hält. Es gibt Länder mit zwei- und mit dreipoligen Steckdosen. Geräte mit einem zweipoligen Anschluss können nicht ohne weiteres mit einem dreipoligen Anschluss verbunden werden. Die beiden Schnittstellen wären in diesem Fall die Steckdose und der Stecker am Gerät. Glücklicherweise gibt es für alle Reisenden entsprechende Reisestecker oder eben Adapter. Diese besitzen zwei Schnittstellen, von denen jeweils eine vom Gerät und eine von der Steckdose genutzt werden kann. Innerhalb des Adapters werden die anliegenden Ströme umgewandelt und umgeleitet, damit sie zum jeweiligen Zielinterface passen. Exakt die gleiche Funktion erfüllt ein Adapter in der Softwareentwicklung.

Ein Adapter lässt sich prinzipiell mit C# sehr leicht implementieren. Komplexität kann hierbei lediglich durch den Umfang der beteiligten Schnittstellen entstehen. Abbildung 3.4 zeigt die schematische Darstellung des Patterns mit UML.

Abb_3_4.png

Abbildung 3.4: Adapter-Pattern – UML-Darstellung

Der Client interagiert ausschließlich mit einem abstrakten Target. Das eigentliche Zielinterface ist für ihn transparent. Ein konkreter Adapter implementiert das abstrakte Target und wandelt den Aufruf des Clients in einen für das Ziel (Adaptee) verständlichen Aufruf um.

Als Beispiel für die Implementierung dient ein Validierungsservice für Adressen. Der Service selbst ist dabei das Target. Der Adaptee ist im Beispiel ein Google Web Service, der Geocoding-Informationen bereitstellt. Der Client soll verständlicherweise nicht direkt den Aufruf gegen das Google API stellen, sondern ein abstraktes Target verwenden, dessen konkrete Implementierung leicht austauschbar ist. Der Adapt...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Gewinner-Teams

Wir bieten Lizenz-Lösungen für Teams jeder Größe: Finden Sie heraus, welche Lösung am besten zu Ihnen passt.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang