Posts mit dem Label Event-based Components werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Event-based Components werden angezeigt. Alle Posts anzeigen

Donnerstag, 23. Juni 2011

Flow-Design Motivation - Ein XING-Auszug

Warum ist Flow-Design eigentlich so gegen den üblichen Programmierstrich gebürstet? Dazu habe ich anlässlich einer Diskussion im CCD XING-Forum etwas geschrieben, das ich auch hier für mitteilenswert halte.

Anlass war ein Diskussionsbeitrag von Michael van Fondern:

Ich habe allerdings doch noch eine Frage, die du mir sicher beantworten kannst. Ich bin bisher bei der AppKata im Wesentlichen so vorgegangen

1. Datenabstraktionen bilden
(z.B. in Iteration 1: CsvDataTable/CsvDataRecord als Abstraktion für eine Tabelle)
Einfache Operationen, die sich unmittelbar auf diesen Daten ausführen lassen, die aber ansonsten keine spezielle Sicht implizieren, habe ich direkt bei diesen Klassen platziert (z.B. die CSV-Zerlegung direkt im Konstruktor des CsvDataRecord, oder die Spaltenbreitenberechnung).

2. Abstraktionen für einzelne Prozessschritte bilden (die zudem auf den Daten aus Schritt 1 arbeiten)
- als groben Prozessschritt habe ich die z.B. formatierte Sicht auf eine bestimmte Seite der Tabelle identifiziert und daraus eine Klasse gemacht; die einzelnen Teilschritte spielen sich dann als Funktionen in dieser Klasse ab. Wenn ich bei der Entwicklung / Weiterentwicklung irgendwann merke dass bei einzelnen Funktionen das SRP-Prinzip oder das SLA-Prinzip verletzt werden, refaktorisiere ich diese aus, platziere diese entweder in der Klasse selbst, oder bei einer Datenklasse, oder bilde ggf. auch neue Klassen dafür.

Meine Frage in Bezug auf EBC: was ist mit "Schritt 1 - Datenabstraktionen bilden" (auch in Form von "Objekten" der klassischen Objektorientierung, und ggf. unter Zuhilfenahme von Klassendiagrammen). Diesen Modellierungsschritt habe ich in deinen Artikeln, die du über EBC geschrieben hast, bislang nicht mehr gesehen, aber der ist doch immer noch sinnvoll, oder? Gerade, weil EB-Komponenten nur Nachrichten mit genau einem Parameter verschicken / empfangen. Oder hab ich das was nicht verstanden?

Die beiden Schritte, die er zur Lösung der CSV Viewer AppKata getan hat, scheinen mir typisch. Deshalb hier meine Antwort in Gänze (mit einigen Hervorherbungen und Bonusbildern):

Gut, dass du fragst :-) Du hast einen zentralen Punkt von Flow-Design identifiziert.

Dein Vorgehen:
1. Identifiziere Daten und unmittelbar auf ihnen ansiedelbare Operationen
2. Identifiziere sonstige Operationen

Das entspricht dem üblichen Vorgehen, würde ich sagen. So lehrt es die traditionelle Objektorientierung. Sie fokussiert auf deinem Schritt 1 - Schritt 2 ist eher ein Anhängsel, ein notwendiges Übel.

Objektorientierung traditionell bedeutet für mich Fokus auf Daten und Zuordnung von Funktionalität zu diesen Daten. Das Ergebnis sind Klassen als Blaupausen für Objekte.

Flow-Design stellt das auf den Kopf. Mit voller Absicht. Weil der OO-Ansatz zu den Ergebnissen geführt hat, die wir heute allerorten sehen.

Das Missverständnis des OO-Ansatzes ist es, dass Software soetwas ist wie eine Maschine. Maschinen bestehen aus Teilen, Kolben, Zündkerzen, Lichtmaschinen, Transistoren, ICs, Widerständen, Zeilentransformatoren, Tastaturen usw. usf.
Wenn man eine Maschine bauen will, dann überlegt man sich, wie die Bauteile aussehen sollen. Man denkt in distinkten Funktionseinheiten, die Zustand haben und mehr oder weniger tun. Eher mehr. Allemal bei Software, da man dort quasi immer bei Null anfängt.

Ein Elektrotechniker hat es einfacher: Der sitzt vor einem Kasten mit Standardbausteinen, die er "nur noch" zu etwas Neuem "verrühren" muss.

Softwareentwicklung kennt solche Standardbausteine im Grunde nicht (lassen wir ein paar Bibliotheken und Steuerelemente mal außen vor). Jedes Projekt erfindet sie daher neu in Form von Objekten. Dabei schießt man schnell über das Ziel hinaus. Die Standardbausteine sind keine Standardbausteine, weil sie einfach so groß werden. Deshalb immer wieder das Gejammer über mangelnde Reusability. Man möchte in die Position eines Elektrotechnikers kommen.

Wenn wir uns aber von dem Missverständnis verabschieden, dass Software eine Maschine ist, dann wird alles leichter. Es ist müßig, nach "Standardbausteinen" zu suchen. Der Setzkasten ist leer. Unsere Aufgabe sollte nicht sein, ihn erst zu füllen und dann mal zu schauen, was wir mit unseren eigenen Bausteinen bauen können.

Also geben wir den Fokus auf Datenstrukturen mit Funktionalitätsanhängseln auf. Weg mit dem Objektfokus. Weg mit dem Bauteildenken. (Dass auch im Flow-Design noch von Platinen und Bauteilen die Rede ist, ist ein Fehler, der korrigiert werden wird.)

Flow-Design hat ein anderes Softwarebild. Für FD ist Software keine Maschine, sondern eine Ansammlung von Prozessen, oder - weil der Begriff Prozess schon so besetzt ist - eine Ansammlung von Verhaltensweisen.

Bei FD beginnst du deshalb mit der Identifikation von Verhaltensweisen statt Daten, mit Verben statt Substantiven.

Was soll ein CSV Viewer leisten? Er soll eine CSV Datei seitenweise anzeigen.
Irgendwie werden also mal ganz grundsätzlich Textzeilen eines bestimmten Formats in Seiten eines bestimmten Formats transformiert.

Für denn OOPler stecken da natürlich hübsche Daten drin: Textdatei, Zeilen, Seiten. An die hängt er geistig schnell die ablesbaren Funktionen: lesen, formatieren.

Aber da beginnt schon das große Rätselraten: Wozu soll denn eine Funktionalität wie das Auseinandernehmen eines CSV Textzeile gehören? Ist das eine Aufgabe des Textdateiadapterobjektes? Es liefert CSV-Datensätze zurück, die aus Spaltenwerten bestehen? Oder soll sich der Adapter darauf beschränken, Textzeilen zu liefern und die Formatierung bricht sie auf? Hm...

FD ist da viel pragmatischer, direkter, natürlicher. Man fängt einfach mal an, die obige Anforderung zu formalisieren:

(run) -> (Dateiname von Kommandozeile holen)
      -(dateiname)-> (Erste Seite aus CSV Datei lesen)
      -(seite)-> [Seite anzeigen].

image

