Follow my new blog

Donnerstag, 14. Juni 2012

Angst essen Entwickler auf 2

Woher kommt denn die Angst, die viele Entwicklerherzen bestimmt? Meine Vermutung, es gibt dafür zwei Gründe:
  1. Angst ist ein Kontrollinstrument. Wer Bedenken äußert und Unsicherheitsszenarien ausmalt, der erlangt Bedeutung und damit Macht. In einer Welt, in der Softwareentwicklung oft noch als lästiges Anhängsel gesehen wird und man mit Unkenntnis in IT-Dingen auf Partys und im Feuilleton kokettieren kann, da schrumpft auf der anderen Seite schnell mal das Selbstwertgefühl – und will kompensiert werden. Angstbasiertes Bedenkenträgertum ist da ein probates Mittel.
  2. Wem der vorgenannte Grund zu psychologisch-schwammig ist, kann hoffentlich zumindest mit diesem zweiten etwas anfangen: Die ängstlichen Bedenken sind Ausdruck einer noch tieferliegenden Angst. Und das ist die Angst, Code später nur noch schwer verändern zu können. Der Gedanke dahinter: “Wenn ich heute etwas nicht berücksichtige, dann habe ich morgen oder übermorgen große Probleme, neue Erkenntnisse in den Code einzubringen.” Spätere Codeanpassungen können zu Regressionen führen, Codeveränderungen können schlicht viel, viel, viel aufwändiger sein, als es gleich “richtig” zu machen.

Der zweite Grund ist für mich der gravierendere. Denn dahinter steckt ein Bild von Code, das mir essenziell anti-agil scheint.

Oberste Priorität hat für mich daher jedes Prinzip und jede Praktik, um diese Angst zu reduzieren. Eine gute Abdeckung mit automatisierten Tests hilft. Ein Versionsverwaltungssystem hilft. Ein automatischer Buildprozess hilft. Sie wirken angstlösend. Regressionen werden zumindest schnell erkannt, wenn nicht gar verhindert.
Für die Flexibilisierung von Code müssen wir aber noch mehr tun. Die Kosten für Änderungen dürfen nicht weiter exponenziell steigen. Aber auch einen linearen Anstieg empfinde ich als noch zu schwachen Trost. Warum wird es mit der Zeit nicht immer leichter und leichter, Änderungen in Software einzubringen? Wenn wir Software als Wissenspool ansehen, dann wächst der doch über die Zeit. Und mit mehr wissen, sollten Probleme in der Zukunft leichter zu lösen sein.

Dieser kühnen Vision stehen aber natürlich alte, feste Glaubenssätze entgegen. Aus denen speist sich auch die Angst. Da Angst jedoch ein ganz unschönes Gefühl ist, finde ich es natürlich, nach Wegen zu suchen, die Angst zu überwinden. Auch und gerade, wenn wir dafür über die langen Schatten heimeliger Glaubenssätze springen müssen.
Welche Glaubenssätze halten uns also davon ab, Software in anderer als der heutigen Weise zu entwerfen und zu codieren? Was können wir anders machen, um keine Angst mehr davor zu haben, Veränderungen auf einen beliebigen Zeitpunkt in der Zukunft zu verschieben? Wie kommen wir weg von dem Satz, “Ja, wenn Sie uns das vor einem halben Jahr gesagt hätten, dann hätten wir das ganz leicht einbauen können. Jetzt ist das sehr, sehr schwierig.” Der ist nämlich nicht nur demotivierend für Kunden wie Entwickler, sondern steht in ständigem Konflikt zur Realität der Anforderungskenntnis. Kunden wissen nun mal sowenig darüber, was sie morgen oder übermorgen vielleicht haben wollen. Und sie können das, was sie ungefähr wissen, nur schlecht formulieren. Also müssen wir ohnehin mit ständigen Veränderungen fertig werden. Besser, wir verlieren endgültig die Angst davor. Stattdessen sollten wir jeden Änderungswunsch willkommen heißen. Denn er zeigt uns, dass unsere Software gewollt wird. Wer aufhört, Änderungen zu wünschen, der hört sehr wahrscheinlich auf, unsere Software zu nutzen.

Montag, 11. Juni 2012

Angst essen Entwickler auf 1

Mir scheinen in der Branche zwei Modi vorzuherrschen, in denen entwickelt wird:

Modus #1, Sorglosigkeit: Es wird Code geschrieben, als gäbe es kein Morgen. Raushauen, was das Zeug hält. Gummi auf die Straße bringen. Den Coderevolver schneller ziehen als der eigene Schatten. Wichtig ist, dass der, der grad am lautesten schreit, möglichst schnell zufriedengestellt wird. Ob das dann eine Qualität hat, die Anwendern Freude macht, ist eine zweite Frage. Darüber macht man sich keine Sorgen. Es wird in den Tag hinein entwickelt. Kurzfristiger Erfolg ist wichtig. Langfristiger Erfolg wird als selbstverständlich angenommen oder ist irgendwie gar kein Thema.

Modus #2, Angst: Bevor Code geschrieben wird, werden die Implikationen aller Ramifikationen jeder Änderung länglich gewälzt. Es gibt immer viel zu bedenken. Da will zum Beispiel sorgfältig geprüft werden, was eine Veränderung für Auswirkungen auf die Sicherheit des Systems hat. Und ganz wichtig: Ist der gewählte Ansatz nicht doch zu speziell? Könnte er nicht erweitert und verallgemeinert werden, um dermaleinst auch noch unbekannte Anforderungen in der Zukunft eventuell leichter abzudecken? Und wie ist es mit Sonderfällen? Finden die schon genügend Berücksichtigung?

Beide Modi führen in die Lähmung.

Sorglosigkeit erzeugt Lähmung in naher oder mittlerer Zukunft. Denn dann ist der Code durch die sorglose Anhäufung unwartbar geworden. Für methodisches Vorgehen, Strukturplanung, Bemühen um Evolvierbarkeit war ja nie Zeit.

Angst erzeugt Lähmung in der Gegenwart. Man kommt vor lauter Überlegungen zu Unwahrscheinlichem und Unbekanntem nicht zum Wesentlichen, der Produktion von Code, der jetzt schon etwas nützt. Ultimativer Nutzen möglichst ohne Lücke in der Zukunft ist wichtiger.

Die sorglose Programmierung scheint in den letzten Jahren allerdings im Rückgang begriffen. Zum Glück. Der Diskurs über Softwarequalität im Allgemeinen und Testmethodiken und –tools im Speziellen hat etwas bewirkt.

Die angstgehemmte Programmierung hingegen ist immer noch weit verbreitet. Sie zeigt sich im Kleinen, wenn an jeder Ecke try-catch-Blöcke oder Code Contracts geschrieben werden. Und sie zeigt sich im Großen, wenn aus Anforderungen Dinge herausgelesen werden, die dort nicht stehen. Oder Anforderungen zwar als nicht explizit beschrieben erkannt, aber dennoch felsenfest ab-so-lut sicher vorhergesehen werden. Zeuge der Vorhersage ist dann die Erfahrung. Und verräterisch ist die Formulierung “Aber was, wenn XYZ so oder so wäre?”

Woher kommt nur diese Angst? Warum ist sie so unausrottbar?

Mittwoch, 6. Juni 2012

