Follow my new blog

Dienstag, 26. März 2013

Entwerfen fürs Schreiben

Gerade merke ich es am eigenen Leibe, wie wichtig Entwurf ist. Nein, Software entwickle ich gerade nicht. Ich schreibe Texte. Aber ich erlaube mir, meine Erfahrungen dort auf die Softwareentwicklung zu übertragen.

Dass ich viel schreibe, ist ja nichts Neues. Warum fällt mir das Thema Entwurf dazu gerade jetzt ein? Weil ich anders schreibe als sonst.

Normalerweise sind meine Texte bis 20-25 A4 Seiten lang, inklusive Code, Abbildungen, Tabellen. Und das Thema kenne ich gut. Solche Texte kann ich leicht runterschreiben. Thema überlegen – es reicht eine grobe Überschrift oder die Idee für eine Anwendung – und dann losschreiben. Der Rest ergibt sich von selbst. Der Text gliedern sich beim Schreiben von allein. Manchmal unterbreche ich das Schreiben, um zuerst etwas zu codieren, danach geht es dann umso flotter weiter. Alles kein Problem.

Aber jetzt sitze ich an zwei Texten, die sind anders. Da kann ich nicht einfach so drauflosschreiben. Meine Schreiberfahrung und Themenkenntnis gliedert den Text nicht just-in-time. Deshalb muss ich einen Gang zurückschalten. Nachdenken ist angesagt. Entwerfen.

Das Drehbuch

Der eine Text ist ein Drehbuch. Das schreibe ich mit einer Freundin zusammen. Trotzdem ist es nicht einfach. Form und Genre sind für mich neu. Zum Inhalt nur soviel: Es ist eine Liebeskomödie. (Ja, ja, ich höre schon die Lacher. Aber mir macht es Spaß, mal etwas ganz anderes auszuprobieren. Hat sich einfach so ergeben. Ob das mal verfilmt wird? Vielleicht. Erstmal interessiert mich der Prozess.)

Bei dem Dreh konnten wir nicht einfach drauf los schreiben. Wir hatten zwar eine Idee – doch für 90 oder 100 Minuten Film ist das nicht genug. Da muss man sich an den Text ranrobben. Das ist Entwurfsarbeit at its best. Dazu gibt es eine Menge Literatur. Ein Klassiker ist Syd Fields “Das Drehbuch”. Den hatte ich schon vor vielen Jahren mal gelesen… und jetzt ziehe ich endlich Nutzen daraus. Die Zeit musste einfach reif werden. (Es hilft natürlich auch, dass ich nun eine Co-Autorin mit Erfahrung in dem Metier habe.)

Und was tun wir? Wir strukturieren. Kein Satz “Drehbuchcode” ist bisher geschrieben. Stattdessen hangeln wir uns top-down heran.

  • Ebene 1: Die Idee. Wir überlegen ganz grob, worum es geht. Was ist der Kern der Story? Welche Entwicklung sollen die Protagonisten durchlaufen? Haben wir eine “Moral von der Geschicht”? Das ganze passt auf 1 A4 Seite.
  • Ebene 2: Wir verfeinern die Geschichte. Ein paar Highlights kommen auf den Zettel. Wir überlegen, was Wendepunkte sein könnten. Woraus könnten sich Konflikte speisen? Erste Charakterstudien für die wichtigsten Protagonisten. Das passt auf vielleicht 10 A4 Seiten.
  • Ebene 3.1: Nächste Verfeinerungsstufe. Jetzt überlegen wir uns einen Handlungsverlauf, der für bestimmte Entwicklungen/Phasen schon Minutenzahlen zugeordnet bekommt. Dafür gibt es sogar eine Entwurfsskizze. Das ist schon alles ziemlich detailliert. Insgesamt kommen am Ende 30-40 Seiten Fließtext heraus, das sog. Treatment. Die Geschichte wird grob in Szenen eingeteilt erzählt.

image

  • Ebene 3.2: Gleichzeitig entwickeln sich die Charaktere. Wir erfahren immer mehr über ihre Motive und ihr Vorleben. Manchmal geben wir ihnen bewusst gewisse Züge, um in der Geschichte etwas zu bewirken; aber manchmal ergeben die sich von allein. Als hätten die Figuren ein Eigenleben.
  • Ebene 4: Das Drehbuch wird geschrieben. So richtig mit Dialogen und Einstellungsbeschreibungen. Der Umfang wird ca. 90-100 Seiten haben. Aber da sind wir noch nicht.

Dass es so am besten funktioniert, habe ich immer gewusst. Aber ich habe nie selbst Zugang zu diesem Vorgehen gefunden. Insofern kann ich jetzt besser mit Ihnen fühlen, falls Sie es schwierig finden, sich auf Softwareentwurf einzulassen.

Mit ein wenig Anleitung durch meine Co-Autorin hab ich jetzt aber den Sprung in dieses Vorgehen geschafft – und es macht Spaß. Wir haben auch eine schöne Rollenverteilung gefunden. Ich bin eher der Mann fürs Grobe :-) Sie feilt dann die Details aus. Auch beim Drehbuch konzentriere ich mich also eher auf den Entwurf, das Big Picture, den Rahmen. Das liegt mir irgendwie.

Natürlich geht es am Ende wie bei Software nicht ohne Details. Die dreckige Dialogrealität will ausgefleischt werden. Aber das geht viel, viel einfacher auf der Basis eines mehrstufigen Entwurfs.

Der ist auch auf jeder Ebene ein Durchstich. Die ganze Geschichte wird immer wieder erzählt – nur eben mit unterschiedlichem Detailierungsgrad.

Das läuft natürlich nicht sauber in Wasserfallmanier von oben nach unten ab. Es ist eher Jo-Jo-Schreiben. Mal ganz oben, dann wieder kurz durchfallen auf die unterste Ebene. Wir wissen schon ein paar Einstellungen und Sätze für Dialoge, auch wenn wir eigentlich noch beim Treatment sind. Und manchmal gehen wir auch wieder auf eine höhere Ebene und drehen den Fluss der Geschichte in eine andere Richtung.

Das Fachbuch

Das Drehbuch ist Hobby. Arbeit ist das Buch über Flow-Design, an das ich mich nun endlich doch mal gemacht habe. Die Nachfrage ist gestiegen – und ich empfinde das Thema als inzwischen so stabil, dass sich das Aufschreiben lohnt.

Hier bin ich nun voll in meinem Metier. Trotzdem kann ich nicht einfach losschreiben. Dafür ist der Scope zu groß. Ein Blogposting wie dieses oder einen Artikel von 10 Heftseiten für die dotnetpro sind überschaubar. Da kann ich die Text-IDE Word aufklappen und losschreiben. Aber ein ganzes Buch… Nein, das geht nicht. Das würde zwar am Ende auch irgendwie alle Details enthalten – nur in einer nicht sehr leserfreundlichen Weise. Einen so großen Text beim Schreiben entstehen zu lassen, würde zuviel Refaktorisierungsaufwand bedeuten. Das ist wie beim Coden.

Also denke ich vorher über den Text nach. Auch auf mehreren Abstraktionsebenen.

  • Ebene 1: Grobe Gliederung. Ich unterteile das Thema in Bände. Jeden einzelnen will ich als eBook veröffentlichen, sobald er fertig ist. Es kommt also nicht alles erst am Ende als ein Buch heraus.

image

  • Ebene 2: Gliederung eines Bandes. Zu jedem Band überlege ich mir eine Kapiteleinteilung für den groben Erklärungsfluss. Das habe ich aber bisher nur für Band 1 getan. Ich möchte mich konzentrieren auf ein Inkrement. Denn nichts anderes sind die Bände. Jeder für sich bietet schon einen Nutzen.
    Ob es am Ende wirklich soviele Bände oder genau die werden? Keine Ahnung. Das ist auch nicht wichtig. Bisher ist ja kein Aufwand in Band 2 bis 5 geflossen.

image

  • Ebene 3: Ich schreibe mir zu jedem Kapitel Stichpunkte auf. Eine Sammlung von Material, das ich in den Kapitel behandeln will. Das habe ich für Band 1 auch für alle Kapitel gemacht. Hier nur ein Auszug für ein Kapitel:

image

  • Ebene 4: Ich schreibe jedes Kapitel in einem eigenen Word-Dokument. Das mache ich vor allem, um mich besser fokussieren zu können. Aber ich will damit auch technischen Problemen vorbeugen. Wer weiß, wie Word reagiert, wenn am Ende hunderte Bilder in einem Dokument sind und dessen Größe mehrere hundert MB umfasst?
    Das ist jetzt “Codieren”: Ich schreibe den Text, den Sie später lesen sollen. Aber beim Schreiben gliedere ich weiter. Das ist sozusagen situativer Entwurf. Vorher war strategischer und taktischer Entwurf. Hier die dabei entstandene Gliederung für ein Kapitel:

image

Auch dieser Prozess ist natürlich jo-jo-iterativ. Ich kann vom Schreiben jederzeit wieder in den Entwurf. Und ich kann jederzeit zwischen den Entwurfsebenen wechseln.

Der Entwurf gibt mir einen Überblick und definiert Ösen, in die ich dann Fließtext einhängen kann. Ich kann jederzeit an jeder Stelle arbeiten, weil ich weiß, wie der Zusammenhang ist. Aber ich ziehe es vor, Inkremente zu produzieren. Zuviel Springen ist auch nicht gut. Außerdem habe ich zu bestimmten Themenblöcken, die weiter hinten im Buch liegen, noch keine rechte Idee, wie sie konkreter aussehen sollten. Das mache ich auch abhängig von dem, was ich weiter vorne an Text produziere oder was mir bis dahin noch für Gedanken kommen mögen. Ich will keine Entwurfshalde auftürmen.

Auch beim Fachbuch erfahre ich also, wie wichtig es ist, vor dem Codieren, äh, Schreiben mal nachzudenken, einen Rahmen aufzuziehen, ein Big Picture zu entwickeln. Ein Plan ist gut – und ich erwarte nicht, dass der 100% eingehalten wird. Trotzdem ist er besser als keiner.

Beim Entwurf geht es nicht darum, alles komplett vorwegzunehmen. Dann wäre es kein Entwurf mehr, sondern schon Schreiben bzw. Codieren. Ein Entwurf ordnet vielmehr grob. Sein Ergebnis sind handhabbare Blöcke. Ein Kapitel ist so groß wie ein Artikel. Das kann ich ad hoc formulieren und strukturieren. Ein ganzes Buch jedoch, das ist zu groß. Dito bei Software. Eine ganze Software ist zu groß, um einfach mit dem Codieren anzufangen. Aber eine Klasse oder eine Methode, die durch einen guten Entwurf entstehen, sind überschaubar.

Es braucht daher für Software wie für Texte eine Entwurfsmethode. Das ist mir jetzt durch diese Schreiberfahrungen nochmal klar geworden.

