Laden...

Sample-Anwendung zur Entwicklung von eventbased Components (EBC)

Erstellt von ErfinderDesRades vor 13 Jahren Letzter Beitrag vor 13 Jahren 6.089 Views
ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 13 Jahren
Sample-Anwendung zur Entwicklung von eventbased Components (EBC)

Hi!

Ich hab mal eine Sample-App gemacht, bei der ich die Konzepte von EBC versucht hab, umzusetzen. Die Idee, so eine Sample-App zu erstellen, ist von gfoidl, und auch die Anforderungen habich von ihm übernommen. Das Sample ist mit VS2010 gemacht, auch weil FW4 die für Threading-Szenarien hochinteressante BlockingCollection bereithält.

Anforderungen
Es handelt sich um eine App, die eine zeilenweise, zeitaufwändige Transformation von TextFiles durchführt.
Der User wählt ein InputFile, ein OutputFile, und startet die Transformation.
Es wird das InputFile zeilenweise eingelesen, gefiltert (jede 3. Zeile), und zw. alle Worte werden Kommata eingefügt.
Die Ergebnis-Zeilen werden sowohl im Frontend angezeigt, als auch im OutputFile niedergeschrieben.
Die Transformationsberechnung im "Processor" wird als zeitintensiv angenommen (wird mit Thread.Sleep() simuliert).
Daher wird der Processor doppelt gepuffert, sodaß der Lese(+Filter)-Vorgang, die Transformation, und der SchreibVorgang in jeweils eigenem Thread stattfinden.
Also der Reader füllt den 1. Puffer, der Transform-Processor arbeitet diesen im anderen thread ab und füllt den 2. Puffer, der vom Writer (im eigenen thread) ausgelesen und weggeschrieben wird.
Die Threads sind unabhängig vom Gui, damit dieses nicht blockiert.
Soweit noch einfach.
Aber das Gui muß den Start-Button deaktivieren, solange der Vorgang läuft - der Transformer muß also ein IsBusy-Event bereitstellen.
Die Lese- und Schreib-zugriffe sollen mit TryCatch bestimmte Exceptions abfangen - für den Reader ist die FileNotFoundException gewählt, für den Writer die DirectoryNotFoundException.
In der Praxis würde man wohl mit BordMitteln der File- und Directory-Klassen absichern, um TryCatch nicht für KontrollFluss zu mißbrauchen. Ich will aber ausdrücklich mit EBC ein Exception-handling realisieren, und die genannten Exceptions sind leicht zu provozieren, was mein Exception-handling leicht testbar macht.
Beim Auftritt einer Exception soll die Transformation abgebrochen werden, und der Fehler im Frontend angezeigt - kein Programm-Abbruch.
Als ungecatchte Exception lässt sich eine DirectoryNotFoundException für den Reader provozieren, welche global behandelt wird, bevor die App geschlossen wird. Das hat nicht eigentlich mit EBC zu tun, ist IMO aber auch ein interessantes Feature, und die elegante Vorgehensweise, insbes. in Threading-Szenarien nicht überall bekannt.
Innerhalb der IDE springt immer noch der Debugger ein, aber wenn man die App außerhalb startet, greift der globale Catch - dort wäre dann etwa zu loggen.

Implementation
Wie gesagt, die App ist eine Übung in EBC, ich habe also die meisten Konzepte umgesetzt, denke ich.
Das fängt mit dem Konzeptionieren an, man betrachte die Diagramme:

Der frühe Apfel fängt den Wurm.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 13 Jahren

hier das Transformer-Board, ein "Zoom-In" des obigen Transformers:

Der frühe Apfel fängt den Wurm.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 13 Jahren

Mainboard zeigt die Kommunikation der 2 Komponenten FrontEnd und Transformer - ich denke, da ist nicht viel zu erläutern - eine leicht verständliche Visualisierung der Anforderung.
Das Transformer-Diagramm ist ein "Zoom-In" in die im Mainboard dargestellte Transformer-Komponente.
EBC an beiden Diagramm ist, dass sie 1:1 in Code umgesetzt werden.
Die Pfeile zeigen die Verbindung von einem Event zu einer Methode, die es abonniert. Die Verschachtelung der Blöcke spiegelt die Objekt-Hierarchie wieder, also Mainboard enthält 1 FrontEnd und 1 Transformer (Composition-Pattern), Transformer enthält 7 Unterkomponenten, die noch zu besprechen sind.

