Donnerstag, 28. April 2011

Der Objekte Kern

Was ist eigentlich Objektorientierung? Eigentlich sollte das doch klar sein – und doch erhitzen sich die Gemüter immer wieder darüber. Wenn ich Flow-Design vorstelle, höre ich z.B. den Einwand, das sei doch gar nicht mehr objektorientiert. Das ist dann nicht nur Feststellung, sondern auch Kritik. Aber ist das wirklich so? Und ist das kritikwürdig, wenn es denn so wäre?

Ich denke, wir müssen uns die Objektorientierung ein wenig näher ansehen, um das beurteilen zu können. Die scheinbar eine Objektorientierung gibt es nämlich nicht.

Harte Objektorientierung – Technik

Unter Objektorientierung verstehen die meisten Entwickler zunächst einmal Mittel einer Programmiersprache. Es geht also um Technik, um Code. Diese technische Objektorientierung lässt sich für mich so auf das absolut Essenzielle reduzieren:

Hinter einem Interface verborgene allozierbare strukturierte Speicherbereiche.

Jup, das ist es für mich. Mehr ist technische Objektorientierung nicht. Alles andere ist nice to have, aber nicht wirklich essenziell.

Allozierbarer Speicherbereich: Ein Speicherbereich, den Code durch einen Befehl für sich reklamieren kann; der Code erhält ein Handle für diesen Speicherbereich, so dass er ihn benutzen kann. Der Speicherbereich wird nach einem Schema strukturiert – aber auch das ist nicht zentral für die Objektorientierung.
Da der allozierende Code nur ein Handle auf den Speicherbereich bekommt, weiß er nicht, wo genau der Speicherbereich liegt. Er hat also keinen direkten Zugriff auf ihn.

Interface: Eine Menge von Prozeduren und Funktionen. Nur die Prozeduren und Funktionen des bei der Allokation eines Speicherbereichs angegebenen Interface haben Zugriff auf ihn.

Technische Objektorientierung verbindet also Speicher mit Funktionalität, die auf diesem Speicher arbeitet.

Wie passen dazu aber…

  • Klassen: Klassen sind die Kombination von Schema und Interface. Ihre Felder strukturieren Speicherplatz und ihre Methoden bilden das default Interface für die Arbeit mit dem strukturierten Speicherplatz.
    Speicherallokation findet statt unter Angabe einer Klasse. Sie instanziiert eine Klasse zu einem…
  • Objekte: Objekte sind allozierter, hinter einem Interface verborgener Speicher.
  • Felder: Bereiche mit spezieller Semantik im allozierten Speicherbereich. Das Schema für Speicherbereiche besteht aus Felddefinitionen.
    Von außen sichtbare Felder (public, internal) widersprechen der obigen Definition von technischer Objektorientierung.
  • Prozeduren/Funktionen: Objekte kommunizieren via Nachrichten. Prozeduren und Funktionen sind die nachrichtenverarbeitenden Funktionseinheiten auf Interfaces.
    Ob die Kontinuität der Prozedur/Funktionssyntax von C eine gute Wahl zur Definition dieser Funktionseinheiten war, lasse ich mal dahingestellt.
  • Properties: Prozeduren bzw. Funktionen, die suggerieren, dass ein Durchgriff auf den Speicherbereich von außen möglich ist.
    Von außen sichtbare Properties (public, internal) widersprechen nicht direkt der obigen Definition von technischer Objektorientierung, da es sich ja um Methoden handelt. Dennoch ist Vorsicht angezeigt, denn wo Properties ins Spiel kommen, liegt es nahe, dass Schemadetails nach außen sichtbar gemacht werden.
  • Vererbung: Vererbung ist für die obige Definition von Objektorientierung nicht essenziell. Sie ist nice to have, um hier und da Schemata oder Interfaces wiederzuverwenden.
  • Polymorphie: Polymorphie gehört auch nicht essenziell zur Objektorientierung nach obiger Definition. Sie ist nice to have, um denselben Speicherbereich in unterschiedlichen Zusammenhängen auch durch unterschiedliche Interfaces gekapselt angesprechen zu können.
  • Dynamische Programmierung: Ob das Interface, hinter dem ein Speicherbereich versteckt ist, fix oder dynamisch ist, ist unwesentlich für die obige Definition von Objektorientierung. In einigen Situation ist solche Dynamik nice to have.

Von der Wikipedia-Definition der Objektorientierung bleibt für mich also nur die Kapselung als essenziell übrig. Damit möchte ich nicht den Nutzen von Polymorphie oder Vererbung negieren, sondern nur fokussieren. Um was geht es wirklich, wirklich ganz fundamental bei der Objektorientierung? Eben um hinter einem Interface verborgenen nach einem Schema allozierten Speicher. Nicht mehr, nicht weniger.

Ich würde daher eigentlich auch gern den Begriff “Objekt” aus der Objektorientierung herausnehmen. Er ist mir zu suggestiv. Die scheinbare Entsprechung von programmiersprachlichen Objekten zu realweltlichen hat schon viel Schaden angerichtet.

Für mich geht es eher um Funktionale Strukturen oder so, d.h. nach Schema strukturierte Speicherbereiche wie sie schon C und Pascal kannten – die nun aber eben reflexive Funktionalität tragen. Das gab es in C und Pascal nicht.

Ob der Speicherbereich umfangreich oder detailliert strukturiert ist… das hängt von seinem Zweck ab. Er kann 0 Bytes umfassen oder 2 GB; sein Schema kann kein Feld oder hunderte Felder definieren.

Wie der Umfang des Interface aussieht, ist jedoch relevant. Ist das Interface leer und liegt die Struktur damit zwangsläufig offen (sonst hätte die Umwelt ja keinen Zugriff darauf), dann handelt es sich im Grunde nicht mehr um Objektorientierung. Ihr zentraler Zweck, die Kapselung, wird dann ja nicht mehr verfolgt. (Standardfunktionen wie Equals() oder ToString() zähle ich hier mal nicht mit. Das wirkliche Interface eines Objektes beginnt erst jenseits von ihnen.)

Die Entwicklung hin zur Objektorientierung ähnelt mithin ein wenig der Entwicklung primitiven Lebens. Dessen Evolution führte von einzelnen Molekülen über semilebendige Ansammlungen (die heutigen Zellorganellen) zu Zellen. Der entscheidende Schritt war dabei der der Entwicklung einer Membran, d.h. die Schaffung einer Blase. Die Membran oder Zellwand trennt Innenwelt von Außenwelt. Sie entkoppelt das Zellinnere von der Umwelt und ermöglicht damit den Aufbau von Zustand in gewisser Unabhängigkeit.

image

Das Wesentliche an Objektorientierung ist also, dass sie Speicherplatzdetails (Ort und Schema) von der Umwelt, d.h. seiner Verwendung entkoppelt. Objektorientierung dreht sich um Kapselung, Kapselung, Kapselung. Und alles, was die Kapselung aufweicht, widerspricht ihr.

Nicht, dass ungekapselte strukturierte Speicherbereiche sinnlos wären. Man spricht dann nur besser nicht mehr von Objektorientierung.

Ein weiteres Argument spricht dafür, dass offengelegte Speicherbereiche keine Objekte sind. Die Kommunikation mit ihnen erfolgt dann nämlich nicht mehr über Nachrichten. Sie kann es nicht, weil Nachrichten Funktionalität zur Verarbeitung brauchen. Eine Nachricht selbst besteht ja nur aus Daten. Ohne passende Interfacemethode können die aber nicht verarbeitet werden. Daten direkt in einen Speicherbereich zu legen, ist keine Funktionalität, die ein Objekt bräuchte; das konnten schon C und Pascal.

Technisch essenzielle Objektorientierung braucht also sehr wenig. Sie kann mit einer Untermene von C# oder Java betrieben werden. Klassen, Methoden, private/protected Felder, Interfaces – das ist es. Vererbung, Properties… nicht nötig. Für den Zweck der Kapselung reichen wenige Mittel. Alles andere ist nice to have; wir sollten uns darüber nicht die Köpfe heiß reden. Das wäre am wirklich spannenden Thema vorbei. Das ist nämlich: Wie sollte mit technischer Objektorientierung umgegangen werden?

Weiche Objektorientierung – Methode