Donnerstag, 21. März 2013

Softwareentwurf als ökonomische Notwendigkeit

Softwareentwurf ist selbstverständlich kein neues Thema. Früher war Softwareentwurf als der Codierung vorgelagerte Tätigkeit sogar ein zentraler Punkt jeder Informatikerausbildung. Das war dem Mangel an Prozessorkapazität und Speicher geschuldet. Die Turnaround-Zeiten bei der Codierung waren so lang, dass man sich besser gut überlegte, was man schreibt und dann der Übersetzung übergibt und dann laufen lässt. Das Motto lautete geradezu Think before coding. Fehler waren teuer. Ausdrücklicher Softwareentwurf war also ein ökonomisches Gebot.

Ab der zweiten Hälfte der 1980er begannen sich die Verhältnisse jedoch umzukehren, glaube ich. Der Preis für Fehler beim Codieren fiel plötzlich stark. Zuerst wurde die Hardware besser und verfügbarer (Stichwort PC), dann wurden die Codierungswerkzeuge besser (Stichwort RAD).

Der Engpass „Werkzeug“ wurde bald so weit, dass er durch einen anderen abgelöst wurde. Code war nicht länger das Problem, sondern Codierer. Es kam einfach nicht soviel Funktionalität auf die Straße, wie Kunden sich das wünschten. Das hatte mehrere Folgen:

  • Codierer konzentrierten sich mehr und mehr aufs Codieren. Der Softwareentwurf fiel aus der Gnade, da er scheinbar wertvolle Codierkapazität raubte. Man fühlte sich von der Knechtschaft durch die Maschine befreit und schüttelte daher gern damit bisher verbundene Notwendigkeiten ab.
  • Die Ausbildung der Codierer wurde „entformalisiert“, so dass sich schneller mehr von ihnen auf den Markt bringen ließen. Softwareentwurf war Ballast im Curriculum.
  • Man sann auf Mittel, sich das Codieren überhaupt zu sparen. „Wiederverwendbarkeit herstellen!“ wurde zum Schlachtruf der Softwareentwicklung.

Während viele Jahrzehnte lang (die 1950er bis 1980er) die Softwareentwicklung sich im Grunde um sich selbst drehen musste aufgrund einer eklatanten Mangelsituation bei der Hardware, verschob sich ab den 1990ern endlich der Fokus auf den Kunden. Und der ist seitdem unersättlich. Der Hunger nach Software ist ungebrochen, nein, sogar im stetigen Wachsen begriffen. Mehr Software und länger lebende Software sind gefragt. Berge an funktionalen und nicht-funktionalen Anforderungen soll die Softwareentwicklung erfüllen. Und zwar ASAP!

Software überhaupt auf die Straße zu bekommen, ist seitdem wichtiger, als alles andere. Nachfrage nicht zu bedienen kommt teurer als fehlerhafte Software. Nur in wenigen Bereichen ist die ökonomische Priorität bisher anders geblieben: da wo es um Menschenleben geht.

Doch dieses Bild verschiebt sich. Der Engpass hat sich wieder verschoben. Codierer sind nicht mehr das Problem, auch wenn weiterhin Softwareentwickler fehlen. Die grundsätzliche Kapazität von Codierern ist heute durch Hardware, Softwareinfrastruktur, Tools, Bibliotheken sowie Entwicklungs- und laufzeitplattformen so hoch... dass sie sich selbst durch ihre Produktionsweise behindern.

Der Engpass ist von Ressourcen – zuerst Hardware, dann Softwarewerkzeuge und Menschen – zu einem Grundsatz gewandert. Die Theory of Constraints nennt das einen Policy Constraint. Der lautet salopp formuliert: „Wir hauen den Code raus so schnell die Finger tippen!“

Dieser Grundsatz limitiert heute den Durchsatz der Softwareentwicklung. Die kommt nicht ins Stocken, weil sie unter Hardwaremängeln leidet oder softwaretechnisch irgendetwas nicht umsetzbar wäre. Nein, sie stockt, weil ihr Grundsatz einer überholten Ökonomie angehört.

Der Preis dafür, Nachfrage nicht zu bedienen, ist nämlich gesunken. Oder genauer: Der Preis dafür, Nachfrage heute nicht zu bedienen, ist gesunken. Er ist gesunken gegenüber dem Preis für die Auslieferung fehlerhafter Software. Vor allem wird er aber immer kleiner im Vergleich zu dem, was es kostet, morgen Nachfrage nicht mehr bedienen zu können.

Modern ausgedrückt ist der langjährige Grundsatz nicht nachhaltig.

Das war früher kein Problem. Genauso wie es früher kein Problem war, wenn eine Handvoll Menschen in einem großen Wald Bäume für den Hüttenbau und das Herdfeuer abholzte.

Für große, hungrige Zivilisationen funktioniert derselbe Umgang mit dem Wald jedoch nicht.

Wenn bei naiver Nutzung der Ressourcenbedarf über der „Ressourcenauffrischung“ liegt, dann ist das kein nachhaltiger Umgang mit Ressourcen. Dann kann man heute daraus Nutzen ziehen – aber morgen nicht mehr.

So ist das auch bei „naiver“ Softwareentwicklung. Die funktioniert solange auf grüner Wiese codiert wird. Das war viele Jahrzehnte der Fall. Der Markt war grün, die Software war grün. Es gab keine Altlasten. Und der Hunger nach Funktionalität war so groß, dass alles andere nur wenig zählte. Da konnte man überall seine „braunen Flecken“ hinterlassen. (Von drastischerem Vokabular sehe ich hier einmal ab.)

Doch das hat sich in den letzten 10-15 Jahren verändert. Die Kunden werden anspruchsvoller, sie tolerieren immer weniger Bugs. Vor allem aber muss heute Software schneller und enger am Kunden entwickelt werden. Die Budgets sind gesunken. Und Software ist umfangreicher denn je, muss also länger leben. Auch das ist eine Budgetfrage.

Die grüne Wiese ist bis zum Horizont ein braunes Feld geworden.

Deshalb ist nachhaltige Softwareentwicklung das Gebot der Stunde. Deshalb ist der bisherige Grundsatz der neue Engpass als Policy Constraint.

Daraus folgt für mich aber nicht nur, dass mehr Qualitätssicherung und automatisierte Tests und agile Vorgehensmodelle sein müssen. Es folgt auch, dass Softwareentwurf wieder einen festen Platz in der Softwareentwicklung bekommen muss. Das scheint mir eine ökonomische Notwendigkeit im Rahmen der historischen Bewegungen unserer Disziplin. Denn ausdrücklicher Softwareentwurf macht sich Gedanken über das Big Picture und das Morgen. Wer ständig über dem Code hängt und dort lokale Optimierung betreibt, der fliegt am Ziel Nachhaltigkeit vorbei.

Auszug aus dem Buch “Flow-Design – Pragmatisch agiler Softwareentwurf”, an dem ich nun endlich doch arbeite. Jeden Tag 4 Stunden schreiben… Puh… Demnächst mehr dazu hier im Blog.

PS: Gerade stolpere ich über eine Aussage von Dijkstra:

“[The major cause of the software crisis is] that the machines have become several orders of magnitude more powerful! To put it quite bluntly: as long as there were no machines, programming was no problem at all; when we had a few weak computers, programming became a mild problem, and now we have gigantic computers, programming has become an equally gigantic problem.”

Das entspricht ganz meinem Gefühl – auch wenn das, was er 1972 mit “gigantic computers” gemeint hat, heute lächerlich wirkt.

Mittwoch, 13. März 2013

Wider den sprachlichen Plattform Lock-In

One platform to rule them all – so könnte wohl das Motto vieler Softwareunternehmen/-abteilungen lauten. Man ist ein Java-Shop oder eine .NET-Bude oder eine Ruby-Schmiede usw. Hier und da machen einige Entwickler vielleicht einen Ausflug in andere Gefilde, doch das Hauptprodukt wird auf einer Entwicklungsplattform hergestellt.

Das ist kein Zufall. Das ist gewollt. Das will das Management so, weil Vereinheitlichung eine gute Sache ist. Dadurch kann man immer Geld sparen, oder? Und das wollen die Entwickler auch so, weil das weniger verwirrend ist. Es würde sich doch keiner mehr auskennen, wenn ein bisschen Code in Java, ein bisschen in C#, ein bisschen in Ruby und noch ein bisschen in JavaScript oder Clojure oder C++ vorläge. Oder?

Da ist natürlich etwas dran. Eine gewisse Vereinheitlichung, ein Fokus ist ökonomisch. Aber nur bis zu einem gewissen Grad. Darüber hinaus kippt Vereinheitlichung, d.h. Homogenität um in Monokultur mit all ihren Nachteilen.

Aus Plattform-Fokus wird dann Plattform Lock-In. Man kann dann nicht mehr anders. Die Plattform ist dann eine systemrelevante Größe. An der ist nicht mehr zu rütteln.

Das hat negative Auswirkungen auf die Evolution der Software. Die kann dann nämlich nicht mehr mit der Zeit gehen. Entwicklungen auf anderen Plattformen oder gar Entwicklungen ganz neuer Plattformen ziehen an ihr vorbei. Und oft genug sind es sogar Entwicklungen der Plattform selbst, die man nicht nutzen kann. Gerade neulich war ich wieder bei einem Team, das sich noch an .NET 2.0 bindet. Erweiterungsmethoden, Lambda-Ausdrücke, yield return, Linq, dynamische Typen, aync/await und mehr sind dort kein Thema.

Zu den negativen Auswirkungen auf die Software kommen negative Auswirkungen auf das Team. Teams verkalken, wenn sie Jahre lang auf eine Plattform starren. Sie werden immer mehr zu einer Echokammer, durch die dieselben alten Glaubenssätze und derselbe alte Code hallen. Sie werden damit immer unattraktiver für andere Entwickler.

Bei allem Verständnis für den Vereinheitlichungsreflex halte ich diese Nachteile für so offensichtlich, dass ich mich frage, warum der Plattform-Fokus landauf, landab so groß ist. Mir scheint es eine Lücke in der Argumentation dafür zu geben.

Die meine ich nun gefunden zu haben. Ich glaube, ich kenne jetzt das fehlende Argument der Entwickler für die eine Plattform. Und dieses Argumente ist keines, das sich auf einen Vorteil der einen oder anderen Plattform bezieht. Es geht vielmehr um die Kompensation eines Mangels.

Teams mangelt es an einer anderen Sprache für die Beschreibung von Software außerhalb ihrer Programmiersprache.

Oder anders herum: Ich glaube, Teams fühlen sich zu der einen Plattform hingezogen, weil deren Programmiersprache für sie das einzig verlässliche Kommunikationsmittel über Software darstellt.