Entkopplung
Auf die IMO anschaulichen Diagramme folgt der nächste EBC-Effekt: Komponenten sind vollkommen entkoppelt (außer, eine enthält die andere). Programmiert wird nicht gegen ein oder mehrere Interfaces, sondern einfach gegen die eigenen Pins - wo die angeschlossen sind, ist Sache der übergeordneten Komponente.

**Separation of Concerns: Eine Klasse - eine Zuständigkeit. **
Es gibt zusammengesetzte Komponenten: "Boards", und Bausteine: "Atoms". In den Atoms steckt die eigentliche Logik. Board-Concern ist, die enthaltenen Komponenten korrekt miteinander zu verbinden - sonst nichts.

Events
EBC hat sich von der Standard-Event-Signatur (void Eventname(object sender, EventArgs e)) verabschiedet, da für seine Zwecke unpraktisch. Stattdessen ist die Signatur void Eventname(T value) etabliert, wobei T alles sein kann, auch eine komplexe Klassen mit mehreren Membern. Tatsächlich kann man statt Events auch Delegaten einsetzen, womit man sich höhere Flexibilität zum Preis einer schwächeren Kapselung einkauft. Auch wenn es in dem Fall nicht ganz korrekt ist, rede ich weiterhin von Events, denn das ist die Funktion eines als Out-Pin eingesetzten Delegaten.

Der Transformer
Während das Frontend ein Atom ist (hier: ein Win-Form mit den angegebenen Pins) ist der Transformer ein Board, und verdrahtet 7 Atome. Auch dieses Diagramm ist 1:1 in Code umgesetzt, und man kann den Ablauf einer Transformation genau nachvollziehen:
Eingang ist ein TransformInput mit 2 Membern: InputFileName und OutputFileName. Der TransformInput stellt seine Member auf verschiedene Weisen zur Verfügung, und es steht dem Transformer frei, sich Daten per Event "pushen" zu lassen, oder sie bei Bedarf per Callback abzurufen.
Zum Nachvollzug der Transformtion folgen wir einfach dem einzigen ausgehenden Pfeil, ein Event, dass den OutputfileName in den Writer pusht.
Dort wird davon intern ein StreamWriter-Objekt erstellt, und wenn das einen Error hervorruft, geht eine Error-Meldung raus, und das wars.
Andernfalls feuert er IsBusy(true), und es geht weiter:
Nämlich mit dem interessanten asymmetrischen Doppelpfeil - das ist ein _:::


public class Writer {
   public event Action<Action<string>> GetLines;      // Callback-Out_Pin
      
   // = delegate { } prevents ArgumentNullException, if the event isn't subscribed at all
   public event Action<bool> IsBusy = delegate { };   

   // if not subscribed, the ArgumentNullException indicates a faulty wiring up the component
   public event Action<Exception> Error;  

   public void Run(string output) {
      StreamWriter sw;
      try {
         sw = new StreamWriter(output);
      } catch(DirectoryNotFoundException x) {
         Error.Invoke(x);
         return;
      }
      IsBusy.Invoke(true);
      using(sw) GetLines.Invoke(sw.WriteLine);      // redirect answers to StreamWriter.WriteLine
      IsBusy.Invoke(false);
   }
}

Diese elegante Redirektions-Option des Callbacks ist nicht so zufällig, wie es scheinen mag: Es ist durchaus häufig, dass man in dem Moment, wo man Daten anfordert, auch eine passende Methode zu ihrer Verarbeitung der Pfanne hat. Das zeigt auch der nächste Baustein.

AsyncBuffer
ist der Baustein, von dem Writer seine Antworten abruft. Als Puffer Zwischenglied einer Verarbeitungsstrecke - was reingeht, kommt auf der anderen Seite raus. Dieser Puffer ist nun so konstruiert, dass eine(!) Anforderung (Callback-Draht) hineingeht, also verschickt er auf der anderen Seite auch eine Anforderung.
Für diese Figur, "was reingeht, geht auch raus", habe ich eine abstrakte Basisklasse erstellt, im Kern überaus primitiv:


/// <summary> Element of a Message-Pipe. Subclasses may forward the (processed) message. </summary>
public abstract class ActionPipe<T> {

   public Action<T> Out_Message;

   /// <summary> Overrides may forward or suppress a (processed) message </summary>
   public abstract void In_Message(T msg);

Das Async am AsyncBuffer ist nun, dass der Ausgang in einem anderen Thread geraist wird (außerdem ist da noch das public event Item, damit kann man ggfs. abhören, was grade durch den Puffer geht).


public class AsyncBuffer<T> : ActionPipe<Action<T>> {
   public event Action<T> Item;

   /// <summary> forwards a request for many items in a side-thread </summary>
   public override void In_Message(Action<T> itemCallback) {
      using(var buffer = new BlockingCollection<T>()) {
         Action fillBuf = () => {
            base.Out_Message(buffer.Add);    // forward request, redirect answer to buffer.Add
            buffer.CompleteAdding();
         };
         fillBuf.RunAsync();                        // init buffer-filling in other Thread
         Item += itemCallback;                         // join public Event with callback 
         buffer.GetConsumingEnumerable().ForEach(Item); // consume buffer, raise Event and call back
         Item -= itemCallback;
      }
   }
}

Auch hier wird der Callback redirigiert, nämlich auf buffer.Add, und dadurch landen die Daten im buffer.
Da die reinkommenden Items letztendlich durch ein foreach produziert werden, welches innerhalb der Out_Message() enumeriert, ist Out_Message() erst dann abgearbeitet, wenn keine Items mehr kommen - der richtige Zeitpunkt, buffer.CompleteAdding() aufzurufen.
.CompleteAdding() ist nämlich erforderlich, um der BlockingCollection mitzuteilen, dass keine weiteren Daten kommen werden, sie also beim leer laufen nicht mehr blockieren, sondern die Enumeration abschließen soll (foreach verläßt .GetConsumingEnumerable()).
fillBuf() wird asynchron abgefahren, während gleichzeitig der Aufrufer-Thread buffer.GetConsumingEnumerable() konsumiert. Dabei werden die Items sowohl über den itemCallback, als auch über das Item-Event verschickt - praktischerweise wird itemCallback einfach als weiterer Handler des Events eingetragen.

Ja, nach demselben Prinzip sind die anderen Bausteine auch gestrickt - alles ActionPipes mit spezifischen Concerns: Processor, Filter und Reader.
Reader schlägt ein bischen aus der Art, weil hier der Callback nicht viele Antworten einbringt, sondern nur eine: den InputFileName. Aber da die Signatur übereinstimmt, hab ich ihn auch von ActionPipe erben lassen. Das interessante an ActionPipe ist nämlich die Unterstützung einer Flow-Syntax mit operator+. So muß ich nicht für jeden Draht eine Extra-CodeZeile aufwenden, sondern kann die ganze Kette von Verknüpfungen in nur einer Zeile notieren:


// (TransformInput _Input)
_Writer.GetLines += _Outbuf + _Processor + _Inbuf + _Filter + _Reader + _Input.InputFile.Callback;

Die Gesamt-Verdrahtung, sowohl der ein- und aus-gehenden Nachrichten, als auch derer zwischen den enthaltenen Komponenten erfolgt im Transformer-Konstruktor:


public Transformer() {
   _Input.OutputFile.Push += _Writer.Run;
   _Writer.GetLines += _Outbuf + _Processor + _Inbuf + _Filter + _Reader + _Input.InputFile.Callback;
   _Writer.IsBusy += b => IsBusy.Invoke(b);
   _Processor.Line += s => Line.Invoke(s);
   _Reader.Error += x => Error.Invoke(x);
   _Writer.Error += x => Error.Invoke(x);
}

Der Code folgt konsequent dem gegebenen EBC-Diagramm, und hier z.B. gibt es schon vielversprechende Ansätze, EBC-Designer zu bauen, die derartigen Code auch generieren können.

Der frühe Apfel fängt den Wurm.