Follow my new blog

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

Montag, 3. September 2012

Der Fortschritt in Datenflüssen

Datenflüsse sehen einfach aus, sind es auch in gewisser Weise – dennoch haben sie es in sich. Da habe ich neulich gemerkt, als ein Programm, das ich mit Flow-Design locker entworfen hatte, sich dann doch nicht so verhielt, wie erwartet. Es hat funktioniert, das war kein Problem. Aber sein Output war irgendwie, hm, strange.

Deshalb will ich hier einmal die Frage beleuchten, wie denn eigentlich die Verarbeitung in Datenflüssen fortschreitet. Oder besser: Welche unterschiedlichen Fortschrittsweisen kann es denn geben?

Ein Beispielszenario soll die unterschiedlichen Fortschrittsweisen vergleichbar machen. Es ist ganz einfach:

image

Nachrichten, die von einer Funktionseinheit verarbeitet werden, bezeichne ich mit deren Namen: a(x) kommt “von draußen” und wird von a() verarbeitet, b() erzeugt c(y), die von c() verarbeitet wird usw. Nachrichten tragen also das Ziel und nicht die Quelle im Namen. Für das Beispiel reicht das als Identifikation.

Ausgehen von einer Nachricht a() erzeugen die Funktionseinheiten nun diese Nachrichten:

  • a(1)
    • b(11)
      • c(111)
        • e(1111)
        • e(1112)
      • d(111)
      • c(112)
        • e(1121)
    • b(12)
      • d(121)
      • d(122)
      • c(121)
    • b(13)
      • c(131)
        • e(1311)
        • e(1312)
      • d(131)
      • c(132)
        • e(1321)
      • d(132)
      • d(133)
      • c(133)
        • e(1331)
        • e(1332)
      • d(134)

Dieser Baum beschreibt Input-Output-Zusammenhänge, z.B. Input b(12) an b() führt zum Output d(121), d(122) und c(121). Das bedeutet auch, das c(121) nach d(122) erzeugt wird.

Allerdings steckt keine Aussage darüber in dem Baum, wann die Nachrichten verarbeitet werden. Verarbeitet c() die Nachricht c(132) während b() noch weiteren Output generiert oder erst nachdem d(134) ausgegeben wurde?

Darauf geben die Fortschrittsweisen Antwort.

Depth-first

Die naheliegende Vorstellung vom Fortschritt der Verarbeitungsweise des Flows ist wohl, dass jede Nachricht sofort verarbeitet wird. Auf einen Zeitstrahl aufgetragen sieht das so aus:

image

Hier spiegelt sich der Baum direkt wider. So würde die Verarbeitung auch verlaufen, wenn der Datenfluss als Call Stack interpretiert würde, also die Funktionseinheiten sich geschachtelt aufriefen.

imageEine Schachtelung im Sinne einer Servicehierarchie soll ja aber gerade mit Flow-Design vermieden werden. Die Funktionseinheiten a()..e() sollen sich nicht kennen; c() soll nicht von e() abhängen, b() nicht von c() und d(), a() nicht von b().

Alternativ kann diese Fortschrittweise aber auch mit Event-Based Components (EBC) erzielt werden: Wenn a() mit b(11) einen Event feuert, dann arbeitet b() als Event-Handler den erst ab, bevor a() dazu kommt, b(12) auszugeben. Wenn b() währenddessen c(111) erzeugt, arbeitet c() die Nachricht erst ab, bevor b() d(111) erzeugt usw.

Alle Nachrichten werden also erstens sequenziell verarbeitet, d.h. nacheinander und auch noch streng in der Reihenfolge, in der sie erzeugt wurden. Und zweitens werden sie synchron verarbeitet, d.h. während der Verarbeitung wartet die erzeugende Funktionseinheit.

Die synchron-sequenzielle Verarbeitungsweise geht depth-first vor. Die Verarbeitung jeder Nachricht fließt sofort soweit nach rechts durch wie möglich.

Breadth-first