Es geht weniger um technische Vorteile einer bestimmten Plattform. Es geht auch weniger um technische Vorteile durch Konzentration auf nur eine Plattform. Es geht auch nicht um personelle Vorteile durch eine Vereinheitlichung [1].

Nein, die eine Plattform dient auch als die eine Sprache. Denn wie ein Blick auf aktuelle Konferenzprogramme zeigt, gibt es keine Sprache, um über Code zu reden. Es gibt nur Code und Technologien/Frameworks. Auf der Meta-Ebene findet im Grunde nichts mehr statt.

Früher gab es die ehrwürdige Disziplin des Softwareentwurfs. Die hatte den Anspruch, eine Lösung sprach-/plattformunabhängig zu beschreiben. Zugegeben, das ist manchmal ein bisschen weit getrieben worden. Doch der Grundgedanke war richtig, finde ich. Code war “nur” ein notwendiges und nicht triviales Implementationsdetail.

Diese Disziplin hat eigene Hochsprachen entwickelt. EBNF, RegEx, Zustandsdiagramm, Struktogramm, UML-Diagramme, sogar noch Blockdiagramme mit dem Schichtendiagramm als Sonderfall gehören dazu.

Doch das ist heute alles hohles Geschwätz. Wer nicht codiert, verschwendet Zeit an Flüchtiges. Diagramme haben keinen Bestand, sie veralten in dem Moment, da man sie implementiert. Also gar nicht erst damit anfangen. Jedenfalls nicht offiziell.

Ich kenne kein Tutorial, das eine Vorgehensweise zur Entwicklung von Software von Anforderungen bis zum Code beschreibt, in dem nicht schnellstmöglich zur Tastatur gegriffen wird. Code ist Trumpf! Code ist die einzige Wahrheit! Das wird mantrahaft wiederholt. Und alles andere wird entweder gar nicht thematisiert oder rein textuell abgehandelt. Und so wird aus einem Text ein anderer. Keine Abstraktion, keine Visualisierung. Diagramme sind selten und unsystematisch [2].

Code und Infrastruktur: das ist der Inhalt des Entwicklerlebens.

Und weil das so ist, müssen Code und Infrastruktur vereinheitlicht sein. Denn sie stellen die einzige Sprache dar, auf die Verlass ist.

Was aber nun, wenn Entwickler sich üben würden, ihre Software auch anders ausdrücken zu können. UML hat dazu einen Vorschlag gemacht – leider einen zu schwerfälligen. Vielleicht geht es ja aber auch einfacher. Ich denke, die Suche nach einer leichtgewichtigen Weise zur Beschreibung von Software, ist lohnend [3]. Dabei geht es ja nicht um einen Ersatz für Programmiersprachen. Dann wäre nichts gewonnen. Leichtgewichtigkeit bedeutet Entlastung von den ganzen Details, die sich beim und durch Codieren ergeben. So wie das Codieren in heutigen Hochsprachen auch eine Entlastung ist von ekeligen Details, wie sie bei der Assembler- und Maschinencodeprogrammierung wichtig sind. Speichermanagement, Registerallokation, Kontrollstrukturenbau und vieles mehr liegt hinter uns.

Der Gewinn einer plattformunabhängigen, vom Code abstrahierenden Sprache läge für mich nicht nur in verbesserter Möglichkeit, allein oder im Team über Codestrukturen nachzudenken. Softwareentwurf könnte damit also besser skalieren. Er würde von einer persönlichen Angelegenheit, die beim TDD-Codieren passiert, zu einer Angelegenheit des Teams. Die Collective Software Ownership würde steigen.

Der Gewinn würde außerdem in einer Befreiung aus der Umklammerung einer Plattform bestehen. Endlich könnten Entwickler verschiedener Plattformen an einem Tisch sitzen und gemeinsam über Softwarestrukturen nachdenken. Die würden sie unterschiedlich implementieren, je nach Plattform. Aber sie hätten zunächst einmal eine gemeinsame Vorstellung vom bigger picture.

Eine leichtgewichtige Sprache, um über und (weitgehend) unabhängig von Code zu “reden”, würde die Softwareherstellung also aus dem Plattform Lock-In befreien. Endlich könnte Software leichter aus Code auf verschiedenen Plattformen zusammengesetzt werden. Und das würde die Evolvierbarkeit von Codebasis und Team beflügeln.

Expliziter Entwurf in einer Entwurfssprache ist kein Selbstzweck. Mir scheint er vielmehr eine Bedingung für die Möglichkeit nachhaltiger Softwareentwicklung.

Medizin, Soziologie, Psychologie, Land- und Forstwirtschaft und Politik haben erkannt, dass Monokulturen mittelfristig schädlich sind. Überall da, wo es um Leben, um Entwicklung geht, fördert Vielfalt dieses Ziel. Nur bei der Software-Entwicklung glaubt man noch Gegenteiliges. Da setzt man noch auf Homogenisierung, Monokultur, Vereinheitlichung, Konsolidierung.

Damit sollten wir aufhören. Dafür ist aber eine praktikable Entwurfssprache und –methode nötig. Daran sollten wir arbeiten. Dazu brauchen wir mehr Ideen [4].

Endnoten

[1] Eine Plattformvielfalt kann personell nur von Vorteil sein. Nicht nur würde sie Diversität im Denken fördern, was inzwischen allgemein anerkannt einen immer größerer Überlebensvorteil für Unternehmen darstellt. Noch konkreter bedeutet Plattformvielfalt eine viel größere Auswahl an Entwicklern.

Wer nur Java oder C# macht, der kann nur aus der Java- oder C#-Community rekrutieren. Wer aber Java und C# und Python und JavaScript und Ruby macht, der kann in all diesen Communities nach neuen Teammitgliedern suchen.

Wo Plattformvielfalt Programm ist, gibt es keine zähneknirschende Einstellung plattformfremder Entwickler mehr, die dann mühsam auf die eigene Plattform umgehoben werden müssen. Dann kann jeder mit seiner Plattform zum Gelingen der Software viel schneller beitragen. Das spart bares Geld!

[2] Hier uns da tauchen mal UML-Diagramme auf: ein Klassendiagramm, ein Sequenzdiagramm. Auch Blockdiagramme gibt es hier und da. Doch auch wenn deren Syntax definiert sein mag, ihre Nutzung ist unsystematisch. Sie ist punktuell, steht in keinem Zusammenhang, ist nicht Teil einer Methode oder übergreifenden Sprache oder Kultur.

Diagramme werden nicht als gleichberechtigtes Mittel neben Code angesehen. Sie sind eher Notlösungen, wenn man grad nicht anders kann.

[3] Natürlich wäre es auch gut, wenn Entwickler sich mit mehr Programmiersprachen beschäftigen würden. Dann wären andere Plattformen ihnen nicht so fremd; sie wären offener, für manche Funktionalität auch mal etwas anderes auszuprobieren.

Dass der wahrhaft polyglotte Programmierer jedoch bald Realität würde, sehe ich nicht. Genauso wenig wie Menschen, die mehr als zwei Fremdsprachen sprechen, selten sind. Bei den natürlichen Sprachen hat sich deshalb eine Universalsprache zur kulturübergreifenden Verständigung durchgesetzt: Englisch.

Allerdings ist das kein Englisch, das alle Feinheiten der englischen Sprache enthält, sondern eine Verflachung. Allemal das gebrochene gesprochene Englisch. Doch das reicht für die meisten Situationen, in denen viele Details, Feinheiten, Nuancen ausgelassen werden können.

Warum nicht genauso in der Softwareentwicklung? Womit natürlich keine verflachte 3GL gemeint ist, sondern eine echte Abstraktion.

[4] Wie gesagt, die UML hat einmal versucht, so etwas zu sein. Doch sie ist gescheitert. Ich würde sagen, an Hybris. Sie hat sich nicht beschränken wollen in Breite und Tiefe. Oder es waren zu viele Köche an diesem Brei beteiligt?

Das bedeutet nicht, dass die UML nichts verwertbares enthielte. Ich sehe aber nicht UML 3.0 als Lösung, sondern eher etwas anderes, das hier und da aus der UML etwas entlehnt, ansonsten aber eigenständig ist.

Mittwoch, 6. März 2013

Software als Web of Services

Für mehr Evolvierbarkeit von Software wie Team müssen wir Software grundsätzlich anders aufgebaut denken, glaube ich. Derzeit stehen Objekte im Vordergrund. Das halte ich für eine überholte Sichtweise. Sie entstammt einer Zeit, da Software weniger umfangreich war und nur mit knappen Ressourcen betrieben wurde.

Historisch hat alles mit einem bunten Gemisch von Anweisungen und Daten innerhalb eines Betriebssystemprozesses angefangen. Der Prozess spannt die Laufzeitumgebung für den Code auf.

image

Unterprogramme entstanden erst, als Programme umfangreicher wurden. Sie sind ein erster Schritt zur Kapselung von Code. Die Zahl der Anweisungen konnte durch sie weiter steigen, ohne dass die Übersichtlichkeit drastisch gesunken wäre.

image

Als nächstes entwickelte man Container für Daten. Sie sollten zu größeren Einheiten zusammengefasst werden. Die Sprache C spiegelt diesen Entwicklungsstand wider: sie bietet Unterprogramme (Prozeduren und Funktionen) sowie Strukturen.

image

Anschließend gab es einen letzte Schritt: die Objektorientierung. Sie hat Strukturen und Unterprogramme zu Klassen zusammengefasst. Dadurch wurde Software nochmal etwas grobgranularer, so dass sich mehr Anweisungen innerhalb eines Prozesses übersichtlich handhaben ließen.

image

Parallel gab es eine Entwicklung von Bibliotheken. Zuerst wurden in ihnen Unterprogramme und Strukturen zusammengefasst, später Klassen.

imageimage

Doch auch wenn Bibliotheken schon lange existieren, sind sie aus meiner Sicht nie zu einem gleichwertigen Container neben Klassen aufgestiegen. Sie werden nicht konsequent zur Strukturierung und Entkopplung im Sinne der Evolvierbarkeit eingesetzt. Ihr Hauptzweck ist es, Wiederverwendung zu ermöglichen. Sie sind die Auslieferungseinheiten für Code, der in unterschiedlichen Zusammenhängen genutzt werden können soll.

Die hauptsächliche Strukturierung von Software befindet sich mithin auf dem Niveau der 1990er, als die Objektorientierung mit C++, Delphi und dann Java ihren Siegeszug angetreten hat.

Die Bausteine, aus denen Software zusammengesetzt wird, sind unverändert feingranular; es sind die Klassen. Der Umfang von Software ist seitdem jedoch kontinuierlich gestiegen. Mehr Hauptspeicher macht es möglich. Früher waren es 640 KB oder vielleicht wenige MB; heute sind es 4 GB und mehr.

