Ein funktionsübergreifendes, agiles Architekturteam sollte wenig Architektur erstellen und sich die Arbeit mit den anderen Teams aufteilen, so dass die Zusammenarbeit leicht von der Hand geht. Die wesentliche Aussage des vorigen Blog-Posts war:

  • Beginnen Sie mit der Form des Systems.
  • Form gibt es für das, was das System (aus Sicht des Benutzers) ist.
  • Form gibt es auch für das, was das System (aus Sicht des Benutzers) tut.

In solch einer Tabelle hatte ich die Design-Aufgaben zusammengefasst:

Was das System ist Was das System tut
Form Subsysteme, Interfaces, APIs, Domänenobjekte Use Case, Kontext, Methodenlose Rollen
Struktur Module, Pakete, Klassen Methodenreiche Rollen, Algorithmen

In diesem Artikel zeige ich Ihnen, wie Sie die ersten Schritte in Richtung der Form Ihres Systems machen. Das tun Sie im Architekturteam (bestehend aus Benutzern, Kunden, Domänenexperten, Businessleuten, Entwicklern).

Was das System ist

Besprechen Sie mit den anderen Stakeholdern in der Architekturrunde, was das System für sie „ist“. Das beginnt mit dem, was die Benutzer an zentralen Konzepten im Kopf haben, von denen sie immer wieder sprechen. In einer Bank könnten das zum Beispiel Konten und Kontoinhaber sein. Es könnten Girokonten, Sparkonten, Aktiendepots, Kredite, und so weiter sein. Diese zentralen Konzepte und deren Eigenschaften können Sie zu Domänenobjekten und deren Attributen werden lassen, wie es Eric Evans in Domain Driven Design beschrieben hat. (Wenn Sie nicht viel Zeit zum Lesen des DDD-Buches haben, lesen Sie ein informatives, kurzes eBook bei infoQ).

Wenn Sie die Domänenobjekte gefunden haben, gruppieren Sie sie nach stärkstem thematischem Zusammenhang. Zeichnen Sie z.B. „Gummibänder“ zwischen den Objekten, um einen Zusammenhang anzudeuten. Fassen Sie dann stark zusammenhängende Teilmengen der Objekte zu Paketen zusammen, die untereinander jedoch nur lose gekoppelt sind. Das schaffen Sie, indem Sie möglichst wenige der Gummibänder durchschneiden.

Domaenenform

Überlegen Sie, ob sich dem jeweiligen Paket eine eindeutige Verantwortung zuordnen lässt. Wenn nicht, teilen Sie es besser auf. Am besten, Sie berücksichtigen die Aussagen über das Geheimnisprinzip, das David Parnas 1971 unter anderem in seinem Artikel On the Criteria To Be Used in Decomposing Systems into Modules (damals noch für die strukturierte, nicht die objektorientierte Programmierung) formuliert hat:

…it is almost always incorrect to begin the decomposition of a system into modules on the basis of a flowchart. We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others. Since, in most cases, design decisions transcend time of execution, modules will not correspond to steps in the processing. To achieve an efficient implementation we must abandon the assumption that a module is one or more subroutines, and instead allow subroutines and programs to be assembled collections of code from various modules.

Jetzt haben Sie fachliche Pakete, eine erste Grundlage für spätere fachliche Subsysteme. Zu einem echten Subsystem fehlt Ihnen nur noch die Schnittstelle. Die Schnittstelle füllen Sie mit den Operationen, die beim Entwurf des Verhaltens entstehen. Dazu kommen wir in einem späteren Abschnitt, sobald wir besser verstehen, was das System tut.

Was das System tut

Fragen Sie Ihre Stakeholder, was das System für sie tun soll. Was ist die Erwartung der Benutzer? Welche Domänenobjekte kann der Benutzer innerhalb welcher Anwendungsfälle manipulieren? Was passiert dann, und welche Ergebnisse entstehen dabei?