Damit ist ein Programm beschrieben, das schonmal ein Feature der ersten Iteration realisiert. Das Abstraktionsniveau ist sehr hoch, klar, aber das ist ja gerade der Trick. Wir haben das Programm formulieren können, obwohl wir nur eine grobe Ahnung von der Lösung haben.

Die Anforderungen sagen mir vor allem, was zu tun (!) ist. Welche Transformation erwartet der Benutzer? Denn um Transformationen geht es immer. Input wird in Output transformiert. So ist das Softwareleben. Immer. Unzweifelhaft.

Essenziell dreht sich Software damit um Funktion und nicht Daten. In der Mitte von EVA stehen nicht Daten, sondern Transformation.

Natürlich, ohne Daten geht es nicht. Aber wir dürfen uns nicht ins Bockshorn jagen lassen, nur weil Daten am Anfang und am Ende von EVA stehen. Die Daten sind nicht der Grund, warum wir V entwickeln sollen. Die Daten sind vielmehr schon da. Die Vorstellung davon, wie Input und Output aussehen, ist selbst beim Kunden verhältnismäßig klar. Er mag die nicht formalisieren können, aber er hat Daten und will andere Daten bekommen, von denen er weiß, wie sie aussehen sollen. Sonst hätte er nicht den Wunsch nach einer Software, der ihm genau diese Daten erzeugt.

Das, was dem Kunden aber viel, viel unklarer ist (und uns erstmal auch), das ist, wie die Transformation aussieht. Wie macht man das, den Input in den Output zu überführen? Das (!) herauszufinden, ist unsere Aufgabe.

V ist also unklar und wird nicht klarer, indem wir mit OOP lange über E und A grübeln. Wir müssen sofort, wenn wir Anforderungen sehen, V in den Blick nehmen. Zunächst ganz grob, dann immer detaillierter, am Ende indem wir Code schreiben.

Obiger Fluss ist ein grobes V für ein Feature des CSV Viewers. Noch gröber, aber recht uninteressant wäre:

(run) -> (Lade und zeige die erste Seite der CSV Datei an).

image

Im Zweifelsfall kannst du aber gern so beginnen. Dann wäre die Schrittfolge:

1. (run) -> (Lade und zeige die erste Seite der CSV Datei an).

image

2. (run) -> (Dateiname von Kommandoleise holen)
        -(dateiname)-> (Erste Seite aus CSV Datei lesen)
        -(seite)-> [Seite anzeigen].


image

und vielleicht folgende Verfeinerungen:

3.1 Erste Seite aus CSV Datei lesen {
    (in) -(dateiname)-> (Lese Textdatei zeilenweise)
         -(string*)-> (Zerlege CSV Textzeilen in Werte)
         -(CSVRecord*)-> (Sammle Records für erste Seite)
         -(seite)-> (out)
   }


image

3.2. Seite anzeigen {
     (in) -(seite)-> (Normal. Seite auf max Spaltenbreiten)
          -(seite)-> (Formatiere Seite als Tabelle)
          -(string*)-> [Tabelle anzeigen]
   }

image

Jetzt überlegst du, ob du für jede Operation im Flow schon eine konkrete Idee zur Umsetzung hast. Und ob die Umsetzung wahrscheinlich nicht umfangreicher als vielleicht 50 LOC.

Wenn ja, fang an mit dem Codieren, gern nach TDD. Wenn nein, verfeinere weiter, was noch zu kompliziert/unübersichtlich ist.

Wenn du das nicht kannst, dann ist das nicht ein Signal dafür, mit TDD zu beginnen, um Lücken zu schließen, sondern ein Zeichen dafür, dass du das Problem und damit seine mögliche Lösung noch nicht gut genug verstanden hast. Oder vielleicht hast du auch ein Problem mit deinen Technologien. Eine Spike Solution könnte angezeigt sein.

Ein Problem nicht zu verstehen oder keine rechte Idee von der Lösung zu haben, sollte aber allemal ein Warnsignal sein, nicht (!) zu codieren.

Und was ist mit den Daten? Achja... da war doch noch was :-)

Das FD Modell enthält natürlich Daten. Da gibt es Seiten und CSVRecords. Die musst du natürlich auch detaillieren und formalisieren. Aber dazu braucht es nicht mehr als z.B. ein simples Krähenfußdiagramm.

Und was ist mit Funktionalität, die direkt an den Daten hängt? Meine Meinung: die ist überbewertet, weit überbewertet :-)

Dass wir Daten und Funktionen in Klassen zusammenfassen können, ist schön. Das will ich nicht missen. Aber eher nicht für das, was zwischen den Operationen fließt. Das sind Datendaten :-) Datenstrukturen, die im Wesentlichen funktionsfrei bleiben sollten. (ADTs machen da eine Ausnahme.)

Wenn Operationen Zustand haben, dann ist es aber sehr schön, dass ich beides zusammenfassen kann.

Nun hast du zwei ganz einfache Modelle:

1. Ein ganz einfaches Flussmodell für die so wichtige Transformation (V).
2. Und ein ganz einfaches Datenmodell für E und A.


Das nenne ich natürlich, direkt, einfach, verständlich. Kein Rätselraten, sondern ablesen, was in den Anforderungen steht, um es simpelst zu formalisieren.

Verstehst du, was mich motiviert, OO-Technik mit der FD-Methode anzugehen und nicht mit der überkommenen OO-Methode und wie vorteilhaft FD ist?

Mitteilenswert finde ich das unterschiedliche Softwarebild: Maschine vs Verhalten. Denn daraus folgt ein anderer Analyseansatz und eine andere Modellierung.

