Sonntag, 18. Juli 2010

Quando, quando, quando? Vom Zeitpunkt für automatisierte Tests

Grad lese ich Thomas Bandts öffentliche Gedanken zur Frage, ob und wann TDD denn sinnvoll sei. Die treibt ja viele um. Immer wieder. Und so glaube ich, dass irgendwas an ihr noch nicht stimmt. Irgendwie ist die Perspektive falsch.

Wenn es in einer Partnerschaft krieselt und sie klagt “Immer bringst du den Müll nicht runter!” worauf er fragt, “Wann soll ich den Müll denn runterbringen? Wenn die Tüte richtig voll ist oder früher?”, dann ist das auch die falsche Frage. Sie nimmt nur ein Symptom in den Blick, nicht das Wurzelproblem. Das könnte nämlich darin bestehen, dass sie sich in ihrem Bemühen um eine schöne Wohnung nicht anerkannt fühlt.

Also: Was könnte falsch an der Perspektive hinter der ewigen Frage nach “Soll ich eigentlich immer (!) mit TDD arbeiten?” falsch sein?

Widerstand gegen die Silberkugel

Ich glaube, die Frage ist Ausdruck einer großen Skepsis. Entwickler sind zurecht skeptisch, dass es eine Praktik wie TDD gibt, die “alles” besser machen könnte. Aber insofern ist die Frage auch Ausdruck eines Missverständnisses. TDD will gar nicht “alles” besser machen. Nur manches.

Die Frage ist damit falsch gestellt, weil sie suggeriert, TDD würde einen Absolutheitsanspruch formulieren. TDD ist “nur” eine Methode, um ein Ziel zu erreichen: korrekten Code mit einer hohen Testabdeckung und gut evolvierbarem Design.

Evolvierbares Design ist die Voraussetzung für langlebige Software.

Hohe Testabdeckung ist eine gute Grundlage dafür, dass Bugs sich nicht verkriechen können in ungetesteten Code. Verkriechen ist aber nur möglich bei Code, der entweder nicht trivial ist oder der nur sehr selten ausgeführt wird. Ist beides nicht gegeben, dann stolpert man bei Integrations- oder Akzeptanztests sehr schnell über den Bug. Und sehr wahrscheinlich ist dann auch sofort klar, wo und warum es geknallt hat.

Beispiele für solchen Code sind Getter/Setter oder Mappings. Die funktionieren entweder -- oder eben nicht.

Wer also Code schreibt, der nicht evolvierbar sein muss oder der weitgehend aus Trivialitäten besteht… der muss TDD nicht einsetzen. Forms-over-Data Szenarien fallen für mich z.B. in diesen Bereich.

Widerstand gegen Veränderung

Entwickler widersetzen sich aber nicht nur Silberkugeln, sondern auch Veränderungsansinnen. Und nichts anderes ist TDD. TDD will den Entwickler verändern. Und zwar sehr tiefgreifend. Alte Gewohnheiten sollen abgestreift werden, ja geradezu Gewohnheitsrecht soll aufgegeben werden.

Bisher war “Softwareentwicklung ist Spaß beim codieren von spannender Funktionalität codieren”, nun soll aber sein “Softwareentwicklung ist nicht nur spannende Funktionalität programmieren, sondern auch langweilige Tests, die das eigene Werk in Frage stellen und langsam machen.” Das ist ein tiefgehender Eingriff. Denn erstens wird dem Entwickler der Spaß an der Programmierung vergellt. Zweitens setzt er ihn noch mehr unter Druck, denn der Chef ist gewohnt, schnell rausgeschossene Funktionalität zu sehen. Mit TDD kann die Feuerfrequenz nur sinken.

Das Recht auf Spaß und Spannung, das Recht auf Entlastung durch Fokus auf Funktionalität soll mit TDD beschnitten werden. TDD bedeutet also einige Veränderung. Und die macht nie Freude. Deshalb setzt sich der Entwickler zur Wehr wie jeder es tut, dem ungewollte Veränderungen ins Haus stehen.

Doch auch hier lauern Missverständnisse. Erstens ist das Schreiben von Tests nicht pure Langeweile. Im Gegenteil! Es kann sogar eine größere intellektuelle Herausforderung darstellen, als Funktionalität zu programmieren. Code testbar machen, ist oft keine Kleinigkeit. Wer also was auf sich hält als Entwickler, der sollte mehr Tests schreiben.

Zweitens verlängern automatisierte Tests die Softwareentwicklung nicht per se. Es kommt vielmehr auf die Perspektive an. Wer nur auf diesen Moment schaut, der sieht natürlich die Zeit dahin rinnen, wenn er nicht nur 3 Zeilen Funktionalität, sondern auch 6 Zeilen Test schreiben muss.

Diese Perspektive ist jedoch kurzsichtig. Sie folgt dem Gedanken eines Rechnungswesens, das immer nur auf die Kosten stiert. Und dahinter steht die Vorstellung, dass ein optimales Gesamtergebnis erzielt wird, wenn jeder Einzelschritt optimal ist.

Das stimmt aber nicht. So funktioniert die Welt nicht. Oder nur in besonderen Situationen tut sie das.

Diese Perspektive verliert schlicht den Gesamtprozess und auch die Zeit im Großen aus dem Blick. Denn nicht nur die Zeit eines Entwicklers ist wichtig. Viel wichtiger ist der Gesamtaufwand an Zeit über längere Zeit.

Wer nur auf hier und jetzt schaut, kann das nicht sehen. Wirtschaftlichkeit bedeutet nicht, jetzt etwas in einer bestimmten kurzen Zeit zu produzieren, sondern langfristig zu überleben.

Tests heute beim Entwickeln dienen nun dieser Wirtschaftlichkeit. Denn sie verringern die Wahrscheinlichkeit, dass Bugs in der Zukunft die Arbeit behindern. Das tun sie nämlich in zweierlei Hinsicht: Erstens halten Sie davon ab, Funktionalität zu produzieren. Zweitens stören Sie den Fokus des Teams. Beides ist kontraproduktiv.

Auf längere Zeit gesehen, sind automatisierte Tests (bzw. TDD) also wirtschaftlicher. Das ergibt sich auch ganz leicht aus einer simplen Rechnung: Nach jeder Veränderungen am Code müssen alle relevanten Tests wiederholt werden, um sicherzustellen, dass nichts kaputtgegangen ist. Das sind umso mehr, je verquarzter Code ist. Damit ist es umso teurer, je mehr manuelle Tests gefahren werden müssen. Die Ausführung von automatischen Tests hingegen kostet nichts.

Umgekehrt bedeutet das allerdings: Wer sich um längere Zeit nicht scheren muss, der kann auf autom. Tests/TDD verzichten.

Allerdings würde ich immer hinterfragen wollen, ob einer, der aus dem Grunde TDD von der Hand weist, wirklich eine Ahnung davon hat, was in der Zukunft mit seiner Software passiert. Aus so mancher Software, die für einmaligen Gebrauch gedacht war, ist ja schon eine unternehmenskritische Anwendung geworden.

Niemand will ja das Brownfield. Das ist, glaube ich, unzweifelhaft. Aber warum ist es dann überall? Warum gibt es solche Massen an unwartbarem Code, dessen Korrektheit niemand beweisen kann. Nach jeder Änderungen hoffen und bangen alle nur, dass nichts vormals Korrektes nun kaputt ist.

Widerstand gegen Angriffspunkt

Nicht nur gegen Veränderungen und Silberkugeln wehren sich Entwickler mit der kritischen Frage, ob TDD denn wirklich sein müsse. Sie wehren sich auch dagegen, angreifbar zu werden. Sie wehren sich dagegen, sich erklären zu müssen.

imageWer bisher keine Tests geschrieben hat und nun damit anfängt und schon deshalb, weil er ein Anfänger ist, länger braucht, der sieht sich Stirnrunzeln ausgesetzt. “Muss das mit diesen neumodischen Tests denn sein?” fragt der Chef?

Darauf dann selbstbewusst mit “Ja, das muss sein” zu reagieren, fällt vielen schwer. Sie können sich auf keinen Berufsethos zurückziehen. Sie haben keine Branche hinter sich, in der es alle so tun. Die Ausbildung steht nicht hinter ihnen. Sie sind auf sich gestellt. Ihre Überzeugung steht gegen die Weisungsmacht des Chefs, der von Softwareentwicklung allzuoft nichts versteht und Experimente so gar nicht mag.

