Struktur in die Form füllen

Unter Form des Systems verstehe ich diese Elemente: Subsystem, Interface/API, Domänenobjekt, Use Case, Kontext, methodenlose Rolle. Heute geht es darum, wie Sie in den Entwicklungsteams diese Form mit Inhalt füllen. Diesen Inhalt nenne ich Struktur.

Form? Struktur? Manchmal eine Frage der Sichtweise

Form ist der für irgendeinen Stakeholder sichtbare und interessante Teil der Struktur, sozusagen die Essenz der Struktur. Form ist das, was auf einer bestimmten Abstraktionsebene für andere interessant und deshalb darstellenswert ist. Struktur ist das, was aus Sicht dieser Abstraktionsebene diese Form ausfüllt und stützt. Was auf einer Abstraktionsebene Form ist, kann auf einer anderen Struktur sein. Beispiel: Wenn ich ein Haus entwerfe, gehört das Aussehen der Fassade zur Form des Hauses. Wenn ich eine Stadt entwerfe, sind die Häuser nur noch Struktur.

Form ist das, was man auf einer bestimmten Abstraktionsebene konstant halten möchte. Struktur ist das, was sich innerhalb dieser Form auch jederzeit ändern darf. Das Ganze ist unter anderem eine Frage des Blickwinkels.

Die Form mit Strukturelementen ausfüllen

Genug der Philosophie. Jetzt wieder zum ausführbaren Code. Es geht darum, es den Entwicklern einfach zu machen, gut lesbaren, peer-review-fähigen und wartbaren Code zu schreiben. Die letzte Folge der Artikelserie endete mit der Domänenklasse Account, mit den methodenlosen Rollen MoneySource und MoneySink, und mit dem Kontext TransferMoneyContext.

Die Entwickler schreiben nun Code, um die Rollen mit Leben zu füllen. Hier zuerst die Füllung für MoneySource, nämlich die methodenreiche Rolle TransferMoneySource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Methodful role for the source account
 * for the money transfer.
 */
class TransferMoneySource implements MoneySource {
 
    def transferTo (Long amount, MoneySink recipient) {
        // This code is reviewable and
        // meaningfully testable with stubs!
        if (availableBalance() < amount) {
            throw new InsufficientFundsException()
        }
        else {
            decreaseBalance (amount)
            recipient.transferFrom (amount, this)
            updateLog ("Transfer Out", new Date(), amount)
        }
    }
 
}

Diese Rolle implementiert das Verhalten des Quellkontos und benachrichtigt das Zielkonto, ebenfalls seinen Stand zu aktualisieren. Beachten Sie, dass die Rolle auch Methoden aus der Domänenklasse Account aufruft, die in TransferMoneySource gar nicht definiert sind, z.B. decreaseBalance(). Warum das funktioniert, sehen sie ein paar Absätze weiter unten.

Jetzt zur Implementierung der methodenreichen Rolle TransferMoneySink für das Verhalten des Zielkontos:

1
2
3
4
5
6
7
8
9
10
11
/**
 * Methodful role for the recipient account
 * for the money transfer.
 */
class TransferMoneySink implements MoneySink {
 
    def transferFrom (Long amount, MoneySource source) {
        increaseBalance (amount)
        updateLog ("Transfer In", new Date(), amount)
    }
}

Um Testmaterial zu haben, leite ich hier noch zwei Klassen von Account ab, nämlich SavingsAccount (für ein Sparkonto) und CheckingAccount (für ein Girokonto). Um den Leser nicht durch Details abzulenken, bringe ich darin jedoch kein sinnvolles Verhalten unter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 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 sind die Rollen mit Verhalten gefüllt, also kann der Kontext jetzt seine Arbeit tun. Sie erinnern sich noch an TransferMoneyContext aus der letzten Folge? Hier seine Methoden, die die Entwickler jetzt definieren:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// this is from class TransferMoneyContext...
MoneySource source
MoneySink   recipient
 
def bindObjects() {
    // this would normally be a database lookup!
    def savings = new SavingsAccount()
    savings.increaseBalance (1000L)
    def checking = new CheckingAccount()
 
    // now mix-in the roles of these objects
    // so that each object gets the methods needed by
    // the use case and by the other roles.
 
    savings.metaClass.mixin TransferMoneySource
    checking.metaClass.mixin TransferMoneySink
 
    this.source    = savings  as MoneySource
    this.recipient = checking as MoneySink
}

Besonders interessant sind die Zeilen 15 und 16. Dort mischt Groovy (mit Hilfe des Wortes mixin) die Methoden eines Objektes und einer weiteren Klasse zusammen. Das Objekt hat den Typ Account und bekommt zur Laufzeit zusätzlich die Methoden des Typs TransferMoneySource bzw. TransferMoneySink injiziert.

Das Programm ausführen

Es fehlt nun noch ein kleiner Testtreiber, der den Use Case anstößt, damit Sie auch sehen, dass es funktioniert. Hier ist er:

1
2
3
4
5
6
7
8
9
10
11
12
// -------------------------------------------------------
// --  Test driver                                      --
// -------------------------------------------------------
 
def testContext = new TransferMoneyContext()
 
