Follow my new blog

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.

Kommentare:

Peter Pallhuber hat gesagt…

Du schlägst also so eine Art hybrides Design vor. Die Vorteile von beiden Welten zu verbinden, mag verlockend klingen. Das ist aber trotz der Warnungen, an den du es nicht fehlen lässt, mehr als nur gefährlich. Damit lösen sich alle fünf Vorteile, die du in Steckspiele - Event-Based Components verbinden mühsam erarbeitet hast, in Luft auf. Das ist mehr als ein kleiner Schönheitsfehler, das ist schon eine existenzielle Krise.

Um nur einen Punkt heraus zu greifen: Die in Holarchie – Event-Based Components für natürliche Softwaremodellierung zu recht gescholtenen Abstraktionsebenen aufgrund von Benutzung sind zurück. Im Code kann man nicht mehr zwischen echter hierarchischer Gliederung und einfacher Benutzung unterscheiden. Der Vorteil, dass der Code die Architektur spiegelt, ist verloren. Denn wenn er das nur an einer Stelle nicht mehr tut, kann man sich an keiner Stelle mehr sicher sein.

Man kann alle fünf Punkte durchgehen und sieht, dass plötzlich alles in sich zusammenbricht. Abwiegeln reicht nicht. Nur eine echte Lösung kann die Situation bereinigen.

Ralf Westphal - One Man Think Tank hat gesagt…

@Peter: Ich freu mich, dass du die Vorteile von Flow Design/EBC so deutlich siehst. Und ich kann auch verstehen, dass du Angst hast, sie könnten verloren gehen. Aber keine Sorge, ich glaube nicht, dass die beschriebenen Abhängigkeiten diesen Effekt haben.

1. Die Abstraktionsebenen von Flow Designs verschwinden nicht. Platinen gibt es weiterhin.

2. Abhängigkeiten werden klar in ihrer Art unterschieden. Abstraktion geschieht durch Einschachtelung, Nutzung wird durch "Linien mit Knubbel" angezeigt.

3. Der Code spiegelt natürlich weiterhin den Entwurf wider, da ja Abstraktion und funktionale Abhängigkeit darin ganz unterschiedlich ausgedrückt werden. Auch insofern ist ein Interface für die funkt. Abhängigkeit ein Vorteil.

Ich habe mich lange auch gegen funktionale Abhängigkeiten gewehrt. Oder gar nicht mal gewehrt, sondern sie schlicht ausgeblendet.

Am Ende musste ich jedoch feststellen, dass beim Flow Design immer wieder Friktionen auftraten. Die habe ich im Artikel beschrieben.

Wenn nun aber bei gegebenem Ansatz immer wieder dieselben Probleme auftreten, dann ist irgendwas mit dem Ansatz noch nicht richtig. Dann muss man drüber nachdenken, wie er nachgebessert werden kann.

Zumindest für mich haben sich mit den beschriebenen funktionalen Abhängigkeiten nun die wiederkehrenden Probleme gelöst.

Ich denke nicht, dass damit das grundlegende Paradigma des Flow Design kompromittiert ist. Wie gesagt, die funktionalen Abhängigkeiten sollen ja nicht weitergetrieben werden. Ich sehe nicht, dass in Flow Designs nun großartige Abhängigkeitsbäume auftauchen. Aber hier und da eine Abhängigkeit herstellen, das ist ok. Sozusagen ein Tor in eine andere Dimension - die in einem anderen Diagramm dann weiter beschrieben werden kann.

Du wirst in der Zukunft Beispiele dafür sehen. Und ich hoffe, dass du mir dann zustimmen kannst, dass die Vorteile diese kleine, scheinbare Unsauberkeit ausgleichen.

Al3x3 hat gesagt…

Interessanter Ansatz. Aber irgendwie fällt es mir schwer so eine "universelle" Funktionseinheit vorzustellen, die unterschiedlich strukturierte Objekte entgegennimmt, versteht und verarbeitet. Was ist mir der Struktur bzw. der Bedeutung der einzelnen Elemente von so einem State? Wird sie von IDependsOn<> vorgegeben? Wozu dann die Trennung zwischen Zustand und Verhalten? Na ja, vielleicht verstehe ich die EBCs auch noch nicht wirklich :) Gibt es eine Beispiel-Applikation die nach diesem Muster implementiert ist (evtl. eine die man direkt mir anderen Mustern wie CQRS/DDD vergleichen kann)?

-Alex

Lars hat gesagt…