Das sind verständlicherweise unschöne Aussichten. Also ist der Reflex die sehr skeptische Frage, ob denn TDD überhaupt etwas brächte.

Fazit

Natürlich hat die Frage, ob und wann TDD denn sein müsse, eine Sachebene. Die Frage ist sachlich berechtigt. Und die Antwort sollte nicht pauschal sein, sondern differenziert. Entscheidungskriterien sind gefragt.

Lebensdauer und Kompliziertheit und Art von Code gehören für mich als Aspekte in die Antwort. Nicht für jeden Bau sind statische Berechnungen im selben Umfang nötig. Nicht für jedes Unternehmen ist ein Rechnungswesen im selben Umfang nötig. Deshalb: Nicht für jedes Softwareprojekt sind automatisierte Tests im selben Umfang nötig.

Kontraproduktiv im Sinne einer differenzierten Antwort finde ich es jedoch, sich über die wahren Beweggründe der Frage nicht klar zu sein. Jeder, der so fragt, prüfe sich da also am besten erstmal selbst. Das ist schwierig – aber wenn man es nicht tut, läuft man immer wieder gegen Wände aus Missverständnissen. Und nichts ist lähmender als Konflikte, deren Ursachen man nicht kennt bzw. verschleiert.

Ebenfalls kontraproduktiv empfinde ich es, ganz grundsätzlich den Nutzen von automatisierten Tests zu hinterfragen. Das widerspricht aus meiner Sicht der Zivilisationsgeschichte der letzten 1000 Jahre, die ganz klar zeigt, dass Automatisierung wo immer es geht, Vorteile bringt. Menschen ersparen sich langweilige, fehlerträchtige, gefährliche Arbeit, indem sie automatisieren. Warum sollte das nicht auch für Tests gelten?

Nein, da ist für mich die Grenze erreicht. Wer Testautomatisierung pauschal in Frage stellt, der widerspricht grundlegenden Prinzipien. Und dazu sagten schon die Lateiner: contra principia negantem disputari non potest (Mit dem, der die gemeinsamen Prinzipien leugnet, ist nicht diskutieren.) Den Diskussionsaufwand spare ich mir dann.

Also: Wenn die Frage sachlich gestellt ist und nicht als Rationalisierung, d.h. als Abwehrmaßnahme dient, wenn ernstlicher Erkenntnisgewinn gewollt ist und die grundsätzliche Möglichkeit der Beiträge von automatisierten Tests/TDD zu Produktionseffizienz, Korrektheit und Evolvierbarkeit anerkannt sind… dann lässt sich auch eine Antwort für jedes Projekt finden. Und die lautet nicht immer “Ja! TDD musst du machen auf Teufel komm raus.”

Jedes Projekt hat sein Mix an automatisierten Tests wie Akzeptanztests, Integrationstests, Performancetests, Lasttests, Unit Tests. Und ob die nach TDD oder anders geschrieben sind… das ist durchaus auch immer wieder anders.

Nur eines halte ich für unverbrüchlich: Den Willen zur Automatisierung. Denn alles, was automatisiert getan wird, das ist quasi kostenlos wiederholbar, das ist nachvollziehbar, das ist von keinem bestimmten Teammitglied abhängig, das ist schneller als von Hand getan. Wer wollte diese Vorteile der Automatisierung ausschlagen?

Die Rückkehr der Täter – EBC-Prozesse übersetzen

Einen Moment das objektorientierte Denken auszusetzen, darum hatte ich Sie in ???meinem Blogartikel über die nächste Entwicklungsstufe von EBCs gebeten. Ich weiß, das ist eine große Bitte gewesen. Deshalb möchte ich Ihr Wohlwollen auch nicht länger strapazieren. Kommen Sie zurück in die Welt der Objekte bzw. Klassen und der Komponenten.

In diesem Artikel möchte ich Ihnen zeigen, wo Klassen ihren Platz auch in einer Welt der Featureprozesse haben, d.h. der Prozesse, die ein Anwendungsfeature realisieren.

Übersetzung in Klassen

Bisher habe ich vorgeschlagen, Anforderungen in Features zu zerlegen, von denen jedes durch einen Prozess “hergestellt” wird. Hier nochmal das Feature “Funktion plotten” als EBC-Prozess:

image

Initiator des Prozesses ist das GUI, Empfänger für das Resultat ebenfalls. Ich denke, soweit bereitet Ihnen das Verständnis keine Probleme.

Allerdings werden Sie sich fragen, wie das Prozessdiagramm in Code übersetzt werden kann. Abgesehen vom GUI, d.h. einer Formularklasse, ist darin ja noch keine Klasse zu sehen. Beschrieben sind lediglich Taten, aber keine Täter. Das ist natürlich gewollt, denn sonst wäre das Diagramm kein Modell. Modelle sollten nämlich nicht mit dem Vokabular der Implementation beschrieben werden.

Also: Wo sind die Klassen?

Aus meiner Sicht ist der Default, jede Tat, jede Aktivität, jeden Prozesschritt in eine Klasse zu übersetzen. Das sähe dann so aus:

image

Damit machen Sie nichts falsch. Das entspräche den bisherigen Übersetzungsregeln für EBC-Bauteile und –Platinen. Bitte beachten Sie jedoch: Auch wenn Sie Aktionen in Klassen übersetzen, werden daraus nicht gleich Täter. Sie würden den Prozessschritt “In Symbole zerlegen” nicht als Klasse Scanner implementieren. Wenn Klassen Aktivitäten repräsentieren, dann sollten sie auch so benannt sein:

class Funktion_in_Symbole_zerlegen
{
    public void In_Process(string funktion)
    {…}

    public event Action<Symbol[]> Out_Symbole;
}

Da nun jedoch die “Kästchen” in den EBC-Diagrammen nicht mehr durch Substantive beschrieben werden, da sie keine Täter, sondenr Taten sind, liegt es für mich sehr viel näher als bisher, allemal die atomaren Aktionen nicht in Klassen, sondern in Methoden zu übersetzen. Klassen weisen dann mehrere solcher Methoden-Taten zu einem Täter zusammen:

image

Eine Klasse könnte dann so aussehen:

class Funktionsanalyse
{
    public Symbol[] In_Symbole_zerlegen(string funktion)
    {…}

    public AST Auf_syntaktische_Korrektheit_prüfen(Symbol[] symbole)
    {…}
}

Wann sollten Prozessschritte Methoden werden, wann Klassen? Hier sind verschiedene Prinzipien in Anschlag zu bringen. Wenn die Methoden sehr überschaubar sind und nicht in viele weitere zerfallen, um dem SLA-Prinzip zu dienen, dann würde ich dazu raten. (Im vorliegenden Beispiel des Funktionsplotters ist das eher nicht der Fall. Gerade ein Scanner kann schon sehr umfangreich werden.) Auch könnte für Methoden sprechen, dass sie gemeinsamen Zustand haben – wobei anzumerken ist, dass der eine Abhängigkeit darstellen würde, die man aus einem Prozessdiagramm nicht herauslesen kann. Das wäre der Evolvierbarkeit abträglich.

Oder lassen Sie es mich anders sagen: Methoden sind absolut ok, um Ihnen die Arbeit etwas leichter zu machen, wenn Ihnen kein Prinzip entgegensteht. Vor allem könnten das SRP und SoC sein.

Täter durch Abstraktion

Vergleichen Sie nun die Klassennamen der Übersetzung in Klassen und der Übersetzung in Methoden. Sehen Sie den Unterschied? Bei einer Übersetzung in Klassen beschreibt der Klassenname die Aktivität, die Klasse ist eine Tat. Anders ist es bei der Klassen, die Prozessschritte als Methoden implementiert. Ihr Name beschreibt einen Verantwortlichen, sie ist ein Täter.

Dieser Wechsel in der Semantik ist nötig, da ein Menge von Methoden, d.h. Taten nicht ohne weiteres zusammen eine “Großtat” ergeben. Wer sie zusammenfasst, ist also keine Tat, sondern tut viele Taten und ist somit ein Täter. Ein Substantiv als Name ist angezeigt.