Nur eine weitere Veränderung hat sich noch ergeben. Die Zahl der Prozesse pro Anwendung ist gewachsen, weil die Zahl der beteiligten Maschinen gewachsen ist. Das Ideal ist zwar immer noch, so wenige Prozesse wie möglich laufen zu haben, doch Mehrbenutzeranwendungen erfordern für Performance und Skalierbarkeit oft mindestens zwei Prozesse, einen für den Client und einen für den Server.

image

Im Vergleich zur Zahl der in einer Anwendung involvierten Bibliotheken und vor allem der Klassen, ist die Zahl der Prozesse jedoch verschwindend klein. Prozesse sind heute keine Mittel zur Strukturierung von Software im Sinne der Evolvierbarkeit. Sie werden als notwendige Übel betrachtet, um andere nicht-funktionale Anforderungen zu erfüllen.

Komponenten für industrielle Softwareentwicklung

Bibliotheken sind Konglomerate von Klassen. Die gehören inhaltlich zwar zusammen, nur ist ihre Leistung nicht sehr klar definiert. Ihr Angebot, der Kontrakt, ist implizit.

Das ändert sich erst mit Komponenten. Komponenten sind Bibliotheken mit explizitem und separatem Kontrakt.

Mit Komponenten ist es möglich, Software industriell zu entwickeln. An allen Komponenten kann gleichzeitig gearbeitet werden, auch wenn sie gegenseitig Leistungen nutzen. Die Abhängigkeit zur Entwicklungszeit bezieht sich ja nur auf den Kontrakt.

image

Idealerweise würde Software also mit Komponenten statt Bibliotheken strukturiert. Erstens ist die Kopplung zwischen Komponenten durch den expliziten Kontrakt geringer als zwischen Bibliotheken. Implementationen lassen sich dadurch leichter austauschen [1]. Zweitens steigt durch Komponenten die Produktionseffizienz.

image

Komponenten verändern gegenüber Bibliotheken allerdings nicht die Granularität von Software. Sie ziehen eher kein neues Abstraktionsniveau ein, wie es Bibliotheken in Bezug auf Klassen und Klassen in Bezug auf Methoden getan haben.

Außerdem verlangen Komponenten wie Bibliotheken und Klassen für eine Zusammenarbeit Übereinstimmung in der Plattform. .NET-Komponenten können .NET-Komponenten nutzen und Java-Komponenten können Java-Komponenten nutzen. Aus meiner Sicht gibt es keine breit praktikable Lösung, um universell plattformübergreifend Komponenten zu koppeln.

Das soll den Wert von Komponenten nicht schmälern. Sie sind sehr hilfreich und weit unterschätzt. Dennoch sollten wir auch noch einen Schritt weiter denken.

Services für Flexibilität

Der Betriebssystemprozess ist heute immer noch die Funktionseinheit, mit der wir am natürlichsten und einfachsten Code entkoppeln. Er spannt einen eigenen Adress- und Sicherheitsraum auf. Und er läuft autonom auf mindestens einem eigenen Thread.

Darüber hinaus ist der Prozess aber auch die Funktionseinheit, die wir mit einer Entwicklungsplattform assoziieren. Ob der Code eines Prozesses mit C, C++, C# oder Ruby oder Python oder Perl oder in Assembler geschrieben wurde, sehen wir dem Prozess nicht an. Von Prozess zu Prozess kann die Plattform wechseln.

Damit wird der Prozess für mich interessant im Sinne der Evolvierbarkeit. Denn die ist begrenzt, solange die Plattform eine systemrelevante Größe ist. Wer sein ganzes Team auf unabsehbare Zeit an eine Entwicklungsplattform bindet, wer den Plattformwechsel also nicht vom ersten Tag an als (wünschenswerte) Möglichkeit im Auge behält, der begibt sich auf einen unglücklichen Pfad von Co-Evolution von Software und Team: beide erstarren im Verlauf von 4-8 Jahren. Nicht nur wird es immer schwieriger, neue Funktionalität in die Codebasis einzupflegen; es wird auch immer schwieriger, echte Innovationen denken zu können und neue Leute ins Team zu holen.

Am Ende wacht dann ein Team nach Jahren auf und findet sich abgehängt. Die Kunst hat sich weiterentwickelt – nur kann man davon nicht profitieren. Weder hat sich das Team kontinuierlich Kenntnisse angeeignet, noch konnten Kenntnisse in die Codebasis einfließen.

Ohne Komponenten gibt es kaum Codeeinheiten, an denen etwas hinter einer Barriere einfach mal ausprobiert werden könnte, wenn die Plattform neue Features hergibt.

Und selbst mit Komponenten gibt es keine Codeeinheiten, an denen eine vielversprechende Plattform ausprobiert werden könnte. Oder ein vielversprechender Bewerber, dessen Plattformkenntnisse leider nicht so tief sind – der aber Experte auf einer anderen Plattform ist.

Der Code ganzer Prozesse ist heute gewöhnlich zu groß, um solche Veränderung zu erlauben. Auf der anderen Seite sind Prozesse die Codeeinheiten, mit denen solche Veränderungen auf Plattformebene einzig möglich sind.

Deshalb plädiere ich dafür, Software aus mehr, viel mehr Prozessen zusammenzusetzen als heute üblich.

Der Zweck dieser Prozesse ist aber ein anderer als der heutige. Heute dienen sie vor allem kundenrelevanten nicht-funktionalen Anforderungen wie Skalierbarkeit oder Sicherheit. Ich möchte sie nun ja aber zu einem Mittel der Flexibilität, der Evolvierbarkeit von Code und Team erheben. Deshalb nenne ich die Prozesse, um die es mir geht, Services.

Ein Service ist für mich ein Prozess mit einem expliziten und separaten und vor allem plattformneutralen Kontrakt.

image

Ein Service ist mithin eine Kombination aus Komponente und Betriebssystemprozess. Er definiert ganz genau und unabhängig von jeder Implementation, was er leistet. Und er ist autonom, d.h. ausgestattet mit eigenem Adressraum und asynchron gegenüber anderen Services.

Das macht einen Service zu einer vergleichsweise schwergewichtigen Funktionseinheit. Aber ich behaupte, das macht nichts. Es lassen sich in jedem Prozess jeder Anwendung Aspekte finden, die in Services verpackt mit einander arbeiten können, ohne dass dadurch ein Performanceproblem entstünde.

Mein Bild von Software sieht damit so aus:

image

Die Services innerhalb eines Prozesses können in einer Hierarchie zu einander stehen. Eher sehe ist sie aber auf derselben Ebene, verbunden in einem Netzwerk: a web of services.

image

Jenachdem, wie der plattformneutrale Kontrakt eines Services aussieht, kann er auch einfach von einem Prozess in einen anderen bewegt werden. Am Ende sind die umschließenden Prozesse nur Hosts für Services, leisten aber nichts selbst für die Domäne. Sie werden quasi unsichtbar.

Software besteht damit aus Services. Und Services können zusammenarbeiten, auch wenn sie auf verschiedenen Entwicklungsplattformen implementiert sind. Innerhalb eines Host-Prozesses können Services geschrieben in C#, Java, Ruby und Python kooperieren.

image

Solche Vielfalt muss nicht sein. Aber sie kann sein! Es ist möglich, eine Software, deren Services zunächst mit nur einer Plattform implementiert sind, schrittweise zu überführen. Service für Services wird einfach neu geschrieben. Das ist bezahlbar, solange ein Service keine systemrelevante Größe hat. Wenn man es sich nicht leisten kann, einen Service (oder zunächst einmal eine Komponente) neu zu schreiben, falls es schwierig wird mit Veränderungen und Refactoring… dann hat man sich in die Ecke gepinselt. Dann ist die Software wahrlich zum Monolithen erstarrt.

Sie mögen nun einwänden, die Kommunikation zwischen Services sei doch viel zu langsam. Aber wissen Sie das? Haben Sie es ausprobiert, ob Sie höhere Geschwindigkeit brauchen? Ein Roundtrip mit TCP braucht ca. 0,125msec, einer über die I/O-Streams 0,1msec, einer über einen embedded Web-Server wie Nancy im schlechtesten Fall 0,55msec – und ein direkter Aufruf nur 0,00065msec, d.h. er ist rund 500 Mal schneller.

Ja, so ist das. Aber diese im Vergleich schlechteren Werte für cross-service Kommunikation im Vergleich zu cross-component Kommunikation sagt nichts darüber aus, ob cross-service für Ihren konkreten Fall wirklich zu langsam wäre. Haben Sie das überhaupt schon einmal in Erwägung gezogen und ausprobiert? Wahrscheinlich nicht.

Genau dazu möchte ich Sie aber ermuntern. Ich möchte Sie einladen, ein web of services auszuprobieren. Experimentieren Sie damit – denn es gibt etwas zu gewinnen. Sie gewinnen all die Vorteile, die das Internet für seine Evolution und die der an ihm beteiligten Services schon nutzt.

Falls Sie glauben, ich sei der einzige mit solch einer spinnerten Idee, dann schauen Sie einmal bei infoq.com vorbei: Der Vortrag “Micro Services: Java, the Unix Way” bricht ebenfalls eine Lanze für diese Denkweise.

Ich bin überzeugt, wir müssen diesen evolutionären Schritt gehen. Wir müssen ein nächstes Abstraktionsniveau für Funktionseinheiten erschließen. SOA ist mir da schon zuviel. Aber Actors sind mir zuwenig.

Ich glaube mit “einfacheren” Services, die keinen weiteren Anspruch haben als einen separaten plattformneutralen Kontrakt für Funktionalität in einem eigenen Prozess, kann das funktionieren. Technisch ist das kein Hexenwerk. Jeder kann ein web of services aufziehen. REST sehe ich in dieser Hinsicht nicht als hübschen Ansatz für Web-Anwendungen, sondern als grundsätzliches Paradigma der Servicekopplung auch im Kleinen. Für REST braucht es keinen Web-Server; Sie können REST auch mit Service implementieren, der Requests per Standard-Input entgegen nimmt und Responses auf dem Standard-Output liefert.

Es ist vielmehr eine Frage des Willens, so zu denken. Es ist eine Frage Ihres Wertesystems: Was schätzen Sie höher ein? Garantiert bessere Evolvierbarkeit durch Plattformflexibilität – oder vermeintlich benötigte höhere Kommunikationsgeschwindigkeit?

Das ist für mich der Weg für die effizientere Herstellung von evolvierbarer Software: Funktionalität in Komponenten kapseln für bessere Entkopplung innerhalb einer Plattform; Funktionalität in Services kapseln für bessere Entkopplung in plattformneutraler Weise.

Und wenn Sie dadurch nebenbei auch noch Ihre Prozessorkerne besser ausnutzen und die Verteilbarkeit von Code steigt und Sie auf einen größeren Pool von Entwicklern zurückgreifen können bei Neueinstellungen… dann ist das auch nicht schlecht, oder?