SEACON 2012 - Vortrag zu agilen Softwarestrukturen

Heute habe ich auf der SEACON 2012 Flow-Design vorgestellt, ohne es so zu nennen :-) Mein Ansatz war prinzipieller: Für mich ist das, was wofür Flow-Design steht, ein Ergebnis konsequenter Anwendung des Single Responsibility Principle. Flow Design trennt die Verantwortlichkeiten Integration und Operation.

Operation: Hier spielt die Logik-Musik. Hier kommen 3GL-Kontrollstrukturen und Ausdrücke zum Einsatz. Das Ergebnis sind Codeeinheiten, die mit Unit Tests solide abgedeckt sein sollen. Um diese Codeeinheiten überschaubar und leicht testbar zu machen, sind sie unabhängig von einander. Deshalb werden sie über Datenflüsse zu Größerem verbunden.

Integration: Hier werden die Operationen zu Größerem verbunden. Integratoren enthalten keine Kontrollstrukturen oder Ausdrücke. Sie sind frei von solchen Logikkonstrukten. Ihre einzige Aufgabe ist es, Operationen und andere Integratoren zu integrieren. Das geschieht auf beliebig vielen (Abstraktions)Ebenen.

Weil Operationen nicht von einander und am besten auch von keinem gemeinsamen Zustand abhängen sollen, setzen sie auf Datenfluss zur Kommunikation. Weil es um Datenfluss geht, sind Operationen und die daraus entstehenden Integrale eher Aktionen oder Prozess(schritte), denn Akteure. Es geht um Verben statt Substantive. Und weil das so ist, können die entstehenden Hierarchien unmittelbar ausdrücken, wie Code die Funktionalität für Features der Anforderungen herstellt.

Eine Softwarestruktur, die diese Trennung lebt, ist deshalb bestens geeignet, agiles Denken im Code widerzuspiegeln.

Mein Credo:

Process over Data

Wenn wir agil Vorgehen, sollten wir das datenorientierte Denken der bisherigen üblichen Objektorientierung zugunsten eines prozessorientierten Denkens aufgeben. Damit tragen wir auch dem alten Verständnis von Software Rechnung, dass sie als EVA begreift: Eingabe – Verarbeitung – Ausgabe.

Wer nun mehr über diesen Ansatz erfahren möchte, der kann in den Flow-Design Ressourcen bei den Clean Code Advisors stöbern.

Oder in der dotnetpro meine Artikel verfolgen.

Oder hier meinen Ausflug ins Single Responsibility Principle mitmachen.

Oder nicht nur dieses Blog verfolgen, sondern auch mein englisches Blog lesen. Ich werde mich bemühen, dort mehr zu schreiben, um das Konzept international zur Diskussion zu stellen. Es bewegt sich ja gerade recht viel dabei: Flow-Design wird “entzaubert” durch eine Gründung in fundamentalen Prinzipien. Und Flow-Designs ausführbar zu machen, wird durch die Flow Runtime NPantaRhei noch einfacher.

Montag, 30. Januar 2012

TDD im Flow – Teil 3

Was bisher geschah:

Test #4: Blockierte Entnahme

Nun geht es an den Kern meines Abstrakten Datentyps: die Sequentialisierung.

image[42]_thumb

Ich muss die Entnahme aus den Queues nach Round Robbin einschränken. Es darf nur entnommen werden, wenn eine Queue nicht gerade blockiert wird. Die Blockierung beginnt, wenn ein Worker aus einer unblockierten Queue entnimmt – und wird automatisch aufgehoben, wenn er wieder nach Arbeit fragt.

Anders als im Datenmodell entworfen, implementiere ich die Sperren für Queues in einem Dictionary. Die Einträge darin sind die Flags des Datenmodells. Ich finde es jedoch besser, sie von der Liste der Queues zu trennen. Die Blockade der Entnahme ist ein anderer Aspekt als die Entnahme nach Round Robbin. Beide durch Verwaltung in unterschiedlichen Datenstrukturen zu trennen, scheint mir konsequente Anwendung des SRP.

Falls sich etwas an der Blockadestrategie ändert, muss ich nicht die Datenstruktur für die Entnahme anfassen. Es ist auch nur TryDequeue() betroffen.

internal class NotifyingMultiQueue<T>
{
    …
    readonly Dictionary<string, string> _readLocks =
        new Dictionary<string,string>(); 

    …
    public bool TryDequeue(string workerId, out T message)
    {
        message = default(T);

        var namedQueue = _queues[0];
        _queues.RemoveAt(0);
        _queues.Add(namedQueue);

        if (_readLocks.ContainsKey(namedQueue.Key) &&
            _readLocks[namedQueue.Key] != workerId)
            return false;

        message = namedQueue.Value.Dequeue();
        _readLocks[namedQueue.Key] = workerId;

        return true;
    }
    …

Diese Implementation erreicht das Ziel natürlich noch nicht ganz. Sie dient nur der Erfüllung eines Tests, ohne die vorherigen zu brechen.

Bevor ich aber den nächsten Test beschreibe: Haben Sie meine TDD Sünde entdeckt?

Ich habe mehr Code geschrieben, als für den Test nötig ist. Die Prüfung, ob der “arbeitsuchende” Worker derjenige ist, der eine Queue bisher blockiert hat, kommt nicht zum Tragen. Dafür müsste ich einen weiteren Test schreiben. Aber das ist nicht nötig, weil der nächste Test dafür sorgen wird, dass diese Prüfung gar nicht mehr nötig ist.

Warum habe ich dann aber _readLocks[namedQueue.Key] != workerId geschrieben? Weil es mir in dem Moment cool vorkam, an diese Feinheit gedacht zu haben. Da hab ich mich von der Idee davontragen lassen… Erst später beim nächsten Test ist mir die Überflüssigkeit der Bedingung aufgefallen.

Ich hatte sie aber nach erfolgreichem Test eingecheckt. Deshalb zeige ich sie Ihnen hier auch. So kann es halt kommen, auch wenn man sich bemüht. Mit Pair Programming wäre das vielleicht nicht passiert. Am Ende ists aber auch kein Beinbruch. Mir ist es ja aufgefallen. Wichtig ist, daraus zu lernen. Nobody is perfect – but everybody should strive for improvement. Oder so ähnlich ;-)

Test #5: Blockierte Queue wieder freigeben

Bisher wurde eine Queue bei Entnahme nur gesperrt. Jetzt muss sie wieder entsperrt werden. Das geschieht, wenn der Worker, der sie gesperrt hat, wieder frei ist. Der ADT bemerkt, wann ein Worker seine Arbeit an einer Nachricht beendet hat daran, dass der Worker wieder um eine Nachricht bittet:

image[47]_thumb

Beim ersten Aufruf von TryDequeue() sperrt w1 die Queue q1. Bei zweiten Aufruf sperrt er q2 – und gibt damit implizit q1 wieder frei, so dass w2 daraus entnehmen kann.

In der Implementation erreiche ich das, indem ich einfach bei Aufruf immer eine eventuell durch den anfragenden Worker gesetzte Sperre lösche:

internal class NotifyingMultiQueue<T>
{
    …

