Follow my new blog

Sonntag, 11. April 2010

Feingranular – Methoden als Event-Based Components

Mit Event-Based Components können Sie sehr schön top-down, bottom-up oder nach dem Jo-Jo-Prinzip entwerfen. Es ist egal.

Holarchische Modellierung

Fangen Sie einfach an. Malen Sie einfach Systeme von Funktionseinheiten, die Ihr Problem durch den Austausch von Nachrichten lösen.

image

Diese Funktionseinheiten sind einfach Verantwortlichkeiten. Wie sie später mal implementiert werden…? Keine Ahnung. Während der Entwurfszeit, mache ich mir da nicht viele Gedanken. Ich male einfach hin, was mir auf dem aktuellen Abstraktionsniveau in den Sinn kommt, das hilfreich sein könnte. Für eine kleine Funktionsplotteranwendung sind das halt zunächst diese beiden Funktionseinheiten.

Wenn ich dann aber weiter überlege, dann fallen mir natürlich Subfunktionalitäten ein, z.B.

image

Was werden denn die eingeschachtelten EBCs in der späteren Implementierung? Immer noch keine Ahnung. Muss auch nicht sein. Es sind halt Funktionseinheiten, die mir sinnvoll erscheinen. Sie zerlegen die große ganze Funktionalität in besser handhabbare Teilfunktionalitäten. Je weiter mein Verständnis der Problemdomäne voran schreitet, desto mehr solcher Teilfunktionalitäten erkenne ich. Wenn ich z.B. darüber nachdenke, wie so ein Compiler aussieht, dann komme ich auf folgende Verfeinerung:

image

Was sind Scanner und Parser nun am Ende in der Implementation? Keine Ahnung. Damit muss ich mich während des Entwurfs nicht beschäftigen. Das ist das schöne an einer “holarchischen Modellierung” :-)

Holarchisch ist dieser Entwurf, weil das System der Funtionseinheiten hierarchisch ist und auf jeder Ebene wieder aus “Teil-Ganzen” (Holon)  besteht. Jedes Blatt einer Ebene (im letzten Bild z.B. Parser) kann jederzeit weiter zerlegt werden. So ein EBC-Softwaresystem ist eine selbstähnliche Struktur, quasi fraktal :-)

Ein Modell ist dieser Entwurf, weil die Elemente eben keine Codeartefakte sind. Ich denke beim EBC-Entwurf nicht in Assemblies oder Klassen oder Methoden. Ob eine Funktionseinheit wie Plotter oder Scanner am Ende als dieses oder jenes Artefakt implementiert wird, das ist während des Entwurfs unwichtig. Der soll nur ein problemlösendes System entwickeln.

So weit, so gut.

Nach dem Entwurf kommt aber natürlich irgendwann doch die Implementation. Dann müssen Sie sich Gedanken machen, wie Sie die geplanten Funktionseinheiten übersetzen.

Funktionseinheiten in Klassen übersetzen

In den bisherigen Blogartikeln habe ich EBCs immer in Klassen übersetzt. Aus einem Blatt ist dort eine EBC-Bauteil-Klasse  geworden, deren Input-Pins Methoden und deren Output-Pins Events waren. (Statt Events werde ich in Zukunft jedoch normale Delegaten benutzen. Events haben einen Sonderstatus als Delegaten, der den Umgang mit Ihnen in generischer Weise manchmal erschwert.)

image

Das ist eine sinnige Übersetzung für die allermeisten Fälle, denke ich, die auch für Nicht-Blätter ausreicht. EBC-Platinen, d.h. Funktionseinheiten, die andere aggregieren, enthalten allerdings keine eigene Domänenfunktionalität, sondern nur Code, um ihre Konstituenten zu verdrahten. Das passiert im folgenden Beispiel im Konstruktor.

image

Diese Übersetzungen für Bauteile und Platinen reichen eigentlich völlig aus, um beliebig tief geschachtelte Systementwürfe in Code zu manifestieren. Es gibt keinen Impedance Mismatch zwischen Modell und Implementierung.

Funktionseinheiten in Methoden übersetzen