Von der technischen Objektorientierung ist zu trennen der Umgang mit ihr. Technische Objekte sind “leer”, d.h. sie geben nicht vor, wie man sie benutzt. Technische Objekte sind sozusagen nur “kleine C Programme” (d.h. Einheiten von Daten und Funktionalität). Die Frage ist also: Wie sollte ein Problem in viele, viele “kleine C Programme” zerlegt werden, damit etwas qualitativ hochwertiges herauskommt?

Dazu gibt es mehrere Ansätze. Der am weitesten verbreitete ist der von Objektorientierte Analyse und Design (OOAD) – oder noch nicht einmal. Verbreitet ist eher eine stark abgespeckte Version davon, sozusagen OOAD ultra light (OOADUL) :-)

Das primäre Entwurfsmittel von OOADUL – wenn denn überhaupt expliziter Codeentwurf stattfindet – ist das Klassendiagramm. Und das primäre Vorgehen ist die Suche nach Substantiven in Anforderungen. Bei einem Tic Tac Toe Spiel würden sofort Spiel, Spieler, Spielbrett, Spielstein als Objektkandidaten identifiziert werden – um dann auf ihnen irgendwie erstens Zustand und zweitens Funktionalität zu verteilen.

Die Prämisse bei OOADUL lautet: Software besteht aus Objekten, die nahe an der realen Welt sind. Was in der Problemdomäne zu sehen und anzufassen ist, das liegt nahe in problemlösenden Software als Objekte repräsentiert zu sein.

An dieser Stelle will ich diesen Ansatz nicht bewerten, sondern ihn nur darstellen. Mir ist im Augenblick wichtiger, ihn als Methode zu trennen von der technischen Objektorientierung.

Zusammenschau

Konzeptionelle/methodische Objektorientierung und technische Objektorientierung sind zwei Paar Schuhe. Man kann auch methodisch objektorientiert Software planen – und dann mit einer technisch nicht objektorientierten Programmiersprache umsetzen. Und umgekehrt.

Technische Objektorientierung ist ein leeres oder neutrales Werkzeug. Es dient durch die Kombination von Daten und Funktionalität im Verein mit der konsequenten Kapselung dem wohl ältesten Prinzip der Softwareentwicklung: der Entkopplung. “Neumodischer Krams” wie Properties oder auch Altmodisches wie öffentliche Felder widersprechen dem jedoch. Deshalb kann man sie trotzdem anwenden – muss nur eben wissen, dass man sich damit jenseits der Objektorientierung bewegt.

Zurück zum Ausgangspunkt: die oft erhitzten Gemüter. Wo sich nun die Gemüter über die Objektorientierung erhitzen, da sollte als erstes gefragt werden, worum es geht. Erhitzt man sich über die Methode oder die Technik?

Mein Gefühl ist, dass die Diskussionen sich weniger um technische Objektorientierung drehen. Die, die am Wert der technischen Objektorientierung zweifeln, treffen einfach relativ selten auf Freunde der technischen Objektorientierung. Am Ende ist sie ja auch unkritisch. Solche Daten-Funktionalität-Blasen zu haben, ist einfach eine nützliche Sache. Warum darauf verzichten.

Viel schwieriger und inzwischen kontroverser ist jedoch, wie mit diesen Blasen umgehen? Hitzige Diskussionen entstehen, wenn die Methode angezweifelt wird. (Oder auch, wenn es um die Methode geht, aber jemand glaubt, es ginge um die Technik.)

Auch ich wende mich mit Flow-Design gegen die Methode und nicht gegen die Technik. Zukünftig will ich das noch besser deutlich machen, um Missverständnissen vorzubeugen. Flow-Design ist eine Methode, um Software zu entwerfen. Und Event-Based Components (EBC) sind eine Übersetzung von Flow-Designs in C# Code; da geht es also um die Nutzung objektorientierter Technik.

Wer Flow-Design für kritikwürdig hält, weil es nicht der (oder seiner Sicht auf die) Objektorientierung entspricht, der muss sich also mit seinen Argumenten auf der methodischen Ebene bewegen. Pauschal zu sagen, Flow-Design sei nicht objektorientiert, ist mithin falsch. Flow-Design ist nur methodisch nicht objektorientiert; und mit der Technik hat Flow-Design nichts zu schaffen.

Und wer Event-Based Components als nicht objektorientiert kritisiert, der muss sich auf Technik konzentrieren. Er muss zeigen, inwiefern EBC der technischen Objektorientierung widerspricht. Und das wird schwer, würde ich sagen :-)

Sonntag, 24. April 2011

Mehr als ein Job in der Softwareproduktion

Gibt es in der Softwareentwicklung nur Architekten? Jens Schauder meint es so, wenn er in seinem Blog schreibt:

“I currently work on a team of 8. 8 architects. […] There is no human construction worker involved. That is the part of the metaphor that falls apart. All the building happens by software and each developer is an architect.” [Meine Hervorhebung]

Hört sich irgendwie nett an. Klar, wer möchte nur Maurer sein, wenn er doch Architekt sein kann? Architekt klingt viel besser als Maurer oder Programmierer.

Ich halte diese Sicht jedoch für kontraproduktiv. Ebenso wie die simple Sicht, Code sei schlicht Design – und damit das Ende der Entwurfsfahnenstange erreicht (s. dazu auch diesen Blogartikel).

Wer die Softwareentwicklung runterbricht auf nur eine Tätigkeit – Architekt sein –, der stellt sie nicht nur undifferenziert dar, sondern verschenkt Chancen zur Verbesserung der Softwareentwicklung.

Schade, dass Jens nicht den Mut hat, konsequent zu sein. Denn eigentlich ist er nicht undifferenziert. Nach ihm ist der Job eines Architekten:

  • Anforderungsanalyse, “They analyse what the needs of the clients are”
  • Softwarerahmendefinition, “They make very detailed prescriptions about how the software is to be build”
  • Usability Design/GUI Programmierung, “They design carefully how the user will interact with the resulting program”
  • Build-/Releaseplanung, “They plan when what piece of the software gets build”
  • Codieren, “We write programs and build scripts”
  • Qualitätssicherung nicht funktionaler Anforderungen, “We carefully tune all things which the customer will see. Most notably the GUI, but also performance, stability, maintainability”

Jens sieht eine Menge verschiedener Tätigkeiten (die sogar noch weiter aufgefächert werden könnten) – doch für ihn gibt es nur einen Job, der sie erledigt: von der Kundeninteraktion bis zur Codeoptimierung.

Damit steht er nicht allein da, würde ich sagen. Andere geben dem Job vielleicht einen anderen Titel und nennen ihn nicht Architekt, sondern bescheidener einfach nur Softwareentwickler. Am Ergebnis ändert das nichts. Nach 60 Jahren Softwarebranche ist die Differenzierung damit nicht vorangekommen. Eine sich entwickelnde Branche verweigert sich damit förmlich bewusst der natürlichen Evolution jedes anderen Gewerbes.

Entweder, man ist Softwareentwickler/Softwarearchitekt – oder man gehört sozusagen nicht dazu. Alles, was damit zu tun hat, aus Anforderungen auslieferbaren Code zu machen, wird durch eine Jobbeschreibung abgedeckt.

Aber wer hätte das je bei etwas komplizierteren Gewerben wie der Gesundheitsbranche, dem Baugewerbe, dem Flugzeugbau, der Filmproduktion, der Lebensmittelproduktion oder sonstwas gehört?

“Gesundheit wird durch den Arzt hergestellt. Basta!”, “Die Butter wird vom Bauern hergestellt. Basta!”, “Den Film dreht der Regisseur. Basta!”

Ich denke, die Softwareentwicklung tut sich keinen Gefallen, aus einer grundsätzlich richtigen Analyse wie der von Reeves, dass Softwareentwicklung Design und nicht “normale” Produktion sei, zu folgern, dass alle irgendwie am Code beteiligten Architekten seien.