    public bool TryDequeue(string workerId, out T message)
    {
        message = default(T);

        if (_readLocks.ContainsValue(workerId))
            _readLocks.Remove(
                 _readLocks.Where(kvp => kvp.Value == workerId)
                           .Select(kvp => kvp.Key)
                           .First());

        var namedQueue = _queues[0];
        _queues.RemoveAt(0);
        _queues.Add(namedQueue);

        if (_readLocks.ContainsKey(namedQueue.Key))
            return false;

        message = namedQueue.Value.Dequeue();
        _readLocks[namedQueue.Key] = workerId;

        return true;
    }
    …

Damit entfällt dann auch die Prüfung, ob eine Sperre durch den aktuellen Worker gesetzt worden ist. Der Fall kann nicht mehr eintreten.

Leider ist das Löschen eines Dictionary-Eintrags über den Wert statt dem Key nicht so leicht. Ich muss erst den Key (Queue-Name) aus dem Wert (Worker-ID) ermitteln. Die Linq-Query liest sich etwas umständlich.

Alternativ hätte ich ein zweites Dictionary aufsetzen können, in dem die Worker-ID als Key steht und über den Queue-Namen auf _readLocks zeigt. Aber das würde zusätzlichen Pflegeaufwand bedeuten. So scheint mir der hier eingeschlagene Weg KISS-konform.

Test #6: Blockierte Queue überspringen

Es ist merkwürdig, aber bisher bin ich ohne eine Schleife bei der Entnahme ausgekommen. Wäre ich nicht nach TDD vorgegangen, hätte ich die wahrscheinlich schon gleich am Anfang eingebaut – und die Lösung damit komplexer gemacht.

Mit TDD habe ich dagegen einige Schwierigkeiten schon aus dem Weg geräumt. Anweisungssequenzen sind leichter zu verstehen und zu testen als Schleifen.

Jetzt hilft es aber nichts mehr. Eine Schleife muss sein, wenn blockierte Queues übersprungen werden sollen. Es müssen bei TryDequeue()-Aufruf potenziell ja mehrere Queues geprüft werden.

image_thumb[23]_thumb

TDD besteht aus 3 Phasen: roter Test, grüner Test und Refactoring. Bisher habe ich die letzte Phase übersprungen. Es gab nicht viel zu refaktorisieren. Jetzt wird es mir aber zuviel, was da alles in TryDequeue() passiert. Und auch KeyValuePair ist mir zu wenig aussagekräftig; mehr Domänensprache darf sein.

Zur Befriedigung des neuen Tests füge ich deshalb nicht nur Code hinzu, sondern ziehe auch Code raus in eigene Methoden. Das Listing unterscheidet Änderungen im Rahmen des Refactoring und Änderungen zur Erfüllung der neuen Anforderungen.

internal class NotifyingMultiQueue<T>
{
 
   readonly List<NamedQueue> _queues =
        new List<NamedQueue>();
    readonly Dictionary<string, string> _readLocks =
        new Dictionary<string,string>();

    class NamedQueue
    {
        public string Name;
        public Queue<T> Queue;
    }


    public void Enqueue(T message, string queueName)
    {
        var queue = _queues.Where(nq => nq.Name == queueName)
                           .Select(nq => nq.Queue)
                           .FirstOrDefault();
        if (queue == null)
        {
            queue = new Queue<T>();
            _queues.Add(new NamedQueue{Name=queueName, Queue=queue});
        }
        queue.Enqueue(message);
    }


    public bool TryDequeue(string workerId, out T message)
    {
        message = default(T);

        Free_queue_locked_for_worker(workerId);

        NamedQueue namedQueue = null;
        for (var i = 0; i < _queues.Count(); i++)
        {
            namedQueue = Get_next_queue();
            if (Queue_not_locked(namedQueue)) break;
            namedQueue = null;
        }
        if (namedQueue == null) return false;

        message = namedQueue.Queue.Dequeue();
        Lock_queue_for_worker(workerId, namedQueue);
        return true;
    }
    …


Hier zeigt es sich jetzt, dass TDD letztlich kein Verfahren ist, das zu Unit Tests führt. TryDequeue() ruft jetzt andere Methoden auf, d.h. es integriert. Noch sind diese Methoden einfach und wurden 1:1 aus Code, der eben noch in TryDequeue() stand erzeugt. Bei der Weiterentwicklung des ADT kann es aber jederzeit passieren, dass Änderungen an diesen Methoden nötig werden. Und dann stellt sich die Frage, wie diese Änderungen getestet werden.

Klar, ich kann dafür dann gezielte Tests schreiben. Doch beim Refactoring mit ReSharper sind die “Hilfsmethoden” automatisch als private deklariert worden. Es kostet dann schon einige Überwindung, die auf internal zu setzen und als Units für sich zu testen. Deshalb wird in den meisten Fällen weiter durch das Interface getestet, d.h. mit Integrationstests gearbeitet. Die testen dann natürlich auch immer noch alles andere mit. Vorteil: Black Box Tests sind unabhängig von interner Struktur. Nachteil: Black Box Tests können aufwändig werden, wenn für Kleinigkeiten das Drumherum mit getestet werden muss. Das wird besonders auffallend, sobald Attrappen ins Spiel kommen. Dazu kommt, dass bei Integrationstests eine Fehlerquelle nicht so leicht lokalisiert werden kann.

Fazit

Mit diesem Artikel wollte ich Ihnen zeigen, dass ein expliziter Entwurf von Software mittels Flow-Design nicht bedeutet, alles über Bord zu werfen, was Ihnen lieb und teuer geworden ist: Ich finde, der Abstrakte Datentyp NotifyingMultiQueue<T> ist ein solider Vertreter der Objektorientierung. Und seine Entwicklung ist eine solide Anwendung von TDD.

Darüber hinaus wollte ich Ihnen an einem realen Beispiel zeigen, wie TDD funktionieren kann: mit expliziter Testplanung, in kleinen Schritten. Nicht perfekt, aber good enough.

Schließlich haben Sie gesehen, dass selbst ich als Verfechter von Nachdenken vor dem Codieren nicht dogmatisch bin ;-) Wenn die Codierungsrealität es nahelegt, dann kann ich von meinem Entwurf auch abweichen. Erkenntnisse sind jederzeit willkommen.

Wenn Sie mögen, verfolgen Sie meine TDD-Fortschritte auch im Code. Hier die relevanten Changesets (insb. 43..50) im Mercurial Repository npantarhei.codeplex.com.

image_thumb[24]_thumb

Und nun kommen Sie mit TDD und FD.

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Freitag, 27. Januar 2012

TDD im Flow – Teil 2

Was bisher geschah:

Test #2: Ein Worker entnimmt aus mehreren Queues

Der dritte Test in meiner Planung bleibt sinnig. Er führt zu Änderungen am Produktionscode.

image[27]

Den Testcode zu zeigen, lohnt nicht. Er entspricht der Skizze im Bild. Aber hier der Produktionscode:

internal class NotifyingMultiQueue<T>
{
    List<KeyValuePair<string, Queue<T>>> _queues = 
        new List<KeyValuePair<string,Queue<T>>>();
 

    public void Enqueue(T message, string queueName)
    {
        var queue = _queues.Where(nq => nq.Key == queueName)
                           .Select(nq => nq.Value)
                           .FirstOrDefault();
        if (queue == null)
        {
            queue = new Queue<T>();
            _queues.Add(new KeyValuePair<string, Queue<T>>(queueName,
                                                           queue));
        }
        queue.Enqueue(message);
    }