Eigentlich könnte ich zufrieden sein mit der Übersetzung von EBCs in Klassen. Aber irgendwie wurmt es mich, dass ich mit einerseits bei der Modellierung keine Gedanken machen soll darüber, wie die Implementierung einer Funktionseinheit aussieht. Andererseits jedoch werde ich quasi gezwungen, darüber nachzudenken, ob denn eine Funktionseinheit auch nicht kleiner als eine Klasse sei. Zwar sollte ein Architekturentwurf mit EBCs nicht zu detailliert ausfallen; immerhin soll sich ein Architekt nicht um Klassen, sondern um Komponenten kümmern. Doch irgendwie kann ich der Zerlegung nicht freien Lauf lassen, wenn Blätter zwangsweise in Klassen übersetzt werden müssen.

Beim Funktionsplotter macht mir das noch keine Probleme. Scanner, Parser, Codegenerator sind umfangreich genug, um eine Übersetzung in Klassen zu rechtfertigen. Doch wie steht es mit einer Übersetzung einer Zeichenkette in ein Dictionary? Aus “a=1;b=2;…” soll ein Dictionary<string, string> werden.

Ein Softwaresystem könnte ich mir dafür so vorstellen:

image

Wenn ich´s jedoch genauer bedanke, dann lohnen eigene Klassen für die Blätter in dieser Holarchie nicht. SplitInfoPairs usw. sind so einfach, dass diese Funktionseinheiten als Methoden implementiert werden können. Das erkenne ich jedoch erst am Ende; während des Entwurfs habe ich mir darüber keine Gedanken gemacht, sondern das Gesamtproblem soweit in EBCs zerlegt, wie ich Ideen für die Verarbeitungsschritte hatte.

Wenn ein Problem vor Ihnen liegt, wissen Sie einfach nicht, wie tief Sie ein Lösungssystem schachteln können, ohne zu implementiere. Deshalb wissen Sie auch nicht, ob Sie am Ende nicht bei Funktionseinheiten ankommen, die im Grunde zu klein für eine Implementierung in eigenen Klassen sind.

Aber wie können EBC-Bauteile, also Blätter des Zerlegungsbaumes, genauso systematisch in Methoden überführt werden wie in Klassen? Bei Klassen sind die Übersetzungen von Input- und Output-Pins klar. Was entspricht denen aber bei Methoden?

Ich glaube, folgende Übersetzungen sind naheliegend:

image

Blätter, die in Methoden übersetzt werden sollen, haben nur 1 Input-Pin, aber beliebig viele Output-Pins. Der Input kommt als Parameter in die Methode, wie bei den Input-Pin-Methoden von Klassen-EBCs. Für den Output gibt es allerdings keine globalen Output-Pin-Delegaten; stattdessen werden die ebenfalls als Parameter hineingereicht.

Warum keine Übersetzung dieser Blatt-EBCs in Funktionen? Weil ein Rückgabewert pro Input-Nachricht nur einmal geliefert werden kann. Wenn ein Input jedoch n>1 oder n=0 Output-Werte erzeugen kann, dann ist deren Lieferung über einen Rückgabewert nur umständlich möglich.

Im Sinne einer systematischen Übersetzung bin ich deshalb dafür, kleine Blatt-EBCs immer in Methoden nach dem obigen Schema zu übersetzen. Das mag zunächst eine wenig umständlich erscheinen, doch ich denke, die Regelmäßigkeit und damit leichtere Verständlichkeit ist dafür eine Entschädigung. Denken Sie also nicht über die “beste” Übersetzung nach, sondern konzentrieren Sie sich auf die Ausfleischung der systematisch erzeugten Form.

Eine Ausnahme finde ich allerdings doch akzeptabel: Wenn eine Blatt-Funktionseinheit nur einen Output-Pin hat, kann sie als Funktion mit Rückgabewert IEnumerable<T> definiert werden. So sind auch n=0 und n>1 Rückgabewerte möglich.

image

Das eröffnet auch Möglichkeiten der Verkettung von EBC-Methoden mittels Linq.