Solche enge Sichtweise ist kontraproduktiv aus mehreren Gründen:

  • Sie steht einer unabhängigen Weiterentwicklung der unzweifelhaft sehr verschiedenen Tätigkeiten entgegen, die zur Softwareentwicklung gehören. Wo alles auf einem Haufen liegt, fällt es schwer, etwas herauszulösen und sich konzentriert um seine Verbesserung zu kümmern.
  • Sie steht einer bewussten Modusänderung im Wege, die nötig ist, um die unterschiedlichen Tätigkeiten effektiv durchzuführen. Anforderungsanalyse ist schlicht etwas ganz anderes als Buildscriptbastelei.
  • Sie steht einer Ausbildung im Wege, die unterschiedlichen Persönlichkeitstypen unterschiedliche Tätigkeiten effizient nahebringt. Wenn es nur einen Job gibt, dann müssen alle den einen Job lernen. Wer aber visuell inkliniert ist und sich vor allem auf Usability konzentrieren will, der muss auch den ganzen Rest pauken; und es ist fraglich, ob er seiner Neigung fokussiert später nachgehen kann, weil ja alle alles machen müssen.
  • Sie steht einer gehaltsmäßigen Differenzierung im Wege, die in einer ohnehin an Aufstiegschancen schwachen Branche, nötig ist, um zukünftig mehr Menschen zu begeistern.

Dazu kommt, dass ich ganz einfach überhaupt keinen Vorteil darin sehe, erstens alle Softwareentwickler über einen Kamm zu scheren und zweitens sie auch noch alle zu jeder Zeit Architekt zu nennen. Was soll das? Hat das einen Vorteil in der Sache? Jens nennt keinen. Hat das einen Vorteil für die Personen? Jens nennt keinen. Gibt es ansonsten eine Not dazu? Jens nennt keine.

Meine Position ist daher der von Jens entgegengesetzt. Ich bin für ausdrückliche Differenzierung der Jobs in der Softwareentwicklung – oder besser in der Softwareproduktion. Denn um nichts anderes geht es: um die Produktion von Code aus Wünschen. Ideen, Wünsche, Vorstellungen werden in einem mehrstufigen Prozess in Softwareprodukte transformiert, die hoffentlich die Initiatoren und Geldgeber begeistern.

Weg mit dem allgemeinen Gerade über “Code ist Design und keine Produktion”. Klar, das ist irgendwo richtig und Unterschiede gegenüber Schusterhandwerk oder Autoindustrie sind deutlich zu machen. Deshalb aber nur noch von Architektur zu reden, schüttet das Kind allerdings mit dem Bade aus.

Wenn wir Softwareentwicklung nicht anfangen als Produktionsprozess zu sehen, der methodisch und technisch stetig verbessert werden kann, dann haben wir keinen Ansatz für Veränderungen. Dann sind Veränderungen anekotisch und subjektiv. Dann klappt etwas für den einen, für den anderen aber nicht gleich, also lässt der es sein. Allemal ist dann unklar, wo mit Veränderungen angefangen werden sollte. Solange alles undifferenziert irgendwie Teil desselben Jobs ist, ist alles gleich, was irgendwie nach Verbesserung riecht. Expression Blend steht dann auf einer Stufe mit Scrum oder Unit Tests oder Continuous Integration oder WCF oder zwei Monitoren für jeden Arbeitsplatz.

Und so haben wir uns auch in den letzten Jahren verhalten. Undifferenziert, unsystematisch. Das Ergebnis ist denn auch allerorten zu sehen: schlechte Qualität. Hohe Bugzahlen, Unwartbarkeit, Lieferverzug, Überstunden, Unsicherheit und Uneinigkeit in Entwicklergruppen (von Teams mag ich kaum reden)… das ist Normalität.

Ja, ich denke, der Grund für all das ist fundamental in einem zu suchen: in mangelnder Systematik. Man geht die Softwareentwicklung weitgehen irgendwie an. Jeder nach seiner Manier, jeder aus seiner begrenzten Perspektive. Die Welt der Projekte ist so groß wie der Teller Suppe, in dem die Mitglieder schwimmen. Darin strampelt man sich ab. Unter Druck, mit wenig Fortbildung, mit Fokus auf Code.

Das geht. Irgendwie. Muss ja auch.

Dabei könnte es mir viel weniger Schmerzen noch besser gehen. Schneller, leichter, befriedigender. Doch dafür müssen die Softwareentwickler den Kopf recken und über ihren Tellerrand blicken. Sie müssen die Komplexität des Business ernst nehmen und den Weg gehen, den alle anderen komplizierten Businesses auch gegangen sind: den Weg der Systematisierung und der Differenzierung.

Einen Vorschlag für einen systematischeren Blick auf die Softwareproduktion habe ich in einem früheren Blogartikel gemacht. Da gibt es sieben Jobs auf dem Weg von der Anforderung bis zum auslieferbaren Code:

  1. Anforderungsanalyst
  2. Architekt
  3. Modellierer
  4. Arbeitsvorbereiter
  5. Codierer
  6. Prüfer
  7. Qualitätssicherer

Sieben Jobs, sieben ganz unterschiedliche Tätigkeiten, sieben Persönlichkeitstypen, sieben Verantwortlichkeiten, sieben Denk-/Arbeitsmodi…

Wenn das keine gute Voraussetzung für gezielte Verbesserungen ist, weiß ich auch nicht mehr. Wer so unterscheidet, kann viel besser Fragen stellen und Qualität prüfen. Wer so unterscheidet, kann viel besser Engpässe feststellen und optimieren.

Softwareentwicklung ist mehr als ein Job; alles andere ist eine kontraproduktive Wunschvorstellung. Ob diese Jobs dann in Personalunion, allein oder in einer Gruppe erledigt werden, ist eine ganz andere Frage. Begrenzte Ressourcen und Psychologie stellen dafür die Spielregeln auf.

Unzweifelhaft ist jedoch, so denke ich, dass es eben mindestens diese verschiedenen Jobs gibt. Nicht alles, was in der Softwareproduktion passiert, ist Architektur. Und vor allem nicht alles ist Design oder Codierung.

Wer sich fragt, ob Softwareentwicklung für ihn etwas sei, muss sich also nicht fragen, ob der die nächsten Jahrzehnte “Code Slinger” sein will. Softwareentwicklung macht ein vielfältiges Jobangebot. Da ist für jeden etwas dabei, auch wenn man nicht Architekt werden will.

Samstag, 23. April 2011

Lernkartei III – Vom Stapel lernen

Die Lernkartei “zuckt schon”, wie der vorherige Artikel beschrieben hat. Als Anwender kann ich im Lernmodus Karteikarten “durchblättern”, also schon beurteilen, ob mir Darstellung und Interaktionen gefallen. Lernen, im Sinne einer Wiedervorlage von nicht gewussten Antworten, kann ich mit dem Programm allerdings noch nicht. Das soll in der zweiten Iteration hier nun nachgerüstet werden.

Arbeiten mit der Lernkartei

Bevor ich mich aber in die Modellierung stürze, muss ich die Anforderungen verstehen. Was bedeutet denn das Lernen mit Karteikarten konkret, wie gehe ich dabei vor? Wie soll sich das Programm verhalten?

Struktur der Lernkartei

Ich stelle mir das so vor:

image

Die Karten, die ich lerne, nehme ich von einem Stapel (Batch). Der enthält für einen Stapeldurchlauf (Batch Run) nicht zuviele Karten.

Gefüllt wird der Stapel aus Fächern (Compartment). Es gibt n+1 Fächer, von denen das 0-te eine besondere Bedeutung hat. Das 0-te Fach ist die Halde (Heap). Zu Beginn des Lernens mit einer Lernkartei liegen alle Karteikarten in der Halde.

Wenn ich frische Karteikarten für den Stapel brauche (weil in den anderen Fächern gerade keine zum Lernen anstehen), nehme ich sie von der Halde. Und vom Stapel wandern sie in die Fächer 1..n. Dazu später mehr.

Eine Karteikarte, deren Antwort ich weiß, die aus dem Fach n kommt, geht schließlich ins Archiv (Archive). Sie verlässt damit die Fächer und wird nicht wieder vorgelegt.

Lernen ist mithin der Prozess, der die Halde ins Archiv transferiert.

Lernalgorithmus

Die Karten, die ich lernen will, liegen auf dem Stapel. Der sollte nicht zu hoch sein, damit nicht gewusste Karten immer wieder mal angeboten werden. Ich sag mal, mehr als 20-25 Karten liegen nicht auf dem Stapel.

Vom Stapel lerne ich solange, bis nur 3 Karten darauf sind. Denn ab 3 Karten ist die Wiedervorlage nicht gewusster Karten so zügig, dass sie mir beim Lernen nicht mehr wirklich hilft.