    public bool TryDequeue(string workerId, out T message)
    {
        var queue = _queues[0];
        _queues.RemoveAt(0);

        message = queue.Value.Dequeue();
        return true;
    }
    …

Die “Änderung mit Zukunft” betrifft Enqueue(). Dort werden nun nach Name unterschieden Nachrichten in verschiedene Warteschlangen eingetragen. Dass ich dafür die Warteschlangen in einer Liste statt einem Dictionary organisiere, ist nicht nur einer gewissen Voraussicht geschuldet – ich habe eine Idee, wie ich das Round Robbin Verfahren einfach implementieren kann –, sondern auch dem Wunsch, TryDequeue() für den Moment möglichst einfach zu halten.

Zwar könnte ich Enqueue() noch simpler mit einem Dictionary arbeiten, doch dann könnte TryDequeue() bei der Entnahme nicht durch die Warteschlangen fortschreiten – und sei es auch nur so simpel wie jetzt. In einem Dictionary gibt es keine verlässliche Reihenfolge der Einträge. Ein Fortschreiten durch verschiedene Warteschlangen ist aber nötig, um hier eine neue Äquivalenzklasse anzugehen. Ansonsten ließe sich das Ergebnis dieses Tests auch mit der bisherigen Implementation erreichen.

Test #3: Queuewechsel mit Round Robbin

Jetzt wird es spannend. Nachrichten in separate Queues zu stellen, ist einfach. Sie aber im Round Robbin Verfahren daraus zu entnehmen, das ist schon kniffliger. Das muss ich nun angehen:

image[32]

Dazu müssen auch in den Queues mehrere Nachrichten stehen, weil es ja der Trick beim Round Robbin ist, nicht erst eine Queue abzuarbeiten, sondern für jede Nachricht eine weiter zu rücken.

Oben habe ich mir Gedanken zu einer Datenstruktur gemacht, mit der das möglich ist. Ein hübscher Plan… den ich nun fallen lasse. Nein, den ich schon mit der vorherigen Implementation habe fallen lassen.

Ich baue mir nicht selbst eine verkette Liste von Warteschlangen, sondern nehme eine normale Liste, der ich Queues in ihrer Reihenfolge vorne entnehme und hinten wieder anfüge. So wandern sie im Kreis durch das Fenster des Listenkopfs.

internal class NotifyingMultiQueue<T>
{
    List<KeyValuePair<string, Queue<T>>> _queues =
        new List<KeyValuePair<string,Queue<T>>>();

    …
    public bool TryDequeue(string workerId, out T message)
    {
        var queue = _queues[0];
        _queues.RemoveAt(0);
        _queues.Add(queue);

        message = queue.Value.Dequeue();
        return true;
    }
    …

Um diesen Test “ergrünen zu lassen” ist nur eine weitere Zeile Code in TryDequeue() nötig. “Pointergehansel” wie im Datenmodell ist nicht nötig. Das hatte ich geahnt beim vorherigen Test und deshalb eine Liste statt eines Dictionary für die benannten Queues gewählt.

Hm… man könnte nun argumentieren, dass hier eine Datenstruktur (Liste) zwei Zwecken dient: der Haltung benannter Queues (insb. für Enqueue()) und dem Durchlaufen der Queues in bestimmter Reihenfolge (TryDequeue()). Diesem Hinweis auf das SRP halte ich aber KISS entgegen: für den Moment stellt diese Verquickung kein Problem dar. TryDequeue() wird dadurch nicht komplizierter. Und Enqueue() eigentlich auch nicht. Im Falle des Wechsels der Entnahmestrategie müsste ich ohnehin beide Methoden anfassen.

Noch ein Testszenario entfällt

Beim nächsten Testszenario muss ich schon wieder feststellen, dass ich übers Ziel hinausgeschossen bin. Es zu erfüllen, bedarf keiner Änderung am Code.

image[37]

Das erkenne ich aber erst jetzt, da ich die Lösung besser verstehe und schon Code geschrieben habe. Macht nichts. Ein Test weniger fällt mir leicht ;-)

Weiter geht es im nächsten Teil…

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Donnerstag, 26. Januar 2012

TDD im Flow – Teil 1

Steht Flow-Design eigentlich im Gegensatz zu Test-Driven Design? Nein. Zwar bin ich überzeugt, dass TDD viel weniger wichtig ist, als viele Vertreter agiler Softwareentwicklung glauben, aber deshalb hat TDD doch seinen Platz. An einem Beispiel möchte ich das demonstrieren.

Vor einiger Zeit habe ich Gedanken zu einer Flow Execution Engine geäußert. Die habe ich inzwischen angefangen zu bauen. Ihr Name ist “PantaRhei” und der Quellcode für eine C#-Implementation liegt bei CodePlex: npantarhei.codeplex.com.

Selbstverständlich ist diese Flow Runtime selbst im Flow-Design entstanden. Ihre Aufgabe ist also, Flows wie den, der sie selbst beschreibt, auszuführen. Hier ein Ausschnitt aus dem Modell:

image

Gezeigt ist die asynchrone Verarbeitung von Nachrichten, die von außen hereinkommen:

  • Sie fließen in einen Flow, der bei der Runtime registriert ist, über die Methode ProcessAsync() hinein.
  • Dann werden sie über die Funktionseinheit Asynchronize auf einen Hintergrund-Thread gehoben.
  • Vor der Ausführung wird dann nochmal geprüft, ob die Verarbeitung parallel zu anderen Nachrichten erfolgen soll (Schedule processing).
  • Zwei Parallelverarbeitungsmodi stehen zu Auswahl: echte Parallelverarbeitung, d.h. jede Nachricht an einen Port wird parallel zu anderen Nachrichten verarbeitet, oder eingeschränkte Parallelverarbeitung, da Nachrichten an den selben Port nur sequenziell verarbeitet werden.

Der Flow im Bild ist nicht durch TDD entstanden. Klar. Den habe ich iterativ wachsen lassen: ein bisschen entwerfen (auf dem Papier), ein bisschen implementieren, dann wieder ein wenig entwerfen usw. Im Repository kann man verfolgen, wie es voran gegangen ist.

Die (Sub-)Flows sind trivial in der Implementation. Die Musik spielt in den Operationen, den Blättern des oben grob erkennbaren Schachtelungsbaums. Blätter sind z.B. Register operation, Asynchronize und Execute task.

Diese Operationen sind so einfach, dass ich sie runtergeschrieben habe. Einige ohne Tests, einige mit Tests – hinterher. Ja, der TDD-Freund wird es den Tests ansehen, dass ich sie hinterher geschrieben habe ;-) Macht aber aus meiner Sicht nichts. Ich brauche sie ja nicht, um zu entwerfen, sondern nur zur Feststellung von Korrektheit kleiner Funktionseinheiten.

Nun aber bin ich an einem Punkt, wo ich meinen Modus umschalte. Das will ich einmal dokumentieren, um zu zeigen, wie TDD und FD Hand in Hand gehen können.

Anforderungen

Ganz rechts im Bild finden Sie die Operation Sequentialize. Bei ihr geht es darum, Nachrichten zur Verarbeitung auf einen von mehreren Threads zu heben. Parallelize tut das ganz einfach: Nachrichten werden dem nächsten frei werdenden Thread zugewiesen. Das führt zu unterschiedsloser Parallelverarbeitung aller Nachrichten. Bei Sequentialize soll es dagegen etwas differenzierter vorgehen. Da sollen manche Nachrichten parallel und manche sequenziell verarbeitet werden.

Unterschieden werden Nachrichten bei Sequenzialize nach den Ports, zu denen sie fließen. Nachrichten an unterschiedliche Ports werden parallel verarbeitet, aber Nachrichten an den selben Port werden sequenziell verarbeitet. Damit wird eine häufige Fehlerquelle des concurrent programming automatisch ausgeschaltet.

Ganz einfach ließe sich die sequenzielle Verarbeitung aller Nachrichten an einen Port natürlich dadurch lösen, jeden Port mit einem eigenen Thread und einer Warteschlange auszustatten. Das würde die Verarbeitung insgesamt jedoch eher verlangsamen, dass es immer viel mehr Ports als Prozessorkerne gibt.

Also soll die sequenzielle Verarbeitung der Nachrichten an einen Port bei gleichzeitiger Parallelität aller Ports mit nur wenigen Threads realisiert werden. Sequentialize muss daher für jede Nachricht prüfen, ob sie an einen Port geht, der gerade noch mit der vorhergehenden beschäftigt ist. Falls ja, muss die Nachricht warten; andernfalls kann sie vom nächsten freien Thread verarbeitet werden.

Wenn eine Nachricht warten muss, kann eine andere, die zu einem anderen Port fließt, an den nächsten Thread zugewiesen werden. Die wartende Nachricht darf darüber natürlich nicht vergessen werden.

Lösungsidee

Mir scheint es für dieses Problem keine Lösung in der TPL zu geben, da ich ja keine fixen Task-Netzwerke aufbaue. Also muss ich selbst Infrastruktur basteln. Das sollte kein Hexenwerk sein – aber es ist schwieriger als das, was ich bisher für die Runtime zu tun hatte.

Der Rahmen ist noch simpel:

image

Zur Sequentialisierung in beschriebener Weise, müssen die Nachrichten in einen Puffer geschrieben werden: Enqueue message. Das geschieht auf dem Thread, der sie annimmt für die Verarbeitung (s. Asynchronize in Flow asynchronously).

Die vielen Threads, auf denen Nachrichten dann parallel abgearbeitet werden können, entnehmen sie aus diesem Puffer, sobald sie frei für neue Arbeit sind: Parallel dequeue.

Diese beiden Funktionseinheiten sind trivial. Die Musik spielt im Message store. Der kann nämlich nicht eine simple Warteschlange wie bei Asynchronize sein. Vielmehr muss der Message store dafür sorgen, dass Nachrichten erstens in der Reihenfolge ihres Eingangs herausgegeben werden, zweitens dabei aber eine Einschränkung für Nachrichten an denselben Port gilt.

Das ist eine Funktionalität, die ich nicht eben mal so runterschreibe. Und dazu fällt mir auch gerade kein sinniger Flow ein. Mein Gefühl ist, hier bei einer Funktionseinheit angelangt zu sein, die aus Sicht von Flow-Design ein Blatt, eine Black Box darstellt.

Dennoch hat der Message store eine Struktur: eine Datenstruktur wie auch eine Verarbeitungsstruktur. Wie komme ich an die heran?

Meine Lösungsidee für die Datenstruktur sieht erst einmal so aus:

  • Jeder Port bekommt eine eigene Warteschlange. Damit wird sichergestellt, dass pro Port die Nachrichten in der ursprünglichen Reihenfolge bearbeitet werden.
  • Allerdings hat jede solche Port-Warteschlange eine Flagge, die anzeigt, ob gerade an einer Nachricht dieses Ports gearbeitet wird. Solange das der Fall ist, kann kein Thread eine Nachricht aus dieser Warteschlange entnehmen. Sie ist dann blockiert.
  • Da es mehrere Warteschlange gibt, die mehrere Threads mit Arbeit versorgen, muss es gerecht zugehen. Kein Port darf bevorzugt werden. Deshalb sollten die Warteschlangen von freien Threads im Round Robbin Verfahren abgefragt werden. Entnimmt ein Thread aus einer Warteschlange eine Nachricht, entnimmt der nächste freie Thread aus einer anderen. Die Warteschlangen bilden deshalb einen Kreis in Form einer einfach verketteten Liste mit einem Zeiger auf die nächste zu befragende Warteschlange.

Im Bild sieht das so aus:

image

TDD-Anhänger mögen angesichts von soviel Entwurf sagen, das sei vielleicht nicht so einfach, wie es sein könnte. Darauf sage ich: Mag sein. Aber warum sollte ich nicht eine Lösungsidee haben auf der Basis der Kenntnis der Anforderungen? Warum sollte ich mich dümmer stellen als ich bin? Ob ich die Datenstruktur am Ende so umsetze, ist ja noch eine zweite Frage. Zunächst trenne ich aber die Phasen Lösungsentwurf und Implementierung. Damit entlaste ich die Implementierung. Ich habe dann nämlich schon eine Vorstellung von der Lösung – an die ich mich allerdings auch nicht sklavisch halten sollte.

Es gibt mehrere Werte, die es auszugleichen gilt. Ein Wert mag die einfachst mögliche Implementation sein. Aber ein anderer ist Geschwindigkeit. Und die ist höher, wenn ich eine (zumindest grobe) Struktur habe, auf die ich hinarbeiten kann, ein Ziel. Dann muss ich nämlich nicht dauernd refaktorisieren, weil mich Erkenntnisse während der TDD-Codierung überraschen.

Solange mein Entwurf verständlich ist und mit Augenmaß auch simpel gehalten, finde ich es völlig ok, vorauszudenken. Nein, ich finde es sogar empfehlenswert. Zu oft habe ich nämlich gesehen, dass mit TDD eine Implementation angegangen wird, ohne eine Vorstellung von der Lösung zu haben – um dann nach anfänglichen Erfolgen steckenzubleiben.

Mit einem Entwurf schaffe ich mir sozusagen selbst eine Karte für ein bis dahin unbekanntes Terrain. Und mit Karte wandert es sich leichter. Das heißt nicht, dass ich auf dem Weg nicht auf Hindernisse treffe. Aber ich kann die dann in einem bigger picture verorten und schauen, wie ich sie umwandere.

Und wie sieht meine Lösungsidee für die Verarbeitungsstruktur aus? Da kenne ich nur die Schnittstelle. Ich will die Datenstruktur als abstrakten Datentyp (ADT) realisieren. D.h. nach außen ist von einem Objektgraphen nichts zu sehen. Funktionseinheiten, die mit der Datenstruktur umgehen, sollen es so einfach wie möglich haben. Ich stelle mir das so vor:

class NotifyingMultiQueue<T> {
    public void Enqueue(T message, string queueId) {…}
    public bool TryDequeue(string workerId, out T message) {…}

    public void Wait(int milliseconds) {…}
}