Und warum ein so anderes Softwarebild? Weil Software sich eben als so volatil erwiesen hat. Maschinen sind statisch, Prozesse hingegen sind (im doppelten Sinn) immer im Fluss. Die Agilität hat das in puncto Vorgehen bei der Softwareentwicklung schon verstanden. Das Softwarebild hinkt mit dem OO-Fokus aber noch hinterher. FP ist ein Lichtblick, doch (noch lange) keine Option für viele Entwickler. Und warum auch dringend Technik (F# statt C#) und Methode (FD statt OOAD) ändern, wenn es (erstmal) reicht, nur die Methode zu ändern? Denn mit FD lässt sich sehr bequem “flüssige Software” entwickeln auf der Basis dessen, was wir gut kennen: OO-Technik.

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

Donnerstag, 16. Juni 2011

Flüssiges SOLID

Es gibt unerwartete Hilfe für das Flow-Design von den objektorientierten Freunden des Clean Code. Innerhalb zweier Tage bin ich über sehr ähnliche, aber unzusammenhängende Aussagen gestolpert.

Da schreibt einerseits der Chefredakteur des ehrwürdigen Dr. Dobb´s Journal, Andrew Binstock, in einem Editorial, wie wichtig er es fände, Klassen klein zu halten:

“Small classes are much easier to understand and to test. If small size is an objective, then the immediate next question is, "How small? Jeff Bay […] suggests the number should be in the 50-60 line range. Essentially, what fits on one screen.”

Und da schreibt andererseits der dänische Fachbuchautor Mark Seemann in seinem Blog:

“Each class is very small, so although you have many of them, understanding what each one does is easy. […] However, when thinking about SOLID code, it actually helps to think about it more like a liquid […]. Each class has much more room to maneuver because it is small and fits together with other classes in many different ways.”

Zwei Autoren stellvertretend für weitere, die dasselbe sagen: Klassen müssen klein sein, um evolvierbaren Code zu erhalten. Das ist ein Schluss, den sie aus anerkannten SOLIDen Prinzipien der Softwareentwicklung ziehen.

Kleine Klassen, nicht nur kleine Methoden

Bevor ich laut darüber nachdenke, was kleine Klassen für den Code bedeuten, möchte ich einen Einwand vorwegnehmen: “Warum sollen Klassen so klein sein und nicht Methoden? Reicht es nicht, wenn jede Methode einer Klassen vielleicht maximal eine Bildschirmseite füllt? Dann ist die doch auch schon viel übersichtlicher als üblich.”

Absolut. Methoden sollten auch nicht umfangreich sein. Jeder Code, der mehr als eine Bildschirmseite umfasst, ist schwieriger zu verstehen. Auf einer Seite können wir Schachtelungen überblicken; wenn wir dafür scrollen müssen, verlieren wir schnell den Zusammenhang.

Aber auch ich finde es nicht genug, wenn nur Methoden klein sind. Denn Methoden sind nicht die für die Laufzeit wesentlichen Funktionseinheiten. In einem objektorientierten Programm geht es eben um Objekte. Wie reden darüber, wie Objekte in Beziehung gesetzt werden. Abhängigkeiten gibt es nicht von Methoden, sondern von Interfaces, d.h. “ganzen” Objekten.

Wenn ich mich durch Code arbeite, will ich daher schnell verstehen, was diese Objekte tun, nicht nur einzelne ihrer Methoden. Ich will die “rekombinierbaren Einheiten” der Software überblicken. Objekte sind mehr als Methoden, da sie Zustand haben können. Das macht ja gerade die Objektorientierung aus: Wir stellen Netzwerke aus zustandsbehafteten Funktionseinheiten her, die Anforderungen erfüllen.

So ein Netzwerk gut zu verstehen, ist die Voraussetzung, es leicht zu verändern. Dazu kommt noch die Granularität seiner Bestandteile, der Objekte. Das ist ja Seemanns Argument: Strukturen aus feingranularen Objekten lassen sich leichter umformen, wenn neue Anforderungen das nötig machen.

Explodierende Abhängigkeiten

Wenn wir uns einig sind, dass Klassen die Funktionseinheiten sein sollten, die klein1 zu halten sind, dann jetzt zu den Folgen.

Die erste offensichtliche Folge ist, dass die Zahl kleiner Klassen sehr viel größer sein muss, als die Zahl der bisherigen. Klassen von mehreren Hundert Zeilen sind keine Seltenheit; solche mit mehreren Zehntausend Zeilen habe ich aber auch schon gesehen. Wenn ich “klein” für den Gedankengang hier einmal mit 100 LOC gleichsetze und übliche Klassen von 300 bis 1000 LOC haben, dann wird die Zahl kleiner Klassen wohl mindestens 5 Mal so groß sein.

Aber nicht nur das. Wenn aus einer Klasse 5 oder mehr werden, dann soll die Summe ja immer noch dasselbe leisten wie vorher. Das heißt, die Funktionalität muss immer noch irgendwie zusammenhängen. Diese vielen Klassen sind also notwendig voneinander abhängig. Wo vorher eine Klasse allein stand…

A

…da ist es zukünftig ein Wald aus Klassen:

A1
  B
  C
    D

A2
  B
  E
  F
    D

(Einrückung bedeutet hier Abhängigkeit, d.h. A1 braucht die Dienste von B und C, C wiederum von D usw.)

Selbstverständlich sind diese Abhängigkeiten nicht alle statisch. Grundsätzlich isolierte Testbarkeit wird erhalten durch dynamische Abhängigkeiten und Dependency Injection. Das ist technisch nicht kompliziert – hat aber seinen Preis beim Testen. Abhängige Klassen können entweder nur mit Integrationstests geprüft werden – was die Fehlerfindung erschwert. Oder sie müssen mit Attrappen ihrer Abhängigkeiten ausgestattet werden – was den Testaufbau selbst mit Mock-Frameworks umständlich macht.

Das hört sich nicht gut an, oder? Steigt die Evolvierbarkeit wirklich, wenn die Klassen klein und übersichtlich werden? Wird da der Teufel nicht mit dem Belzebub ausgetrieben? Vorher war der Code unübersichtlich, weil lang – nun ist der Code unübersichtlich, weil die Abhängigkeiten stark zugenommen haben.

Auch wenn ich das nicht so einfach negativ sehe2, spüre ich auch einen Schmerz bei solcher Zunahme der Abhängigkeiten. Da helfen alle DI Container der Welt nicht. Sie verwalten nur das Elend.

Konzeptionelle Klimmzüge

Doch lassen wir die explodierenden Abhängigkeiten einmal außen vor. Sehen wir sie positiv im Sinne der Prinzipien “Lose Kopplung, hohe Kohäsion” und “Single Responsibility”. Das Entwicklerleben ist halt kein Ponyhof; Opfer sind zu bringen für die Evolvierbarkeit.

Was aber mit den konzeptionellen Klimmzügen die Sie im Rahmen der Klassenverkleinerung vollbringen müssen? Da haben Sie in langen Entwurfssitzungen – oder agilen TDD-Impulsen – nun Ihre Klassen geschnitten; jede ist sorgfältig an die Problemdomäne angepasst; alles hat seine Ordnung – nur leider haben alles Nachdenken und auch der TDD-Druck es nicht geschafft, die Klassen wirklich klein zu halten. 500+ LOC sind herausgekommen für die zentrale Domänenmodellklasse oder die Verschlüsselungsklasse oder die Datenzugriffsklasse. Sie haben sich sogar bemüht, die Methoden klein zu halten. SLA rulez! Und nun kommt einer daher und sagt, wahrhaft SOLIDe clean sei Ihr Code erst, wenn die Klassen selbst klein seien. Ja, wie soll das denn gehen? Ein Kunde ist ein Kunde ist ein Kunde. Den kann man nicht so einfach aufteilen. Und die Verschlüsselungsfunktionalität passt auch so schön unter den Hut einer Klasse.

Diesen Einwand verstehe ich auch sehr gut. Wer die Welt in funktionsreiche Akteure benannt mit Substantiven aufgeteilt hat, der tut sich schwer, diese Funktionalität weiter aufzuteilen. In was sollten Sie einen Kunden denn zerlegen? Naheliegende Abspaltungen von Funktionalität werden Sie schon selbst vorgenommen haben; Sie haben substantivische Sinnzusammenhänge selbstverständlich hergestellt. Die Bonitätsprüfung ist schon nicht mehr Bestandteil des Kunden, sondern eine eigene Klasse, von der der Kunde abhängig ist…

public class Kunde
{
  …
  private IBonitätsprüfung _bp;

  public Kunde(IBonitätsprüfung bp) {…}
  …
}

…damit man hübsch objektorientiert fragen kann: kunde.HatBonitätFür(100000).

Es geht also nicht mehr kleiner, wenn die Problemdomäne sich noch sinnvoll im Code widerspiegeln soll.

Die wunderbaren Flexibilitätsvorteile, die wahrhaft SOLIDer Code durch kleine Klassen verspricht, erscheinen damit unerreichbar. Sie sind dazu verdammt, unSOLIDe zu arbeiten, um nicht in Abhängigkeiten zu ersticken und/oder sich in einem konzeptionellen Wirrwarr zu verlieren.

So scheint es zumindest…

Erlösung durch Perspektivwechsel

So gern Sie den guten Rat von Binstock und Seemann annehmen würden, Sie werden im Augenblick nicht können. Er käme Ihnen teuer zu stehen. Das ist auch meine Meinung. Aus Ihrer Perspektive der üblichen, der OOAD-geprägten Objektorientierung wäre der Preis für den Gewinn an Evolvierbarkeit durch wirklich kleine zu hoch.

Solange Sie aus dieser Perspektive auf den Ratschlag blicken, kann ich Ihnen nicht dazu raten. Was aber, wenn Sie die Perspektive wechseln? Was, wenn Sie sich nicht mehr vor explodierenden Abhängigkeiten fürchten müssten? Was, wenn es keine konzeptionelle Schwierigkeit gäbe, Klassen zu zerteilen?

Eine solche alternative Perspektive gibt es. Es ist die Perspektive des Flow-Designs (FD) und der Event-Based Components (EBC)3.

Im Flow-Design sind Funktionseinheiten (lies: Klassen) nicht mehr voneinander abhängig. Wenn die eine Klasse Daten lädt, die andere sie verarbeitet und eine dritte sie speichert, dann kennen diese Klassen einander nicht, weder statisch noch dynamisch. Es gibt keinen Abhängigkeitsverhau und auch keinen Testattrappenalbtraum.

In FD/EBC gibt es zwar noch Abhängigkeiten, doch die sind streng systematisiert und unkritisch. Kein Grund für Schlaflosigkeit. Die Evolvierbarkeit leidet unter ihnen nicht. Sie folgen einer grundsätzlichen Separation of Concerns.

Darüber hinaus gibt Flow-Design den vorherrschenden Fokus auf Substantive auf. Funktionseinheiten (lies: Klassen) sind keine Akteure, sondern Aktionen. Denken Sie im Augenblick einfach mal nur “zustandsbehaftete Funktion”. Mit FD versuchen Sie zur Lösung eines Problems nicht ein paar Substantive zu finden, auf denen Sie dann in einem umständlichen Verfahren Funktionalität verteilen. Sie suchen vielmehr “nur” nach Funktionen oder “Verhaltensschritten”. Die können Zustand haben oder nicht. Egal. Vor allem Arbeiten Sie auf Input und erzeugen Output.

Dieser Fokuswechsel von Substantiven/Akteuren/Dingen nach Verben/Aktionen/Verhalten führt automatisch zu sehr kleinen Funktionseinheiten (lies: Klassen). Und wenn nicht, dann lassen sich EBC-Klassen (lies: Verhaltensweisen) sehr viel leichter als OOAD-Klassen (lies: Dinge) in weitere Klassen zerlegen.

Fazit

SOLIDer Rat ist tatsächlich viel Wert – wenn Sie ihn im rechten Kontext beherzigen. Solange der die übliche methodische OOAD-Objektorientierung ist, wird es sie schmerzen. Doch mit Flow-Design und Event-Based Components wird aus vormals festen, unhandlich großen Klassen eine SOLIDe Flüssigkeit. Der Entwurf fließt, die Daten fließen, die Strukturen passen sich fließend an.

SOLID ohne Flow-Orientation ist noch fest. Nomen es omen. Doch SOLID mit Flow-Orientation erzeugt Evolvierbarkeit und Testbarkeit. Oder umgekehrt: Flow-Orientation führt automatisch zu SOLIDem Code.

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

Fußnoten

1 Ob “klein” nun heißt 25 Zeilen oder 50 oder 75, das finde ich an dieser Stelle nicht so wichtig. Bildschirmseiten bieten ja je nach Bildschirmauflösung und Font und Monitorgröße unterschiedlich viel Platz. Klassen, deren Methoden sehr unabhängig voneinander sind, verstehe ich auch schnell, wenn nur die Methoden leicht zu überblicken sind; dann wäre es ok, wenn ich ein bisschen scrollen muss. Bestehen unter den Methoden aber vielfältige Abhängigkeiten, dann ist das etwas anderes. Dann möchte ich möglichst viel der Funktionalität auf einen Blick sehen. Wie der Berater so schön sagt: Es kommt darauf an. Eine allgemeingültige Zeilenzahl, die niemals überschritten werden darf, lässt sich nicht definieren. Aber ich kann für mich sagen, dass ich unruhig werden, wenn der Umfang einer Klasse mehr als 2-3 Bildschirmseiten hat. Und ich kann nur an Ihre Sensibilität appellieren, den Schmerz zu spüren, wenn Sie zum Verständnis scrollen müssen.

2 Oder genauer: Nicht die Abhängigkeiten haben zugenommen, sondern sie haben sich verändert. Vorher waren sie weniger sichtbar und einfacher. Die Methoden innerhalb einer solchermaßen geshredderten Klassen waren ja auch voneinander abhängig. Diese Abhängigkeiten sind nun explizit gemacht. Das könnte man auch positiv Entkopplung nennen, zu der ebenfalls positiv eine Zusammenfassung nach Kohäsion in neuen Klassen tritt.

3 Über Flow-Design und EBC habe ich ausführlich in diesem Blog und in der dotnetpro geschrieben. Wer mehr dazu erfahren will, finde auf dieser Ressourcenseite Berge an Material: http://clean-code-advisors.com/ressourcen/flow-design-ressourcen

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 :-)

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.