Mit der reinen Objektorientierung können Sie quasi nicht anders: Sie suchen sofort nach Tätern. Sie hätten sich gefragt, welche Verantwortlichen an einem Funktionsplot beteiligt sein könnten. Und da wären Ihnen sofort Scanner, Parser, Codegenerator, Plotter usw. Dann hätten Sie die in irgendeiner Form in Abhängigkeiten zueinander gesetzt (Klassendiagramm, Schichtendiagramm o.ä.).

Mit EBC-Prozessdiagrammen jedoch müssen Sie sich nicht so anstrengen. Sie listen einfach die Tätigkeiten, die ein Feature ausmachen. Das ist ein viel kleinerer Schritt, wenn Sie von den Anforderungen ausgehen.

Und erst danach schauen Sie, ob Sie mehrere Prozessschritte demselben Täter zuordnen können. EBC-Prozessdiagramme sind also ein Synthesewerkzeug. Statt top-down vorzugehen und mit Abstraktionen zu beginnen, die Sie nicht kennen, starten Sie bottom-up. Sie beginnen bei relativ leicht identifizierbaren Aktivitäten und abstrahieren dann von ihnen. So kommen Sie zu Klassen, wenn Ihre Prozessschritte Methoden sind. Aber was, wenn sich für eine Übersetzung von Aktivitäten in Klassen entschieden haben?

Komponenten als Großtäter

Klassen enthalten Methoden. Assemblies enthalten Klassen. Für alle Klassen – egal, ob die Taten oder Täter darstellen – müssen Sie sich entscheiden, wie Sie sie zu Assemblies zusammenfassen.

Bei EBC-Schaltplänen habe ich noch die Auffassung vertreten, jede EBC-Komponente (allemal Bauteile) wirklich als Komponente zu implementieren. Für EBC-Prozessdiagramme sehe ich das nun anders.

Wie Sie Taten als Klassen oder Taten als Methoden in Täter-Klassen zu Komponenten zusammenfassen, ist völlig getrennt vom Prozessdiagramm zu entscheiden. Eine automatische Übersetzung von Diagrammelementen in Komponenten (d.h. Assemblies mit separatem Kontrakt) gibt es für mich nicht mehr.

Komponenten sind insofern darauf reduziert, Einheiten für die starke Entkopplung und eine isolierte bzw. parallele Entwicklung zu sein. Fragen Sie sich bei der Entscheidung für die Zusammenfassung z.B.

  • …ob Klassen eine genügend hohe Kohäsion haben, um im selben VS-Projekt realisiert zu werden? Das könnte z.B. der Fall sein, wenn sie in Bezug auf die Problemdomäne eng beieinander liegen oder auf derselben Technologie basieren.
  • …ob die Implementierung einer Klasse risikoreich ist und daher parallel zur Implementation anderer stattfinden sollte, um deren Umsetzung nicht zu behindern?
  • …ob für die Implementation von Klassen besondere Kompetenz nötig ist und sie daher parallel zu anderen stattfinden sollte?

Hohe Kohäsion sollten Sie durch Zusammenfassung in einem VS-Projekt, d.h. in einer Komponente ausdrücken. Hohes Risiko, spezielle Kompetenz oder auch lange Entwicklungsdauer sollten hingegen eher dazu führen, dass Klassen getrennt von anderen in Komponenten realisiert werden.

Dasselbe gilt auch für die Art ihrer Herstellung. So kann es sein, dass zusammengesetzte Aktivitäten (die früheren Platinen) generiert werden. Damit kommen sie ohnehin in anderen Assemblies zu liegen als manuell zu entwickelnde atomare Aktivitäten. Ich fasse deshalb alle zusammengesetzten Aktivitäten (z.B. “Funktion berechnen”) in einem von allen anderen VS-Projekten getrennten zusammen.

Unterm Strich sind Komponenten heute für mich “nur noch” Container, die Sie nach sehr pragmatischen Gesichtspunkten füllen. Sie sollen ihre Inhalte von anderen physisch und inhaltlich isolieren. Nur weil Sie in einem Diagramm aber irgendwelche Kästchen zeichnen, entstehen noch keine Komponenten. Ihre Bildung ist orthogonal zur Formulierung eines Lösungsmodells. Machen Sie Komponenten in EBC-Prozessdiagrammen ruhig kenntlich. Aber sehen Sie sie als das, was sie sind: als Einheiten der Codeorganisation und nicht als Bestandteile des Lösungsraumes.

image

Diese Auffassung unterscheidet sich deutlich von meiner bisherigen. Und sie widerspricht eigentlich auch der Bezeichnung Event-Based Components, denn die primären Elemente von Prozessdiagrammen sind keine Komponenten mehr. Es tut mir leid, wenn ich Sie damit verwirren sollte. Aber es hilft nichts: Es fühlt sich besser an, Komponenten in dieser Weise zu benutzen. Und ich glaube auch, dass es leichter zu erklären ist.

Komponenten sind Strukturierungsmittel für Code zur Entwicklungszeit. Sie sind damit genausowenig funktional lösungsregelavant wie VS Solutions oder Klassendateien. Fassen Sie in ihnen Klassen für Täter und Taten schlicht zu “Großtätern” zusammen, wie es für die Evolvierbarkeit und Produktionseffizienz günstig ist.

Komponenten entstehen mithin wie Täter-Klassen durch Abstraktion. Versuchen Sie nicht, sie durch Analyse der Anforderungen zu entdecken. Lassen Sie sie entstehen durch Zusammenlegung von Klassen, die nach einem Gesichtspunkt wie Risiko oder Entwicklungsdauer oder Technologie zusammen gehören.

Komponenten elastisch

Das bedeutet aus meiner Sicht sogar im Umkehrschluss, dass Komponenten in ihrem Inhalt variabel sein können. Was heute in bestimmter Hinsicht günstig zu einer Komponente zusammengefasst werden sollte, kann morgen anders zusammengefasst werden. Hier ist die Softwareentwicklung im Vorteil gegenüber der Elektrotechnik oder dem Maschinenbau. Deren Komponenten sind fix. Wir können Komponenten jedoch “atmen” lassen. Wir können sie elastisch gestalten.

Einzig wichtig für eine Software sind die Aktivitäten. Weder ist wichtig, ob sie als Klassen oder Methoden realisiert werden. Noch ist wichtig, welche Methoden-Prozessschritte in welchen Klassen zusammengefasst sind, noch ist es interessant, in welchen Komponenten Klassen gemeinsam liegen.

Nun mag es schwierig sein, Aktivitäten von Klasse auf Methode umzustellen oder Methoden-Prozessschritte aus Klassen herauszutrennen. Täter- bzw. Tat-Klassen (mit ihren “Anhängseln”) jedoch aus der einen in eine andere Komponente zu verschieben, sie heute so und morgen anders zu gruppieren, scheint mir relativ leicht. Das sollten wir als Chance begreifen und ausnutzen.

Denken Sie also in elastischen Komponenten. Fühlen Sie sich recht frei, sie mit Aktivitäten zu füllen, um ihren Produktionsprozess zu optimieren. Komponenten mit ihren Komponentenwerkbänken schaffen beste Voraussetzungen für zügige Parallelentwicklung am Code. Im CCD Praktikum sind wir durch sie in fünf Tagen über nicht einen Mergekonflikt gestolpert.

Namensräume

Klassen und Komponenten sind Mittel zur physischen Strukturierung. Die Beweggründe zu Trennung oder Zusammenfassung von Code sind vielfältig. Sie können mit der Arbeitsorganisation zu tun haben, mit technologischen Abhängigkeiten oder einem Implementationsdetail geschuldet sein.

EBC-Prozesshierarchien stellen demgegenüber eine funktionale Strukturierung dar. Aktivitäten sind geschachtelt oder nachgeschaltet, um eine Anforderung zu erfüllen.

Physische Nähe und funktionale Nähe drücken aber noch nicht unbedingt inhaltliche Nähe aus. Von der physischen Nähe ist das zwar eher anzunehmen, aber es ist auch nicht zangsläufig. Vor allem kann sich die physische Struktur ändern, ohne dass deshalb die inhaltliche, logische Struktur auch ändern sollte. Wie drücken Sie im Code also inhaltliche, logische, konzeptionelle Nähe aus? Wie fassen Sie etwas logisch zusammen, auch wenn es physisch getrennt z.B. in verschiedenen Komponenten liegt?

Das Mittel für die logische Zusammenfassung von Klassen sind Namensräume:

image