  • Um eine Nachricht zur Verarbeitung auf irgendeinem Thread einzustellen, wird Enqueue() für eine Warteschlange aufgerufen.
  • Um eine Nachricht von der nächsten Warteschlange zur Verarbeitung abzuholen, wird TryDequeue() aufgerufen. Dabei ist anzugeben, wer die Nachricht dann verarbeitet, damit sichergestellt werden kann, dass keine ungewollte Parallelverarbeitung stattfindet.
    Eine erfolgreiche Entnahme aus einer Warteschlange sperrt diese durch Hinterlegung der Id des Workers, an den eine Nachricht ausgegeben wurde. Wenn der Worker dann das nächste Mal eine Entnahme tätigen will, wird die durch seine vorherige Entnahme gesperrte Warteschlange wieder freigegeben. 
  • Und falls gerade keine Nachricht zur Verarbeitung anliegt (TryDequeue() liefert false), kann ein Thread sich mit Wait() schlafen legen, bis es wieder Arbeit gibt.

Das klingt hoffentlich sinnig. Dass das grundsätzlich funktioniert, habe ich schon bei der asynchronen Nachrichtenverarbeitung überprüft. Mit so einer Schnittstelle läuft das Umheben auf andere Threads ordentlich.

Test-Driven Development

Die Anforderungen sind klar, die Schnittstelle ist einfach – aber wie sieht nun der Code dahinter aus? Den könnte ich jetzt versuchen runterzuschreiben; durch die Datenstruktur habe ich ja einen Überblick über das Nötige gewonnen. Aber dabei würde ich mich nicht wohl fühlen. Etwas mehr Systematik darf sein bei einem so wichtigen Bestandteil der Flow Runtime. Wenn die Datenstruktur nämlich nicht sauber aufgesetzt ist, dann kommt es zu Fehlern in der Arbeit mit mehreren Threads – und die sind sicher nur schwer zu reproduzieren und zu lokalisieren.

Wie aber anfangen mit TDD? Jetzt Visual Studio anwerfen und einen ersten Test für eine der Schnittstellenmethoden schreiben? Nein! Am Anfang von TDD steht die Sammlung von Testfällen. Darauf, dass die Ihnen während der Codierung einfallen, sollten Sie sich nicht verlassen. Und schon gar nicht sollten Sie erwarten, dass sie Ihnen in einer guten Reihenfolge einfallen. Damit würden Sie das TDD-Vorgehen überfrachten.

Testfälle für das Ganze, das Sie realisieren wollen, sind vor dem Beginn der Codierung festzulegen. Es sind schließlich die Akzeptanzkriterien für Ihren Code. Wenn Sie die nicht vorher kennen, lügen Sie sich schnell mit der Codierung – selbst wenn die Test-First geschieht – etwas in die Tasche. Außerdem dient die Sammlung der Testfälle dem tieferen Verständnis der Anforderungen.

Hier die ersten 4 Testfälle von 14, die mir zu dieser speziellen Warteschlange eingefallen sind:

image

Sie sind in aufsteigender “Schwierigkeit” sortiert. Mit jedem Testfall wird das Szenario etwas komplizierter. Es gibt ja mehrere Variablen:

  • Die Zahl der Einträge in einer Warteschlange
  • Die Zahl der Warteschlangen
  • Die Zahl der Worker, die auf die Warteschlangen zugreifen
  • Den Blockierungszustand
  • Benachrichtigungszustand

Allerdings gibt es noch ein zweites Sortierkriterium: den Interessantheitsgrad eines Tests bzw. die Relevanz der durch ihn abgedeckten Funktionalität. Deshalb steht am Anfang ein happy day Szenario und nicht die sonst häufig zu sehende Prüfung, ob eine leere Datenstruktur korrekt behandelt wird. Das ist erst der 9. Test.

Vor dem ersten Test bin ich so frei und setze meine Klasse mit den obigen Methoden auf. Das empfinde ich als keinen schlimmen Bruch mit der strengen TDD-Praxis. Alle Methoden werfen die NotYetImplemented Exception. So mache ich mir das Schreiben der Tests etwas einfacher und habe gleich sozusagen ein kleines Backlog im Code.

Gleichfalls lege ich mir in meiner Testklasse das System under Test (SUT) zurecht. Wie mir die Testplanung zeigt, brauche ich ja immer wieder eine Instanz von NotifyingMultiQueue, die via TryDequeue() eine Nachricht zurückgibt.

[TestFixture]
public class test_NotifyingMultiQueue
{
    private NotifyingMultiQueue<string> _sut;
    private string _result;

    [SetUp]
    public void Before_each_test()
    {
        _sut = new NotifyingMultiQueue<string>();
        _result = null;
    }
    …

Test #1: Ein Worker entnimmt aus einer Queue

Der erste Test ist ganz einfach. So soll es ja auch sein. Leicht beginnen und in kleinen Schritten vorgehen:

[Test]
public void Single_worker_takes_from_single_queue()
{
    _sut.Enqueue("a", "q1");
    Assert.IsTrue(_sut.TryDequeue("w1", out _result));
    Assert.AreEqual("a", _result);
}

Die Zeilen setzen die Testplanung treu um:

image

Ich habe länger überlegt, ob ich den ADT als Black Box testen soll. Am Ende habe ich mich dafür entschieden. Auch wenn ich eine hübsche Datenstruktur entworfen habe, will ich mich nicht daran in Tests binden, wenn es nicht absolut nötig ist. Also teste ich die Funktionalität immer durch den API hindurch. Bei der zu erwartenden Größe des Codes scheint es mir vertretbar, ganz auf Integrationstests zu setzen, auch wenn sich intern die funktionalen Strukturen ausdifferenzieren.

Allerdings muss ich dadurch meist paarweise Änderungen vornehmen: bei Enqueue() und bei TryDequeue() bzw. Wait(). Anders kann ich nicht überprüfen, ob eine interne Zustandsveränderung korrekt erfolgt. Auf beiden Seiten muss ich mich also konzentrieren, das KISS Prinzip nicht aus den Augen zu verlieren.

Für den ersten Test ist das aber noch einfach. Das Szenario kann mit einer Queue abgehandelt werden:

internal class NotifyingMultiQueue<T>
{
    Queue<T> _queue = new Queue<T>();

    public void Enqueue(T message, string queueName)
    {
        _queue.Enqueue(message);
    }

    public bool TryDequeue(string workerId, out T message)
    {
        message = _queue.Dequeue();
        return true;
    }
    …

Die Implementation muss zur Zeit nur einen einzigen Test erfolgreich machen. Wenn also nicht alle Parameter benutzt werden oder von Round Robbin nichts zu erkennen ist, dann macht das nichts. Diese Implementation ist die einfachste mögliche, zur “Testbefriedigung”.

Zumindest ist das so, wenn man den Anspruch hat, dass im Code etwas halbwegs sinniges passiert. Oft ist ja bei TDD Demos zu sehen, dass bei den ersten Tests triviale Implementationen vorgenommen werden, z.B.

internal class NotifyingMultiQueue<T>
{
    public void Enqueue(T message, string queueName)
    {}