testContext.amount = 200
testContext.bindObjects()
testContext.doIt()
 
assert 800 == testContext.source.availableBalance()
assert 200 == testContext.recipient.availableBalance()

Die assert-Anweisungen prüfen, ob auch das Richtige passiert ist. Die Ausgabe des kleinen Programms haben Sie wahrscheinlich schon vor Ihrem geistigen Auge. Hier ist sie:

Checking Account: Transfer In, Wed Sep 05 11:56:04 CEST 2012, 200
Savings Account: Transfer Out, Wed Sep 05 11:56:04 CEST 2012, 200

Der Sinn der Sache

Wahrscheinlich haben Sie sich schon gefragt, warum ich ein solch einfaches Beispiel in aller Ausführlichkeit darstelle. Kurt Häusler hatte ja in einem Kommentar zur letzten Folge dieser Serie schon gesagt:

Wenn als Software-Entwickler meine Rolle so stark vereinfacht wird, um nur den Inhalt vordefinierter Methoden einzutippen, würde ich sofort kündigen. Das Ganze, was an Softwareentwicklung Spaß macht, ist in ein Team von Eliten ausgelagert.

Es klingt wie die schlechte alte Zeit, wo ein paar kluge “Designer” alle Entscheidungen trafen für die Massen der “normalen” Programmierer.

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

Zugegeben: Wenn all dies hier nur für ein Team gemacht würde, bei dem die Entwickler sowieso design-affin sind und sich in der Architektur engagieren, dann würden natürlich alle auf den Form/Struktur-Split achten und bräuchten keine Vorgabe oder Orientierungshilfe. Diejenigen im Team, die auf die Form achten…

  • würden im Daily Scrum natürlich sofort sagen, wenn die Form verletzt wird.
  • Sie würden noch nicht einmal bis zum nächsten Morgen warten, sondern sich ihre Kollegen sofort an den Tisch holen, wenn sie eine Verletzung der Form sehen, und diese dann gemeinsam korrigieren.
  • Oder sie würden gemeinsam erkennen, dass die Form tatsächlich verändert werden muss, um weiteres werthaltiges Verhalten für den User zu ermöglichen.
  • Sie würden gemeinsam herausfinden, was eine Formänderung für alle bedeutet und das auch kundtun.
  • Alle gemeinsam wären “die Architekten”.

Doch ist das in der Praxis wirklich so? Ich habe die Erfahrung gemacht, dass es Menschen gibt, die einen Blick für Form haben und solche, die ihn nicht haben oder die diesen Blick nicht interessant finden. Und ich habe gelernt, dass der Blick auf die Form bei großen Systemen, die von vielen Teams gemacht werden, noch viel wichtiger ist. Er geht auch vor lauter Blick auf die Struktur leicht verloren.

Beide Faktoren zusammen (die Eigenart der Menschen und die Skalierung von Architekturarbeit auf große Teams) haben mich lernen lassen, dass es gut ist, wenn man leichte, kleine Vorgaben für die Form macht und sich als Entwickler bemüht, sie einzuhalten. Ein großes System behält so besser seine Integrität.

Wie stark vorgeben? Von wem? Für wen? Warum?

Wie stark waren die Form-Vorgaben oben denn wirklich? Hier die Liste:

  • ein Use Case
  • ein Algorithmus
  • eine Domänenklasse Account
  • zwei methodenlose Rollen MoneySource und MoneySink
  • eine Methode im Kontext, also TransferMoneyContext.doIt()

Wer hat diese Vorgaben erstellt? Eine Architekturrunde, zusammengesetzt aus den Mitgliedern derjenigen Teams, die die Software tatsächlich schreiben müssen, und aus den Stakeholdern, die wirklich Input für die Form liefern können (z.B. Domänenexperten aus dem Fachbereich und aus dem Betrieb, User Experience-Experten für die graphisch-/interaktiven Aspekte der Form, usw.). Die Entwickler waren also dabei und haben die Vorgaben mitgestaltet.

Ist es also für die Entwickler so schlimm, sich selbst zusammen mit anderen eine Vorgabe für die Form zu erstellen bevor sie losarbeiten? Ich behaupte, nein. Die Entwicklung wird dadurch schneller, weil die Form einen guten Anhaltspunkt für die Richtung der Entwicklung liefert. Der Bedarf an Refactoring sinkt, weil auf den Unterschied zwischen konstanten oder wenigstens stabilen Teilen und den äußerst variablen Teilen geachtet wird und dadurch Auswirkungen von Änderungen minimal bleiben.

Für die nächste Folge

Kurt, ich muss es bei nochmaligem Lesen zugeben: Das Beispiel ist deutlich zu klein, um zu erkennen, dass man so einen Form-/Struktur-Split braucht. (Es musste jedoch klein sein, um die Prinzipien zu erkennen). Daher schreibe ich in der nächsten Folge dieser Serie darüber, wie Sie dasselbe mit 15 Teams und 75 Personen machen. Architekturarbeit fraktal organisiert – dann wird der Nutzen dieses Aufwandes tatsächlich sichtbar.

Diese Beiträge könnten Sie noch interessieren:

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>