Ohne Ansehen des Definitionsortes (physische Struktur) und des Einsatzortes (funktionale Struktur) verbinden Namensräume Klassen. Sie schaffen damit eine weitere “Großtäterdimension”.

image

Zwischenfazit

Für mich fühlen sich EBC jetzt stimmiger an. Klare Regeln für das, wie ein EBC-Diagramm aufgebaut ist, leiten meine Gedanken bei der Ableitung des Entwurfs aus den Anforderungen:

  1. Finde Quelle und Senken für den Prozess. Diese Verantwortlichen sind vergleichsweise einfach zu bestimmen.
  2. Zerlege den Prozess in Aktivitäten; zerlege die Aktivitäten falls nötig in weitere Aktivitäten. Diese Ableitung von Schritten aus einer Anforderung ist vergleichsweise naheliegend; man beginnt mit dem Konkreten, statt mit Abstraktem/Unbekanntem.

Und klare Übersetzungsoptionen helfen mir beim Codieren des entworfenen Modells. Die Möglichkeiten moderner Sprachen werden ausgenutzt.

  • Übersetze Aktivtitäten in Klassen (oder auch Methoden); finde die Täter hinter den Taten; abstrahiere vom Bekannten, suche Muster
  • Finde Namensräume, um Klassen logisch/inhaltlich in die Nähe zu rücken
  • Organisiere Klassen in Komponenten, um ihre Implementation effizient betreiben zu können – aber auch, um “Übergriffe” zu vermeiden. Komponenten dienen der Stabilisierung der funktionalen Strukturen; es sind Bollwerke gegen die Tendenz zu steigender Code-Entropie

Unterschiedliche Plattformfeatures dienen unterschiedlichen Zwecken:

  • Klassen, Methoden: funktionale Strukturierung
  • Namensräume: logische Strukturierung
  • Assemblies: Strukturierung der Code-Produktion

Da schaut konzeptionell sauber aus, oder? Alles hat seinen Zweck.

Fragts sich abschließend nur, was mit den “normalen” Klassen/Objekten ist? Gibt´s die noch? Ja, die gibt´s noch. Sozusagen in den Zwischenräumen ;-) Davon mehr beim nächsten Mal.

Freitag, 16. Juli 2010

Prozesse klammern

Prozesse, Tätigkeitsfolgen, Herstellungsschritte stehen für mich derzeit am Anfang des Softwareentwurfs, wie ich hier beschrieben habe. Aus Anforderungen lassen sich nur schwer Klassen als Verantwortliche einer Lösung ableiten. Viel einfacher ist es, die Tätigkeiten zu ermitteln, die in Summe den Wunsch eines Anwenders erfüllen.

Wenn Sie ins Restaurant gehen, dann wollen Sie ein leckeres Essen. Dafür ist es wichtig, dass ein Prozess reibungslos abläuft:

  1. Sie wollen einen Tisch zugewiesen bekommen
  2. Sie möchten die Karte gereicht bekommen
  3. Sie möchten eine Bestellung aufgeben
  4. Sie möchten, dass das Gericht schmackhaft und zügig zubereitet wird
  5. Sie möchten, dass das Gericht serviert wird
  6. Sie möchten am Ende reibungslos bezahlen

Aus der Anforderung “Lecker essen” lassen sich ganz simpel “Herstellungsschritte” ableiten. Es ist klar, was (!) getan werden muss, um Ihren Wunsch zu erfüllen. Wer das tut, ist hingegen nicht herauszulesen. Ihnen als Gast ist es letztlich egal, ob die Tischzuweisung durch Sie selbst geschieht oder Restaurantpersonal. Ihnen ist auch egal, ob ein Kellner Ihnen die Karte bringt oder die schon auf dem Tisch liegt. Und wer das Gericht zubereitet, ist auch einerlei, solange es Ihren Qualitätsanforderungen entspricht. Am Ende müssen Sie auch nicht bei einem Kellner bezahlen, sondern wären womöglich bereit, automatisch bei Verlassen des Restaurants zu bezahlen.

Wenn Sie ein Programm für die Anforderung hätten entwerfen sollen, dann wären Sie jedoch nach OOP-Manier sehr schnell darauf gekommen, einen Kellern, einen Koch, einen Restaurantchef zu modellieren. Denen hätten Sie Aufgaben/Dienstleistungen zugeordnet.

Objektorientierung legt es mir Klassen/Objekten einfach nahe zu überlegen, wer (!) an einer Lösung beteiligt sein kann. Ein Prozessdenken, wie ich es hier beschreibe und derzeit übe, stellt hingegen das Was (!) an den Anfang. Was tun? Nicht: Wer tut?

image

Spüren Sie den Vorteil der Prozessdenke schon bei diesem Beispiel? Wenn Sie in Objekten denken, also in Verantwortlichen, dann beschränken Sie den Lösungsraum. Wenn Sie Kellern, Koch, Restaurantchef an den Anfang stellen, kommen Sie nicht auf ein Konzept wie es z.B. hinter der Restaurantkette Vapiano steht. Dort gibt es nämlich keinen Kellner. Sie interagieren direkt mit dem Koch. Und sie bezahlen beim Rausgehen das, was auf einer “Kreditkarte” registriert wurde. Dass dort noch jemand steht und kassiert, hat keinen Nutzen in Bezug auf die Bezahlung, sondern ist nur noch eine Freundlichkeitsgeste.

In einer reinen Prozessbeschreibung zur Erfüllung der Anforderung “Lecker essen” ist das Vapiano-Konzept erhalten. Es ist eine funktionierende Ausprägung. Prozessschritte sind invariant. Sie müssen sich immer setzen, bestellen, Essen muss zubereitet werden, für Bestelltes muss man bezahlen. Sitzen, bestellen, zubereiten, bezahlen. Ohne diese Aktivitäten geht es nicht.

image
Wer dafür jedoch verantwortlich ist… das ist eine ganz andere Frage. Es geht ohne Kellner, es geht ohne Koch, es geht ohne Restaurantchef, es geht ohne Kassierer. Die Hotelkette MotelOne macht es auch vor: Sie durchlaufen beim MotelOne einen Prozesse wie in jedem anderen Hotel – aber die Verantwortlichen sind anders verteilt. Alle “Herstellungsschritte” für das Produkt “Angenehme Übernachtung” werden erbracht, nur anders, von anderen als üblich.

Quellen und Senken für Prozesse

Am Anfang des Softwareentwurfs sehe ich derzeit also ganz klar die Aktivitäten. Was muss für jedes Feature geleistet werden? Das beginnt beim allerkleinsten Feature und hat nach oben keine Grenze. Ob die Farbe einer Zahl verändert werden soll, jenachdem ob ihr Wert positiv oder negativ ist, oder eine Funktion zu plotten ist… immer steht dahinter ein Prozesse bestehend aus Aktivitäten. Eine Tat nach der anderen muss vollbracht werden. Von wem? Erstmal egal.

image

Im Entwicklungsprozess steht also zunächst die Identifikation eines Features und die Bestimmung von Input/Trigger sowie Output/Zustandsänderung.

image

Diese Features leiten Sie aus den Anforderungen ab. Das ist natürlich eine Kunst für sich. Sie sollen so groß sein, dass sie einen Anwendernutzen darstellen. Andererseits sollen sie so klein sein, dass sie sich schnell umsetzen lassen. Nur so können Sie in einen kontinuierlichen Feedbackprozess mit dem Anwender treten.

Diese Feature sind kleine System-Umwelt-Diagramme. Sie benennen einen Prozess, der durch die Umwelt gestartet wird und auf die Umwelt wirkt. Deshalb sind noch Quellen und Senken zu bestimmen. Wer initiiert einen Prozess? Wer nimmt die Produkte des Prozesses entgegen? Quellen und Senken sind Verantwortliche. Bei ihnen geht es um das Wer.

image

Typischer Initiator, typische Quelle für den Prozess eines Feature ist natürlich das UI. Ebenso ist es typische Senke, typischer Empfänger. Aber auch ein Webservice-Stub kann Prozessinitiator sein oder ein Timer. Und Ressourcen wie eine Datenbank können Senke sein.

Im Augenblick sehe ich also die Verantwortlichen Quelle/Initiator und Senke/Empfänger als Klammern um featureproduzierenden Prozesse. Ganz einfach, ganz systematisch.