Für Platinen bedeuten Methoden als EBCs natürlich eine Veränderung der Verdrahtung:

image

Die Verdrahtung bleibt beschränkt auf den Konstruktor. Deshalb leitet der Input-Pin der Platine die Nachricht weiter an einen Delegaten, der die eigentliche Methode “umwickelt”. Der Input-Pin weiß dadurch nichts von einem evtl. Output des Pins, an den er weiterleitet.

Dasselbe gilt für die Output-Pins, die an die Methoden übergeben werden. Auch das sind Delegaten, um dem Methoden-EBC Details über die Weiterverarbeitung seines Output zu verbergen.

Ob die Platine wie hier die Methoden-EBCs selbst implementiert und somit nicht mehr sauber nur verdrahtet, sondern auch Domänenfunktionalität implementiert, ist nicht so wichtig. Ich finde das erstmal ok. Aber Sie können natürlich auch die Methoden als Delegaten via ctor-Parameter injizieren. Oder Sie könnten eine Basisklasse definieren, die nur die Methoden-EBCs implementiert und die Platine davon ableiten.

Funktionseinheiten im Flow

Methoden-EBCs wie eben gezeigt zu verdrahten, ist simpel, aber etwas nervig. Die temporären Variablen für die Lambda Funktionen machen den Code unübersichtlich. Deshalb komme ich hier auf ein Thema zurück, dass ich schon früher hier im Blog am Wickel hatte: Flows.

Wenn Methoden-EBCs so regelmäßig gebaut sind, dann ist dafür auch etwas Toolunterstützung möglich. Hier meine Idee davon, wie ein Flow-API für EBCs die Verdrahtung von Methoden-EBCs erleichtern könnte. Statt die Methoden mit Hilfsvariablen und auch noch in umgekehrter Reihenfolge zusammenzustecken

public B2()
{
  Action<W> input_z = m => Z(m, this.Output);
  Action<V> input_y = m => Y(m, input_z);
  this.input_x = m => X(m, input_y);
}

könnten Sie sie in besser lesbarer Form verdrahten:

public B2()
{
    this.input_x = Flow
                     .StartWith<string, StringBuilder>(X)
                     .Then<int>(Y)
                     .Then<bool>(Z)
                     .Finally(b => this.Output(b));
}

Hier ist vor allem die Reihenfolge erhalten, so wie die Nachrichten durch die EBCs fließen, von X nach Y nach Z.

Die Implementation des APIs dafür sieht so aus:

public static class Flow

{

    public static Flow<TInput, TOutput> StartWith<TInput, TOutput>(Action<TInput, Action<TOutput>> stage)

    {

        return new Flow<TInput, TOutput>(GenerateInputPinFactory<TInput, TOutput>(stage));

    }

 

 

    internal static Func<Action<TOutput>, Action<TInput>> GenerateInputPinFactory<TInput, TOutput>(Action<TInput, Action<TOutput>> stage)

    {

        return outputPin => message => stage(message, outputPin);

    }

}

 

 

public class Flow<TInput, TOutput>

{

    private readonly Stack<Delegate> inputPinFactories;

 

 

    internal Flow(Delegate inputPinFactoryForStage)

    {

        this.inputPinFactories = new Stack<Delegate>();

        this.inputPinFactories.Push(inputPinFactoryForStage);

    }

 

    internal Flow(Stack<Delegate> inputPinFactories,

                  Delegate inputPinFactoryForStage)

    {

        this.inputPinFactories = inputPinFactories;

        this.inputPinFactories.Push(inputPinFactoryForStage);

    }

 

 

    public Flow<TInput, TNextOutput> Then<TNextOutput>(Action<TOutput, Action<TNextOutput>> stage)

    {

        return new Flow<TInput, TNextOutput>(this.inputPinFactories,

                                            Flow.GenerateInputPinFactory(stage));

    }

 

 

    public Action<TInput> Finally(Action<TOutput> stage)

    {

        Delegate inputPin = stage;

        while(this.inputPinFactories.Count() > 0)

        {

            Delegate inputPinFactory = this.inputPinFactories.Pop();

            inputPin = (Delegate)inputPinFactory.DynamicInvoke(inputPin);

        }

        return (Action<TInput>)inputPin;

    }

}