Endnoten

[1] Es gibt mindestens zwei Gründe, Funktionseinheiten durch einen expliziten Kontrakt leicht komplett austauschbar zu halten.

Der erste ist leichtere Testbarkeit. Auch wenn es nur eine Implementation des Kontraktes für Produktionszwecke je geben wird, kann es für Tests sehr hilfreich sein, ganz einfach eine Attrappe hinter dem Kontrakt anfertigen zu können.

Der zweite Grund ist die Möglichkeit zur Neuentwicklung. Hinter einem Kontrakt kann die gesamte Implementation ausgetauscht werden, ohne dass Kontraktnutzer davon tangiert würden.

Beispiel: Wenn eine Komponente den Datenbankzugriff kapselt, dann wollen Sie in Tests von abhängigen Komponenten, nicht immer auch tatsächlich auf die Datenbank zugreifen. Also entwickeln sie z.B. eine Attrappe, die Datenbankzugriffe durch Zugriffe auf das Dateisystem simuliert.

Und auch wenn Sie nie beabsichtigen, ein anderes Datenbanksystem als MySQL zu benutzen, kann es Ihnen helfen, die gesamte Implementation der Datenbankzugriffskomponente einfach wegzuschmeißen und neu zu schreiben. Denn Refactoring kann sich irgendwann als zu aufwändig herausstellen.

Ganz zu schweigen davon, dass es mit einer Komponente für den Datenbankzugriff einfach ist, von MySQL umzusteigen auf Oracle oder SQL Server alternativ zu MySQL anzubieten. Doch diese Art der Austauschbarkeit sowie die Möglichkeit zur Wiederverwendung sind für mich eher die geringeren Gründe, mit Komponenten zu arbeiten.

Sonntag, 24. Februar 2013

TDD ohne Zauberei und Überraschung

TDD bleibt für mich aktuell, auch wenn es ein alter Hut ist. Das liegt einfach daran, dass TDD landauf, landab nicht das liefert, was es verspricht. Wenn gewöhnliche Entwickler nur nach monatelangem Studium in Klausur die TDD-Weihen empfangen können, dann liegt etwas im Argen.

In der dotnetpro stelle ich daher meine Gedanken zu einer Version 2.0 von TDD vor (Ausgaben 3/2013 und 4/2013). Aber auch hier im Blog habe ich dazu schon einiges gesagt, z.B. dass TDD in der heutigen Form für mich das Single Responsibility Principle verletzt. Anlass war für mich ein Video von Corey Haines.

Da war dann insbesondere ein Kommentator zu dem Blogartikel anderer Meinung. Und der hat auf ein aus seiner Sicht vorbildliches Beispiel verwiesen: Brett Schucherts Demonstration der TDD-Implementation eines RPN-Rechners.

Leider, leider hat mich die Demonstration auch wieder enttäuscht. Und wieder aus den selben Gründen wie bei Corey Haines:

  1. Es gibt keine Erklärung des Problems.
  2. Es gibt keine Sammlung und Priorisierung von Testfällen.
  3. Lösung und Implementation sind nicht von einander getrennt. Damit fällt die Lösung während des Codes quasi vom Himmel.

Zumindest Punkt 2 widerspricht ausdrücklich jeder TDD-Empfehlung, würde ich sagen. Deshalb kann die Demonstration nicht vorbildlich sein.

Und Punkt 3 macht die Demonstration zu einem “magischen” Event, der suggeriert, so könne es jeder Entwickler: Die Lösung einfach kommen lassen. Das wird schon.

Dabei haben sich alle, die TDD so demonstrieren, selbstverständlich über das Problem und auch ihren Lösungsansatz vorher ausführlich Gedanken gemacht. Entweder, indem sie sich hingesetzt und überlegt haben oder indem sie das Szenario einige Male ohne zu überlegen implementiert haben.

Ich kenne keine (!) TDD-Demonstration, bei der die Aufgabe live gestellt wurde und dem Demonstraten vorher unbekannt war. (Falls jemand jedoch zu so etwas einen Link hat, möge er ihn bitte in einem Kommentar hier hinterlassen.)

Also: TDD-Demos sind heutzutage weitgehend unrealistisch. Damit suggerieren sie, dass die TDD-Schritte red-green-refactor es allein richten. Das halte ich für mindestens fahrlässig, weil es ganz sicher in große Frustration bei vielen Entwicklern führt.

Und wie sollte dann eine TDD-Demo aussehen?

Ich lehne mich mal aus dem Fenster und behaupte: So wie im Folgenden.

Dafür nehme ich das Beispiel von Brett Schuchert auf: den RPN Rechner. Das Problem ist mir damit zwar auch nicht unbekannt, aber ich habe versucht, mein Vorwissen so weit wie möglich zurückzunehmen. Allemal esse ich mein eigenes Hundefutter und gehe das Problem für alle sichtbar nach den von mir vorgeschlagenen TDD 2.0 Schritten an.

image

Los geht´s…

Was ist eigentlich das Problem?

“Without requirements or design, programming is the art of adding bugs to an empty text file.” - Louis Srygley

Ich habe mir Brett Schucherts Lösung gar nicht ausführlich angeschaut. Seine Videos sind über 2 Stunden lang; er hat sich sehr viel Mühe gegeben. Außerdem wollte ich mich nicht für meine eigene Durchführung “kontaminieren”. Trotzdem habe ich natürlich beim Überfliegen Eindrücke gewonnen.

Mein erster Eindruck: Brett sagt niemandem, was das Problem eigentlich genau ist. Weder zeigt er am Anfang mal ein Beispiel für einen RPN Rechner, noch sagt er, was das Ziel seiner Entwicklungsarbeit ist. Er mokelt vielmehr 51 Minuten im ersten Video an einem API herum, von dem niemand so recht weiß, warum er so aussieht, wie er aussieht.

Wie kann man es besser machen?

Man macht erstmal eine Skizze vom Problem. Was sind die Anforderungen? Man versucht das Problem zu verstehen. Hier die erste Doppelseite meiner Analyseskizze:

image

Auf der linken Seite ist oben ein UI angedeutet und unten eine Folge von Eingaben mit zugehörigen Ausgaben; so habe ich mir die Funktionsweise eines RPN Rechners vorgestellt. (Dass Sie meine Schrift nicht lesen können, ist quasi unvermeidlich ;-) Aber das ist auch nicht nötig. Ich möchte Ihnen mit den Skizzenblättern nur einen groben Eindruck von meinem Vorgehen-/Denken vermitteln.)

imageIn der Mitte der linken Seite sehen Sie allerdings eine Verirrung. Da lag ich falsch mit meiner Vorstellung. Erst ein Blick auf den Taschenrechner von Mac OS X im RPN Modus hat mich eines Besseren belehrt. Das habe ich dann unten links und auch noch rechts ganz oben korrigiert.

Nach ca. 5-10 Minuten “Scribbeln” und mit dem RPN Rechner herumspielen war mir das Problem klar. Dafür musste ich aber auch ein Anwendungsszenario im Blick haben. Deshalb findet sich links oben zuerst eine UI-Skizze. Nur wenn ich weiß, wie ein Benutzer wirklich mit einem RPN Rechner umgehen will, sollte ich mir Gedanken zu einem darunter liegenden API machen.

Zu oft entwickeln wir einfach im luftleeren Raum, ohne Kontext. Wir imaginieren dann eine ganze Menge, was alles nötig sein könnte. Wir setzen dann ganz schnell eine technische Brille auf – und verlieren den Benutzer aus dem Blick. Damit ist der Verschwendung Tür und Tor geöffnet. Wir basteln dann nach den Regeln der Kunst an Zeugs herum, das nur wenig Realitätsbezug hat.

Das halte ich für einen großen Übelstand. Da werden unbewusst unökonomische Muster eingeschliffen. Deshalb finde ich es bei jeder Übung wichtig, möglichst konkret und benutzerbezogen zu sein. Nur so üben wir uns auch ständig in agilem Denken.

Lösungsansatz formulieren

“First, solve the problem. Then, write the code.” - John Johnson

Erst nachdem ich ein Verständnis für das Problem entwickelt hatte, konnte ich mich daran machen, über eine Lösung nachzudenken. Genau: nachdenken. Ich habe also nicht Visual Studio angeworfen, um mit dem Codieren anzufangen.

Erstens ist nichts einschnürender als Code. Zweitens wäre ich mit der Arbeit an den Rechner gekettet gewesen.

Mit meinem Notizbüchlein konnte ich jedoch in der S-Bahn weiter über die Lösung nachdenken. Und zwar so konkret, dass die Codierung für mich hinterher ganz leicht war [1].

Das Ergebnis sind die rechte Seite im oberen Bild und die beiden Seiten im nächsten Bild.

Im ersten Bild rechts sehen Sie mein Flow-Design. Rechts oben der big picture Flow mit allen Interaktionen des einzigen Dialogs der Anwendung. Das sind Enter (Zahl eingeben), Drop (Zahl vom Stack entfernen) und Operator auslösen.

Alle Domänenlogik fasse ich in einer EBC-Funktionseinheit zusammen: dem RPN Calculator.

Unten auf der rechten Seite im ersten Bild sehen Sie dann eine Verfeinerung der Interaktion, die über einen Operator ausgelöst wird. Da habe ich mir klar gemacht, was mit dem Operator und der aktuellen Zahl im “Rechenwerk” passiert, wenn denn etwas berechnet werden soll.

Bis hierhin hat es ca. weitere 7 Minuten gedauert. Nach runden 15 Minuten hatte ich also nicht nur das Problem verstanden, sondern auch einen Lösungsansatz. Dessen Kern war erstens ein Stack für die Zahlen, die auch im UI zu sehen sind, also die Operanden. Und zweitens gab es die Vorstellung eines Verzeichnisses von Operationen auf diesen Zahlen. Die ergab sich ganz natürlich aus der Verallgemeinerung der Interaktion des Benutzers in Bezug auf die Operatoren. Nicht jeder Operator für sich war für mich eine Interaktion, sondern sie alle sollten mit einer Interaktion abgehandelt werden, die mit dem konkreten Operator parametriert sein sollte [2].

Testfälle sammeln

Nach der Modellierung kannte ich den API des RPN Rechners. Ich musste mir nichts aus den Fingern saugen wie Brett. Dessen get/set für einen Akku und die Enter-Methode finde ich gänzlich unnatürlich. Er versucht da etwas 1:1 in einen API zu übernehmen, das er (ohne es uns wissen zu lassen) von einem UI abgeschaut hat. Aber warum sollte in einem API ein Repräsentant einer UI-Design-Entscheidung stehen? Dass es dort einen Enter-Button gibt, kann sich doch morgen ändern.