Lernen vom Stapel bedeutet:

  1. Ich nehme die oberste Karteikarte und schaue mir die Frage an.
  2. Dann schaue ich mir die Antwort an und beurteile, ob ich sie gewusst habe.
    • Wenn ich die Antwort gewusst habe, stecke ich die Karte ein Fach weiter. Jede Karte kommt aus einem Fach auf den Stapel; ich weiß also, welches Fach für sie dann das nächste Fach ist. Gewusste Karten wandern so von Fach zu Fach bis ins Archiv.
      Karte aus Fach f kommt auf den Stapel und wenn ich ihre Antwort weiß, vom Stapel in f+1 (bzw. ins Archiv).
    • Wenn ich die Antwort nicht gewusst habe, stecke ich die Karte ans Ende des Stapels. Sie “blubbert” dann langsam wieder an seine Oberfläche, so dass ich mich mit ihr früher oder später wieder beschäftige.
      Karten aus Fach f, deren Antwort ich nicht weiß, verlieren ihre Herkunft und werden zurückgestuft auf Fach 1.
  3. Wenn ich mit dem Lernen aufhöre und noch Karten auf dem Stapel sind, kommen die zurück in Fach 1.

image

Wenn der Stapel am Anfang des Lernens leer ist oder immer wenn er während des Lernens die minimale Anzahl an Karten erreicht, fülle ich ihn aus den Fächern wie folgt:

  1. Alle Karten, die in Fach 1 sind, kommen auf den Stapel.
  2. Wenn noch Platz auf dem Stapel ist, dann fülle ich ihn mit Karten aus dem letzten Fach, das voll ist. Die Fächer haben eine steigende Kapazität, damit Karten immer seltener zum Lernen vorgelegt werden. Das ist ja der Trick am Lernen mit der Lernkartei. Fach 1 hat eine Kapazität wie der Stapel k1=ks. Die weiteren Fächergrößen verdoppeln die Kapazität, k2=40, k3=80, k4=160, k5=320.
    Ich schaue also zuerst, ob Fach n voll ist, wenn nicht, dann ob Fach n-1 voll ist usw. Fach 1 ist zu diesem Zeitpunkt immer leer (s. Schritt 1). Aber Fach 0, die Halde, ist immer voll, egal wieviele Kartei noch auf Halde liegen.
    Dieses Vorgehen sichert zu, dass volle Fächer langsam abgearbeitet werden und dass immer wieder neue Karten von der Halde “ins Spiel kommen”. Schonmal gewusste Karten (in vollen Fächern) haben also Vorrang vor Karten von der Halde.
  3. Falls der Stapel immer noch nicht gefüllt ist (weil die Halde leer ist und kein anderes Fach voll), wird er aus den Fächern in der Reihenfolge 2..n bestückt.

Ich finde den Umgang mit der Lernkartei (Flash Card Box) in dieser Weise geradlinig. Gelernt werden die Karten, die ich sozusagen in der Hand halte (Stapel) und gespeist wird der Lernstoff aus dem Karteikasten, d.h. den Karten, die ich schonmal in der Hand hatte, oder der Halde. Wenn ich neuen Lernstoff brauche, greife ich einfach in die Fächer.

Flow modellieren

Bisher sieht der Flow für das Lernen so aus:

image

Die Karten, die am Ende herauskommen, sind “Zufallsprodukte” von Get next card.

Ab jetzt sollen die Karten jedoch vom Stapel kommen. Advance card muss die aktuelle Karte entweder unter den Stapel schieben (wenn Antwort nicht gewusst) oder vom Stapel nehmen und ein Fach weiter stecken. Und Get next card muss die nächste Karte vom Stapel holen bzw. ggf. den Stapel neu füllen.

Beide Funktionseinheiten müssen deshalb den Stapel kennen:

image

Die kleine Tonne an den Funktionseinheiten ist die Kurzschreibweise für eine Abhängigkeit.  Advance card und Get next card sind also abhängig vom Stapel, sie haben damit gemeinsamen Zustand.

Fragt sich jetzt nur, wie der Stapel initial gefüllt wird und wie die erste Karte bei Start der Anwendung in den View bzw. das ViewModel kommt. Dafür ist ein “Nebenfluss” nötig:

image

Open box öffnet die Lernkartei (Flash Card Box) und sorgt dafür, dass Get next card die erste Karte vom Stapel ans ViewModel schickt.

Achten Sie auf das (C) bei Open box, es zeigt an, dass die Funktionseinheit in der Config-Phase des Programmstarts ausgeführt wird. Zu dem Zeitpunkt sind alle Funktionseinheiten erzeugt, gebunden und mit ihren Abhängigkeiten versorgt (Phasen Build, Bind, Inject).

Auf die Config-Phase folgt dann die Run-Phase, die eine ausgezeichnete Funktionseinheit startet, so dass es auch für den Anwender losgeht.

image

Der View selbst ist diese ausgezeichnete Funktionseinheit, der EntryPoint für die Flows.

Feature Slicing

Da ich noch keine echten Karteikarten habe, füllt Open box die Lernkartei mit Dummy-Karten. Im Fokus dieser Iteration ist das Vorgehen beim Lernen; dafür brauche ich als Anwender noch keine echten Karten, sondern muss nur beurteilen können, ob das Programm gem. Algorithmus mit der Lernkartei umgeht.

Für diese Iteration specke ich sogar noch weiter ab. Das Programm soll noch nicht einmal den ganzen Lernalgorithmus implementieren, sondern nur das Lernen vom Stapel. Die Karten werden also noch nicht aus Fächern geholt und auch nicht weitergesteckt.

Aus dem ganzen Feature “Lernen nach Lernalgorithmus” schneide ich mir nur eine dünne Scheibe (Feature Slice), um schneller etwas auf die Straße zu bekommen. Das Modell erfährt dadurch schon eine wichtige Erweiterung (es kommt Zustand hinzu, der Zustand wird initialisiert, der ganze Programmstart bekommt mehr Systematik) und als Anwender habe ich einen überschaubaren sowie schnell überprüfbaren Nutzenzuwachs.

Daten modellieren

Bisher waren die Daten, die da im Flow flossen, sehr einfach. Die Karte enthielt nur zwei Felder: Frage und Antwort. Dazu war nicht viel zu sagen. Doch jetzt kommt einiges hinzu: Stapel, Fächer, Archiv. Ein explizites Datenmodell lohnt sich daher:

image

Die Flash Card Box ist die Spinne im Netz. Sie zieht Stapel, Fächer und Archiv zusammen. Und mehr nicht.

Alle Datenfunktionseinheiten haben möglichst simple, auf den hiesigen Zweck zugeschnittene Schnittstellen.

Für diese Iteration brauche ich allerdings nur die Flash Card Box, Batch und Batch Card. Card habe ich schon.

Als Notation für das Datenmodell habe ich bewusst die “Krähenfußnotation” gewählt (mit etwas API-Zucker oben drauf). Ich stimme nämlich Jim Stewart zu, dass die am leichtesten verständlich ist.

Ebenfalls bewusst habe ich im Diagramm auch Funktionseinheiten wiederholt (Card). Viele Linien, die alle auf den selben Kasten weisen, finde ich verwirrend. Sie machen das Verstehen von Diagrammteilen schwieriger und suggerieren Abhängigkeiten, wo keine sind.

Feature Slice implementieren

Plan a little, code a little. So geht die Implementierung für das Feature Slice leicht von der Hand. An den Kontrakten ist nur wenig zu machen:

image

Und der Code für die Datentypen ist ganz einfach, weil er im Grunde nur eine Queue kapselt. Spannender ist da schon die Implementierung für die Aktionen Advance card usw. Die sind ja nun zustandsbehaftet:

image

Da schien mir ausnahmsweise mal eine Ableitung angebracht. Alle Aktionen, die sich eine FlashCardBox als Instanz teilen, erben von FlashCardBoxEntity. Die Klasse implementiert IDependsOn<T> und enthält eine Variable für den Zustand:

image

So werden die Ationen übersichtlicher, weil sie sich aufs Wesentliche konzentrieren:

image

Die Initialisierung erfolgt in der Startup-Phase Inject:

image

Wenn Sie genau hinschauen, sind die Aktionen jedoch nicht direkt von FlashCardBox abhängig, sondern von SharedState<FlashCardBox>. Warum das? Weil nur so es möglich ist, dass Open box für alle anderen den Zustand erzeugt und setzt.

image