    public bool TryDequeue(string workerId, out T message)
    {
        message = “a”;
        return true;
    }
    …

Der Test würde damit grün – ansonsten wäre aber nichts gewonnen. Es wäre kein Schritt in Richtung einer Lösung unternommen worden.

Tests sind ja aber kein Selbstzweck. Das Ziel ist nicht, Tests grün zu bekommen. Tests sind nur ein Mittel, um das Ziel zu ereichen. Das ist mit und ohne Tests grundsätzlich dasselbe: Produktionscode, der die Anforderungen erfüllt.

Es ist gut, wenn kleinschrittige Tests zu einem Wachstum der Implementation auch in kleinen Schritten führt. Triviales Wachstum aber, von dem man weiß, dass es nicht belastbar ist im Sinne der Anforderungen, sollten Sie vermeiden.

Ein Testszenario entfällt

Nicht nur Implementation kann allerdings trivial wachsen. Auch bei Tests besteht die Gefahr. Das weiß man aber nicht immer, wenn man den Test formuliert. Der folgende schien mir beim Entwurf noch sinnig:

image

Nach der einfachen, aber nicht trivialen Implementation zu Test #1 war dieser Test jedoch überflüssig. Es wäre sofort grün gewesen. Ich habe ihn deshalb ausgelassen.

Die Maxime dahinter lautet: Tests, die du keiner Änderung an der Implementation führen (d.h. die nicht zuerst rot sind), sollten nicht geschrieben werden. Sie treiben den Produktionscode nicht voran. Sie gehören zur selben Äquivalenzklasse wie ein anderer Test. Hier wäre es die Äquivalenzklasse “Ein Worker entnimmt aus einer Queue” gewesen.

Würde ich eine Queue implementieren, wäre der Test nötig. Aber ich benutze in meinem ADT eine Queue. Und deren Funktionstüchtigkeit muss ich nicht auch nochmal testen. Darauf verlasse ich mich. In Bezug auf benutzte ADTs sind meine Tests also Integrationstests.

Weiter geht es im nächsten Teil…

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Mittwoch, 11. Januar 2012

Schätzungen auf dem Prüfstand

Kann man verlässlich schätzen, wie lange ein Team braucht, Anforderungen in einsetzbare Software zu übersetzen? Kann man das für einen Horizont von wenigen Wochen? Kann man das für Monate oder Jahre?

Diese Frage führt immer wieder zu hitzigen Diskussionen. Eine der am besten besuchten Sessions auf der letztjährigen DevCon drehte sich auch um diese Frage. Das Interesse an einer Antwort, ja geradezu an einer Erlösung von diesem Thema ist groß.

Ich habe dazu auch meine Meinung und bin stolzes Mitglied der XING-Gruppe "Stop Software Estimation Now!" :-)

image

Aber wenn ein Thema so lange und so fortschrittslos diskutiert wird, frage ich mich auch: Gibt es hinter der Frage nicht ein grundsätzlicheres Problem? Sitzen wir vielleicht einem Missverständnis auf?

Vielleicht rennen wir ja kollektiv gegen eine Wand - und sehen nicht, dass nur ein paar Schritte weiter eine Tür ist.

Solch eine Tür meine ich nun entdeckt zu haben. Ich glaube, die Diskussion zum Thema Schätzen kommt aus einem ganz simplen Grund nicht voran: Wir messen einfach nicht den Erfolg des Schätzens. Keine Partei weiß so richtig, ob Schätzen wirklich erfolgreich ist.

Der Weg aus dem Problem mit dem Schätzen besteht aus meiner Sicht daher aus zwei Schritten:

1. Festlegen, wann eine Schätzung erfolgreich ist; wir brauchen Kriterien.
2. Messen, ob eine Schätzung nach den aufgestellten Kriterien erfolgreich war.

Klingt einfach, oder? Und man fragt sich, ob das nicht immer schon so gehandhabt wurde. Ich glaube, nein. Mit dem Schätzen ist man schnell bei der Hand. Aber weder werden Schätzungsqualitätskriterien bilateral (!) festgelegt, noch wird geprüft, wie die Schätzqualität am Ende war.

Bilateral Schätzungsqualitätskriterien festlegen

Beim Schätzen sind mindestens zwei Parteien beteiligt: der Kunde und das Entwicklungsteam. Der eine hat das Geld und die Zeit, die anderen sollen sagen, was sie im Rahmen dieser Budgets leisten können. Das Team muss also abschätzen, wie viel Geld und Zeit es für einen Scope braucht oder ob es einen Scope innerhalb eines gewissen Budgets realisieren kann.

Wenn nun Geld, Zeit und Scope zu einem Vertrag zwischen diesen Parteien gehören, dann natürlich auch die Schätzung. Deshalb müssen sich beide einig darüber sein, wann eine Schätzung gut war. Sonst ist es schlecht mit der Schätzerfolgskontrolle für beide Seiten. Und ohne Erfolgskontrolle kein Lernen, um es das nächste Mal genauso zu machen, weil es gut war, oder es besser zu machen, weil es nicht gut war.

Hier sehe ich das erste Defizit: Üblicherweise werden Schätzungsqualitätskriterien nicht explizit festgelegt und schon gar nicht bilateral. Es gibt einfach keine Diskussion darüber. Die Schätzung besteht aus zwei Zahlen (“Wir brauchen M Monate und G Euro für den gegebenen Scope.”), die irgendwer irgendwie im Blick behält. Wenn M und G zur Neige gehen, schaut man, wie viel vom Scope noch übrig ist. Dann stellt man fest, dass M und G nicht reichen werden und die Nachverhandlungen beginnen.

So tut man das halt. Darüber wird vorher nicht gesprochen. Das nehmen beide Seite als normal hin – und ärgern sich doch. Oder zumindest eine Seite ärgert sich. Die andere mag es nicht kratzen, weil sie eine Horde von Anwälten für solche Nachverhandlungen beschäftigt.

Dazu kann man nun sagen: “So ist halt die Welt.” Doch das will ich nicht akzeptieren, solange diese Parteien auf der anderen Seite klagen, “alles” sei so teuer. Denn Aufwand für Nachverhandlungen aufgrund schlechter Schätzung ist unnütz, falls schlechte Schätzungen die Norm sein sollten. Es wäre dann auf die Dauer billiger das Schätzen zu verbessern. Dafür braucht man jedoch Qualitätskriterien. Die müssen bilateral und explizit definiert sein, weil sie zum Vertrag gehören. Und die müssen dann auch am Ende überprüft werden.

Explizite Schätzungsqualitätskriterien

Müssen denn die Softwarevertragsparteien aber länglich über Schätzungsqualitätskriterien sprechen? Sind die nicht offensichtlich?

Nein. Das ist ja das Problem. Darüber bestehen unterschiedliche Meinungen, über die nicht gesprochen wird.

Offensichtlich sind natürlich die Schätzwerte selbst, z.B. geschätzte Zeit und geschätztes Geld. Die werden mit dem Soll an Scope verglichen und man erfährt, ob das geschätze Budget ausgereicht hat oder nicht.

Zu diesen offensichtlichen Schätzwerten sollten dann allerdings noch mindestens drei weitere Kriterien treten:

Hinnehmbare Budgetabweichung: Es sollte ausdrücklich darüber gesprochen werden, welche Abweichung von den Schätzwerten noch als Erfolg verbucht werden darf. Sind 5% Abweichung ok oder 10% oder gar 20%? Schätzungen sind eben Schätzungen. Dass ein Team punktgenau landet, ist nicht zu erwarten. Also sollte man sich darüber unterhalten, wie groß der Landeplatz ist.

Hier sind sogar zwei Seiten zu unterscheiden: Am Anfang stehen ein Termin und ein Geldbetrag in Bezug auf einen Scope. Der Kunde ist zufrieden, wenn aus seiner Sicht beides innerhalb einer zu definierenden Abweichung eingehalten wird.

Das Team hat zur Erreichung dieser Fixpunkte aber auch noch einen Aufwand geschätzt, der durch den Geldbetrag gedeckt werden soll. Selbst wenn der Kunde also zufrieden ist, kann es sein, dass das Projekt aus Teamsicht floppt. Falls es den Aufwand erhöhen musste, um den Scope zum Termin zu liefern, ohne mehr Geld zu bekommen, ist die Schätzung auch schlecht gewesen – ohne, dass der Kunde davon etwas merken muss.

Hinnehmbarer Qualitätsverlust: Inzwischen wissen wir ja, dass es eben nicht nur um Geld, Zeit und Scope geht, sondern immer auch um Qualität. Der Kunde stellt funktionale und nicht-funktionale Anforderungen (Scope), die ein Entwicklungsteam mit unterschiedlicher interner Codequalität umsetzen kann. Inwiefern der Scope innerhalb des geschätzt nötigen Budgets umgesetzt wird, beobachtet der Kunde natürlich genau. Fällt der Erfüllungsgrad unter eine bestimmte Marke, übt der Kunde Druck aus. Darunter leidet gewöhnlich die interne Codequalität. Das sollte so nicht sein, ist aber so. Deshalb ist es wichtig, sich darüber Gedanken zu machen, ein wie großer Verlust an dieser Qualität noch als Erfolg beim Schätzen verbucht werden darf. Denn beim Schätzen besteht ja der Anspruch, dass die interne Qualität konstant über die geschätzte Dauer gehalten wird. (Dass man die interne Qualität dann auch noch messen können muss, um eine Abweichung vom Soll feststellen zu können, steht auf einem anderen Blatt.)

Hinnehmbarer Zufriedenheitsverlust: Der Kunde ist zufrieden, wenn er seinen Scope innerhalb des geschätzten Budgets bekommt. Wie ein Team das schafft, ist ihm in der Regel ziemlich egal. Überstunden, Wochenendarbeit, Urlaubssperre, Zuckerbrot, Peitsche… das schert ihn nicht. Schade – aber wohl nicht zu ändern.

Ein Team sollte für sich allerdings in dieser Hinsicht einen Anspruch definieren. Ist eine Schätzung erfolgreich, wenn zwar der Scope zum geschätzten Termin abgeliefert wird – aber die Stimmung auf Null ist? An dieser Stelle geht es mir nicht um eine Abweichung beim Aufwand, ohne dass der Kunde das zu spüren bekommt. Dafür gilt es, eine hinnehmbare Budgetabweichung zu definieren (s.o.).

Nöjd Crispare HistorikIch glaube, genauso wichtig wie die Beobachtung der Ressourcen Zeit und Geld, ist die der Motivation, der Stimmung, der Zufriedenheit, der Kompetenz (s. dazu z.B. Jeff Sutherland, “Happyness Metric – The Wave of the Future”). Alle Teammitglieder sind wertvolle Ressourcen – warum sollte man sie sonst bezahlen? Also sollte man nicht leichtfertig mit ihnen umgehen. Sie können ihren Wert nur voll einbringen, wenn sie “wie geschmiert funktionieren”.

Eigentlich mag ich diesen Ressourcen-Jargon nicht, aber an dieser Stelle scheint er mir nützlich, um den Kontakt zu anderen Schätzungsqualitätskriterien zu halten.

“Wie geschmiert funktionieren” die Teammitglieder nur, wenn sie das Gefühl haben, dass ihre persönlichen Bedürfnisse erfüllt werden. Sie haben einen Anspruch daran, wie ihr Arbeitsumfeld zur Befriedigung ihrer Bedürfnisse beitragen soll. Dazu zählt z.B. “regelmäßiges Gehalt für das Bedürfnis ‘Certainty’” oder “nette Kollegen für das Bedürfnis ‘Connection’” oder “Zeit fürs Lernen für das Bedürfnis ‘Growth’” (Bedürfnisbezeichnungen nach Tony Robbins “Why we do what we do”).

Dass nicht immer alle Bedürfnisse voll erfüllt werden können, weiß jeder. Man ist deshalb damit zufrieden, wenn sie recht verlässlich innerhalb eines gewissen Bereichs erfüllt werden. Geschieht das allerdings nicht… dann geht die Stimmung in den Keller. Unaufmerksamkeit schleicht sich ein, die Fehler nehmen zu, Demotivation zieht ihre Kreise, Dienst nach Vorschrift bekümmert den Kunden, Krankmeldungen nehmen zu, Fluktuation entsteht usw.

Das sind Entwicklungen, die nicht nur persönlich bedauerlich für die Teammitglieder sind, sondern Unternehmen Geld kosten. Diese Kosten sind allerdings meist unsichtbar für das Projekt. Wenn Teammitglieder aus Frust über das Projekt kündigen, das unter hohem Druck steht, weil ein Termin zu halten ist, dann wird die Einarbeitung eines neuen Teammitglieds nicht dem Projekt zugeschlagen – obwohl das Projekt sie verursacht.

Deshalb scheint es mir nützlich, die Zufriedenheit der Teammitglieder als Kriterium heranzuziehen. Dieser Messwert kann innerhalb des Teams erhoben werden, auch wenn niedrige Zufriedenheit außerhalb des Teams zu Kosten führt.

Ein Projekt kann also mit dem gewünschten Scope zum geschätzten Termin mit dem geschätzen Aufwand und mit hinnehmbarer Qualität abgeschlossen werden – und doch war die Schätzung insgesamt nicht erfolgreich, wenn nämlich trotz der Einhaltung dieser Kriterien die Zufriedenheit aus dem definierten Rahmen gefallen sein sollte. Das kann z.B. passieren, wenn die Einhaltung der anderen Kriterien dazu führt, dass Stress entsteht, der messbar unzufrieden macht. Das ist dann ein Raubbau an der “Ressource Entwickler”.

Fazit

Ob Schätzungen funktionieren oder nicht… Ich habe da zwar meine Meinung, doch ich empfehle ihnen heute nur: Messen Sie doch einfach mal. Aber richtig.

Setzen Sie sich im Team oder noch besser mit dem Kunden zusammen und definieren Sie die Erfolgskriterien für Schätzungen. Bleiben Sie allerdings nicht bei Geld und Zeit stehen. Weiten Sie Ihren Blick und definieren Sie auch Ihren Anspruch an die innere Qualität und Ihre persönliche Zufriedenheit.

Dann messen Sie vorher, während und nachher. Und dann vergleichen Sie die Messwerte mit Ihren vorher definierten Ansprüchen.

Wenn die Messungen innerhalb der Toleranzgrenzen sind, dann funktioniert das Schätzen. Glückwunsch.

Aber wenn sie wiederholt außerhalb der Toleranzgrenzen liegen… tja, dann funktioniert das Schätzen eben nicht. Soviel Einsicht sollten Sie dann haben und Ihre Praxis ändern.

 

PS: Dass die Diskussion über das Schätzen so hitzig verlaufen, liegt also daran, dass da unterschiedliche Wertesystem aufeinanderprallen. Die Kriterien, wann Schätzungen erfolgreich sind, differieren. Deshalb lohnt es, einen Schritt zurückzutreten und erst einmal zu schauen, was denn diese Kriterien überhaupt sind.