Deshalb gibt es bei mir keinen sichtbaren Akku und auch keine Enter-Methode, sondern ein Push() und ein immer gleiches Resultat nach jeder Aktion. Push() abstrahiert von jedem UI. Es sagt vielmehr etwas über den Lösungsansatz für den RPN Rechner aus, dass es darin nämlich einen Stack gibt. Das ist echte Domänenlogik. Die abstrahiert von UI-Eigenheiten.

Mit dem Entwurf für den RPN Rechner konnte ich dann konkret über Testfälle nachdenken. Die sehen Sie auf der linken Seite der nächsten Abbildung:

image

Für die drei Methoden meines API gibt es drei Gruppen von Testfällen, die ich als Tabellen notiert habe. Etwas schöner sieht so eine Tabelle natürlich in Excel aus. Sie ist der Reinschrift meiner Skizzen für diese Dokumentation entnommen.

image

Weil man es nicht häufig genug sagen kann: Über Testfälle nachdenken und sie priorisieren kann man nur und ausschließlich, wenn man einen Lösungsansatz hat, zu dem ein API gehört.

Wer TDD vorführt, ohne Testfälle zuerst zu benennen, und wer nicht erklärt/erklären kann, warum deren Reihenfolge so ist, wie sie ist, der führt TDD falsch vor.

Wo Testfälle vom Himmel fallen oder sich überraschend ergeben, ist TDD magisch und suggeriert Einfachheit, die nicht vorhanden ist.

Was ist meine Erklärung?

  • Meine Testfälle gliedern sich von vornherein nach API-Methoden. Da gibt es keine Überraschung. (Was nicht heißt, dass ich mich Änderungen während der Implementation verschließe. Da kann es immer neue Erkenntnisse geben. Aber ich darf mit einer Idee beginnen.)
  • Innerhalb der Testfälle für eine API-Methode gibt es eine klare Aufteilung zwischen Eingaben und zugehörigen erwarteten Ausgaben.
  • Aus den Inputs kann nicht jeder Output erklärt werden. Der RPN Rechner hat einen Zustand. Der muss ebenfalls in die Testfälle eingehen.
  • Die Testfälle wachsen in Richtung zunehmendem Zustand, da der Input immer dieselbe Form hat.

Mein TDD wie unten zu sehen, ist also nicht magisch und nicht überraschend, sondern ganz handfest. Es folgt einem Plan, der durch Überlegen entstanden ist. Das hat maximal 5 Minuten gedauert.

Wem dieses Überlegen schon gleich zu viel sein sollte, wer das schon im Widerspruch zum rechten TDD sieht… Nun, dem habe ich eben nichts zu sagen. Wir leben dann auf verschiedenen Planeten. Das ist ok. Ich freue mich aber über jeden, der sich drauf einlässt. Dann können wir auch trefflich darüber debattieren, ob nicht der eine Testfall früher oder später liegen sollte oder der eine Schritt mehr oder weniger KISS ist.

Inkrementell vorgehen

Auch TDD tut gut daran, an den Kunden zu denken. Brett hat da allerdings niemanden im Blick. Er startet auch ohne UI. Das ist aus meiner Sicht aber kein Grund, nicht agil/inkrementell vorzugehen. Auch Entwickler, die einen API nutzen sollen, sind Kunden. Die Frage lautet deshalb für mich immer: Wie kann ich möglichst schnell einen kleinen Nutzenzuwachs bieten? Welchen Schritt kann ich tun, um etwas von Wert herzustellen, zu dem ein Kunde/Anwender Feedback geben kann?

Deshalb habe ich einen Moment darauf verschwendet, die rechte Seite im zweiten Skizzenbild zu füllen. Dort sehen Sie fünf Inkremente angedeutet, die sich an den Interaktionen des Dialogs orientieren.

  • Inkrement #1: Der Anwender kann Zahlen auf den Stack schieben, die Operanden. Das entspricht Push() auf dem RPN Calculator. Dazu kann der Anwender schon mal Feedback geben.
  • Inkrement #2: Der Anwender kann die aktuelle Zahl zum Stack-Top addieren. Jetzt ist der RPN Rechner schon ein bisschen nützlich, auch wenn er nur eine Operation beherrscht.
  • Inkrement #3: Der Anwender kann Zahlen vom Stack entfernen. Das entspricht Drop() auf dem RPN Calculator.
    Für diese Funktionalität in Inkrement #3 habe ich mich nur in Anlehnung an Bretts Demonstration entschieden. Er hat Drop() sogar noch vor der ersten Operation realisiert. Da wollte ich mich nicht lumpen lassen ;-)
    Ohne seine Vorlage hätte ich jetzt mit Operationen weitergemacht.
  • Inkrement #4 und #5: Jetzt endlich weitere Operationen. –, *, / als binäre Operationen und ! als unäre.

Damit fiel es mir leichter, mich bei der TDD-getriebenen Implementation zu konzentrieren. Den Preis von weiteren 3 Minuten habe ich dafür gern gezahlt. Denn ich konnte sicher sein, selbst wenn ich bei der Implementation unterbrochen werde – was sehr wahrscheinlich ist –, habe ich immer etwas in der Tasche. Nach jedem Inkrement kann ich den Griffel fallen lassen und der Kunde hat schon etwas in der Hand; zwischen Inkrementen liegen Sollbruchstellen der Implementation.

Implementation nach red+green+refactor

“Programs must be written for people to read, and only incidentally for machines to execute.” - Abelson / Sussman

Auch wenn ich nach meinen Überlegungen gar nicht mehr so viel Lust hatte zu implementieren – ich hatte den Eindruck, das Spannendste schon erledigt zu haben: die Problemlösung –, habe ich mich natürlich an Visual Studio gesetzt.

Das folgende Script zeigt die Schrittfolge meiner Codierung des RPNCalculator-Klasse. Das UI und die Integration von UI und RPNCalculator habe ich ausgelassen. Die sind nicht so spannend.

NUnit Tests für das UI gibt es – aber die sind als Explicit markiert. Sie dienen nur der Überprüfung, ob die Events korrekt gefeuert werden bzw. die Anzeige des Resultats korrekt erfolgt. Das ist kein Hexenwerk, ändert sich nicht häufig und kann auch mal manuell getestet werden, falls nötig.

Die Musik spielt in der Domänenklasse RPNCalculator. Darauf liegt auch bei Brett Schuchert das Augenmerk. Also los…

RPN Desktop Calculator TDD by Ralf Westphal

Wenn Sie sich die Schrittfolge näher ansehen, stutzen Sie vielleicht hier und da. Nicht alles mag in Ihren Augen TDD der reinen Lehre sein. Die strebe ich aber auch nicht an. Mir geht es um pragmatisches, realistisches TDD.

Dennoch hier ein paar Erklärungen, die Sie besänftigen mögen:

  • Bei Schritt 2.1 sehen Sie, dass ich über einen Konstruktor in das System under Test (SUT) Zustand injizieren will [3]. Ja, da bin ich ganz schamlos. Das mache ich einfach. Ich weiß, dass das SUT Zustand hat. Warum soll ich den nicht explizit setzen? Das spart mir u.U. eine Menge Verrenkungen, um mit anderen API-Aufrufen an einen Punkt im Test zu kommen, wo ich endlich das überprüfen kann, was gerade Thema ist.
  • In Schritt 3.2 habe ich mich hinreißen lassen, wider besseren Entwurfswissens eine ganz einfache Implementation zu wählen. Die arbeitet schon irgendwie richtig, aber sie entspricht nicht dem, was Ziel sein muss: eine Auswahl der Operation aus einer Liste. Ich verdrahte die Addition fest.
    Das habe ich gemacht, um hier die TDD-KISS-Gemüter zu beruhigen ;-) Im richtigen Leben hätte ich mir erlaubt, schon die Struktur zu implementieren, die Sie nun erst in 6.1 eingeführt sehen.
  • Zu meiner eigenen Überraschung hat der Test in Schritt 3 (leider irrtümlich so benannt) gleich grün geliefert. Das hätte eigentlich anders sein sollen – aber es zeigt, dass auch ein Nachdenken über Testfälle in endlicher Zeit nicht perfekt ist.
    Dafür will ich mich nicht schelten. Kann halt passieren. Macht nichts.
    Da muss ich mich nächstes Mal nicht noch doller anstrengen, um bessere Testfälle zu finden. Ich nehme es einfach so und freue mich, dass ich etwas Implementationsaufwand spare.
  • Bei der Implementation zu 4.2 könnten Sie einwerfen, dass die nicht KISS sei. Das mag sein – aber: WTF. Das ist mir egal. Ich weiß doch schon, wie Resultate aus dem RPNCalculator geliefert werden. Warum soll ich mich dümmer stellen, als ich bin?
  • Ha, jetzt haben Sie mich: Nach der Implementation von 5.1 ist ein anderer Test auf Rot gegangen. Das darf doch nicht sein!
    Ja, das mag gegen die reine Lehre verstoßen. Aber auch hier: WTF. Das kann mal passieren. Ich finde das nicht schlimm. Es ist eher ein Zeichen dafür, dass die Implementation in 5.1 KISS ist. Sie konzentriert sich nur darauf, einen Test grün zu bekommen.
    Mit einer kleinen Nachbesserung zurre ich die Regression dann in 5.2 wieder fest. Es ist kein größerer Schaden entstanden.
  • Nun Trommelwirbel… Jetzt zum Kern der Domänenlogik: der leicht erweiterbaren Liste von Operationen. Auf die hatte ich in 3.2 noch verzichtet – doch nun kann ich nicht mehr an mich halten.
    In einem Rutsch refaktorisiere ich die bisherige Addition und füge auch noch die Fakultät hinzu. Ja, bin ich denn wahsinnig geworden?
    Nein, ich finde das halb so wild. Es sind ein paar Änderungen, die ich da vornehme – aber sie sind allesamt trivial. Und wenn dabei etwas verrutschen sollte, sehe ich sofort, wo das Problem liegt.
    Geht gar nichts mehr, dann ist etwas mit Auswahl und Aufruf der Operationen falsch.
    Schlagen nur die Additionstests fehl, dann habe ich die Addition falsch implementiert.
    Schlägt nur der Fakultätstest fehl, dann ist dort etwas falsch implementiert.
    Der neue Inhalt von Calculate() ist auch nicht spontan entstanden, sondern steht schon im erste Skizzenbild rechts unten. Ich lese die Implementation quasi nur ab.
  • Für die weiteren Operationen gehe ich eine Abkürzung in 7.*. Ich stecke die Testfälle in TestCase-Attribute und füge dem _operations-Verzeichnis einfach nur kurze Lambda-Ausdrücke hinzu. Das Muster ist immer gleich. Die Addition hat es vorgemacht.

That´s it. Ich denke, damit habe ich eine Lösung geliefert, die nicht nur funktioniert, sondern auch Martin Fowlers Forderung erfüllt:

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” - Martin Fowler 