Natürlich hätte außerhalb eine Instanz von FlashCardBox erzeugt und in alle injiziert werden können, doch dann hätte die einen parameterlosen Ctor haben müssen. Das fand ich unschön. Mit dieser Lösung jedoch ist es erstens möglich, die Lernkartei-Instanz auszutauschen und zweitens die Initialisierung mit einem Ctor sehr schön in einer Aktion zu kapseln.

Zwischenstand

Der Code ist wie immer im Mercurial Repository zu finden: http://code.google.com/p/wpfflashcards/

Mir hat diese Iteration Spaß gemacht. Der entsprang besonders der Auflösung einer Spannung, in die ich mich hineinmanövriert hatte. Zu Anfang hatte ich nämlich geplant, das komplette Feature zu implementieren. Ich hatte es auch modelliert. Aber dann… war mir die Zeit zur Implementierung zu knapp. Ich fühlte mich unwohl. Sollte ich versuchen, alles huschhusch runterzucoden?

Doch dann habe ich mich zum Feature Slicing entschieden. Warum nicht aus dem Gesamtmodell für das Lernen nur den Stapel herauslösen und implementieren? Ja, warum eigentlich nicht? Wenn unter Druck, dann ist das ein probates Mittel, ihn zu reduzieren: einfach die Nutzenscheibe dünner schneiden. Das ist viel besser, als mit dem ganzen Feature anzufangen und nicht fertig zu werden.

Aus Anwendersicht ist nicht soviel auf die Straße gekommen, doch das, was da ist, ist solide gemacht. Das Modell ist sauber und zukunftstauglich. Und die Implementierung ist durch Tests gestützt.

Aus Entwicklersicht kann ich zufrieden sein, weil ich Nutzen geliefert habe. Und gleichzeitig habe ich das Modell insgesamt runder gemacht, weil nun ein vernünftiger Anfang (Open box) für die Daten da ist. Die muss ich nun nicht mehr wie in der ersten Iteration in den Flow “hineinmogeln”.

Mal schauen, was beim nächsten Mal dran ist. Wahrscheinlich werde ich das Lernen komplettieren mit Halde, Fächern und Archiv.

Freitag, 15. April 2011

Lernkartei II – Erste Lerninteraktion

Wie ist eigentlich das Vorgehen beim Flow-Design und der Umsetzung mit Event-Based Components? Das – so hatte ich in einem früheren Posting angekündigt – möchte ich mal anhand einer Beispielanwendung zeigen. Das Szenario ist einfach zu verstehen, aber nicht trivial: ein Lernkarteiprogramm. Ich würde sogar sagen, nach oben gibt es da nicht so bald eine Grenze. Das könnte ich sogar mit der Cloud verbinden ;-)

Aber erstmal klein anfangen. Einen Schritt nach dem anderen. Heute nehme ich mir nur eine dünne Featurescheibe für Modellierung und Implementierung vor:

Der Anwender soll das Programm aufrufen und mit einer Lernkartei interagieren können. Es sollen ihm Fragen präsentiert werden und er kann bewerten, ob er die Antworten gewusst hat oder nicht.

Es geht mir nur darum, die Interaktion zwischen Anwender und Programm während des Lernens umzusetzen, ohne mich dabei dumm zu stellen. Das Ergebnis soll kein Prototyp sein, aber auch nicht die volle Abfragefunktionalität enthalten. Eben nur eine dünne Scheibe vom kompletten Feature “Karten abfragen”. Die Musik spielt zwar im GUI, doch dahinter soll auch schon rudimentäre Logik stehen.

Vor Modellierung und Implementation haben die Softwaregötter jedoch die Architektur gestellt. Einen groben Entwurf in Bezug auf die nicht-funktionalen Anforderungen will ich also auch liefern.

Architektur

Die Architektur beginnt für mich immer mit einem System-Umwelt-Diagramm:

image

Es zeigt das Softwaresystem als Ganzes in der Mitte und drumherum sowohl die Rollen, die damit arbeiten, wie die Ressourcen, auf die es zugreifen muss. In diesem Fall ist die Lage sehr simpel:

  • Es gibt nur eine Anwenderrolle, den Lernenden
  • Es gibt Lernkarteien auf der Festplatte (FlashCardBox)
  • Es gibt Karteikartenstapel auf der Festplatte (FlashCardFile), die in Lernkarteien importiert werden können; so wie man Karteikärtchen in den Lernkarteikasten steckt

FlashCardFiles sind Textdateien, wie den Anforderungen im vorherigen Artikel zu entnehmen ist. FlashCardBoxes denke ich mir derzeit als XML-Dateien.

Die Interaktion mit dem Benutzer erfolgt über ein WPF-GUI. Das Anwendungssystem besteht mithin aus nur einem Betriebssystemprozess, einer EXE.

Daraus ergibt sich eine Zerlegung in Belange (Concerns) wie folgt:

image

Die Anwendung (WPF FlashCard, WPFFC) zerfällt in “Codekategorien” für die WPF-Interaktion und den Zugriff auf die Ressourcen. Und das WPF-GUI Belang zerfällt nochmal in einen View und ein ViewModel. Ich folge hier also dem MVVM-Pattern. Rollen und Ressourcen treiben die Belangidentifikation.

Belange sind allerdings nur Geschmacksrichtungen oder Farben für Code und keine Codecontainer. Architektur produziert keinen Code, sondern nur einen (gedanklichen) Rahmen dafür. In den müssen sich das nachfolgende Modell und der Code einpassen.

Ab jetzt habe ich eine Erwartung, bei der Modellierung auf Funktionseinheiten zu stoßen, die keine funktionale Anforderung erfüllen, sondern zu einem dieser nicht-funktionalen Belange gehören. Die Belange geben mir ein Messer in die Hand, mit dem ich Lösungsideen zerlegen kann (und muss); sie geben vor, in welche grundsätzlich verschiedenen Kategorien ist Code einteilen sollte.

GUI-Skizze

Der Entwurf des Codes für die funktionalen Anforderungen beginnt immer bei der Benutzeroberfläche. Nur, was über eine Interaktion angestoßen wird, muss realisiert werden. Deshalb sollte am Anfang der Modellierung eine GUI-Skizze stehen.

Wie stelle ich mir (oder der Kunde/Anwender sich) die Benutzeroberfläche vor? Das Wichtigste dabei: Welche grundsätzlichen Interaktionen gibt es? Es geht also nicht darum, welche Buttons oder Menüpunkte es in einem Dialog gibt, sondern nur, welche Kommandos irgendwie ausgelöst werden sollen.

image

Für den Längsschnitt heute denke ich mir die Benutzerschnittstelle so:

  • Frage und Antwort werden in zwei Kästchen gezeigt, die Vorder- und Rückseite einer Karteikarte darstellen. Die Antwort ist aber erst zu sehen, wenn man sie ausdrücklich aufdeckt. Bis dahin steht an ihrer Stelle nur ein Fragezeichen.
  • Nach Aufdecken der Antwort kann man sich selbst einschätzen: Hat man die Antwort gewusst oder nicht. Dafür reichen erstmal zwei Buttons.

Es gibt also nur zwei grundsätzliche Interaktionen: Antwort zeigen (ShowAnswer) und Selbsteinschätzung (ScoreKnowledge).

Nach der Selbsteinschätzung wird dann die nächste Karte in der aktuellen Lernkartei abgefragt.

Modell

Ausgehend von der GUI-Skizze kann ich mit der Modellierung beginnen. Die erste Funktionseinheit ist das GUI oder genauer, der View im GUI. Der View zeigt aber nur an und ist Interaktionspartner, darin steht jedoch kein Code. Codebehind will ich mir verkneifen. Gerade mit WPF sollte das möglich sein. Der View ist also reines XAML.

Das heißt, die Daten, die angezeigt werden sollen und die Kommandos, die bei Interaktion ausgeführt werden sollen, müssen in anderen Funktionseinheiten stehen. Das ist das ViewModel.

image

Die Skizze zeigt, dass der View abhängig ist von den ViewModel-Kommandos (rechts) und den ViewModel-Daten (unten). (Und die Kommandos kennen durchaus auch die Daten, um darauf unmittelbar einwirken zu können, falls es um etwas sehr Einfaches geht.)

So die grundsätzliche Trennung nach dem MVVM-Pattern.

Zu beachten: Bei aller Flussorientierung sind hier noch Abhängigkeiten zu finden. Die sind jedoch technologiebedingt unvermeidbar. Um Kommandos und Daten deklarativ an View-Elemente binden zu können, muss der View abhängig davon sein. Aber das macht auch nichts, weil Kommandos und Daten erstens einfach sind und zweitens der View auch keine Logik enthält.