Dieses Denken hat uns jedenfalls im CCD Praktikum sehr geholfen. Hier ein Tafelbild von unserer Planungsarbeit:

image

Die Prozesse für zwei Features des Kinokassenprogramms, an dem wir zwei Tage gearbeitet haben, sind grün hervorgehoben. Die Verantwortlichen für ihre Initiierung bzw. Ergebnisverarbeitung sind rot markiert. Für das obere Feature ist sogar das untere eine Senke; der obere Prozess liefert Daten an den unteren.

Initiatoren und Senken als Klassen umzusetzen, liegt natürlich nah. Aus meiner Sicht sind sie EBC-Komponenten, wie ich sie bisher gedacht hatte. Denn bisher ging es in EBC-Schaltplänen eigentlich immer um “Wer tut etwas?”

Diese Frage lässt sich natürlich mir einigem Aufwand beantworten – doch aus heutiger Sicht ist die Diskussion um Request/Response-Drähte ein Symptom dafür, dass das Denken in Verantwortlichen, die per Events kommunizieren, falsch ist.

Wenn ein Verantwortlicher eine Query absetzt, dann will er zurecht eine Antwort. Wenn aus einer Aktivität jedoch eine Query hinausfließt, dann will die keine Antwort. Die Antwort fließt vielmehr in eine nachgeschaltete weitere Aktivität. Mit Prozessdenken verliert also das Request/Response-Denken an Dringlichkeit. Das fühlt sich für mich gut an.

Zwischenstand

Das Vorgehen bei der Modellierung sieht bisher für mich also wie folgt aus:

  1. Aus den Anforderungen Features ableiten; Features haben immer Wert für den Anwender
  2. Für die Features Input/Trigger und Output/Zustandsänderungen bestimmen
  3. Bestimmen, wer triggert und wer am Ende Ergebnisse entgegennimmt
  4. Ausgehend vom Trigger den Prozess formulieren, der seinen Input in Output bzw. Zustandsänderungen transformiert. Der Prozess besteht aus Aktivitäten, d.h. er dreht sich um das, was zu tun ist, nicht darum, wer es tut.

Donnerstag, 15. Juli 2010

Code als Fabrik – Event-Based Components weitergedacht

Ein Gedanke, der mich gerade nicht loslassen will ist, dass Code eigentlich eine Fabrik ist. Seit EVA wissen wir es eigentlich: “Eingaben verarbeiten zu Ausgaben” ist die Aufgabe von Code. Im Laufe der Objektorientierung scheint mir das jedoch in Vergessenheit geraten zu sein.

Statt um einen Fluss von Datenquelle zu Datensenke geht es der OOP um Zustand. Kochtopf statt Durchlauferhitzer. Und das hörte sich irgendwie auch plausibel an, da die Welt doch aus Objekte zu bestehen scheint. Vom Atom bis zum Galaxienhaufen alles Objekte mit einem Zustand. Oder?

image Eine Zeit lang und für einige Anwendungsszenarien hat dieses Denken funktioniert. Ziel dieser Programmierung ist es, gesunde Klassen mit roten Bäckchen zu entwerfen. Klassen mit wenig Funktionalität auf ihrem Zustand gelten als kränklich, als anämisch.

Doch mir kommen immer mehr Zweifel, ob das der beste Modellierungsansatz für Problemlösungen mit Software ist.

Nicht, dass ich OOP verwerfen wollte. Nein, nein. Ich mag OOP-Sprachen wir C# sehr. Keine Frage. Alles wunderbar. Ihre Möglichkeiten möchte ich nicht missen. Selbst Vererbung kann nützlich sein. Zurück zu prozeduralen Sprachen möchte ich nicht.

Doch ich möchte von diesen Möglichkeiten nicht auch noch eine bestimmte Modellierung aufgezwungen bekommen. In gewisser Weise ist das natürlich unvermeidbar. Ob man mit Stahl, Stein oder Holz eine Brücke baut, macht auch einen Unterschied für die grundsätzliche Bauweise. “Form follows Material”, würde ich mal sagen. Bei Software möchte ich mich jedoch weniger eingeschränkt fühlen.

Und so mache ich mich mal frei von der Objektorientierung und der von-Neumann-Maschine. Das ist nicht leicht, auch mir Sitzt die prozedurale und objektorientierte Prägung im Stammhirn. Aber wenn ich mich recke, meine ich ein Bild von jenseits des Tellerrandes erhaschen zu können.

Nach meinen bisherigen Experimenten mit Event-Based Components (EBC) habe ich mich deshalb in der letzten Woche nochmal auf die Zehenspitzen gestellt. Im CCD Praktikum habe ich EBC bewusst in neuer Weise eingesetzt. Ich wollte ausprobieren, wie es sich anfühlt, ganz bewusst “unobjektorientiert” zu entwerfen. Selbst die bisherige Sichtweise auf EBC-Komponenten als “Dinger, die etwas tun” haben wir daher aufgegeben. Stattdessen haben wir Software als Fabriken angesehen. Fabriken, deren Produkte Daten sind. Fabriken, die Feature-Wünsche des Kunden erfüllen.

Verantwortlichkeiten als Phantasieprodukte

Lassen Sie sich doch einmal darauf ein. Vergessen Sie die Objektorientierung für einen Moment. Wenn ich Ihnen nun sage, dass wir einen Taschenrechner programmieren wollen, woran denken Sie bei einem Lösungsansatz als erstes?

Sie denken an eine Funktionseinheit, die etwas tut. Nennen Sie sie Rechenwerk. Der Anwender gibt zwei Zahlen ein, wählt eine Rechenoperation und das Rechenwerk stellt aus einem Tripel wie (2, 3, +) das Ergebnis 5 her.

image

Jetzt erweitern wir die Aufgaben des Taschenrechners. Er soll Ergebnisse zwischenspeichern können. Welche Funktionseinheit ist dafür verantwortlich? Vielleicht ein Zwischenspeicher? Dann brauchen wir zwei Funktionen darauf: Wir wollen Ergebnisse speichern und auch wieder auslesen können.

 image

Vielleicht wollen wir sie sogar beim Speichern hinzuaddieren (akkumulieren)?

image

Fühlt sich das für Sie ok an? Wir haben zwar noch nicht von Klassen gesprochen, aber Sie denken natürlich reflexartig, dass Rechenwerk und Zwischenspeicher als Klassen implementiert werden können.

Dass dies schon kleine EBC-Schaltpläne waren, ist Ihnen nicht so sehr ins Auge gestochen. Aber woran liegt das? Ich glaube, das liegt daran, dass die Kästchen mit Substantiven bezeichnet sind. Sie heißen Rechenwerk und Zwischenspeicher. Es sind Verantwortliche, es sind Arbeiter, nun, es sind Objekte.

Jetzt festhalten:

Mein Gefühl ist, dass wir damit schon zu kompliziert denken.

Wenn die Aufgabe lautet: “Entwerfe ein Programm, dass addiert und Zwischenergebnisse zwischenspeichern und akkumulieren kann”, dann ist es ein recht großer mentaler Aufwand, aus der Anforderungen Verantwortliche, Arbeiter, Objekte abzuleiten. In der Anforderung steckt kein Rechenwerk drin. Auch kein Zwischenspeicher ist darin beschrieben. Wir phantasieren sie nur reflexartig hinein. Weil wir so OOP-trainiert sind. Denn einer muss die Aufgaben ja am Ende übernehmen, oder? Einer muss addieren, einer muss zwischenspeichern. Na, dann nennen wir den doch Addierer oder allgemeiner Rechenwerk und Zwischenspeicher. Liegt das nicht auf der Hand?

In diesem Fall ist das sehr naheliegend, weil die Anforderungen trivial sind. Wenn es aber komplizierter wird… was dann? Sind unsere Reflexe dann auch verlässlich im Sinne evolvierbarer Strukturen?

Ich glaube, unsere Reflexe sind umso kontraproduktiver, je unbekannter die Problemdomäne ist. Verantwortliche lassen sich schlecht durch Analyse finden. Stattdessen sollten Objekte/Klassen als Abstraktionen durch Synthese entstehen. Klassen und Komponenten sind Orte der Verantwortlichkeit, die etwas zusammenfassen. Was das jedoch ist, das (!) muss erstmal ermittelt werden. Damit sollte der Entwurf beginnen.