Nicht nur den Code kann man verstehen. Der ist ja denkbar kurz. Er ist auch erweiterbar – und zwar von vornherein und nicht erst nach einem überraschenden Refactoring. Er war ja schon so gedacht. Wer einen weiteren Operator hinzufügen will, der setzt in das UI einen Button dafür und trägt eine Operation in das Verzeichnis ein. Fertig.

Eine Injektion von Operationen von außen oder auch nur eine eigene Klasse halte ich derzeit allerdings für überflüssig. Eine Refaktorisierung in dieser Hinsicht wäre für mich vorzeitige Optimierung. Die Anforderung dafür ist derzeit nicht aus dem Gesamtszenario ablesbar. Und einfach nur Prinzipien anwenden, weil ich sie gerade kenne, ist mir nicht Grund genug.

Nicht nur der Code ist aber verständlich, die ganze Lösung ist es. Weil es dazu ein Modell gibt und der Code dieses Modell widerspiegelt. Dazu gehört natürlich auch noch die Integration, die ich hier bewusst ausgelassen habe, weil ich mich auf TDD konzentrieren wollte. Die finden Sie bei Interesse jedoch im Repository.

Fazit

Bubbles don´t crash – das ist wahr. Genauso kann man sagen, Landkarten seien nicht das Terrain. Ja, und? Sind Landkarten deshalb nutzlos? Kaum. Genauso wenig sind Bubbles, d.h. Entwürfe auf dem Papier nutzlos oder verschwendete Zeit, nur weil sie noch kein Code sind. Das ist ja gerade ihr Zweck. Sie sollen kein Code sein, damit man mit ihnen viel schneller vorankommt bei der Lösungsfindung.

Wo Code ins Spiel kommt, wird es morastig. Wenn ich also über eine Lösung sinnen kann, ohne codieren zu müssen, kann ich mich nur freuen. Die ersten zwei Bilder haben gezeigt, dass ich das kann. Die spätere Implementation hat mich keines Besseren belehrt. Dennoch ist der Lösungsansatz auf Papier natürlich zunächst nur eine Hypothese. Das macht aber nichts. Im Gegenteil: Es geht gar nicht anders. Auch der Lösungsansatz, den Sie ohne Papier nur im Kopf während des TDD Code Slinging entwickeln, ist nur eine Hypothese.

Wenn ich Ihnen hier Skizzen und Nachdenken präsentiert habe, dann nur, um etwas explizit zu machen, das sich nicht einmal vermeiden lässt. Corey Haines tut es, Brett Schuchert tut es, Sie tun es, wir alle tun es – fragt sich nur wann und wie nachvollziehbar.

Ich finde meine Lösung insgesamt viel nachvollziehbarer als die der zitierten Herren. Da steckt keine Magie drin. Ich bin nicht (künstlich) überrascht über Testfälle, die sich auftun. Meine kleinen Überraschungen sehen Sie klar dokumentiert in der TDD-Schrittfolge und im Vergleich zu meinen Skizzen. Das ist real und nicht poliert.

Wenn Sie ein Guru sind, kommen Sie natürlich ohne all das aus. Ich kann das leider noch nicht. Ich muss noch nachdenken – und dann tue ich das gern in Ruhe. Nicht immer, aber allermeistens. Ab und an klappe ich Visual Studio auch schneller auf. Doch dann meist, um im Rahmen meiner Lösungsfindung etwas zu explorieren. Dann bin ich jedoch in einem anderen Modus, dann bin ich Forscher und nicht Ingenieur.

  • Forscher finden heraus, was ist. Sie dokumentieren Zusammenhänge.
  • Ingenieure nehmen die Ergebnisse von Forschern und finden sehr kreativ Problemlösungen. Sie schaffen Neues.
  • Handwerker schließlich wenden Lösungen vor allem an. Sie setzen um, reproduzieren.

Das sind drei zentrale Rollen, deren Hüte wir bewusst wechseln sollten. Wenn wir es nicht tun, riskieren wir Frust oder Schlimmeres.

Durch die bewusste Herausstellung von Problemanalyse und Lösungsfindung/Entwurf möchte ich den Rollen Forscher und Ingenieur Raum geben. Wir müssen Probleme zunächst erforschen, dann müssen wir mit den Heuristiken und Technologien unseres Entwicklerwerkzeugkastens state-of-the-art Lösungen entwickeln – und erst am Ende setzen wir die handwerklich sauber mit TDD um.

Alle drei Rollen nur in den red+green+refactor-Phasen innehaben zu wollen, tut uns nicht gut und dem Ergebnis auch nicht. Und letztlich tut es auch der guten Idee hinter TDD nicht gut.

Bottom line: TDD ist eine feine Sache – wenn man die nicht magisch betreibt und überlastet.

Endnoten

[1] Ja, ich will es geradeheraus sagen: Das TDD-Vorgehen, welches ich hier zeige, weil ich mich nicht lumpen lassen wollte, hat mich am Ende auch langsam gemacht. Die kleinsten Schritte waren für mich nicht wirklich nötig. Ich hätte größere machen können – und hätte nur in eine kleinschrittigere Gangart zurückgewechselt, wenn ich in ein Problem gelaufen wäre.

Das ist natürlich gegen die reine TDD-Lehre… Deshalb hab ich mich auch nicht hinreißen lassen… Doch es ist für mich ein realistischeres Vorgehen.

TDD-Kleinstschritte sollten kein Dogma sein, sondern eine Methode unter bestimmten Bedingungen. Und diese Bedingungen sind, dass man eben noch nichts über die Lösung weiß. Das tue ich ja aber, indem ich vorher darüber nachdenke.

Und dass das nicht nur eitle, nutzlose Gedanken sind, die nichts mit der Coderealität zu tun haben, dass ich mir so gar nichts vor dem Codieren vorstellen könne… Das versuche man mir bitte nicht einzureden. Irgendetwas müssen ein paar Jahrzehnte Softwareentwicklungserfahrung doch in meinem Hirn hinterlassen haben, oder?

Wichtig sind nicht stets und ausschließlich die allerkleinsten Schritte, sondern ein gesundes Gefühl dafür, ob man sich noch in bekannten Gefilden mit dem Code bewegt. Je unbekannter, desto kleiner die Schritte. Selbstverständlich. Aber Lösungen lassen sich eben auch noch auf andere Weise als durch Codieren erkunden.

Falls dabei mal etwas herauskommen sollte, das nicht KISS ist, dann finde ich das nicht schlimm. Ich habe in den Fall nämlich sehr wahrscheinlich Zeit gespart. Das kann mehr Wert haben als KISS-Code – solange die Verständlichkeit nicht grundsätzlich leidet.

[2] Für Brett Schuchert ist das eine überraschende Erkenntnis, auf die er nach knapp einer Stunde Codieren stößt. Er muss dafür seine Implementation weitere 20 Minuten refaktorisieren.

Das halte ich für unökonomisch, wenn ich durch 5 Minuten Nachdenken gleich darauf kommen kann, dass Operationen nicht durch einzelne API-Methoden repräsentiert werden sollten.

Ich halte es für keine Tugend, sich solcher Erkenntnis mit Macht zu widersetzen. Und es ist auch keine Tugend, nicht nach Wegen zu suchen, um solche Erkenntnis möglichst früh zu gewinnen.

[3] Eigentlich soll dieser spezielle Konstruktor internal sein. Das hab ich im TDD-Eifer übersehen. Sorry. Bei dem jetzigen Codereview für den Blogartikel kommt es ja aber heraus :-)

Donnerstag, 21. Februar 2013

Vorzeitige Optimierung durch Monolithen

Softwareentwicklung ist historisch ein Geschäft der knappen Ressourcen. Speicher war knapp, Speicher war langsam, die Prozessorgeschwindigkeit war gering, die Prozessorverfügbarkeit war niedrig und die Kommunikation zwischen Prozessoren/Maschinen war unmöglich bis schwierig.

Ich glaube, diese jahrzehntelange Not ist zum Bestandteil der Kultur der Softwareentwicklung geworden. So wie die Not der (Vor)Kriegszeit zur Kultur unserer Eltern und Großeltern gehört.

Der Teller wird leer gegessen. Man schmeißt keine Lebensmittel weg. Mit dem Essen spielt man nicht. Kerzenstummel und andere Reste werden gehortet. Socken müssen gestopft werden. Wer kennt solche Ermahnungen und Verhaltensweisen nicht von Eltern und Großeltern? Sie entstammen einer tief liegende Angst, dass das, was heute verfügbar ist, morgen wieder weg sein könnte. In der heutigen Zeit des Überflusses sind es allerdings Anachronismen [1].

Ähnlich kommen mir nun viele Reflexe von Entwicklern vor. Sie mahnen, nicht verschwenderisch mit Speicher umzugehen; sie bedenken zu allererst die Performance; sie misstrauen jeder Ressource und jeder Interaktion [2].

Dabei leben auch wir inzwischen in Zeiten des Überflusses. 48 KB oder 640 KB Hauptspeicher oder 5 MB Festplatten sind heute nicht mehr Stand der Dinge. Für viele (oder gar die meisten?) Anwendungsszenarien ist Hardware heute so groß, schnell und billig, dass sie keine Begrenzung mehr darstellt.

Komplette Unternehmensdatenbanken können wir heute im Hauptspeicher halten. Prozessoren haben mehrere Kerne. Festplatten sind schnell und groß. Rechner sind in Gigabit-Netzwerke eingebunden. Und quasi unendlich viele Rechen- und Speicherressourcen stehen auf Abruf weltweit zur Verfügung.

Doch die Mentalität ist immer noch eine der Knappheit. Das kann ich irgendwie verstehen; ich bin ja selbst in Zeiten der Hardwaremängel mit der Programmierung groß geworden. Jetzt sollten wir aber mal diese Haltung langsam ändern. Solange wir uns ihrer nämlich nicht bewusst sind und sie versuchen zu überwinden, verschwenden wir unsere Zeit und geistige Kapazität. Die sind nämlich tatsächlich noch genauso begrenzt wie früher.

In vielen Bereichen ist die Mangelkultur ein Anachronismus. Wir verhärten uns mit ihr gegen nützliche Veränderungen. Denn dass sich einiges verändern muss, ist unzweifelhaft. Beispiel: Softwareentwurf. Wir müssen Software anders entwerfen, damit die Strukturen evolvierbarer werden. Das heutige Legacy Code Problem darf sich nicht ungebremst so fortsetzen.

Für mich ist es ein Problem des Mangeldenkens, weil die uns so drückenden Softwaremonolithen aus meiner Sicht Kinder des Mangeldenkens sind. Denn warum sollte man Code zu einem Monolithen zusammenschweißen, wenn nicht aus Angst vor Performance- und Speicherproblemen?