Mit jedem Do<>() wird eine Factory-Methode auf einen Stack gelegt. Diese Methoden erzeugen später die Input-Pin-Delegaten für die Methoden. Die wiederum werden an die nächste Factory-Methode übergeben. Das geschieht in der handgeschriebenen Version mittels der temporären Delegaten wie input_z.

Fazit

Ich denke, es spricht für EBCs, dass das Konzept sich auch auf Methoden ausdehnen lässt. Bei der Planung müssen Sie sich nicht beschränken. Wenn Sie fertig sind mit dem Architekturentwurf auf beliebig vielen Abstraktionsebenen des Lösungssystems, dann schauen Sie einfach, wie Sie die Blätter des Funktionseinheitenbaums implementieren. Für viele werden sich Klassen anbieten. Für manche aber sicherlich Methoden. Für Platinen-Klassen, Bauteil-Klassen und auch Bauteil-Methoden haben Sie nun Implementierungsregeln.

Ich finde das sehr beruhigend. Denn so kann ich mich einerseits auf den Entwurf konzentrieren; dabei muss ich nicht immer im Hinterkopf schon die Frage der Implementierung wälzen. Und andererseits kann ich mich bei der Implementierung auf die Domänenlogik konzentrieren, d.h. das Ausfleischen der Funktionseinheiten; ich muss mir den Kopf nicht über ihre Form zerbrechen.

Systematik und Regelmäßigkeit helfen, den Blick auf dem Wesentlichen zu halten. Mein Gefühl ist, davon brauchen wir mehr.

7 Kommentare:

oo hat gesagt…

Schöne Zusammenfassung! Und kommt gerade richtig ;) Ich versuche das mal mit Rhino ESB zu koppeln.

Jan Fellien hat gesagt…

Hallo Ralf, das sieht wirklich toll aus, aber was ist mit Sprachen fern von Gererikern, Typensicherheit oder Delegaten?

(Ich weiss, Disziplin, Disziplin und nochmals Disziplin)

Es muss doch auch möglich sein für den Rest der Welt EBCs zu formulieren.

Ralf Westphal - One Man Think Tank hat gesagt…