Jetzt zur Modellierung der Kommandoflüsse. Erst jenseits der ViewModels wird es interessant.

Beim Kommando zum Anzeigen der Antwort verzichte ich auf einen Fluss. Es ist trivial, da es nur die Sichtbarkeit der Antwort setzt und die das Kommando zur Selbstbewertung anschaltet. Es agiert ausschließlich auf dem ViewModel.

Das Kommando zur Selbstbewertung ist da schon interessanter. Im Moment soll zwar noch nicht viel passieren; es gibt ja noch keine echten Lernkarteien. Dennoch möchte ich die grundsätzlichen Prozessschritte modellieren:

image

Das Kommando wird für beide Bewertungen ausgelöst, einmal mit dem Parameterwert True (wenn die Antwort gewusst wurde), einmal mit False. Selbst ist es aber nicht dafür verantwortlich, die Funktionalen Anforderungen zu erfüllen. Das soll Domänenlogik tun.

Und was soll passieren bei einer Selbstbewertung? Zuerst soll die gerade abgefragte Karte in der Lernkartei passend zur Bewertung verschoben werden; wurde die Antwort gewusst, wandert die Karte in das nächste Fach, wurde sie nicht gewusst, dann zurück ins erste Fach. Danach soll die nächste Karte aus der Lernkartei abgefragt werden.

Welche Karte “die nächste” ist, ist heute noch nicht wichtig. Irgendein Algorithmus wird darüber später entscheiden. Mir reicht es für heute, dass es eine nächste Karte geben muss. (Hm… was soll eigentlich passieren, wenn alle Karten gelernt wurden und im Archiv liegen? Dann gibt es keine nächste Karte. Darüber mache ich mir jetzt aber keine weiteren Gedanken; in diese Glaskugel will ich nicht schauen.)

Diese beiden Schritte der Domänenlogik habe ich im Modell eingezeichnet. Advance_card steckt die aktuelle Karte je nach Bewertung weiter. Get_next_card holt anschließend die nächste.  Und am Schluss wird die neue aktuelle Karte ins ViewModel eingetragen; dafür gibt es eine extra Funktionseinheit, einen Mapper. Der entkoppelt ViewModel und DomainModel.

Mapper und View sind also beide abhängig vom ViewModel. Das fühlt sie wie eine saubere Klammer um die Domänenlogik an.

Arbeitsorganisation

Ich arbeite allein am Code. Deshalb will ich mich hier nicht so mit der Arbeitsorganisation beschäftigen. Komponenten im Sinne VS Projektmappen spare ich mir im Augenblick also.

Dennoch will ich natürlich den Code sauber strukturieren. Also bringe ich mal die Belange der Architektur in Anschlag und teile die Funktionseinheiten zu.

  • Der View gehört zum Belang WPF GUI/View und kommt in ein eigenes Projekt.
  • Das Mapping gehört zum View bzw. zum ViewModel. Ich stecke es auch in ein eigenes Projekt. Weitere Mappings mögen dazu kommen. Zum ViewModel packe ich es nicht, denn…
  • …das ViewModel (Daten und Kommandos) ist für mich ein Kontrakt und damit in einem eigenen Projekt. Dito die Klasse zur Repräsentation einer Karte (Card).
  • Und die beiden Domänenlogik-Funktionseinheiten fasse ich auch zusammen in einem Projekt, sie bilden den Grundstock für die FlashCardBox.

Damit ergibt sich eine Projektmappenstruktur wie folgt:

image

Die Kontrakte sind derzeit noch am umfangreichsten. Aber das macht nichts. Sie sind dafür ganz einfach.

Alles steckt zusammen in einer Projektmappe, weil ich der einzige bin, der daran arbeitet. Und ich bin diszipliniert genug – nehme ich mal an ;-) -, nicht “zu luschern”, wenn ich an einer Funktionseinheit sitze. Und die expliziten Kontrakte tun ihr Übrigens, um die entworfene Entkopplung bei der Implementierung aufrecht zu halten. Ich kann mir also das bisschen mehr Bequemlichkeit leisten, das eine Projektmappe für alles bringt. But don´t try this at home ;-) In Ihren Projekten, an denen viele Entwickler sitzen, sollten Sie das nicht tun.

Implementierung

Die Implementierung ist derzeit trivial. Einzig der View hat Mühe gemacht. Ein elendes Gewiggel ist das mit dem WPF ;-) Wenigstens ist er rein deklarativ geblieben. Allerdings war ich mir unsicher, ob die ganzen Bindungen ans ViewModel passen; deshalb habe ich ein kleines Testprojekt aufgesetzt.

Das App-Projekt ist der Startpunkt. Dort werden die Flow-Funktionseinheiten zusammengesteckt und der View geöffnet.

image

Das ist überschaubares Event-Based Components “Plumbing” würde ich sagen.

Eine Unsauberkeit steckt allerdings noch drin: die Initialisierung des Views mit einer Karte. Ich habe die Start-Interaktion nicht modelliert, deshalb gibt es aus der Domänenlogik keine erste Karte. Für den Moment ist das ok, denke ich. Denn auch die weiteren Karten sind ja keine echten, sondern nur Dummykarten. So sieht der GUI dann nach Aufdecken einer Dummykarte aus:

image

Nicht superschön, aber funktional. An “schön” kann sich gern jemand mit Designambitionen versuchen. Der Code liegt bei Google in einem Mercurial Repository: http://code.google.com/p/wpfflashcards/

Zwischenstand

Ich habe einen ersten dünnen Längsschnitt durch die Anforderungen gemacht. Inklusive Modellierung hat mich das netto 2 Stündchen gekostet, schätze ich mal. Wie gesagt, der View war am Ende der Engpass.

Jetzt kann der imaginierte Kunde erstes Feedback geben. Und der sagt: “Ok, good enough. Weitermachen…”

Also mache ich mich beim nächsten Mal an, hm, ja, an was eigentlich? Was ist das nächste wichtige Feature? Die Lernkarteiübersicht? Oder der Import eines Kartenstapels? Nein, ich denke, beim nächsten Mal muss ich mich an die Lernkartei machen. Es müssen Karten durch die Fächer bewegt werden. Der Kern der ganzen Anwendung wird dran sein.

Zum Glück wird das wohl nichts mit dem View zu tun haben. Der ist vorbereitet auf echte Karten und echtes Weiterstecken. Gut, dass ich schon dieses Mal die Kommandos zumindest realistisch verdrahtet habe und auch schon Platzhalter für die wesentlichen Funktionsschritte vorhanden sind.

Freitag, 1. April 2011

Skalierbarkeit als Gläserne Decke in der Agilität

Agile Softwareentwicklung fokussiert sich auf verlässliche Lieferung von Nutzen. Je schneller sie den liefert, desto besser. Meistens. Die Frage ist nur, wie die Geschwindigkeit erhöht werden kann. Wie skaliert Softwareproduktion?

Skalieren ganz allgemein

Skalieren oder skalierbar sein heißt, auf mehr Bedarf so reagieren zu können, das er gedeckt wird. Wenn ein Website normalerweise 10 Transaktionen in der Sekunde verarbeitet, zu Weihnachten aber 1000 Transaktionen pro Sekunde anfallen würden, die Technik die jedoch nicht verarbeiten kann, dann skaliert der Website schlecht.

Einen Produktionsschritt wie “Transaktion verarbeiten” oder “Supportanfrage beantworten” oder “Briefe zustellen” können Sie skalieren, indem Sie die Ausführung beschleunigen (scale-up) oder mehr Ressourcen für die Ausführung bereitstellen (scale-out).

image

Supporter könnten schneller sprechen oder weniger lange überlegen müssen für eine Antwort, Briefzusteller könnten schneller radeln oder weniger mit Passanten plaudern. Beides würde die jeweiligen Produktionsschritte beschleunigen, so dass in derselben Zeit mehr Anfragen beantwortet oder mehr Briefe zugestellt würden.

Oder der steigende Bedarf wird auf mehr Ressourcen verteilt; die Supporter und Briefträger werden nicht schneller, aber es gibt einfach mehr. Wenn 1000 Briefe nicht mehr von einem, sondern 10 Briefträgern zugestellt werden, dann braucht das sicherlich nur noch ein zehntel der Zeit.