Ist der depth-first Fortschritt der richtige, der beste, der einzig wahre Fortschritt für Datenflüsse? Ich glaube nicht. Er mag der naheliegendste sein, doch das scheint mir für eine Bewertung zu wenig. Denn warum soll es richtiger sein als etwas anderes, Nachrichten sofort in der Tiefe zu verarbeiten und dabei Quellfunktionseinheiten darauf warten zu lassen?

Ich denke, zunächst einmal gleichberechtigt ist die breadth-first Verarbeitung von Nachrichten:

image

Die hervorgehobenen Nachrichten zeigen den Unterschied gegenüber dem depth-first Fortschritt. a() erzeugt die Nachrichten b(11), b(12) und b(13) und die werden zuerst komplett abgearbeitet, bevor deren Output dran kommt. Und auch der wird zuerst abgearbeitet, bevor sein Output dran kommt usw. Die am weitesten rechts stehende Funktionseinheit e() erhält also hier als letzte Arbeit, weil sie am tiefsten im Baum liegt als der der Fluss angesehen werden kann, wenn man ihn um 90°dreht.

Die Verarbeitung eilt damit nicht mehr bei jeder Nachricht zum Ende der Verarbeitung, sondern schreitet sequenziell über alle “Flussarme” hinweg fort. Ich stelle mir das als eine breite Welle vor.

image

Vorteil von depth-first ist, dass nach Anstoß eines Flows schnell erste Ergebnisse am Ende heraustropfen. Das bedeutet aber nicht, dass die Verarbeitung von weit vorangeschritten ist. Bei breadth-first hingegen können Sie sicher sein, dass Arbeitsschritte abgeschlossen sind, wenn ihre Ergebnisse verarbeitet werden.

Das fühlt sich für mich mehr nach Datenfluss an: Ein Input kommt bei einer Funktionseinheit “auf dem Tisch”, wird verarbeitet, dabei wird Output erzeugt – und wenn das alles fertig ist, dann geht es bei der nächsten Funktionseinheit weiter.

Zumindest empfinde ich das als “fluss-mäßiger”, wenn ich die Verarbeitung als synchron denke. Weder findet Verarbeitung auf mehreren Threads innerhalb einer Funktionseinheit statt, noch arbeiten mehrere Funktionseinheiten parallel. Wenn Funktionseinheiten ihre Arbeit abschließen können, bevor ihr Output verarbeitet wird, dann sind sie auch hübsch unabhängig von einander.

Synchrone Verarbeitung ist für mich der default beim Flow-Design. Sowohl Flow-Design Implementierungen mit EBC wie mit der Flow Runtime folgen dem auch – auch wenn sie sich in der synchronen Verarbeitung unterscheiden, wie Sie hier sehen.

Dennoch war ich damit nicht zufrieden. Denn dieser breadth-first Fortschritt hat sich in der eingangs erwähnten Anwendung als etwas merkwürdig angefühlt. Warum?

Solange am Anfang eines Flusses nur eine Nachricht steht, macht breadth-first kein Problem. Dann läuft die große Welle langsam in die Tiefe.

Falls auf a(1) jedoch noch a(2), a(3) usw. folgen und a(i) nicht komplett in der Tiefe verarbeitet ist, bevor a(i+1) eintrifft, kann es zum Stau kommen [1]. Es geht dann zwar alles ganz gerecht zu im Sinne sequenzieller Verarbeitung. Doch solche Gerechtigkeit ist nicht in allen Fällen wünschenswert. Manchmal wäre es gut, wenn Nachrichten einander überholen könnten – zumindest wenn sie in unterschiedlichen Flussarmen fließen. Warum muss ein d(2…) auf ein e(1…) zwangsläufig warten?

Round-robin

Angesichts des merkwürdigen Verhaltens der Anwendung habe ich einen Mittelweg zwischen depth-first und breath-first Verarbeitung gesucht. Eingefallen ist mir eine round-robin Verarbeitung von Nachrichten. Um das zu verstehen, hier die grundsätzliche Arbeitsweise der Flow Runtime:

image

Nachrichten kommen von außen zur Flow Runtime, die sie asynchron verarbeitet. Nachrichten, die bei der Verarbeitung entstehen, fließen entweder hinaus, weil sie Endergebnisse darstellen – oder sie fließen zurück in die Runtime, um von folgenden Funktionseinheiten verarbeitet zu werden. a(i) ist eine Nachricht, die von außen zur Runtime kommt. b(i) usw. sind Nachrichten, die die Runtime quasi an sich selbst schickt.

Jede Nachricht, die bei der Runtime eintrifft, wird auf deren einzigem Thread abgearbeitet; das symbolisiert der Kreis in der Funktionseinheit. Sie ist insofern autonom gegenüber ihrer Umwelt.

Immer wenn eine Funktionseinheit eine Nachricht verarbeitet hat, schaut die Runtime nach, ob weitere Nachrichten zur Verarbeitung anliegen. Die stehen in einer Queue, über die die Runtime in einer Schleife läuft. Der Inhalt dieser Queue sieht für das Beispiel über die Zeit so aus (von links wird angehängt, von rechts entnommen):

image

Das erklärt die breadth-first Verarbeitung: Jede Funktionseinheit wird für eine Nachricht abgearbeitet und stellt ihren Output ans Ende der Queue. Der kommt dann erst dran, wenn der Output vorheriger Funktionseinheiten verarbeitet wurde.

Dieses System habe ich nun aufgebrochen, indem nun jede Funktionseinheit eine eigene Queue besitzt:

image

Über diese vielen Queues läuft nun die Flow Runtime im round-robin Verfahren. Das bedeutet, für jede Nachricht geht sie eine Queue weiter. Es entsteht folgendes Muster:

image

Sie sehen, die Verarbeitung wird Nachricht für Nachricht gleichmäßig über die Funktionseinheiten verteilt. Die Verarbeitungsreihenfolge hat im Grunde nichts mehr mit der Tiefe einer Funktionseinheit im Fluss zu tun. Wo Output erzeugt wird, da wird er auch abgearbeitet.

Käme nun ein a(2) zwischendurch an, so würde es alsbald zur Verarbeitung gebracht, wenn seine Queue an der Reihe ist. Es müsste nicht warten, bis alles, was vorher schon aufgelaufen war, abgearbeitet ist.

Dieses Verfahren scheint mir noch gerechter als breadth-first. Es hat allerdings eine Besonderheit, derer man sich bewusst sein muss: Aufs Ganze betrachtet, erfolgt die Abarbeitung der Nachrichten nicht mehr notwendig streng in Erzeugungsreihenfolge. Nachrichten können einander überholen: e(1112) wird zum Beispiel vor b(13) verarbeitet.

Bei depth-first Fortschritt ist b(13) noch nicht erzeugt, wenn e(1112) abgearbeitet wird. Bei breadth-first wurde b(13) erzeugt und schon abgearbeitet lange vor e(1112). Bei round-robin jedoch steht b(13) noch unverarbeitet in der b()-Queue, während e(1112) schon in Arbeit ist.

Asynchron im Kreis

Auch bei round-robin findet innerhalb der Flow Runtime noch keine Parallelverarbeitung statt. Trotzdem geht es überall voran, sobald die Runtime Gelegenheit hat, eine Nachricht zu verarbeiten. Das ist kein pre-emptive Multitasking, weil ja jede Funktionseinheit so lange an einer Nachricht herumlaborieren darf, wie sie mag. Insgesamt auf den ganzen Fluss gesehen, fühlt es sich dennoch so an, als würde quasi parallel gearbeitet.

Richtig ernst wird das, wenn einzelne Funktionseinheiten asynchron arbeiten. Dann kann der Output während ihrer Laufzeit von der Runtime schon weiterverarbeitet werden.

imageBeispielhaft setze ich mal b() auf asynchrone Verarbeitung, damit Sie sehen, wie sich das Muster dann verändern könnte. Die Länge der Nachrichtenkästen soll nun die Verarbeitungsdauer andeuten.

 