Nehmen Sie als Beispiel eine Bank. Dort gibt es vielleicht einen Anwendungsfall „Geld überweisen“. Es spielen dabei zwei Konten eine Rolle. Das eine Konto ist das Quellkonto, von dem das Geld kommt. Das andere Konto ist das Zielkonto, auf dem das Geld ankommen soll.

Der Anwendungsfall sieht vielleicht so aus:

Schritt Akteur-Intention Systemverantwortung
1. Kontoinhaber wählt Quellkonto und verlangt Überweisung Bank zeigt Quellkonto, eine Liste von Zielkonten und ein Feld zur Eingabe des Betrags
2. Kontoinhaber wählt Zielkonto, gibt den Betrag ein und bestätigt Bank zeigt Überweisungsinformationen (Quellkonto, Zielkonto, Datum, Betrag) und verlangt eine TAN, um die Überweisung zu legitimieren
3. Kontoinhaber gibt TAN ein und bestätigt Bank bewegt das Geld und führt die Konten.

Im Text sind die Rollennamen fett markiert: Quellkonto und Zielkonto. Die Domänenobjekte, die an diesem Anwendungsfall beteiligt sind, egal ob es nun Sparkonten oder Girokonten sind, sie nehmen die Rolle Quellkonto oder Zielkonto ein. Das wird später den Aufbau einer leicht wartbaren Architektur ermöglichen.

Die Gewohnheit „Geld bewegen und Konten führen“ ist ebenfalls fett markiert. Sie kommt in einer Bank öfters vor, ist in sich geschlossen und braucht keine Benutzerinteraktion. Sie können solch eine Gewohnheit später in einen Algorithmus umwandeln.

Vom Was zum Wer und Wie

Anwendungsfälle beschreiben oft lediglich, was zu tun ist. Unter Systemverantwortung steht entsprechend auch nur „das System tut“ oder in diesem Fall „die Bank tut“. Wenn  Sie die Gewohnheit Geld bewegen und Konten führen in einzelne Was-Schritte aufbrechen, sieht das vielleicht so aus:

Schritt Systemverantwortung
1. Bank verifiziert, dass genügend Geld da ist.
2. Bank aktualisiert die Konten.
3. Bank aktualisiert die Kontoauszüge.

Wenn Sie nun „Bank“ durch die konkreten Rollen der Domänenobjekte ersetzen, machen Sie damit einen Entwurfsschritt, der Sie zum „Wer“ führt:

Schritt Systemverantwortung
1. Quellkonto verifiziert, dass genügend Geld da ist.
2. Quellkonto und Zielkonto aktualisieren ihre Stände.
3. Quellkonto aktualisiert die Kontoauszüge.

Jetzt können Sie vollends zum „Wie“ übergehen, indem Sie die Rollen miteinander interagieren lassen und jeden Schritt dabei beschreiben. Damit bekommen Sie einen Algorithmus:

Systemverantwortung Algorithmusschritte
Quellkonto verifiziert, dass genügend Geld da ist. Quellkonto verifiziert, dass sein Stand größer ist als Minimalstand plus Überweisungsbetrag und wirft eine Exception, falls das nicht der Fall ist
Quellkonto und Zielkonto aktualisieren ihre Stände. Quellkonto reduziert seinen Stand um den Überweisungsbetrag
Quellkonto fordert Zielkonto auf, seinen Stand zu erhöhen
Quellkonto aktualisiert die Kontoauszüge. Quellkonto notiert auf seinem Kontoauszug, dass dies eine Überweisung war
Quellkonto fordert Zielkonto auf, auf seinem Kontoauszug eine Überweisung einzutragen.
Quellkonto signalisiert Erfolg der Überweisung.

Damit ist klar, was jede Rolle zu tun hat. Sie können das nun (noch innerhalb der Architekturrunde) in Operationen übersetzen, ohne die genaue Methode für eine Operation anzugeben. Ich verwende hier die Sprechweise der UML:

  • Operation = reine Signatur,
  • Methode = Signatur plus konkrete Implementierung.