Skalieren durch scale-up (Aufbau) ist eigentlich immer grundsätzlich möglich. Bessere Ressourcen schaffen einfach mehr weg.

Skalieren durch scale-out (Ausbau) ist jedoch nicht immer möglich. Manche Tätigkeiten lassen sich nicht auf mehr Ressourcen verteilen. Einen Roman zu schreiben, lässt sich z.B. nicht dadurch beschleunigen, dass man 5 Autoren damit beschäftigt statt einem.

Was nicht heißt, dass man die Romanproduktion nicht mit mehr Ressourcen beschleunigen könnte. Die “Produktionswerkstatt” von Ken Follett macht es vor.

Daraus folgt, dass es noch einen weiteren Weg gibt, zu skalieren: Fokussierung. Ressourcen schaffen mehr, wenn sie sich auf ihre eigentliche Tätigkeit besser konzentrieren können.

image

Ich nenne das mal scale-in, um auszudrücken, dass hierfür eine Wendung nach innen, eine Besinnung auf das Wesentliche nötig ist.

Die Softwareproduktion skalieren

Auf die Softwareentwicklung als Produktionsprozess lassen sich die Skalierungsmuster natürlich übertragen.

image

Was tun, um mehr “Packerl” zu produzieren? Wenn der Bedarf steigt, dann müssen die Kaufwilligen befriedigt werden. Jeder Kunde bekommt ein “Packerl”. Natürlich müssen diese “Packerl” nicht alle gleich aussehen. Der erste Kunde kann als “Packerl” die Softwareversion 1.0.0.0 bekommen, der nächste die Version 1.0.0.1, der übernächste ebenfalls 1.0.0.1, der vierte 1.0.0.2 usw. Manche Kunden bekommen einfach die aktuelle Version, für andere wird speziell ein Feature eingebaut, ohne das sie nicht kaufen würden.

Nochmal: Wie kann die Softwareproduktion eine steigende Nachfrage befriedigen? Auf dieser Abstraktionsebene kann die Antwort nur lauten: scale-up. Die Softwareproduktion als ganzes muss schneller werden. Sie muss neue Wünsche schneller in “Packerl” transformieren oder sie muss einfach nur mehr “Packerl” hinten rausschieben (also Kunden mit einer existierenden Softwareversion befriedigen).

Und wie kann die Softwareproduktion als Ganzes schneller werden? Keine Ahnung. Für eine Antwort ist mehr Detail nötig. Wie funktioniert die Produktion im Einzelnen?

image

Jetzt gibt es Ansatzpunkte. Zum Beispiel könnte der Vertrieb beschleunigt werden, am ehesten wohl durch scale-out. Aber das lohnt natürlich nur, wenn es genug zu verkaufen gibt, d.h. die Softwareentwicklung mehr liefert als bisher in den existierenden Bedarf hinein abgesetzt werden konnte.

Wenn die das aber nicht tut, wenn sie der Engpass ist, wie könnte die Softwareentwicklung skalieren?

Die Softwareentwicklung skalieren

Mein Gefühl ist, dass Unternehmen sich mit der Skalierung der Softwareentwicklung schwer tun. Sie erkennen sie zwar als Engpass – doch sie reagieren unangemessen.

Kann nicht sein – Scale-in

Das die Softwareentwicklung skalieren könnte, dass sie mehr Kapazität hätte und schneller wäre, wenn sie sich denn mehr auf ihr eigentlichen Geschäft konzentrieren könnte, das haben viele Entscheider nicht im Blick. Es kann doch nicht sein, dass die Softwareentwicklung heute etwas Kontraproduktives tut; alles, was sie tut, muss sie tun. Das ist nötig. Support durch Softwareentwickler muss sein. Teilnahme an vielen Besprechungen muss sein. Statusreports und Timetracking muss sein. Dokumentationen müssen gepflegt werden.

Da ist einfach kein Fett, was weggeschnitten werden könnte.

So sehen es viele Entscheider und können also nicht denken, dass ein scale-in hilfreich wäre.

Allerdings denken Sie das, wenn ein Training oder andere Fortbildungsmaßnahme vorgeschlagen wird. Da haben sie das Gefühl, dass das etwas Unnötiges ist.

Darf nicht sein – Scale-up

Wo jedoch Fortbildung (oder gar Ausbildung) fehlt, da kann auch kein scale-up stattfinden. Denn nur durch Lernen kann die Softwareentwicklung mit den gegebenen Ressourcen ja schneller werden. Das kostet zwar erstmal Zeit, doch dann wird noch mehr Zeit gespart.

Flüssigere Technologiebedienung, bessere Prozesse, geschmeidigere Kommunikation: all das kann man lernen, all das bringt höhere Produktivität. Doch das kostet Zeit und Geld. Also sind Entscheider, die in der “Kostendenke” stecken, sehr unwillig, scale-up-Maßnahmen zu bewilligen.

Muss sein – Scale-out

So bleibt denn nur ein scale-out, um die Softwareentwicklung zu beschleunigen. Die Softwareentwicklung muss sozusagen multipliziert werden:

image

Das bedeutet, die Anforderungen werden von mehreren “Entwicklungsinstanzen” parallel bearbeitet.

Hört sich plausibel an, oder? Entspricht auch der Sichtweise auf Software. Die wird nämlich meist als ein Ganzes, als ein Programm, eine Anwendung so gesehen, wie der Auftraggeber sie ordert.

“Entwickeln Sie für uns eine Faktura!” – also kommt am Ende eine EXE (mit ein paar DLLs) heraus.

Dasselbe Spiel bei einer AVA-Software, einer Warenwirtschaft, einer Branchensoftware für den Rolladen- und Jalousiebau… der Markt will eine Lösung, also wird eine EXE produziert.

image

Die Erfüllung des Kundenwunsches hängt von einer Anwendung ab. Die wiederum hängt davon ab, dass alle Anforderungen erfüllt werden. Und die hängen davon ab, dass sie in einer EXE (mit zugehörigen DLLs) platziert werden.

Scale-out als Gläserne Decke

Die Softwareentwicklung mit scale-out zu beschleunigen, hört sich plausibel an, oder? In jedem Fall ist das das übliche Vorgehen. Wo 5 Entwickler sind, da werden 5 Anforderungen (Features) gleichzeitig in Angriff genommen. Na, vielleicht 4, denn manchmal ist eine Anforderung so schwierig umzusetzen, dass sich besser zwei Entwickler darum kümmern.

In jedem Fall wird bei Produktionsengpässen in der Softwareentwicklung reflexhaft zu scale-out gegriffen. Damit scheinen alle Probleme gelöst. Erstens kann man so alle Entwickler auslasten. Zweitens geht es doch so am schnellsten voran. Oder? Drittens kann man das quasi aus dem Stand. Und umso besser, je mehr Anforderungen schon vorliegen. Man muss dann nur noch ein paar mehr Entwickler dran setzen. Am besten solche, die billiger sind.

Leider entstehen die erwarteten Vorteile dabei – wenn überhaupt – nur kurzfristig. Scale-out auf der Basis von Anforderungen führt nämlich zu einigen Problemen:

Problem #1: Konflikte

Anforderungen sind Durchstiche. Für jede Anforderung muss daher potenziell an allen Schichten einer Codebasis geändert werden. Das kann zu Konflikten führen. Das können physische Konflikte in den Quelldateien sein, die ein Versionskontrollsystem anzeigt. Das können aber auch noch schlimmere Konflikte bei logischen Abhängigkeiten sein.

Problem #2: Verzahnung

Wo Anforderungen parallel umgesetzt werden ohne nähere Planung - Warum sollte es die auch geben, denn ein scale-out auf dieser Ebene soll sie ja gerade vermeiden? Wer mehr plante, der müsste kein scale-out nach Anforderungen betreiben. – da entstehen schnell viele Abhängigkeiten im Code. Code der einen Anforderung nimmt Bezug auf Code, der von einer anderen stammt, weil zwischen beiden ja keine Barriere existiert. Entwickler, die auf Anforderungen konzentriert sind, durchforsten die gesamte Codebasis nach hilfreicher, schon existierer Funktionalität und binden sich daran.

Problem #3: Geringe Geschwindigkeit