Unter Monolith verstehe ich hier Software, die all ihre Aufgaben in möglichst wenigen Prozessen erledigt. Am besten ist das nur einer, eine Desktopanwendung (bzw. ein Batch) oder eine Web-Anwendung. Ein Betriebssystemprozess hostet dabei die gesamte Funktionalität. Das ist das Ideal.

Das ist einfach zu deployen. Das ist vermeintlich einfach zu entwickeln, da man alles in ein Visual Studio Projekt stecken kann. Das ist schnell, weil die gesamte Kommunikation innerhalb des Adressraums eines Prozesses stattfindet. Das ist technisch simpel, weil man sich nicht mit Infrastruktur herumschlagen muss.

Natürlich verstehe ich all diese Beweggründe. Ich habe auch so gedacht. Doch inzwischen glaube ich, dass wir uns damit keinen Gefallen tun, wenn wir so auf diese Mängel stieren. Das Deploymentproblem ist heute nicht mehr so groß wie noch vor 10 Jahren; die Kommunikationsgeschwindigkeit ist mit heutigen Prozessoren, Hauptspeichern und Netzwerkverbindungen auch zwischen Prozessen oft ausreichend. Und die Infrastruktur müssen wir nicht mehr in dem Maße selbst programmieren wie noch vor 10-20 Jahren [3]. Wer heute einen Monolithen entwickelt, der betreibt also vorzeitige Optimierung. Er optimiert für Mängel, deren Existenz nicht gesichert ist im Hinblick auf eine konkrete Domäne.

Unsere Kultur verhindert jedoch, dass wir dieses Potenzial voll ausschöpfen. Wo es nicht anders geht, da wird es getan. amazon, Twitter, Facebook, eBay usw. können nicht anders, als an die heutigen Grenzen zu gehen.

Wer allerdings heute ein CRM oder sonstige Intranet/Desktop-Software programmiert, der verschenkt Potenzial. Der schnürt sich ein in ein Korsett, dass man geradezu masochistische Neigungen vermuten könnte.

Wenn es denn sonst keine Probleme gäbe, wäre das ja nicht schlimm. Jedem seine Neigung ;-) Das Evolvierbarkeitsproblem halte ich jedoch für so groß, dass wir uns “Neigungsprogrammierung” in dieser Hinsicht nicht mehr erlauben können. Die mittel- bis langfristige Gesundheit unserer Software leidet darunter. Und auch die unserer Teams.

Wir müssen bessere Wege finden, um Software über die Jahre nicht nur zu retten, sondern entspannt weiterentwickeln zu können. Dazu gehört für mich immer mehr auch die Neuentwicklung. Ja, genau, Code wegschmeißen und neu machen.

Dass das nicht für komplette Anwendungen funktioniert, ist mir klar. Deshalb müssen wir aufhören, Monolithen zu bauen. Die können wir nämlich nur komplett ersetzen – was unökonomisch ist. Oder wir müssen sie zu Tode refaktorisieren.

Mit geht es um einen Mittelweg. In “Form Follows Feasibility” hatte ich schon darüber nachgedacht. Seitdem bin ich nur sicherer geworden, dass solch ein Mittelweg nötig und gangbar ist – wenn wir uns aus dem Mangeldenken befreien.

Die bewusste Strukturierung auch von Deskptop- oder Web-Anwendungen in mehrere Services, d.h. Komponenten mit plattformunabhängigem Kontrakt, ist mir sogar noch wichtiger geworden.

Erstens kann man sich mit plattformunabhängigen Kontrakten weniger in die Tasche lügen, was die Entkopplung von Code angeht. Zweitens bieten nur solche Kontrakte die Chance, sich vor der drohenden Systemrelevanz der Programmierplattform mit einhergehender Verkalkung des Teams zu schützen.

Jede Software als “web of services” zu denken, sollte der Default werden, glaube ich. Und erst wenn sich das handfest als suboptimal herausstellt, sollte mit einer Optimierung begonnen werden.

Endnoten

[1] Die Motive Respekt und Dankbarkeit hinter solcher Haltung will ich nicht ausschließen. Schön, wenn sie denn mitschwingt. Auch im Überfluss tun wir sicherlich gut daran, Bescheidenheit und Respekt unserer Umwelt entgegen zu bringen.

[2] Schizophren ist, dass diese Haltung komplett umkippen kann. Denn der Umgang mit Objekten in verteilten Anwendungen war (und ist?) von großer Naivität, gar Sorglosigkeit geprägt. Da spielten Skalierbarkeit, Robustheit und Performance lange so gar keine Rolle. Anders sind CORBA, EJB und DCOM und auch noch Teile von .NET Remoting nicht zu erklären.

[3] Eine Kommunikation zwischen Prozessen, die über die Welt verteilt sind, ist via Internet und Cloud API wie Pubnub oder iron.io heute eine Sache weniger Zeilen Code.

Dienstag, 19. Februar 2013

Bessere Prozesse für den Rest von uns

Die “Gurus” können es. Die können ihr TDD, OOP, XP, Scrum usw. Aber was ist mit dem Rest der Entwicklergemeinde?

In den meisten Teams gibt es auch einen, der es kann. Einen der ansagt. Aber was ist, wenn der mal ausfällt? Was ist mit dem Rest des Teams.

Mein Eindruck ist, wir verlassen uns noch zu sehr auf “Gurus”, “Meister” und “starke Persönlichkeiten”. Die bestimmen unsere Arbeit. Entweder verteilen sie sie. Oder sie definieren Methoden auf ihrem Level – die dann Normalsterbliche nur schwer selbst in ihren Arbeitsalltag transferieren können.

Wie anders ist es zu erklären, dass zum Beispiel TDD nach 10 Jahren immer noch kein Standard ist? Wie anders ist es zu erklären, dass Entwurf mit der UML nach mehr als 10 Jahren immer noch kein Standard ist? Mit Standard meine ich so etwas wie “Bei 80% der Entwickler akzeptiert und in täglichem Gebrauch.”

Ich behaupte nicht, dass TDD oder UML schlecht seien. Fragt sich nur, wem sie real helfen. Ich sehe einfach eine große Differenz zwischen Anspruch und Implementation. An der Verfügbarkeit von sprachgewandten Verkündern dieser und anderer Techniken oder Literatur darüber kann es nicht liegen. Woran dann?

Eine Folie in einem Vortrag von Sam Aaron hat mich darauf gebracht:

image

Sam hat eine Geschichte von Schachgroßmeister Gary Kasparov erzählt: Nachdem Kasparov gegen IBMs Deep Blue verloren hatte, hat er den Gegner Computer nicht verteufelt. Er hat ihn umarmt und versucht, mit Hilfe von Software insgesamt zu einem besseren Spieler zu werden. Er hat sich von Software beim Schachspiel sozusagen beraten lassen. Damit hat er dann jeden der willig war herausgefordert. Und geschlagen wurde Kasparov von zwei Unbekannten.

Darauf bezieht sich das Zitat. Kasparov war überrascht, dass ihn, den Großmeister (strong human), der sich auch noch von einer Maschine unterstützen ließ, zwei Nobodys in Schachdingen (weak human) übertreffen konnten, die sich auch von einer Maschine unterstützen ließen. Sein Schluss daraus: Sie hatten einen “better process”, mit dem sie gearbeitet haben.

Wenn mir eines immer bewusster geworden ist in den letzten Jahren, dann ist das die Begrenztheit der Möglichkeiten zu Veränderungen in Teams. Alle sind nämlich heute am Limit. Inwiefern das selbstverschuldet oder schlechtem Management anzuhängen ist, lasse ich mal dahingestellt. Es ist einfach so. Damit müssen wir leben.

Außerdem ist die Ausbildung von Softwareentwicklern immer noch (oder inzwischen?) schlecht. Nicht überall natürlich, aber weitestgehend.

Unterm Strich bedeutet das, Software wird vor allem von “weak humans” geschrieben. Wir brauchen daher Techniken, Methoden, Konzepte für diese “weak humans”. Das bedeutet, sie müssen sehr explizit und konkret sein. Sie müssen stark an die Hand nehmen. Sie müssen in kleinen Schritten zwischendurch erlernbar sein. Sie müssen in einer Welt voller legacy code und mittelalterlichem Management “bootstrapfähig” sein.

Das sage ich mit einer guten Portion Selbstkritik. Was Stefan Lieser und ich unter www.clean-code-developer.de zusammengetragen haben, entspricht dieser Forderung auch nicht immer. Und auch der Flow-Design Ansatz kann noch darauf zugefeilt werden.

Insgesamt jedoch habe ich in dem Kasparov-Zitat meinen Anspruch formuliert gefunden: Ich bemühe mich und will mich noch mehr bemühen, “normalsterblichen” Entwicklern zu helfen. Die große Zahl “weak humans” ist meine Zielgruppe. Für sie muss ich versuchen, was immer ich beschreibe, verständlich und nützlich zu machen.

Und ich glaube, danach sollten wir in der Branche insgesamt mehr streben. Wir müssen erkennen, dass die Softwarekrise nur mit mehr Entwicklern bewältigbar ist. Das führt allerdings fast notwendig dazu, dass deren allgemeines Ausbildungsniveau nicht steigt. Also sind Veränderungen im Alltag nur erfolgversprechend, wenn sie “weak humans” und “weak teams” und “weak companies” adressieren.

Damit will ich niemanden von einer (Selbst)Verantwortung zum Lernen und Streben nach Verbesserung entheben. Es geht mir vielmehr um die Anerkenntnis der schwierigen Umstände in und um Teams. Wir reden ja bei all den zitierten Methoden vor allem darüber, “Softwareherstellungsmaschinen” während des Laufens umzubauen. Und das ist eben schwierig. Da müssen wir bescheiden werden. Wir müssen auch sensibel werden für die Grunde, warum diese Methode oder jener Ansatz nicht so fruchtet, wie wir es uns vorstellen. Liegt das wirklich an unfähigen Entwicklern – oder sollten wir häufiger selbstkritisch sein und schauen, wo Methode und Ansatz verbessert werden können?

Als Lohn der Mühe winkt gem. Kasparov, dass noch bessere Methoden und Prozesse als heute diese “weak humans” in die Lage versetzen, bessere Software zu schreiben, als die heutigen “Meister” mir ihren Helfern es können. Ist das nicht ein attraktives Ziel, das wir versuchen sollten zu erreichen?

Wo Erfolge heute zu verzeichnen sind, ist das selbstverständlich toll. Aber wir sollten uns von diesen Erfolgen nicht einlullen lassen. Sie können unversehens zum Feind des Besseren mutieren. Das wäre doch schade, oder?

Also: Es gibt keine Methode, kein Konzept, keine Technik, keine Technologie, die nicht noch besser, vor allem einfacher gemacht werden könnte - und somit für “weak humans” leichter zu adaptieren ist. Usability ist auch hier Trumpf.