@Agile Ära: EBCs kannst du mit jeder Sprache basteln, die Funktionszeiger bietet. Hier (http://renaud.waldura.com/doc/ruby/idioms.shtml) kannst du z.B. Funktionszeiger in Ruby, Java und Perl sehen.

Je weniger eine Sprache allerdings bietet, desto kruder werden natürlich die EBCs. Das Programmiermodell bleibt aber.

-Ralf

Anonym hat gesagt…

Hallo Ralf,

ich habe noch ein kleines Verständnisproblem. Wie komme ich denn an die Ergebnis der Methodenkaskade wieder ran? Output ist ja als Action definiert.

Angenommen, die Methoden X, Y und Z sind arithmetischer Natur und möchte das Ergebnis aller Rechenschritte einer Variablen z. B. vom Typ double zuweisen. Wie mache ich das? Muss ich dann statt Action eher Func verwenden? Das passt aber nicht zu der allgemeinen Signatur für EBCs, oder?

Viele Grüße,
Marcus

Ralf Westphal - One Man Think Tank hat gesagt…

@Markus: Du du kannst dem letzten Schritt doch noch einen folgen lassen, der das Ergebnis in eine Variable tut.

Mit EBC 2.0 gibt es das "Funktionsproblem" auch nicht mehr. Da sind alle Schritte Aktionen. Und Aktionen bekommen keine Antworten, sondern senden nur Daten weiter. Was weiter geschieht, bestimmen dann andere Aktionen.

Christoph Kögl hat gesagt…

Sorry, dass ich erst so spät kommentiere. Als (u.a.) altgedienter C++-Programmierer finde ich es richtig gut, dass signal/event-basiertes Entwerfen Programmieren seinen Weg nun auch in die .NET-Welt findet (wie in Kommentaren schon erwähnt gibt es ja dieses Konzept in Qt (und das ist beileibe nicht nur eine GUI-Bibliothek!) und in Form von Bibliotheken (Boost.Signal, libsigc++, sigslot) schon seit Jahren).

Bei der Übersetzung von EBCs in Methoden hakt es bei mir jedoch aus. Und auch die vorgeschlagene Implementierung mit Flow/StartsWith/Then/Finally klickt nicht so richtig, ist mir zu viel Rauschen und Zeremonie. Wieso nicht so (Vorsicht, ich bin kein C#-Entwickler, habe den Code nicht ausprobiert):

public B2()
{
this.input_x =
m => X(m, n => Y(n, o => Z(o, this.Output)));
}

Dann sind X, Y, Z in der "richtigen" Reihenfolge, ganz ohne die Flow-API. Und wenn jetzt X, Y, Z schon keine Klassen mehr sind, sondern Methoden, was spricht gegen diese Simpel-Implementierung:

class B2
{
private V X(U m) {...}
private W Y(V m) {...}
private T Z(W m) {...}

public void Input(U msg)
{
this.Output(Z(Y(X(msg))));
}

public Action Output;
}

Hier sind jetzt zwar X, Y, Z in der "falschen" Reihenfolge hingeschrieben, aber das war bei Funktionsanwendungen schon immer so (und ist deshalb REINE Gewohnheit; und was ist mit den Schreibern von Hebräisch, Arabisch, Japanisch, ... die finden "von links nach rechts" auch nicht notgedrungen natürlich; die Mathematiker sind eben auch so welche ...). Dass Funktionsanwendung doch bitte dem "normalen" Lesefluss folgen möge ist eben eine reine Erwartungshaltung der prozeduralen Programmierer, die applikativen haben dieses Problem nie gehabt. Aber wenn es der Akzeptanz dient, sei's drum.
Einen Einwand lasse ich auch noch gelten: Man kann eine Verdrahtung mittels Delegaten auch zur Laufzeit ändern, ein "Z(Y(X(msg)))" nicht. Aber diese Änderbarkeit zur Laufzeit stand ja bei den EBC nicht im Vordergrund.

Generell gefallen mir EBCs sehr gut. Im Kontext von Delphi habe ich sie ausprobiert (das geht jetzt endlich vernünftig, weil Delphi jetzt endlich "Generika" und Closures (aber leider keine nativen Multicast-Events) unterstützt), und es fühlt sich gut an! Ich meine: Aus EBCs kann was, muss was werden!

Ralf Westphal - One Man Think Tank hat gesagt…

@Christoph: Dein

this.input_x =
m => X(m, n => Y(n, o => Z(o, this.Output)));

find ich als eine mögliche Übersetzung von Flow Designs völlig ok. Mach das ruhig so, wenn du willst.

Am Ende finde ich es so aber nicht wirklich flexibel. Ich denke, das wirst du auch merken, wenn du mehr mit EBC machst. Split und Join sind mit der Übersetzung nicht schön zu realisieren.

Auch leidet die Lesbarkeit schnell, wenn es größer wird.

Gegen die bei genügend harter Übung gute Lesbarkeit von f(g(h())), also von Schachtelungen, wehre ich mich. Klar, wenn ich dicke Hornhaut habe, dann merke ich so manches nicht mehr. Aber deshalb ich es trotzdem nicht so gut, in Dornengestrüpp zu packen.

Schachtelungen können Menschen einfach nur schwer lesen. Das ist so. Mit Übung geht es einfacher - aber warum sollte ich üben, wenn ich ohne Übung leben kann?

Der Verweis auf Hebräisch oder Chinesisch hinkt auch. Dort geht es nicht um Schachtelung, sondern nur Richtung. Und es gibt keine mehreren Abstraktionsebenen.

Und wenn doch, wie bei Schachtelsätzen, die man ab und an - insbesondere in "guter" Literatur (wer liest die heute noch, außer bei der Vergabe des Bachmann-Preises?) - findet, dann ist das letztlich doch kein kopierenswerter Stil, wir die jeder Stilratgeber - von Schopenhauer bis Schneider - sagen wird :-)