Solange wir also durch die Objektorientierung getrieben sind, Verantwortliche zu finden, weil wir nur mit Klassen Aufhänger für Methoden haben, solange produzieren wir leichtfertig Phantasiegebilde. Wir wollen dann den zweiten Schritt vor dem ersten tun.

Am Anfang steht der Prozess

Bisher haben noch keine Klassen gesehen, sondern nur erahnt. Jetzt nehme ich Ihnen aber auch noch die Verantwortlichen weg. Denken Sie nicht mehr in Substantiven. Fragen Sie nicht mehr “Wer soll das tun?”

Machen Sie sich leer von jeglicher OOP und sehen Sie nur die Anforderungen. Anforderungen können sich nur um die Problemdomäne drehen. In ihnen kann per definitionem nichts über die Lösungsdomäne, d.h. den Programmcode stehen. Darüber kann und soll sich ein Kunde keine Gedanken machen.

Ich halte es daher für einen ungeheuren Aufwand, aus Anforderungen irgendeinen im Code Verantwortlichen ableiten zu wollen. Anforderungen können nur über Daten und Prozesse sprechen. Es geht nicht anders. Selbst wenn in den Anforderungen ein Verantwortlicher (in der realen Welt) vorkommt, kann das ja nicht automatisch bedeuten, dass der auch im Code 1:1 so repräsentiert werden sollte. Das (!) ist vielmehr die große, kontraproduktive Suggestion der Objektorientierung.

Versuchen wir es daher einmal anders. Nehmen wir die Anforderungen genau so, wie sie formuliert sind und übersetzen sie in ein Modell. Es ergibt sich:

image

Es setzt die Anforderungen sehr viel einfacher, direkter um. Wir haben nichts hinzugedichtet. Kein Rechenwerk mussten wir ersinnen, keinen Zwischenspeicher. Wir haben lediglich die Prozesse in den Anforderungen aufgedeckt und modelliert.

Ja, ich nenne das Addieren und zwischenspeichern mal Prozess. Denn es geht immer um eine Tätigkeit. Es wird etwas verarbeitet. Mal sind es Daten, mal nur ein Signal wie beim Auslesen. Mal ist das Ergebnis ein Output, mal eine Zustandsänderung. Wessen Zustand geändert wird? Keine Ahnung. Das ist erstmal nicht wichtig. Der Zustandsbehaftete steht in den Anforerungen nicht drin. Und selbst wenn… ob wir gut daran täten, die Vorstellung des Kunden zu übernehmen, steht auf einem ganz anderen Blatt.

Ich behaupte nicht, dass wir Hinweise auf Verantwortliche im Code ausschlagen oder bewusst ignoieren sollten. Ich möchte nur zur Vorsicht mahnen, sie gutgläubig zu übernehmen. Wenn wir bei unserer Modellierung selbst auf sie kommen, dann ist es gut. Ansonsten auch.

Und diese Modellierung beginnt eben nicht mit der Suche nach Verantwortlichen, sondern mit einer Modellierung von Produktions- und Transformationsprozessen. Immer wenn der Anwender einen Knopf drückt, einen Menüpunkt aufruft, ein Zeichen in ein Feld eingibt, kurz: Immer wenn der Anwender einen Event auslöst, kann potenziell ein kurzer oder langer, ein kleiner oder großer Prozess ablaufen.

Mit Prozess meine ich hier schlicht eine Reihe von Verarbeitungsschritten. Erst wird das eine getan, dann das andere, dann noch etwas usw.

Aus dieser Perspektive besteht Software zunächst einmal aus einer großen Anzahl von Prozessen. Die werden getriggert durch Events im Frontend – sei das eine Benutzerinteraktion oder eine Nachricht von einer anderen Software auf einem Kommunikationskanal.

Software ist also eine Fabrik mit vielen Prozessen, die jeder für ein anderes Produkt stehen. Ein Tastendruck stößt die Produktion einer Summe an, ein Mausklick stößt die Transformation einer Zahl in einen Speicherzustand an usw. Nennen wir das einmal Feature-Prozesse, weil sie Feature der Anforderungen realisieren:

image

Auf die Struktur der Prozesse möchte ich hier noch nicht näher eingehen. Aber soviel sei gesagt: Natürlich lassen sie sich schachteln. Eine Tätigkeit wie Akkumulieren ist nur auf einer gewissen Abstraktionsebene eine einzige Tätigkeit. Wir können in sie hineinzoomen. Sie könnte z.B. so aussehen:

image

Ha! Was erkennen wir? Akkumulieren ist eine Tätigkeit, die sich durch andere Tätigkeiten, die wir schon modelliert haben, ausdrücken lässt. Wiederverwendung winkt! Und das, obwohl wir immer noch nicht an Objekte oder Verantwortliche gedacht haben. Wir haben nur Tätigkeiten, Verben, Prozessschritte im Blick. Wir kümmern uns nur um Verarbeitung und Transformation, d.h. um das Was, nicht um das Wo oder Wer.

Dafür ist ein Umdenken nötig. Wir müssen aufhören, reflexhaft über rotbäckige Verantwortliche zu phantasieren. Doch mir scheint, das lohnt sich. Wenn wir an den Anfang unserer Modellierung eine ganz schlichte featureorientierte Modellierung von Prozessen setzen, die selbstverständlich Durchstiche im Sinne eines Schichtenmodells darstellen, dann müssen wir weniger Energie aufwenden.

Damit meine ich nicht, dass wir in Flowcharts programmieren sollen. Die obigen Diagramme sehen auch nicht so aus, würde ich sagen. Auch Windows Workflows (WF) sehe ich noch nicht als Realisierungstechnologie. BPMN-Diagramme oder UML Aktivitätsdiagramme liegen da schon näher. Aber so ganz sicher bin ich mir noch nicht, wohin es führt, Software ganz konsequent als Fabrik bestehend aus vielen Prozesse anzusehen.

Aber vielleicht fällt uns zusammen ja etwas ein. Lassen Sie diese Gedanken erstmal auf sich wirken: Tätigkeiten statt Täter, Prozessschritte statt Verantwortliche. Der Unterschied mag ihnen akademisch erscheinen – ich glaube aber, dass er fundamental ist.

Beim nächsten Mal holen ich Sie dann aus der “objektfreien” Welt zurück. Denn es hilft ja nichts, die Verben der Prozesse müssen ja mit Mitteln von C# umgesetzt werden.

Dienstag, 13. Juli 2010

Testnähe aus der Distanz betrachtet

Die Diskussion um den besten Platz für Tests reißt nicht ab. Nach meiner Gegenüberstellung zweier Testplatzierungsstile hat Stefan nachgelegt und nun Ilker gekontert. Die WM ist vorbei, der Testball aber noch im Spiel… :-)

Was für ein Spiel ist das aber? Ist es ein Nullsummenspiel? Und worum dreht es sich? Die erste Frage ist, was “nah am zu testenden Code” bedeutet. die zweite Frage, ob Nähe das wichtigste oder gar einzige Kriterium für die Platzierung von Testcode ist.

Nah und fern

Wie nah ist nah? Ist “nah am Code” nur dies? Test und System-under-Test (SUT) liegen im selben Projekt.

image

Oder wäre es sogar so noch besser? Der Test ist dem SUT visuell untergeordnet, um den Blick auf die Hauptfunktionalität nicht zu verstellen:

image

Oder ist Nähe noch gegeben, wenn Tests in eigenen Unterverzeichnissen stehen?

image

Oder darf ich mit Recht behaupten, dass Tests platziert in einem eigenen Projekt noch nah sein können?

image

Hm… verwirrend diese Optionen. Oder sind sie gar nicht so verschieden, denn wie sieht es während der Arbeit an SUT und Tests in Visual Studio aus? Ist aus diesen Bildern die Platzierung der Tests abzulesen?

image 

image

Ich sehe da keinen Unterschied, obwohl die Tests sehr unterschiedlich platziert sind. Für die Arbeit am Code, also den ständigen Wechsel zwischen Test und SUT und Test und SUT, scheint es mir unbedeutend zu sein, ob die Tests als “near spec” im Projekt des SUT liegen oder in einem anderen Projekt. Solange sie in derselben Solution zu finden sind, können Tests und SUT ganz einfach so dargestellt werden.

Wer es noch verwegener mag und einen großen Bildschirm hat, der legt sich Tests und SUT sogar nebeneinander. Solche Darstellung überwindet jede Distanz. Programmieren wird da zur Kuschelstunde ;-)