Jede Anforderung wird nur von einem Entwickler bearbeitet. Der mag das zwar so schnell tun, wie er kann, doch auf das Gesamtsystem gesehen, ist das nicht die maximale Geschwindigkeit. Bei 5 Entwicklern wird jede Anforderung nur mit 20% der Kapazität bearbeitet. Ihre Umsetzung dauert also 5 mal so lange wie (theoretisch) nötig.

Anforderungen parallel in einem Team bearbeiten entspricht Multithreading-Code. Mehrere Threads im Hintergrund nehmen dem Vordergrundthread Arbeit ab; der friert nicht ein. Doch dafür werden die einzelnen Aufgaben nicht mit maximaler Geschwindigkeit bearbeitet. Der Nutzen: es wird Latenz verborgen.

Ist das aber, was Softwareentwicklung soll? Soll sie möglichst schnell mit vielen Aufgaben beginnen, um den Kunden (oder die Anforderungsdefinition) wieder frei zu geben für anderes Kaum.

Softwareentwicklung soll vielmehr jede Anforderung so schnell wie möglich umsetzen. Erstens, um dem Kunden möglichst schnell Nutzen zu bieten. Es geht also nicht um Entlastung von upstream Produktionsschritten, sondern um Bedienung von downstream Produktionsschritten bzw. des Marktes.

Zweitens um so schnell wie möglich Feedback darüber zu bekommen, ob überhaupt erfolgreich umgesetzt wurde. Denn das wissen wir ja: Nur weil irgendwo Anforderungen stehen, kommt am Ende nicht unbedingt Code heraus, der Gefallen beim Kunden findet. Also muss die Softwareentwicklung daran interessiert sein, mit höchster Geschwindigkeit zu jeder Anforderung Feedback zu generieren. Latenz ist mithin nicht zu verbergen, sondern zu reduzieren.

Problem #4: Hoher WIP

Jede Anforderung wird also nur mit ein Anteil von 1/n bei n Ressourcen umgesetzt. Das heißt, jede Anforderung verweilt sehr lange in der Softwareentwicklung. Immer sind also viele verschiedene Anforderungen in Produktion (work in progress, WIP).

Ob diese Produktion ein Erfolg ist und Geld bringt, stellt sich allerdings erst am Ende heraus, wenn der Kunde sie abgenommen hat. Viele Anforderungen gleichzeitig zu bearbeiten, ist zwar sehr geschäftig, verdient an sich jedoch kein Geld, sondern kostet erstmal nur welches.

Jede Anforderung, die begonnen, jedoch noch nicht fertiggestellt und abgenommen ist, kann potenziell Geld verbrennen. Deshalb ist der WIP zu minimieren. Am besten ist der WIP sogar 1.

Bis eine Anforderung fertiggestellt und abgenommen ist, ist sie auf Halde produziert. Sie ist im System und kostet gerade Geld, weil an ihr gearbeitet wird. Oder sie liegt fertig herum, ohne abgenommen worden zu sein und hat damit Geld gekostet. In beiden Fällen ist es ungewiss, ob der ganze Aufwand gelohnt hat; Kapital ist in ihr gebunden. Das ist betriebswirtschaftlicher Quatsch.

Ob es in besonderen Fällen betriebswirtschaftlich günstig sein kann, Anforderungen auf Halde zu produzieren, um einen Puffer zu haben für nachfolgende Produktionsschritte, lasse ich mal außen vor. Darüber kann man reden. Ich glaube nur nicht, dass daran jeman denkt, der scale-out für die Softwareentwicklung anordnet.

Grenze für die Agilität

Wenn Agilität schnell Kundennutzen herstellen will, wenn Agilität daran glaubt, dass häufiges Feedback nötig ist, um zufriedenstellende Software zu entwickeln, dann kann scale-out nicht der richtige Weg sein.

Scale-out auf Anforderungsebene widerspricht der Agilität oder begrenzt sie zumindest. Das heißt, wer Scrum o.ä. einführt in der Hoffnung, nun endlich maximal schnell Software zu produzieren, der lügt sich in die Tasche, solange er die Features im Sprint parallel entwickeln lässt.

Zu einem agilen Vorgehen muss also noch mehr kommen, um das Ziel der Agilität wirklich zu erreichen. Das geht nur, wenn jede Anforderung mit maximal möglicher Geschwindigkeit bearbeitet wird und der WIP minimal ist.

Architektur erhöht Teamskalierbarkeit

Ich verstehe, dass scale-out so verführerisch ist. Es scheint einfacher zu implementieren zu sein als scale-up oder scale-in. Bei scale-up und scale-in geht es ans Eingemachte, an die Kultur eines Unternehmens. Bei scale-out kann (scheinbar) alles/viel mehr beim Alten bleiben. Deshalb ist als erstes zu fragen, ob scale-out nicht doch irgendwie möglich ist?

Aus meiner Sicht lautet die Antwort ja. Scale-out ist möglich, wenn man ein wenig Architektur betreibt. Statt nämlich die Anforderungen des Kunden einfach nur in eine Anwendung umzusetzen, sollte überlegt werden, wie daraus mehrere Anwendungen werden können.

image

Die Zerlegung eines großen Wunschkonvolutes kann im Rahmen der architektonischen Planung über die Zerlegungsebenen Bounded Context und Partition/App geschehen. Jede App steht dann für eine EXE, die einer Nutzerrolle dient.

Mit scale-out kann dann gleichzeitig an Anforderungen verschiedener Apps gearbeitet werden. Das macht einen gewissen Sinn, weil die Zielgruppen verschieden sind. Gegenüber jeder Zielgruppe beginnt die Arbeit an einer Anforderung also möglichst schnell; die Latenz zu verbergen schafft Zufriedenheit.

Dass die Umsetzung der Anforderungen dann immer noch langsam ist, ist nicht so schlimm (Problem #3), denn die Probleme #1, #2 und #4 verschwinden. Ja, selbst #4 verschwindet, weil WIP pro Anwendung gerechnet werden kann, da die Abnahme pro Anwendung erfolgt.

Der Agilität ist damit durchaus Genüge getan. Pro App wird maximal schnell gearbeitet und Feedback eingeholt. Gleichzeitig kommt aber auch das Gesamtsystem voran.

Eine Zerlegung des großen Ganzen in Bounded Contexts und Apps bringt aber nicht nur für die Skalierbarkeit des Teams Vorteile. Auch die Evolvierbarkeit und die Usability steigen.

A little architecture can go a long way ;-)

Arbeitsorganisation erhöht Teamskalierbarkeit

Was aber, wenn sich das Ganze nicht gut in Apps zerlegen lässt? Oder was, wenn es weniger Apps als Entwickler gibt?

Für noch mehr scale-out Effekt reicht es nicht, die Anforderungen in Apps zu gruppieren. Wirklich höchste Geschwindigkeit pro Anforderung erreicht die Softwareentwicklung nur, wenn sie alle Entwickler (soweit sinnvoll) an eine Anforderung setzt. WIP=1 und Minimierung der Latenz sollten im Sinne agiler Softwareentwicklung das Ziel sein.

Das lässt sich erreichen durch Zerlegung der Anforderungen in Aufgaben. Soweit allerdings nur ein erster Schritt. Probleme #3 und #4 mögen damit gelöst sein. Um jedoch auch noch #1 und #2 in den Griff zu bekommen, ist als zweiter Schritt Komponentenorientierung unverzichtbar.

image

Mit Komponenten können alle Entwickler ungestört an ihren Aufgaben arbeiten. Ob dann ein Entwickler immer in einer Komponente Aufgaben umsetzt oder nur Aufgaben für eine App realisiert, das sei dahingestellt. Hier ist nur wichtig, dass komponentenorientierte Aufgaben der Schlüssel zu maximaler Teamperformance ist.

Nur mit Komponenten lassen sich Anforderungen echt parallel realisieren, um Kunden schnellstmöglichen Nutzen zu bieten und von ihnen Feedback zu bekommen. Ohne Komponenten stoßen Teams an eine gläserne Decke bei der Skalierbarkeit.

Mit Komponenten jedoch bekommt die Softwareentwicklung nicht nur echtes scale-out Potenzial, sondern macht den Code einfacher testbar, erhöht die Evolvierbarkeit und hat bessere Chancen auf Wiederverwendung von Funktionseinheiten.

Aber das bedeutet natürlich nicht, dass deshalb scale-up und scale-in vernachlässigt werden sollten. Auch da gilt es Potenzial zu aktivieren, um schneller und kostengünstiger Software produzieren zu können.