Die Form eines Systems entwerfen

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 in rot 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 rot 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ührenin 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
  1. Quellkonto verifiziert, dass genügend Geld da ist.
  1. Quellkonto verifiziert, dass sein Stand größer ist als Minimalstand plus Überweisungsbetrag und wirft eine Exception, falls das nicht der Fall ist
  1. Quellkonto und Zielkonto aktualisieren ihre Stände.
  1. Quellkonto reduziert seinen Stand um den Überweisungsbetrag
  2. Quellkonto fordert Zielkonto auf, seinen Stand zu erhöhen
  1. Quellkonto aktualisiert die Kontoauszüge.
  1. Quellkonto notiert auf seinem Kontoauszug, dass dies eine Überweisung war
  2. Quellkonto fordert Zielkonto auf, auf seinem Kontoauszug eine Überweisung einzutragen.
  3. 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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
 * 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 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.

Diese Beiträge könnten Sie noch interessieren:

Kommentare

  1. Kurt Häusler meint

    :( Wenn als Software-Entwickler meine Rolle so stark vereinfacht wird um nur den Inhalt vordefinierter Methoden einzutippen, würde ich mich sofort kündigen. Das ganze was Softwareentwicklung spass macht ist in einem Team von Elites ausgelagert.

    Es behindert TDD, es behindert lernen. Es entfernt Entwickler noch weiter vom Kunden. Es nimmt die Craftsmanship weg, und die Möglichkeit für Teams selbst Entscheidungen zu treffen. Es führt auch eine Abhängigkeit ein, was wird wahrscheinlich ein Flaschenhals.

    Es klingt wie die schlechte alte Zeiten wo ein paar kluge “Designers” alle Entscheidungen treffen für die Massen der “normalen” Programmierer.

    Wie wäre es wenn alle Teams “Architekturteams” wären, und dürfen selbst alle oben-beschriebene Aktivitäten machen? Sie werden sowieso die gleichen funktionsübergreifenden Rollen besitzen.

  2. meint

    Kurt, Du stellst genau die richtigen Fragen. Das ist einen weiteren Blog-Eintrag wert, den ich in Kürze schreibe. Darin werden die Antworten stehen. Auf einen Fehler im obigen Text hast Du mich aufmerksam gemacht, den ich besser jetzt sofort beheben sollte: Ich meinte nicht, dass die Entwickler lediglich die Rollen-Methoden ausfüllen – das würde sie zu reinen Codiersklaven machen, das wäre absolut hässlich.

    Ich ergänze deshalb oben schon einmal den Satz “Struktur schaffen, welche die soeben gefundene Form stützt” und beschreibe im nächsten Blog-Artikel, was damit gemeint ist. Es wird um den Aufbau von Struktur gehen, nämlich mit Hilfe von TDD die ganzen anderen Klassen zu entwerfen und zu schaffen, welche die von der Form gegebene Schnittstelle ausfüllen. Ich glaube nämlich nicht an TDD auf Ebene der Form, sehr wohl aber an TDD auf Ebene der Struktur.

    Zu den anderen Themen wie Lernen, Craftsmanship, Abstand zum Kunden, usw. wie gesagt, dazu komme ich im nächsten Blog-Artikel dieser Serie.

  3. meint

    Seltsam, irgendwo im Markup dieser Seite muss ein Fehler sein. Die große Tabelle mit den 6 Algorithmusschritten wird auf dem iPhone in der “Reader”-Ansicht nicht dargestellt. Ich finde den Fehler aber nicht.

  4. meint

    So, nun komme ich endlich zu einem Kommentar. Leider bewegt der sich zunächst auf der Metaebene. Er bezieht sich auf die Grundlage für die Demonstration deines Ansatzes. Und er ist so umfangreich, dass ich dafür einen eigenen Blog-Artikel geschrieben habe: http://blog.ralfw.de/2012/10/gesucht-dreckige-realitat.html.

    Auch wenn mein Kommentar in deinen Ohren persönlich gemeint klingen mag, ist er es viel weniger als du annehmen magst. Dein Blogartikel war mir eigentlich nur Anlass, mal etwas zu formulieren, das mir schon öfter aufgefallen ist. Und wir müssen alle aufpassen, dass wir nicht in diese Falle laufen.

    Durch Kurts Kommentar fühle ich mich in meiner Meinung auch bestätigt. Er konnte sich dazu eigentlich nur bemüßigt gefühlt haben, weil dein Beispiel klein und sauber ist. Eine Notwendigkeit zu einem expliziten Entwurf wird kaum deutlich.

  5. meint

    @Ralf: Inzwischen habe ich ja ein lauffähiges Beispiel in Github gestellt, und wir haben ausführlich diskutiert in Deinem Blog. Demnächst gibt’s hier noch einen Artikel, der sich damit beschäftigt, wie ich zu dem Entwurf für diese Lösung gekommen bin. Stay tuned!

Trackbacks/ Pingbacks

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>