image

Angesichts dessen würde ich sagen, dass es während der Arbeit an Tests und SUT einerlei ist, ob die Tests im SUT-Projekt liegen oder in verschiedenen. Solange sie sich in derselben Projektmappe befinden, ist der Wechsel zwischen beiden einfach möglich. Im Editor kann man sie immer in die Nähe rücken. Dasselbe gilt für Testdaten.

Kohäsion

Für die Arbeitsgeschwindigkeit an Tests und SUT sollte es keinen Unterschied machen, in welchem Projekt Tests platziert sind. Wie sieht es aber mit der Verständlichkeit aus? Ist Code besser zu verstehen, wenn Tests inkl. Testdaten und SUT in einem Projekt liegen? Ist nicht die Kohäsion zwischen Tests und SUT höher als die zwischen zwei unterschiedlichen SUTs? Müssten daher nicht Tests sogar eher in einem Projekt zusammen mit ihrem SUT liegen als zwei verschiedene SUTs?

Hier ein Blick in die Sourcen von RavenDB, der dokumentenorientierten NoSql-Datenbank von Ayende Rahien:

 image

Die Tests für den Client liegen in einem eigenen Projekt. Ob sie sich nur auf das markierte SUT-Projekt beziehen oder auch die anderen Client-Projekte, ist nicht ersichtlich. Das widerspricht der hohen Kohäsion von Test und SUT, würde ich sagen. Die Zusammenhänge zwischen beiden zu erkennen, ist schwierig.

Wer den Code grundsätzlich so strukturiert, d.h. viele Projekte in einer Projektmappe führt, der beklagt zurecht, dass die natürliche Kohäsion durch getrennte Projekte für Tests und SUT schwer auszudrücken ist.

Wie gesagt, dem Wechsel zwischen SUT und Tests tut das keinen Abbruch. Doch die Übersichtlichkeit leidet zunächst.

Was aber, wenn Software besser strukturiert ist? Wenn ihr ein Plan zugrunde liegt, der auch noch umgesetzt wird? Was wenn Plan und Code gar übereinstimmen? Hier ein Beispiel aus dem CCD Praktikum:

image

Das ist der Plan für ein Feature eines Softwaresystems für Kinos, das wir im Praktikum begonnen haben. Der Plan zeigt die Prozessschritte für ein Feature. Er ist als EBC-Schaltplan ausgelegt. Die Kästen werden in Klassen übersetzt. Die Pfeile in Event-Eventhandler-Verbindungen. Darüber hinaus gibt es aber auch noch Zusammenfassungen zu Komponenten. Die habe ich farblich hervorgehoben. Sie werden in Projekte übersetzt.

Im Repository sieht das dann so aus:

image

Die Komponenten finden sich dort als Projektmappen in eigenen Verzeichnissen wieder. (Wer mag, kann die natürlich auch wieder in Unterverzeichnissen gruppieren.)

Und innerhalb der Komponenten sind jeweils mindestens zwei Projekte zu finden, eines für das SUT sowie eines für dessen Tests:

image

Die hohe Kohäsion zwischen Tests und SUT findet dadurch klaren Ausdruck, würde ich sagen. Wer Tests zu einem SUT sucht, weiß genau, wo er sie findet.

Voraussetzung dafür ist natürlich der explizite Wille zur Architektur. Wo es den nicht gibt oder wo der nicht walten kann, weil der Brownfield-Matsch so tief ist… da mag es tatsächlich angezeigt sein, Tests und SUT im selben Projekt zu pflegen. Doch man sei sich bewusst, was dafür der Grund ist. Tests und SUT im selben Projekt ist dann ein Symptom der Grundproblems “Brownfield”. Tests und SUT im selben Projekt sind aus meiner Sicht daher kein Ziel, sondern nur eine Übergangslösung.

Trennung

Worin sich alle Diskutanten auch einig sind, das ist die Notwendigkeit der Trennung von Tests und SUT bei der Auslieferung. Tests und ihre Artefakte sollen nicht zum Kunden.

Wenn Tests und SUT in verschiedenen Projekten liegen, ist diese Trennung trivial. Testsprojekte haben ein anderes Output-Verzeichnis als SUT-Projekte, so dass Visual Studio beim Bau einer Solution automatisch die Trennung vornimmt. Im SUT-Output-Verzeichnis kommt immer nur das an, was auslieferungsrelevant ist.

Anders wenn Tests und SUT zusammen in einem Projekt stehen. Dann muss der Buildprozess explizit angewiesen werden, Tests und Testartefakte auszufiltern. Das mag eine Zeile Scriptcode sein oder viele. Egal. Wer den Buildprozess aufsetzt, muss daran denken. Und Entwickler müssen womöglich auch daran denken und während der Entwicklung Konventionen einhalten, die das ermöglichen.

So hoch die Kohäsion zwischen Tests und SUT prinzipiell sein mag – bei der Auslieferung hört sich dann doch auf.

Wenn es aufgrund von monolithischem Code nicht anders geht, die Kohäsion während der Entwicklung auszudrücken als durch Zusammenlegung von Tests und SUT in einem Projekt, dann sei es so und man filtere am Ende beim Build die Tests heraus. Aber, bitte, bitte, man verkaufe mir das nicht als Tugend. Zusätzlicher Aufwand ist zusätzlich fehleranfällig. Wenn ich ihn nicht treiben muss, dann möchte ich das auch nicht.

Eine saubere, angemessene Planung mit klarer Übersetzungsregel schenkt mir also nicht nur Evolvierbarkeit, sondern auch übersichtliche Codeverhältnisse und weniger Aufwand für den Buildprozess. Dann bin ich für saubere, angemesse Planung, würd ich sagen. (Die nicht zu verwechseln ist mit BDUF.)

Fazit

Tests im selben Projekt wie ihr SUT? Ist einzig das zeitgemäß? Ich würde sagen, wir sollten nicht nach Geschmack und Mode und Zeitgeist entscheiden. Professionelle Arbeit braucht Werte und Prinzipien. Um welche geht es hier?

Produktionseffizienz: Tests im selben Projekt sollen die Arbeit beschleunigen. Der Wechsel zwischen beiden soll leichter fallen. Wie oben gezeigt, glaube ich daran jedoch nicht. Visual Studio macht es leicht, für die Arbeit an Tests und SUT Nähe im Editor herzustellen. Und mit einem Tool wie dotCover wird es noch einfacher.

image

Als Beispiel die Open Source Software Lounge Repository. Sie ist komponentenorientiert entwickelt, d.h. für jede Komponente gibt es auch eine Werkbank bestehend aus Tests und SUT in je verschiedenen Projekten.

image

Allerdings bin ich bei der Codeorganisation einen Kompromiss eingegangen und habe alle Projekte in einer Projektmappe versammelt. Damit wollte ich es dem unbedarften Open Source Freund ein wenig einfacher machen, in die Quellen einzusteigen.

Ich finde das übersichtlich; viel übersichtlicher als die RavenDB Sourcen trotz Multi-Projekt Solution. Die Tests liegen nahe dem SUT, sie sind damit “near specs”. Aber gerade geht es ja um Produktionseffizienz. Die ist nicht nur hoch, weil ich Tests und SUT side-by-side im Editor liegen können (s.o.), sondern auch, weil dotCover mir erlaubt, aus dem SUT zum Test zu springen:

image

Die Liste auf der rechten Seite zeigt die Tests, die eine bestimmte SUT-Zeile abdecken. Damit muss ich gar nicht mehr wissen, wo in einer Projektmappe Tests überhaupt stehen. Ich lasse schlicht alle Tests mit dotCover ausführen, bekomme einen Eindruck vom Nutzen der Tests aufgrund ihrer Codeabdeckung – und kann dann aus den SUT-Zeilen, an denen ich gerade arbeite, zu relevanten Tests springen. Im selben Projekt oder in anderen. Wenn das nicht effizient ist.

Ebenfalls effizient ist, dass bei Trennung von Tests und SUT kein weiterer Aufwand für die Trennung beider Anteile während der Produktion zu treiben ist. Nicht anfallender Aufwand ist ganz eindeutig der kleinste mögliche Aufwand.