image

Jetzt kommt es natürlich auch darauf an, wann b() Output erzeugt. c(111) wird parallel abgearbeitet und erzeugt e(1111). Währenddessen arbeitet b() weiter! Wann fließt dort aber d(111) heraus? Während c() am Werk ist und e(1111) generiert oder erst später? Denn danach richtet sich, ob auf c() unmittelbar e() folgt wie im Bild oder zuerst d().

Fazit

Die Verarbeitung in Datenflüssen ist anders als die in Servicehierarchien. Anders, doch deshalb nicht schlechter. Sie müssen sich umgewöhnen. Das mag schwer fallen, weil die “Stack-Denke” so tief in uns allen drin steckt. Doch ich meine immer noch, dass sich das lohnt.

Denn anders bedeutet hier chancenreich. So strange das Verhalten der eingangs erwähnten Anwendung war, es hat mich wieder beeindruckt, wie leicht die Flow-Operationen zu testen waren, weil sie unabhängig von einander sind. Und es war ganz leicht, individuell für jede zu entscheiden, ob sie synchron oder asynchron laufen soll.

Und letztlich finde ich es auch gut, überhaupt die Wahl zu haben zwischen Verarbeitungsweisen. Die könnte eine Runtime womöglich sogar zur Auswahl anbieten. Sogar den depth-first Fortschritt hatte ich schon einmal implementiert.

Fußnoten

[1] Dass weitere Nachrichten vor Abarbeitung beim Fluss eintreffen, setzt natürlich bei aller Synchronizität seiner Funktionseinheiten voraus, dass der Fluss als Ganzes gegenüber seiner Umwelt asynchron arbeitet. Das ist bei der Flow Runtime der Fall.

Samstag, 25. August 2012

Event-Based Components mit Ruby

Zunehmend bin ich begeistert von dem, was so in der Softwarewelt passiert – aber erst spät in der .NET Welt ankommt. .NET ist immer noch cool – aber was mal leading edge war, fühlt sich für mich immer behäbiger an. Microsoft ist ein Flaschenhals (geworden) und schon gar nicht führend in vielen Aspekten der Softwareentwicklung.

Mono bringt manchmal Erleichterung, zum Beispiel beim Thema Infrastruktur in der Cloud oder iOS Programmierung. Doch noch mehr weitet sich der Horizont mit anderen Plattformen. Deshalb habe ich jetzt mal einen Vorstoß in Richtung Ruby gewagt.

  • F# ist interessant, reizvoll, verlockend… aber am Ende doch zu Microsoft. In der weiten, weiten Welt passiert damit zu wenig.
  • Java ist keine Option. Dazu bedarf es kaum einer Erklärung, oder?
  • Python ist mir inzwischen schon zu alt.
  • Scala mag ein würdiger Java-Nachfolger und hochaktuell sein, aber fühlt sich für mich immer noch zu instabil an. Bleeding edge will ich mir nicht antun. Dafür habe ich keine Zeit.
  • Clojure ist mir ebenfalls noch zu blutig.

Was bleibt ist, ist aus meiner Sicht Ruby. Es mag schon einen Hauch in die Jahre gekommen zu sein. Clojure is the new exciting kid on the block. Aber für mich ist Ruby gerade gut abgehangen. Tools und Technologien sind stabil, scheint mir.

Auf meinem Macbook Air ist Ruby schon installiert. Da kann ich auf der Kommandozeile mit einer REPL rumspielen. Zusätzlich habe ich mir RubyMine von JetBrains installiert. Das ist ne solide IDE für meine Zwecke.

Also bin ich jetzt im Lern- und Bastelmodus. Eine neue Welt kennenlernen. In kleinen Schritten. Aber natürlich nicht ohne Flow-Design!

Wenn schon ein Ausflug auf eine neue Plattform, dann muss ich Flow-Design Entwürfe dort leicht umsetzen können. Geht das mit Ruby? Ja, das geht. Hier das Listing der Implementation eines ganz einfachen Flows:

image

Ich habe den Flow bewusst mit Event-Based Components realisiert, um ein Gefühl für den Unterschied zu C# zu bekommen. Ich bin begeistert! Das macht Laune mit Ruby. Es geht ganz einfach, wie Sie sehen.

Nach ein paar Stunden zugebracht mit der Lektüre von Ruby Büchern und Rumspielen mit der IDE ist mein derzeitiges Urteil: Ruby fühlt sich schlank an und bietet manches, was ich lange in C# vermisst habe, z.B. geschachtelte Funktionen.

Wenn das so weitergeht, dann kann es sein, dass Sie hier im Blog ab und an Ruby-Code zu lesen bekommen :-)

Donnerstag, 5. Juli 2012

Flow Runtime Meets Event-Based Components

Dass die Flow Runtime ein Ersatz für Event-Based Components sein soll, hat nicht jedem Freund des Flow-Design geschmeckt. Nicht alle sind deshalb mit fliegenden Fahnen zu NPantaRhei übergelaufen.
Ok, ok, kann ich verstehen. Aber jetzt ist Schluss :-) Jetzt gehen Event-Based Components (EBC) nämlich auch mit der Flow Runtime. EBC werden von der Runtime einfach in Operationen “übersetzt”. Und das geht so:

class Rechenwerk
{
    public void Teilen(Tuple<int,int> input)
    {
        if (input.Item2 == 0)
            DivisionDurchNull(input);
        else
            Resultat(input.Item1/input.Item2);
    }
 
    public event Action<int> Resultat;
    public event Action<Tuple<int,int>> DivisionDurchNull;
}

Eine Instanz EBC-Klasse wird dann einfach bei der Konfiguration reingereicht:
var config = new FlowRuntimeConfiguration()
        …
        .AddEventBasedComponent("rechenwerk", new Rechenwerk());

Die Runtime analysiert die Klasse und bietet dann alle Methoden mit der Signatur
  • Action
  • Action<T>
als Input-Port an. Der Methodenname ist der Portname. In diesem Fall wäre es nur Teilen.

Alle öffentlichen Events mit ebensolcher Signatur erkennt die Runtime als Output-Ports. Der Event-Name ist der Portname.

Im Flow müssen dann die Ports explizit angegeben werden.

var config = new FlowRuntimeConfiguration()
                .AddStreamsFrom(@"
                                 /
                                 .in, rechenwerk.teilen                                                rechenwerk.resultat, .result
                                 rechenwerk.divisionDurchNull, .error
                                 ")…

image

Bei Func/Action-Operationen gibt es keine Portnamen, weil sie durch den Verwendungsort der Operationen automatisch unterschieden werden kann zwischen Input und Output. EBC sind also ausführlicher. Aber das ist ja auch Sinn der Aktion. Auf EBC werden Sie zurückgreifen, wenn Ihre Operationen mehrere Input- und/oder Output-Ports haben.

Ich hoffe, damit ist es nun für alle attraktiv, sich mit der Flow Runtime zu beschäftigen. Selbst wer ganz auf EBC eingeschossen ist, kann jetzt davon profitieren, dass Flows für die Runtime mit einem Visualizer definiert und sogar aus Assemblies wieder ausgelesen werden können. Oder er kann ganz einfach die Verarbeitung in einer EBC-Instanz in den Hintergrund legen. Oder sie kann Exceptions in einer EBC-Instanz mit einer Causality abfangen – selbst wenn es auf einem Hintergrund-Thread gekracht hat.

Enjoy EBC at the next level!

Mittwoch, 6. Juni 2012

SEACON 2012 - Vortrag zu agilen Softwarestrukturen

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

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

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

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

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

Mein Credo:

Process over Data

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

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

Oder in der dotnetpro meine Artikel verfolgen.

Oder hier meinen Ausflug ins Single Responsibility Principle mitmachen.

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

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.