Dienstag, 29. März 2011

Lernkartei I – Das Szenario

Flow-Design verspricht evolvierbare Software. Wenn ich ein Flow-Design Diagramm zeichne, ist das allerdings noch nicht nachzuvollziehen. So ein Diagramm mag verständlicher sein als andere Diagramme, doch warum sollte Software deshalb evolvierbarer sein?

Um die Evolvierbarkeit von Flow-Designs zu zeigen, ist mehr als ein Diagramm nötig. Nur eine Reihe von Diagramme, die zeigt, wie sich eine Software weiterentwickeln lässt, kann das Versprechen Evolvierbarkeit transportieren.

Deshalb habe ich mich entschlossen hier im Blog eine Software in kleinen Schritten zu entwickeln, zu evolvieren. Ich tue das live, d.h. ohne vorherige Planung. Ich sitze also nicht erst stundenlang, probiere herum und dokumentiere dann nur das wirklich funktionierende Endergebnis. Hier will ich vielmehr simulieren, was in größeren Projekten an der Tagesordnung ist: Unsicherheit.

Welche Features sollen wirklich in die Software rein? Welche Technologien sind die besten für den Zweck? Welcher Ansatz verspricht am meisten Erfolg? Wohin wird sich die Software entwickeln? Diese und andere Fragen treiben die Softwareentwicklung ständig um; damit will auch ich leben. Deshalb mache ich mir zunächst gar keine großen Gedanken über die Anforderungen; so ist garantiert, dass ich mich selbst mit Änderungswünschen überrasche.