Übersichtlichkeit: Der nächste für die Kolokation von Tests und SUT reklamierte Wert ist die Übersichtlichkeit. Es sei übersichtlicher, wenn beide in einem Projekt lägen. Wie gesagt, das kann ich glauben, wenn es denn nicht anders geht. Wenn die Verteilung auf zwei Projekte ihrer natürlichen Kohäsion zuwiderläuft, weil sie dann sehr weit auseinander stünden in einer Projektmappe, dann mag die Kolokation einen Vorteil haben.

Der Umkehrschluss ist jedoch aus meiner Sicht falsch: Weil Zusammenlegung im Matschfeld einen gewissen Vorteil haben kann, weil damit ein grundlegendes Problem pragmatisch kurzfristig kaschiert wird, deshalb sollten Tests und SUT möglichst immer zusammengelegt werden. Nein, so würde der Bock zum Gärtner.

Darüber hinaus sollte Übersichtlichkeit aber noch weiter gefasst werden. Die zu einem SUT gehörenden Tests leicht finden können, ist eine Sache. Wie stehts aber mit der Identifikation von SUTs? Wie steht es mit Verantwortlichkeiten der Software im Allgemeinen auf unterschiedlichen Abstraktionsebenen?

Übersichtlichkeit entsteht nicht nur durch Zusammenlegung, sondern auch durch Trennung. Mir scheint, dass dieses Prinzip schnell übergangen wird, wenn man im Kolokationsflow ist. Denn wo Tests und SUT zusammengeschnürt werden sollen, dann halte ich die Tendenz für hoch, auch anderes zusammen zu schnüren. Und am Ende entsteht bzw. wird erhalten der Monolith, das Softwaresystem mit hoher Entropie, das Brownfield.

Statt Tests als Anlass zu begreifen, zumindest mal einen Concern aus dem großen Brownfield herausziehen zu können, trägt die Kolokation vielmehr zum Erhalt des status-quo bei. Schade.

Mein persönliches Fazit der Diskussion: Zeitgeist und Geschmack bringen uns nicht weiter. Meinungen sollten auf Prinzipien verweisen. Sie sollten zeigen, wie und warum und wann diese oder jene durch eine Praktik besser oder schlechter umgesetzt werden.

Bei trivialem Code mag es daher ok sein, Tests und SUT zusammenzufassen. Eine Code Kata könnte dazu gehören – wenn man nicht gerade üben möchte, eine andere Codeorganisation einzuhalten.

Jenseits des trivialen Codes bezweifle ich jedoch, dass Tests und SUT im selben Projekt der Produktionseffizienz und Übersichtlichkeit wirklich dienen. Sie sind mir eher Symptom für ein Wurzelproblem und Bequemlichkeitsmaßnahme. Mehr als eine Übergangslösung aus der Not geboren sehe ich in ihnen nicht. (Mann, oh, mann, wie störrisch und uneinsichtig ist das denn eigentlich ;-) Aber es hilft nix: “Hier stehe ich, ich kann nicht anders.” Für die Zukunft gelobe ich jedoch, den Kontext besser zu berücksichtigen. Bei einer Code Kata könnte ich mal von dieser “Prinzipienreiterei” abesehen ;-)

Sonntag, 11. Juli 2010

Mit vielen Gehirnen entwirft man besser [OOP 2010]

image Was macht einen erfahrenen Entwickler aus? Dazu habe ich grad hier etwas gelesen und bin beim “Mid-Level Developer” (beim “Mittelmäßigen” möcht ich da mal nich rauslesen ;-) auf diesen Satz gestoßen:

“Can work from user stories”

Was bedeutet das? Ich habe herausgelesen: “Der mid-level Developer kann das, was er am Quellcode zu tun hat, selbstständig aus User Stories ableiten.”

Stimmen Sie mit mir in dieser Interpretation überein? Wenn ja, dann hier zur Begründung, warum ich darüber den Kopf geschüttelt habe.

Welcher Ansatz von Softwareentwicklung steht dahinter, wenn Entwickler ihre Arbeit am Quellcode selbst aus User Stories ableiten? Ich stelle mir das so vor:

Es gibt eine Besprechung, in der User Stories vorgestellt werden. Das Team klärt sein Verständnis der Anforderungen, die darin stecken. Dann wird vielleicht der Aufwand geschätzt. Dann wird geklärt, welche User Stories in Angriff genommen werden sollen. Und dann melden sich Entwickler, die einzelne User Stories umsetzen wollen. Sie arbeiten im Sinne eines Durchstichs, also einer allein (oder vielleicht zwei im Pair) realisieren die User Story.

Ich behaupte nicht, dass der Autor des Blogpostings so arbeitet. Ich behaupte auch nicht, dass er meint, ein mid-level Developer solle so arbeiten. Die obige Formulierung jedoch suggeriert mir so ein Vorgehen. Und das ist so weit weg von dem, wie ich meine, dass Software entwickelt werden sollte, dass ich einfach mal einhake.

image In der letzten Woche habe ich mit Stefan Lieser das Clean Code Developer Praktikum durchgeführt. Fünf Tage lang haben wir darin mit fünf Entwicklern Projekte angepackt. Die kleinsten waren Code Katas und in 2-3 Stunden zu realisieren; das größte dauerte jedoch 2 Tage und ist noch nicht abgeschlossen.

Und wenn mir in den Tagen eines klar geworden ist, dann dies:

Kein Entwickler sollte allein von User Stories ausgehend arbeiten.

Wir haben jedes Projekt mit der Erarbeitung eines gemeinsamen Verständnisses der Anforderungen begonnen. Anschließend haben wir gemeinsam den Lösungsansatz erarbeitet. Viele Gehirne haben also die Anfordeurngen hinterfragt, viele Gehirne haben sich kreativ in den Entwurf eingebracht. Das war für die Projekte unheimlich hilfreich.

Nicht nur wurde dadurch eine gemeinsame Sprache als Basis für den vertrauensvollen Umgang mit der Problemdomäne im Team herausgearbeitet. Nein, alle haben auch dazu beigetragen, dass der Entwurf unter keinen blinden Flecken leidet. Und alle haben einen Überblick über die Gesamtanwendung bekommen.

image Gerade die konzentrierte gemeinsame Entwurfsphase war für mich immer tief befriedigend. Sie hat mir das Vertrauen gegeben, dass wir alle an einem Strang ziehen. Jeder hat sein Bestes gegeben, um einen angemessenen und realistischen Lösungsansatz zu entwickeln.

Das war Team Buildung und Software Design in einem. Ein effizienter und effektiver Prozess – der darüber hinaus auch noch Spaß gemacht hat. Keine Prinzipienreiterrei, sondern kreativer Fluss der Ideen im Rahmen des Clean Code Developer Wertesystems.

Aus meiner Sicht brauchen User Stories daher immer viele Entwickler und nicht einen. Ob der eine allein mit User Stories umgehen kann, ist mir recht egal. Wichtiger ist, ob ein Entwickler sich im Team in den Umgang mit User Stories einbringen kann, ob er einen Beitrag zum Verständnis und zur Planung der Umsetzung leisten kann.

Das hat denn für mich auch weniger mit Domänenwissen oder technischer Erfahrung zu tun, als vielmehr mit Softskills. Wie neugierig ist ein Entwickler? Wie “schnell im Kopf” ist sie? Ist sein Denken flexibel? Ist sie offen für Neues? Kann er querdenken? Wie geht sie mit anderen Teammitgliedern im Verständnis- und Entwurfsprozess um? Welche Erfahrung hat er mit dem Prinzipien- und Praktikenrahmen von Clean Code Developer?

Was das Erfahrungsniveau von Entwicklern definiert, weiß ich nicht so genau. Wie lange ist einer Junior Developer, ab wann ist einer Senior Developer? Mit einem bin ich mir aber sicher: Dass Softwareentwicklung jenseits des Trivialen ein Prozess ist, der entscheidend durch Phasen der Gemeinsamkeit geprägt ist.

Das reine Codieren darf gern im stillen Kämmerlein allein oder zu zweit geschehen. Es ist eine eher umsetzende, ausführende Tätigkeit. Klar, dabei ist auch Kreativität gefragt, aber die kann keinen großen Schaden mehr anrichten ;-) Denn sie findet in einem Rahmen und auf einem Fundament statt.

Und dieses Fundament, dieser Rahmen, die sind das Ergebnis gemeinschaftlichen Denkens. Da sind viele Gehirne gefragt.