Zustände innerhalb von Prozess-Flows sind doch eher Variablen oder?

Also natürlich empfinde ich zumindest den sprachlichen Umgang mit diesem Konstrukt nicht:
"Die Aktionen: Reingehen, Tür zu machen sind abhängig vom Zustand 'Tür offen'".
Wenn die grafische Darstellung eher in Richtung gehen würde: "Wenn die Tür offen ist, dann geh rein und mach sie zu", dann könnte man das im letzten Artikel beschriebene "zu doof für OOP-Problem" vielleicht beim Flow-ansatz umgehen.

Letztendlich ist das auf der unteren Ebene in beiden Fällen ja eine shared-variable...also inhaltlich würde sich ja nichts tun, sondern es würde nur die Natürlichkeit der Entwicklungsmethode vor die theoretisch bessere Variante stellen. Das würde ich für einen guten Tausch halten.

Ausserdem wäre man in der asyncronen Welt dann auch flexibler, wenn schon jemand anders die Tür nachdem man eingetreten ist geschlossen hätte...

"IF THEN" ist um längen verständlicher als "Depends on".

Ralf Westphal - One Man Think Tank hat gesagt…

@Lars: Zustände sind globale Variablen; so könnte man das sagen. Shared state, von aber genau bekannt ist, wer auf ihn kennt. Denn die Abhängigkeiten sind ja im Design explizit.

Was dich daran stört, verstehe ich leider nicht. Dürfen Aktionen oder allgemein Bauteile keinen Zustand haben? Dürfen sie keinen gemeinsamen Zustand haben?

Es mag vorzuziehen sein, keinen und vor allem keinen gemeinsamen Zustand zu haben. Aber es lässt sich nicht immer vermeiden. Irgendwo ist der Zustand - zumindest in einem Persistenzmedium oder im Frontend. Ihn von dort immer wieder zu holen oder mitzuschicken, empfinde ich als umständlich. Deshalb sollte gemeinsamer Zustand in Flow Designs erlaubt sein.

Und wenn er erlaubt ist, dann sollte er so explizit wie möglich sein. Denn dann ist man sich dieser Suboptimalität bewusst.

Das ist auch von Vorteil in async Szenarien. Wenn ich gemeinsamen Zustand im Design sehe, dann kann ich mir viel besser Gedanken darüber machen, wie der Zugriff darauf koordiniert werden sollte.

Toni hat gesagt…

Ich bin begeistert von dem Konzept der Software-Platinen. Das kann intuitv entworfen, einfach verstanden und inzwischen soger grafisch umgesetzt werden. Super!

Aber wie Du selbst erkannt hast: "Frage und Antwort in Input- und Output-Pin zu trennen, fühlt sich nicht intuitiv an." Und mit der vorgeschlagenen Abhängigkeitsmodellierung wird das Systemdesign umfangreicher und damit weniger übersichtlich.

Aus meiner Sicht bietet die Welt der Hardware-Platinen hier andere interessante Lösungsideen.

Zum einen werden hier die Platinen (oder externen Komponenten) nicht mit einzelnen Drähten verbunden, sondern mit Kabeln. Die Kabel mit einem männlichen und einem weiblichen Stecker werden an den entsprechenden Buchsen der Platinen eingesteckt. Den Buchsen entsprechen in der Software einem Interface. Eine Platine hat meist mehrere spezialisierte davon. Und das Kabel ist mit seinen Steckern ein einigermaßen komplexes Bauteil, das auch als eigenständiges Software-Bauteil umgesetzt werden sollte. So lange die Stecker in die Buchsen passen, ist es damit möglich, das Kabel durch beliebig komplexe Geräte zu ersetzen, z.B. ein zweipoliges Crossover-Kabel durch das weltweite Netz.

Zum anderen laufen auf den Hardware-Drähten teilweise höchst komplexe Protokolle, und zwar nicht nur unidirektional. Bei Mikroprozessoren gibt es "Tri-State"-Pins, , deren Ausgänge nicht wie üblich nur zwei (0 und 1), sondern zusätzlich noch einen dritten Zustand annehmen können. (Wikipedia) In diesem dritten Zustand kann der Pin Eingangssignale empfangen. Um bidirektionale Pins und Drähte mit komplexen Protokollen in der Software abzubilden, könnten sich als Alternative zum synchronen EBC vermutlich Delegaten anbieten. Für asynchrone Kommunikation müssen natürlich weiterhin Events verwendet werden, aber das ist wieder intuitiv verständlich.