Man nennt die Rollen, die Sie bis hierher gefunden haben, deshalb auch methodenlose Rollen.

Der Kontext hält alles bereit

Sie brauchen noch pro Anwendungsfall eine Stelle, an der Referenzen auf alle Rollen und Werte hinterlegt sind, die der Algorithmus zum Ablaufen benötigt. Diese Stelle nennt man Kontext. Ein Kontext ist ein Objekt, das Referenzen auf alle methodenlosen Rollen und auf die Werte hält, mit denen die Rollen rechnen wollen. Dieses Objekt bekommt eine Operation doIt(), die dann ohne weitere Interaktion ablaufen kann, weil sie im Kontext bereits alles Nötige vorfindet.

Form in Code übersetzen

Damit das Ganze eine gute Vorgabe für die Entwicklungsteams ergibt, übersetzen Sie die gefundenen Rollen und Operationen in Quellcode. Ich verwende hier die Programmiersprache Groovy, weil sie ein Mixin-Konzept hat, mit dem sich später Domänenobjekte und Rollen verbinden lassen werden (siehe nächste Folge dieser Artikelserie).

Hier zunächst die Daten (Domänenklassen):

/**
 * Domain class that captures the concept of a
 * bank-internal account in general.
 */
class Account {
    private Long balance = 0L
    Long availableBalance() { balance }

    def increaseBalance(amount) { balance += amount }

    def decreaseBalance(amount) { balance -= amount }

    def updateLog(msg, date, amount) {
        println toString() + " Account: $msg, $date, $amount"
    }
}

/**
 * Domain class that captures the concept of a savings account
 */
class SavingsAccount extends Account {
    String toString() { "Savings" }
}

/**
 * Domain class that captures the concept of a checking
 * account.
 */
class CheckingAccount extends Account {
    String toString() { "Checking" }
}

Jetzt die methodenlosen Rollen:

/**
 * Methodless role that captures the form (interface)
 * of one part of the Transfer behavior
 */
interface MoneySource {
    def transferTo(Long amount, MoneySink recipient)
}

/**
 * Methodless role that captures the form (interface)
 * of the other part of the Transfer behavior
 */
interface MoneySink {
    def transferFrom(Long amount, MoneySource source)
}

Und der Kontext, der alles enthält, was zum Ablauf des Anwendungsfalls gebraucht wird:

/**
 * Context of the "Transfer Money" use case.
 */
class TransferMoneyContext {
    MoneySource source
    MoneySink recipient
    Long amount

    def bindObjects() {
        // find objects and assign to source and recipient.
        // this would normally be a database lookup!
    }

    def doIt() {
        source.transferTo(amount, recipient)
    }
}

Zwischenstopp

Damit haben Sie für die Entwicklungsteams sehr klar beschrieben, was ablaufen soll. Die Form des Systems ist gefunden. Diese Form gibt es zweimal: die Form dessen, was das System ist und die Form dessen, was das System tut.

Die Entwickler müssen jetzt noch den Entwurf vollenden:

  • Struktur schaffen, welche die soeben gefundene Form stützt
  • das Verhalten des Systems realisieren, indem sie die Methoden für die Rollen schreiben
  • den Anschluss an die Technik schaffen, indem sie den Code in eine geeignete Umgebung einbetten, die dann Transaktionen startet, Fehler behandelt, Logging durchführt und vieles mehr.

Wie man aus methodenlosen Rollen methodenreiche Rollen macht und mit den Daten (Domänenobjekten) verbindet, beschreibe ich in der nächsten Folge dieser Artikelserie. Seien Sie gespannt, wie elegant diese Verbindung in Groovy funktioniert und wie einfach es die Entwickler haben werden, gut lesbaren, peer-review-fähigen und wartbaren Code zu schreiben.