Einen Ausgangspunkt muss das Ganze aber natürlich haben. Hier mein grober Wunsch, was ist haben möchte:

Auftrag Lernkartei

Ich möchte ein Lernkartei-Programm entwickeln. Mit dem kann ich dann zum Beispiel Vokabeln lernen. Der Lernstoff steht auf Karteikarten in Form von Fragen und Antworten. Auf der einen Seite jeder Karte die Frage, auf der anderen Seite die zugehörige Antwort.

Mehrere solcher Lernkarteikarten sind zusammengefasst zu einer Lernkartei, z.B. Karteikarten mit Französich-Vokabeln.

Der Trick bei einer Lernkartei ist nun, dass man ihre Karten nicht einfach von oben nach unten lernt, so wie sie auf dem Karteikartenstapel der Lernkartei liegen. Die Karten werden vielmehr in einen Kasten gesteckt, der mehrere Fächer hat.

image
Quelle: http://lernkartei.de/prinzip.php

Das Vorgehen beim Lernen ist einfach:

  • Neue Karten kommen ins erste Fach.
  • Sich abfragen kann man mit Karten in jedem Fach. Wird die Antwort gewusst, kommt die Karte ins nächste Fach; bei falscher Antwort wandert sie jedoch zurück in Fach 1.
  • Die Fächer haben unterschiedliche Kapazität. Deshalb kommen die Karten immer seltener dran, je weiter sie in den Fächern gewandert sind. Anfangen sollte man immer mit den Karten im ersten Fach. Die weiteren Fächer kommen dran, wenn sie voll sind.

Nicht alle Fächer sind also gleich. Das erste hat eine gewisse Sonderstellung, weil dorthin alle nicht gewussten Karten zurückwandern. Es füllt sich also am schnellsten mit neuen Karten und nicht gewussten.

Dazu kommen noch zwei weitere Fächer, die ich mal Halde und Archiv nenne. Die Halde sind enthält alle Karteikarten, die noch nicht in Fach 1 waren. Wer nicht kontinuierlich Karten anlegt und gleich in Fach 1 steckt, sondern eine fertige Lernkartei nutzen will, der fängt mit einer großen Halde an, von der dann einige Karteikarten in Fach 1 gesteckt werden.

Das Archiv bilden andererseits alle Karten, die aus dem letzten Fach als gewusst entnommen werden.

Bei einem Karteikasten mit n Fächern (1..n) hat die Halde die Fachnummer 0 und das Archiv die Nummer n+1.

Die Lernkarteien soll das Programm aus Textdateien laden. Ich stelle mir ein sehr einfaches Format vor:

Frage 1
Antwort 1

Frage 2
Antwort 2

Frage 3
Antwort 3

Fragen und Antworten stehen auf je einer Zeile und werden als Paare durch Leerzeilen getrennt.

Dieses Format nutzt ein Lernkarteiprogramm, das mir auf dem iPad ganz gut gefallen hat: iMemento Lernkarten. Ich möchte, dass meine Lernkartei-Dateien mit seinen kompatibel sind.

image

Die beiden Screenshots von iMemento zeigen, wie ich mir mein Programm auch ungefähr vorstelle. Es gibt ein Fenster mit einer Übersicht aller Lernkarteien und ein Fenster für das Lernen mit einer Lernkartei.

Das Programm soll zunächst mal auf dem Desktop als WPF-Anwendung laufen. Später könnte es umgestellt werden auf Silverlight oder MonoTouch oder sogar auf ASP.NET. Mal sehen…

Los geht´s…

Ich finde, das ist ein überschaubares, aber kein triviales Szenario. Es bietet mir (und vielleicht Ihnen auch) Nutzen, die Entwicklung lohnt sich also. Zu lernen gibt es immer etwas. Ein Lernkartei-Programm kann man immer mal zur Hand haben.

Zu diesem Szenario könnte ich nun natürlich viele Fragen stellen. Aber ich tue das bewusst nicht, sondern werfe mich im nächsten Artikel der Serie einfach in die Entwicklung. So will ich die in größeren Projekten quasi immer herrschende Unterspezifikation simulieren. Die übt ja einen nicht geringen Druck auf die Evolvierbarkeit aus.

WPF ist auch eine Herausforderung für mich. Mit der Technologie habe ich nicht viel Erfahrung. Allerdings treibt mit ein Ideal an: Ich möchte mit WPF eine strikte Trennung von GUI und dem Rest leben.

Wenn Sie mögen, grübeln Sie mit. Wie würden Sie dieses Szenario angehen?

Im nächsten Artikel mache ich den ersten kleinen Schritt in der Implementation. Jeder Artikel soll einen kleinen Anwendernutzen produzieren. Es gibt keine lange Planung oder Infrastrukturbastelei. Ran, rauf, rüber – aber mit Methode.

Sonntag, 13. März 2011

It is all design – really?

image

Uncle Bob hat unlängst getwittert: “It is all design.” Und dafür hat er auch großes Lob aus Deutschland bekommen.

Ich frage mich hingegen: Wenn denn alles Design ist, warum sollte ich dann dieses Design in textuellem Quellcode durchführen?

Ja, ja, ich weiß, dass Jack W. Reeves schon in den 1990ern “bewiesen” hat, dass Programmierung nicht Herstellung, sondern Design ist. Dem stimme ich im Vergleich zum Baugewerbe oder der Autoproduktion auch zu. Softwareentwickler heißen nicht umsonst Entwickler und nicht Hersteller. Softwareentwicklung ist eine sehr kreative Tätigkeit. Ein Auto zu produzieren oder ein Haus hochzuziehen hingegen nicht. Da wird weitgehend nur ein Plan abgearbeitet. Softwareentwicklung hingegen ist Planerfindung.

Insofern produziert Codieren natürlich Design. “It is all design” könnte also wahr sein.

Aber: Design verhält sich zu Produktion wie Landkarte zu Terrain – das eine ist Abstraktion, das andere Detail, Konkretes. Was nun aber, wenn das Design so kompliziert ist, dass man es nicht einfach darlegen kann, wenn es selbst ein Terrain ist. Man kann es dann nicht runterschreiben. Selbst eine Landkarte will recherchiert sein. Sie kommt aus vielen Quellen zusammen und ist am Ende doch nur ein recht unkreatives Abbild der Realität. Software aber kann nicht irgendwo abgemalt werden. Software als Design muss erdacht werden. Code, der selbst ja nur ein Produktionsplan ist, muss selbst geplant, entworfen werden.

Und damit sind wir beim terminologischen Dilemma. Es ist eben nicht alles zu Design gesagt, wenn man konstatiert, Code sei Design. Man muss dazu sagen, was denn Design überhaupt seiner Natur nach sei. Man muss den allgemeinen Fall in den Blick nehmen:

Design ist ein Plan für etwas Konkretes.

Wenn etwas Konkretes kompliziert ist, dann kann man es nicht “einfach so” herstellen, sondern dann muss man sich darüber vorher mal Gedanken machen. Allemal, wenn das Konkrete etwas Neues ist.

Design braucht also selbst durchaus Design. Und das Design dann womöglich auch wieder usw. usf. Design ist mithin nur relativ, d.h. in Gegenüberstellung zu etwas Konkretem sein Vorläufer:

Design –> Konkretes

Nimmt man jedoch das Design selbst in den Blick, wird es ebenfalls etwas Konkretes, d.h. ein Produkt, das designt werden kann:

Design' –> Design

Und auch das Design des Designs könnte kompliziert sein, so dass es ein Design brauchen könnte:

Design'' –> Design'

So ergibt sich eine Hierarchie von Designs mit dem endgültig Konkreten als Ziel:

Design''(Design'(Design(Konkretes)))

Das halte ich für ein allgemeines Bild vom Verhältnis von Design zu Produkt. Der Entwurf geht dem Konkreten voraus. Und was bedeutet das für die Softwareentwicklung? Deren endgültig Konkretes ist klar: ausführbarer Code. Quellcode ist laut Reeves ein Design dafür. Uncle Bob sieht das ähnlich, wenn er sagt, Testing und Refactoring seien Designtätigkeiten (weil sie Quellcode formen). Für Sie ist die Hierarchie deshalb nur zweistufig:

Quellcode(Maschinencode)

Ich hingegen behaupte mal, dass das eine ganz und gar unnötig beschränkende Sichtweise ist. Wenn wir sie als Ende der Fahnenstange ansehen, dann müssen wir uns bei der Softwareentwicklung nur noch stärker bemühen, den textuellen Quellcode in den Griff zu bekommen. Refactoring-Tools müssen besser werden, Test-Tools müssen besser werden, die Ausbildung zum Quellcodeschmied muss besser werden… dann wird es schon gehen.

In den letzten 60 Jahren ist die Softwareentwicklung aber nicht besser geworden, weil sie auf der jeweils höchsten Abstraktionsstufe stehengeblieben wäre. Wir entwickeln heute natürlich Komplizierteres viel schneller als vor 60 Jahren. Nur tun wir das nicht mit besseren Maschinencode-Tools. Die Hierarchie sieht vielmehr so aus:

OO-3GL(IL-Code(Machinencode))

Oder sie sah sogar einmal so aus:

OO-3GL(3GL(Assembler(Maschinencode)))

Wir sind voran gekommen, weil wir uns auf höhere Abstraktionsebenen geschwungen haben!

Statt besser mit Maschinencode umgehen zu können, lassen wir ihn lieber generieren. Statt besser mit Assembler umgehen zu können, lassen wir den Code lieber generieren. Statt C zu schreiben, lassen wir den Code von einem C++-Compiler generieren. (Zumindest habe ich das noch 1990 erlebt. Es war aber eine vorübergehende Phase, die hier jedoch eine schöne Illustration des generellen Trends gibt.)

Warum also sollten wir da stehenbleiben? Was ist so heilig an der Hierarchie

C#(IL-Code(Maschinencode))

oder

Java(JBC(Maschinencode))

Ist denn der Softwareentwicklung die Phantasie ausgegangen? Haben sich alle im Schmerz eingerichtet, den es macht, 3GL-Code direkt zu schreiben und auch noch fehlerfrei und evolvierbar zu halten?

Nein, ich finde Uncle Bobs Position uninspirier(t/end). Er hat in “Clean Code” das Richtige gesagt und Gutes bewirkt. Danke dafür! Bei Refactoring und Test-first (und seinem Hobby Clojure) jedoch stehenzubleiben und auszurufen “It is all design.”, das finde ich zuwenig.

Warum nicht einen Schritt weiter denken? Wer füllt das Fragezeichen in dieser Hierarchie mit Leben:

<?>(C#(IL-Code(Maschinencode)))

Ist das der Platz für UML? Das glaube ich nicht. UML führt nicht direkt zu 3GL-Code – oder wenn, dann nur sehr umständlich. UML ist de facto ein Werkzeug, das vor allem der Beruhigung von Management dient. Regelmäßig geben auf Entwicklerveranstaltungen (in der .NET-Community) höchstens 15% der Anwesenden an, dass sie mit UML entwerfen.

Oder ist das der Platz für DSLs? Martin Fowler scheint das zu glauben, wenn er nicht gerade derselben Meinung wie Uncle Bob ist. Dagegen spricht für mich, dass DSLs selbst zunächst entworfen werden müssen, bevor sie helfen, 3GL-Code zu entwerfen:

Meta-DSL(DSL(C#(IL-Code(Maschinencode))))

Das scheint mir einer breiten Adoption entgegen zu stehen. Die Konzepte und Notation auf einer Ebene anzuwenden ist kompliziert genug wie wir am Beispiel C# sehen. Da hilft es wenig, Entwicklermassen zu empfehlen, zuerst Kompetenz auf einer Meta-Ebene zu erwerben, um immer wieder neue Konzepte und Notationen zu entwickeln, die dann erst zum Entwurf von Quellcode eingesetzt werden können. Der begrenzte Erfolg von MDSD scheint mir diese Sicht zu bestätigen.

Und nun? Ich kann leider nicht umhin, einen Anwärter auf die Position einer Entwurfssprache für 3GL in Flow-Design zu sehen. (Wer hätte das gedacht? ;-)

Flow-Design(C#(IL-Code(Maschinencode)))

Die bisher entwickelten Konzepte und die Formensprache für Flow-Design ist einfach zu erlernen und deckt viele typische Entwurfsbedürfnisse für Quellcode ab. Es gibt klare Übersetzungsregeln für Flow-Design Entwürfe in 3GL-Code. Die Ergebnisse sind übersichtlich und skalieren. Flow-Design ist nicht domänenspezifisch; einmaliger Lernaufwand zahlt sich also in allen Problemlebenslagen aus.

Soweit mal mein kühnes Statement :-) Aber ich bin natürlich offen dafür, dass man mir die Nutzlosigkeit einer Design-Abstraktionsebene oberhalb von Quellcode erklärt. Einstweilen erlaube ich mir jedoch, Uncle Bob bewusst misszuverstehen. Ich sage auch: It is all design. Aber ich meine damit etwas anderes. Für mich findet Design auf einer höheren Ebene statt als “Quellcoderumgeschubse”.

Sonntag, 6. Februar 2011

Spielend programmieren

imageBesser wird es nicht durch Klagen. Besser wird es nur, wenn man sich überhaupt vorstellen kann, wie es besser sein könnte. Dafür muss man sich manchmal frei machen von dem, was ist. Einfach alle Begrenzungen hinter sich lassen. Mal frei fabulieren, wie die Welt aussehen sollte, und beherzt eine Antwort finden auf die Frage: “Ja, wie hätte ich es denn gern, wenn ich mir etwas wünschen dürfte von einer Fee?”

Heute habe ich mir gegönnt, diese Frage für die Programmierung mal für mich zu beantworten. Geplant hatte ich das nicht. Eher bin ich ohne zu fragen über meine Antwort gestolpert.

Wie wünsche ich mir also die Programmierung?

Ich wünsche mir die Programmierung spielerisch(er). Ich wünsche mir, dass Programmieren so funktioniert wie TinkerBox von Autodesk.

TinkerBox ist ein Spiel, in dem man “Maschinen” baut bzw. vervollständigt, um eine Aufgabe zu lösen. Zugegeben, das sind sehr, sehr, sehr einfache Aufgaben im Vergleich zu einer Warenwirtschaft oder einem Compiler oder einer Stellwerkssteuerung.

Aber auch Druckpressen, Autos, Fahrstühle sind sehr viel komplizierter als die TinkerBox-Maschinen und doch funktionieren sie letztlich nach denselben Gesetzen.

Hier ein paar Impressionen von TinkerBox:

Als ich mit TinkerBox angefangen habe auf meinem iPad zu spielen, habe ich einfach das Gefühl gehabt: “Wow, so sollte auch die Programmierung laufen!” Ich möchte Programme visuell zusammensetzen. Ich möchte sie sofort probeweise laufen lassen. Dabei möchte ich zusehen, wie die Teile zusammenspielen.

Natürlich kann das nicht ganz so simpel sein wie bei TinkerBox. Aber warum muss es denn sooooo anders aussehen? Warum muss es aussehen wie heute, wo ich eigentlich nur wie vor 30 Jahren Text in einer imperativen Sprache in einen Editor klopfe? Das kann doch nicht das Ende der Fahnenstange sein. Wir können doch nicht ernsthaft der Meinung sein, mit einer textuellen IDE (und ein bisschen Visualisierung drumherum) die Spitze des Möglichen in der Softwareentwicklung erklommen zu haben.

Nein, ich möchte, dass das anders aussieht. Ich will Software aus Bausteinen aufbauen. Ich will sie zusammenstecken. Ich will sehen, wie sie funktioniert – zum einen, indem ich am Code die Funktionalität ablese, zum anderen, indem ich den Code dabei beobachte.

Manche der Bausteine sind dabei Standardbausteine, andere sind Bausteine, die ich noch “zuhauen” muss, wieder andere setze ich aus anderen zusammen.

image

Bei TinkerBox gibt es nur Standardbausteine. Die Kunst besteht darin, sie in zielführender Weise zu kombinieren. Für die Softwareentwicklung reicht das natürlich nicht. Wir brauchen Bausteine, die wir “parametrisieren” können. In die gießen wir mehr oder weniger normalen Quellcode. Das finde ich ok. Denn die Menge des Quellcodes ist dann überschaubar.

TinkerBox hat in mir den Gedanken verstärkt, dass wir in der Softwareentwicklung Konstruktion und “Kreation” strikt trennen müssen. Wir brauchen beides, aber es sind grundsätzlich verschiedene Tätigkeiten.

Bei der Konstruktion nehme ich Bausteine und setze aus ihnen etwas Größeres zusammen.

Bei der Kreation denke ich mir neue Bausteine aus (oder “parametrisiere” Standardbausteine). (Ob “Kreation” der beste Begriff dafür ist, lasse ich mal dahingestellt. An dieser Stelle wollte ich aber nicht Implementation schreiben.)

Bausteine zusammenstecken, sie zu einem funktionierenden Ganzen fügen, das ist etwas ganz anderes, als Bausteine zu entwickeln, zu kreieren.

In der Softwareentwicklung trennen wir aber bisher nicht sauber, sondern sind im Grunde ständig mit der Kreation beschäftigt. Die jedoch ist viel schwieriger als die Konstruktion. Der einfache “Beweis”: Selbst Kinder können mit Legobausteinen tollste Dinge konstruieren – kreiert haben aber nicht Kinder die Legobausteine, sondern Erwachsene.

Genauso ist es mit Excel. Millionen von Poweruser können aus den Standardbausteinen in Excel tollste “Rechengebilde” konstruieren. Kreiert haben diese Standardbausteine jedoch Programmierer.

So ganz neu ist die Vorstellung, die ich hier äußere, natürlich nicht. Von Komponenten, die per glue code nur noch verbunden werden müssen, träumte man schon in der 1990ern oder gar davor. Realisiert ist diese Vision aber nur sehr begrenzt.

Mir geht es auch nicht darum, Laien zu Softwareentwicklern zu machen. Ich möchte den Softwareentwicklern nur das Leben erleichtern. Sie sollen sich mehr auf das Wesentliche konzentrieren. Das – so stelle ich mir in einem kühnen Traum vor – können sie aber besser, wenn Programme visueller machen. Die Anhaftung an Text als primärem Ausdrucksmittel für Software, ist anachronistisch und kontraproduktiv. Ich kenne keine andere Branche, in der Designdokumente primär textueller Art sind; nur die Softwareentwicklung beharrt darauf. Denn Programmierung ist Design.

Also: Befreien wir uns von der Last der Texte! Machen wir die Softwareentwicklung haptischer. Entlasten wir uns durch Trennung von Konstruktion und Kreation. Ich glaube, dann wird vieles besser in der Programmierung. Denn dann können wir besser über Software reden und wir können dann besser gemeinsam an Software arbeiten.

Samstag, 29. Januar 2011

Abhängige Flüsse

Abhängigkeiten sind ein zentrales Problem in der Softwareentwicklung. Deshalb haben Flow Designs den Anspruch, mit ihnen konsequent aufzuräumen. Wie sich nun herausstellt, ist das zwar möglich – aber manchmal umständlich. Abhängigkeiten auch in Flow Designs integrieren zu können, erscheint deshalb sinnvoll. Hier mein Vorschlag, wie das geschehen könnte.

Problem #1: Akteure und Request/Response-Kommunikation

Am Anfang des Entwurfs mit Flow Designs (bzw. Event-Based Components) stand die Verbindung von Akteuren als Funktionseinheiten. Beispiel: Ein Frontend fordert Daten von einem Repository. Das konnte entweder mit zwei Drähten ausgedrückt werden…

image

…oder mit einem Draht, dessen Anfragen einen “ad hoc Antwortpin” mit zum Repository schicken:

image

Solche Reques/Response-Kommunikation kann zwar auch mit Flüssen modelliert und implementiert werden, doch das ist umständlich. Frage und Antwort in Input- und Output-Pin zu trennen, fühlt sich nicht intuitiv an.

Das führte dann dazu, von Akteuren weitgehend auf Aktionen umzustellen. Aktionen kennen per definitionem keine Rückgabewerte. Also stellt sich das Problem einer Request/Response-Modellierung nicht:

image

Das funktioniert gut. So lässt sich schneller ein Fluss entwerfen, weil nicht erst aus Anforderungen umständlich ein Akteur ermittelt werden muss; der liegt nämlich nicht so häufig auf der Hand, wie z.B. die Objektorientierung es gern hätte.

Allerdings wird damit der Bezug zu einer Ressource, wie sie hinter einem Repository steht, auf potenziell viele Funktionseinheiten verteilt:

image

Nicht, dass das sehr schlimm wäre. Damit lässt sich leben. Doch “reibungsfrei” fühlt sich das nicht an.

Und wo ist das Respository im Entwurf? Die drei Funktionseinheiten könnten auf eine Platine gelegt werden. Damit wäre aber wieder ein Akteur im Spiel, der nun zwar nicht das Request/Response-Problem zeigen würde, aber dessen Antworten sich nicht unbedingt Flüssen zuordnen ließen.

image

Das ließe sich durch explizite Lebenszeitverwaltung (Singleton/Multiton) beheben – aber irgendwie fühlt es sich immer noch nicht so richtig gut an. Warum soll ich zwingend die Aktionen in eine Akteur-Platine stecken, um ihren Zusammenhang im Modell zu verdeutlichen? Damit verliere ich einen Verständlichkeitsvorteil von Aktionen.

Problem #2: Aktionsübergreifender Zustand

Auch wenn ich keinen Drang verspüre, Aktionen zu einem Akteur zusammenzufassen, möchte ich manchmal, dass sie etwas gemeinsam haben: Zustand. Wie drücke ich in einem Flow Design aus, dass Funktionseinheiten, die zu ganz unterschiedlichen Concerns gehören, Zugriff auf denselben Zustand haben? Ich hatte mir dazu schon länger diese Notation ausgedacht:

image

Das war ok für mich, fühlte sich jedoch noch nicht rund an. Die Tonnen an den Funktionseinheiten brachen irgendwie aus der Notation aus. Und die Gemeinsamkeit von Zustand war nur über den Namen an den Tonnen ablesbar.

Abhängigkeiten als Lösung

Ich hätte es nicht gedacht, aber Akteurproblem und der gemeinsame Zustand lassen sich mit demselben Mittel lösen: mit Abhängigkeiten. Und das in ganz einfacher Weise, wie ich finde.

Funktionale Abhängigkeiten drücke ich in Flow Designs nun so aus:

image

In der UML werden Abhängigkeiten auch mit Pfeilen ausgedrückt. Das vermeide ich hier. Pfeile stehen für mich nur noch für Flüsse in Pfeilrichtung. Trotzdem muss eine Abhängigkeitsverbindung asymmetrisch sein. Abhängige und unabhängige Funktionseinheit müssen klar zu erkennen sein. Deshalb der Punkt am Ende der Abhängigkeitslinie.

Abhängigkeiten sind damit erstens überhaupt möglich und zweitens nicht im Weg, weil sie orthogonal zum Fluss verlaufen. Mit ihnen vorsichtig umzugehen, versteht sich von selbst. Abhängigkeiten bleiben “böse”. Aber bevor sich Flow Design in den Fuß schießt, um sie ganz zu vermeiden, ist es besser, Abhängigkeiten ausdrücken zu können.

Die beiden geschilderten Probleme lösen sich mit Abhängigkeitsmodellierung in Wohlgefallen auf, finde ich. In Problem #1 bleiben die Aktionen sichtbar und trotzdem ist zusammengehörige Funktionalität an einem Ort versammelt, in einer Repository-Funktionseinheit. Ob die Aktionen Singletons oder Multitons sind, ist nun nicht mehr wichtig. Das Repository kann aber ganz einfach ein Singleton sein.

image

In so einem Diagramm würde ich natürlich Repository nicht um weitere Abhängigkeiten anreichern. Ich sehe die unabhängige Funktionseinheit als Black Box. Wie es da drin aussieht, ob da wieder Flüsse definiert sind oder alles “traditionell” nach OOP funktioniert, das ist mir erstmal egal. Der Vorteil von Flow Designs, aus Abhängigkeitsverhauen zu befreien, darf nicht leichtfertig aufgegeben werden. Also vorsichtig mit solchen Abhängigkeiten.

Für mich ist auch immer Ziel, solche unabhängigen Funktionseinheiten intern dann soweit wie möglich mit Flows zu modellieren. Die Abhängigkeit würde dann dazu dienen, Flüsse zu entkoppeln.

image

Problem #2 löst sich mit Abhängigkeiten auch ganz einfach. Alle Aktionen hängen vom selben Zustand ab.

image

Die “Zustandstonne” steht dabei für eine generische Funktionseinheit zum Halten von Zustand, z.B. SharedState<T>. Auf so einer Funktionseinheit lassen sich auch sehr schön Funktionen anbringen, die nützlich in UI-Szenarien (MVVM etc.) und bei asynchronen Modellen sind.

Abhängigkeiten implementieren

Die Übersetzung von Flow-Design-Funktionseinheiten in Interfaces hat sich bewährt. Wie passt dazu die Einführung von Abhängigkeiten. Zunächst schien es, als sollten Abhängigkeiten dazu führen, dass für abhängige Funktionseinheiten keine Interfaces, sondern abstrakte Basisklassen mit Ctor-Injection generiert werden. Das kann man auch machen, wenn man mag – aber ich denke, es ist universeller, Abgängigkeiten über ein Interface auszudrücken. Beispiel zum vorangehenden Bild:

interface Do_sth_in_concern_A : IDependsOn<StateA>
{…}

interface Do_sth_in_concern_B : IDependsOn<StateA>
{…}

Der Start von Flow-Design-Code bekommt damit eine weitere Phase:

  1. Build
  2. Bind
  3. Inject – Abhängigkeiten via Interface injizieren
  4. Config
  5. Run

Build und Bind laufen eigentlich gleichzeitig ab, wenn Platinen die Verdrahtung im Ctor vornehmen, weil sie die Funktionseinheiten, von denen sie immer schon abhängig waren, per Ctor-Injection bekommen.

Danach werden die nun modellierbaren funktionalen Abhängigkeiten injiziert. Es müssen lediglich alle Funktionseinheiten darauf geprüft werden, ob sie das Interface implementieren. Dann bekommen sie eine Instanz der unabhängigen Funktionseinheit. Das kann z.B. so geschehen:

IDependsOn<StateA> do_sth_in_concern_a = …;
do_sth_in_concern_a.Inject(diContainer.Create<StateA>());

Die Konfigurations- und Run-Phase basieren dann auch auf Interfaces. Aber das ist nichts neues.

Ich finde, so ergibt sich ein sauberes Bild.

Und wie sieht so eine unabhängige Funktionseinheit aus? Ganz normal. Sie kann eine traditionelle OO-“Oberfläche” haben, einen ganz üblichen API. Die Flow-Funktionseinheiten, die davon abhängen kapseln den gegenüber dem restlichen Flow.

Zusammenfassung

Für mich löst sich mit der Möglichkeit, Abhängigkeiten auszudrücken, die derzeit letzte “Holprigkeit” in Flow Designs auf. Erste Erfahrungen mit der Modellierung mit und Übersetzung von Abhängigkeiten fühlen sich gut an. Und auch die Erklärung von Flow Design ist nun noch einfacher. Denn wer eine Hürde sieht, einen traditionellen API in Aktionen umzuformulieren, kann ihn (erstmal) behalten. Das erhöht die Attraktivität von Flow Design für Brownfield-Projekte, würde ich sagen.