Freut mich, dass Thomas Christian im build broken Blog eine Lanze für die asynchrone Kommunikation mit Events gebrochen hat. Schade allerdings, dass der Code dafür so umständlich aussieht.
Kommunikation à la Async-Pattern
Ein kleines Formular fordert von einem Calculator die Quadratur einer Zahl an: m_calculator.CalcAsync(number). Was würden Sie dann im Calculator für Code erwarten? Irgendwo sollte number * number stehen. Klar. Und ansonsten? Schauen Sie einmal, wie der Calculator bei Thomas aussieht:
Puh… Da ist es gar nicht so leicht, das zu finden, worauf es eigentlich ankommt, finde ich. Sehen Sie die kalkulierende Businesslogik? Und was macht der Rest des Codes? Der ist dazu da, ein Pattern zu implementieren.
Wenig Code für das Wesentliche, viel für Infrastruktur – das hört sich für mich nicht so gut an, auch wenn ich das Kommunikationsmodell voll unterstütze.
Ich schlage daher einen anderen Weg vor. Hier meine Version der Szenarios:
Kommunikation à la Event-Based Components
Zunächst die Businesslogik:
Hier finden Sie keine Rauschen durch Infrastrukturcode. Der Fokus ist ganz klar auf der Problemdomäne. Aber Achtung: Ich habe keine Funktion für Calc() geschrieben, sondern eine Prozedur, d.h. eine Methode ohne Rückgabewert. Das Resultat liefert die Methode dann über einen Event: DeliverCalcResult(). So ist das bei Event-Based Components.
Jetzt der Client-Code im Formular:
Auch hier kein Rauschen durch Infrastruktur. Nicht mal der Calculator wird als Komponenteninstanz injiziert. Das Formular kennt keinen Calculator. Es ist völlig unabhängig von einer solchen Funktionseinheit. Es äußert Berechnungswünsche lediglich über einen Event – CalcRequest – und empfängt Berechnungsresultate mit OnCalcResult().
Das ist im Prinzip sehr ähnlich wie bei Thomas. Im Detail ist es jedoch anders, da das Formular eben auf die Kenntnis einer Service-Funktionseinheit verzichtet. Die wird bei Thomas noch im Formular instanziert.
Und wie kommt es nun zu Berechnungen, wenn das Formular den Calculator nicht kennt? Beide Funktionseinheiten werden von außen verdrahtet. Damit sind Logik und Abhängigkeiten als Concerns sauber getrennt.
Voilà! Jetzt kann das Programm starten und berechnet Quadratzahlen. Zuerst werden die Funktionseinheiten instanziert. Dann werden die Eventhandler in die Events gesteckt. Mehr ist nicht nötig. Keine Funktionseinheit weiß von der anderen. Nur der Verdrahtungscode kennt sie.
Asynchrone Kommunikation
Die kleine Anwendung funktioniert nun zwar, aber synchron. Thomas Beispiel will ja aber gerade zeigen, wie einfach asynchrone Kommunikation mit Events ist. Was muss ich also tun, um meinen Nachbau ebenfalls asynchron zu machen?
Asynchronizität erfodert keine (!) Veränderung an der Geschäftslogik oder im Frontend. Das halte ich für den wesentlichen Vorteil meines Vorgehens. Ich muss lediglich die Verdrahtung anpassen:
Ich instanziere zwei kleine Standard-Funktionseinheiten, eine, die Events asynchron weiterleitet und eine, die Events zurück in den GUI-Thread bringt. Diese Funktionseinheiten schalte ich zwischen den Dialog und den Calculator. War die Verbindung z.B. bisher direkt
dlg.CalcRequest = calc.Calc;
so ist sie nun indirekt:
dlg.CalcRequest = asyncProcessAsynchronously;
async.OnAsynProcessing = calc.Calc;
Die Standard-Komponenten sind natürlich auch Event-Based Components, d.h. Input erhalten sie über Eventhandler und Output generieren sie mittels Events.
Das war´s. Mehr ist nicht zu tun. Die Geschäftslogik läuft jetzt im Hintergrund. Und die Ergebnisse kommen problemfrei im Vordergrund an. Wie das funktioniert? Egal :-) Das ist die Magie der Standard-Komponenten.
Fazit
Asynchrone Kommunikation mit Events ist eine gute Sache, da stimme ich Thomas zu. Aber man sollte diese Sache dann auch so einfach wie möglich halten. Das bedeutet für mich, dass ich in der Geschäftslogik (oder auch im Frontend) von technischen Klimmzügen, die die Asynchronizität herstellen, nichts sehen will. Geschäftslogik muss frei von diesem Concern sein. Der steckt am besten in wiederverwendbaren Standard-Komponenten. Event-Based Components bieten dafür eine gute Grundlage.
P.S.
Na, schön. Hier nun doch noch der Code für die Standard-Komponenten. Damit keiner sage, der sei bestimmt total kompliziert ;-)
Dieser Code ist wiederverwendbar. Er folgt nur dem Event-Based Component Muster für die Kommunikation. Deshalb kann er mit beliebigen Pins von anderen Komponenten zusammengesteckt werden, ohne dass die etwas davon merken. Das ist Composability.
26 Kommentare:
Einfach genial.
Könnte man nicht noch den ContextSwitcher mit dem Asynchonizer zusammenpacken. Quasi wie ein Proxy-EBC für asynchrone Vorgänge?
Hallo Ralf,
es ist super, dass wir dir so eine tolle Vorlage liefern konnten, der entstandene Code ist wirklich toll. Aber warum nennst uns "build broker" ? Du weißt, doch, dass heutzutage Broker nicht sehr beliebt sind ;)
Schöne Grüße aus München,
eine Build-Brecherin
Hallo Ralf,
zuerstmal ein sehr gelunger Beitrag!
In letzter Zeit habe ich leider etwas den Anschluss verloren... Für dieses "kleine" Szenario ist die Verdrahtung noch recht einfach, aber gibt es für komplexere schon eine Idee (oder sogar Umsetzung) für ein "autowiring"?
Viele Grüße, Jan
@Anonym: Ja, den Context-Switcher könnte man zusammenpacken mit dem Asyncer.
@Christina: Hab den Tippfehler korrigiert.
@Jan Christian: Autowiring hab ich probiert. Siehe ebcbinder in einem der EBC-Artikel. Das skaliert auch nicht so dolle. Denn da weiß man nicht so recht, ob wirklich die richtigen Komponenten miteinander verbunden werden. Ist auch schwierig bei Standard-Komponenten.
Eigentl hilft nur irgendeine Form von Designer. Daran grüble ich auf einem Hintergrundthread :-)
-Ralf
Hi Ralf,
genau das hat bei EBC noch gefehlt: Wie mache ich dat Dingens asynchron. Jetzt ist die Palette erst mal komplett. Bleibt: Fehlende Tools. Denn: Wie Jan Christian richtig sagt, wird das bei vielen Komponenten unübersichtlich, auch wenn man Platinen bauen und somit größere Baugruppen bilden kann.
Kennst du den Diagram Designer von Nevron? http://www.nevron.com/Products.DiagramDesigner.Overview.aspx
Das ist ein Freeware-Plug-in für Visual Studio. Vielleicht kann man den irgendwie einbinden ...
Hallo Ralf,
ich muss Christina zustimmen, es ist wirklich ein guter Code entstanden. Die EBC-Variante sieht echt super aus.
Ich habe auf unserem Blog die refaktorisierte Version meiner Variante veröffentlicht. Asynchrone Kommunikation mit dem Async-Pattern (Refactored)
Nun sehr schönes Beispiel. Ich finde das Rx Framework für diese Fälle aber auch ganz passend. Besonders bei Komponenten die (noch) nicht EBC sind.
var dlg = new Form1();
var asyncCalc = Observable.ToAsync((int number) => new Calculator.Calculator().Calc(number)); //sync to async
dlg.CalcRequest += number => asyncCalc(number) //execute async
.ObserveOn(dlg) //ui thread synchronisation
.Subscribe(calculated => dlg.OnCalcResult(calculated.ToString())); //result to form
Application.Run(dlg);
Aber im Prinzip das gleiche. Viele Grüße, Mike
Hi all,
Die asynchrone Berechnung einer Multiplikation ist eine feine Sache, weil sie zeigt, wo die Knackpunkte beim Asynchronen liegen. Notwendig ist die Asynchronizität hier aber sicher nicht. Unabdingbar ist die Entkopplung aber, wenn man einen FileSystemWatcher als EBC aufsetzt.
public class FileWatcher
{
public FileWatcher()
{
FileSystemWatcher filewatcher = new FileSystemWatcher();
filewatcher.Path = @"C:\PathToWatch";
filewatcher.Changed += new FileSystemEventHandler(filewatcher_Changed);
filewatcher.EnableRaisingEvents = true;
}
void filewatcher_Changed(object sender, FileSystemEventArgs e)
{
this.OnChanged(e.Name);
}
public Action OnChanged;
}
Wer dann den Event new FileSystemEventHandler(filewatcher_Changed) hart mit dem User Interface verdrahtet,
dlg.OnFileWatcher = fileWatcher.OnChanged;
erlebt sein blaues Exception-Wunder, denn die Überwachung läuft in einem anderen Thread als der UI-Controller.
Hier hilft die EBC SyncContextSwitcher von Ralf sofort weiter. Die klemmt man einfach dazwischen
filewatcher.OnChanged = syncFilewatcher.Process;
syncFilewatcher.OnMessage = dlg.OnFileWatcher;
und schon klappts.
EBC ist das ABC :-)
Tilman
Einmal mehr: Genial!
Auf dieses snippet habe ich noch gewartet. Jetzt her mit den Tools *aufforderung an alle* :-)
Frage am Rande: Ralf wieviele Blogs abonnierst du eigentlich? ;-)
@Laurin: Ja, Aufforderung an alle! Nachdenken über EBC Standard-Komponenten. Und Nachdenken über ein Tooling, Stichwort Designer. Muss erstmal nicht super slick sein, sondern nur funktional.
-Ralf
PS: Blogs? ca. 80 hab ich abonniert. Aber die les ich natürlich nicht alle immer. Ich lass mich von den Beitragstiteln verlocken... ;-)
@Laurin
Ich glaube es ist einfacher die Frage zu invertieren.. "welche Blogs abonniert Ralf nicht" oder alternativ "mit welchem Thema beschäftigt sich Ralf nicht?" ;-)
Gruß,
Paul
Hallo zusammen,
finde die Beträge zum Thema EBC super und nutze es auch schon in einem Projekt.
Zum Thema Designer für die Verdrahtung sollte noch gesagt werden, dass sich dieses Prinzip dem von LabView annähert und so das von dort bekannte Problem der Übersichtlichkeit mit sich bringt.
LabView Probleme:
Unübersichtlich bei großen Projekten, da sich eine Vielzahl von Drähten Kreuzen.
Unübersichtlich bei vielen Komponenten, da immer nur ein Teil der Verdrahtung gesehen wird.
Ansonsten bin ich schon sehr gespannt was sich Ralf da sonst noch einfallen lässt.
mfg Thomas
@Thomas: Toll, wenn du EBCs schon einsetzt. Erzähl doch mal...
Zur Unübersichtlichkeit: Ja, das ist ein grundsätzliches Problem. Am Ende kennen wir das ja aber und beherrschen es auch irgendwie. Die Elektrotechnik geht damit um. Prozessoren funktionieren - auch wenn die Schaltpläne unübersichtlich sein mögen.
Also kriegt die Softwareentwicklung das auch hin. Busse helfen, Aggregationen helfen.
-Ralf
@Ralf,
natürlich können andere Bereiche (Elektrotechnik, Elektronik, Pneumatik, Hydraulik) mit der Komplexität aufwendiger Verdrahtungen/Verrohrungen umgehen. Sollte es uns Softwareentwicklern genügen nur zu anderen Bereichen aufschliessen oder muss das Ziel nicht eher eine Vorreiterrolle sein?
Eine gute Verdrahtung kann ein erster Schritt zum Ziel sein, aber eben nur ein Schritt.
Bei mir war für EBC ein bischen umdenken notwendig. Das Problem waren nicht starre Komponenten sondern die dynamischen. Forms, die dynamisch erzeugt werden, müssen auch dynamisch verdrahtet werden.
Um dieser Unübersichtlichkeit zu begegnen hab ich mir mit "Managern" beholfen. Ein Manager dient mir als Steckdosenleiste. Er ist zentraler Anschluss der von Anfang an da ist und verdrahtet werden kann. Wird eine neue Form benötigt wird diese per Message an den Manager gesendet und dadurch eingesteckt. Alle Messages der Form werden durch den Manager gebündelt und weitergeleitet.
Für alles Andere hat der EBC Ansatz bisher gut funktioniert.
mfg Thomas
@Anonymer Thomas: Das mit der Steckdosenleiste möchte ich gerne in Code sehen.
Tilman
@Tilman,
das Ganze ist für eine Anwendung mit n Forms entstanden. Alle gemeinsam senden Daten an einen anderen Layer der Applikation.
Benötigt der User eine neue Form, so löst er in der Rahmenform eine Message aus. Diese Message wird von einer "Formfabrik" empfangen, woraufhin diese eine Form retourniert. Um mit dieser Form Daten zu senden, muss sie noch verbunden werden. Das geschieht mit der Steckdose.
public class Steckdose
{
//Implementiert den selben Sendepin wie das Objekt das den Request sendet
public event Action> AnzeigeklasseErstellen;
//Implementiert den selben Empfangspin wie das Objekt, welches das Ergebnis liefert
public void RequestAnzeigeklasseErstellen(Action ErstellenMessage)
{
//Lamdaausdruck der die Anfrage an den Ersteller weiterleitet, ...
AnzeigeklasseErstellen(Anzeige =>
{
//... den Rückgabewert verbindet und ...
Anzeige.DatenSenden += nutzdatenMessage => this.DatenSenden(nutzdatenMessage);
//... dann weiterleitet
ErstellenMessage(Anzeige);
});
}
//Implemtiert die selben Pins wie das "eingesteckte" Objekt
public event Action DatenSenden;
}
Ich hoffe das Ganze war hilfreich. Eigentlich ist es das Prinzip des man in the middle Angriffs nur für gute Zwecke genutzt.
mfg Thomas
@Thomas: Warum sollte eine FormFabrik eine Form an den Initiator der Herstellung zurücksenden? Der will ja keine Methoden darauf aufrufen.
Ich hätte angenommen, dass die Initiator-Form an eine Steckdose angeschlossen ist. Und in diese Steckdose schiebt die FormFabrik die neu erzeugte Form.
-Ralf
Hallo Ralf,
erst einmal vielen Dank für die ganzen Beiträge über Event Based Components. Hat bei mir zwar sehr lange gedauert, bis ich wirklich Zugang zu dem Thema gefunden habe, aber jetzt ist der Groschen so langsam gefallen. Nicht zuletzt durch das Beispiel in diesem Beitrag. Klasse!
Eine Sache gefällt bzw. gefiel mir aber noch nicht ganz: Der Verdrahtungswaufwand zwischen Async-Komponente, der eigentlichen Komponente und schliesslich Sync-Komponente. Dadurch verstreut man natürlich haufenweise Code über die gesamte Anwendung, die ja eigentlich eher zu einem Aspekt gehört.
Was hältst Du von generischen Platinen?
---
public class AsyncOperation<TIn, TOut>
{
Asynchronizer<TIn> async = new Asynchronizer<TIn>();
SyncContextSwitcher<TOut> sync = new SyncContextSwitcher<TOut>();
public Action<TIn> Execute
{
get { return async.ProcessAsynchronously; }
}
public Action<TOut> DeliverResult
{
set { sync.OnMessage = value; }
}
public AsyncOperation(Action<TIn> input, ref Action<TOut> output)
{
async.OnAsyncProcessing = input;
output = sync.Process;
}
}
---
Das ist natürlich noch weit weg von AOP, da ich immer noch Code des Aspekts "überall rumliegt":
---
var asyncCalc = new AsyncOperation<int, int>(calc.Calc, ref calc.DeliverCalcResult);
dlg.CalcRequest = asyncCalc.Execute;
asyncCalc.DeliverResult = dlg.OnCalcResult;
---
Finde ich so aber auf jeden Fall schon etwas übersichtlicher ;) Und wenn ich mal merke, dass ich bei diesem Aspekt "AsyncOperation" noch eine weitere EBC hineinverdrahten möchte, dann kann ich das ganz einfach tun. Ich muss meinen Code nirgendwo sonst anpassen :)
Viele Grüße,
Nils
@Nils: Ich kann verstehen, wenn du den Code mit dem Aspekt nicht mehr ganz so gut lesbar findest. Das ist er auch nicht. Das ist überhaupt kein imperativer Code, der mehr als 2-3 Drähte zieht.
Deshalb ist es am Ende gar nicht wichtig, wie der Code aussieht. Entscheident ist nur, dass er simpel und regelmäßig ist. Dann kann er leicht generiert werden. Denn das soll sein. Verdrahtungscode schreiben wir grad mal jetzt von Hand, weil wir noch keinen Designer für EBCs haben. Aber der kommt. Keine Sorge. Und dann gibt es die Verdrahtung nur noch binär generiert in einer Assembly. Unsichtbar.
-Ralf
Hallo Ralf,
der Artikel ist wirklich sehr gut.
Da ich mich aber gerade erst in das Thema einarbeite, habe ich noch ein paar grundsätzliche Fragen zum Verständnis:
Kann man bzgl. EBCs sagen, dass diese immer ein Eingangs- sowie ein Ausgangsobjekt haben (beim Calculator jeweils int)?
Denn wenn dem so wäre, könnte man für EBCs doch ein generisches Interface definieren, welches Aufruf und Finish-Event definiert?
Durch eine weitere Kapselung deiner Klassen Asyncronizer und SyncContextSwitcher bräuchte man dann lediglich die vom o.g. Interface abgeleitete EBC-Instanz übergeben, die Events würden im Hintergrund verdrahtet werden, der primäre Code würde noch etwas übersichtlicher werden, da nur noch zwei Events (Aufruf und Rückgabe) verdrahtet werden müssten.
Ist das in deinen Augen soweit noch EBC-kompatibel?
@Anonym: EBC haben kein Ein/Ausgangs"objekt", sondern höchstens einen E bzw A "Pin". Objekte fließen dann über diese Pins als Daten.
Aber auch mit den Pins ist es nicht so, dass es da immer genau einen von jeder Sorte gibt. Eine EBC kann auch 15 Eingangs- und 13 Ausgangspins haben. Wie ein Hardware IC auch beliebig viele Beinchen haben kann.
In einfachen Architekturen mögen allerdings viele EBC nur je einen E und einen A Pin haben. Aber das ist ein Sonderfall.
-Ralf
Hi, hat jemand mal versucht das per WPF zu realisieren? Mein Problem ist der Einstiegspunkt der Applikation an den man in WPF ja nicht so komfortabl dran kommt wie in der WinForms Anwendung (oder ich weis zumindest nicht wie)
Ich habe ein MainWindow welches einen Navigation Frame enthält. Den Content des Frames möchte ich je nach Aktion durch verschiedene Pages austauschen.
Mein Code für das MainWindow sieht im Moment so aus:
public partial class MainWindow : RibbonWindow
{
public Action NavigateToNewAnalysis;
public MainWindow()
{
InitializeComponent();
// Event-Handling der Applikation...
LogicBoard.Setup(this);
}
private void btnStartNew_Click(object sender, RoutedEventArgs e)
{
if (this.NavigateToNewAnalysis != null)
{
this.NavigateToNewAnalysis(this.navigationFrame.NavigationService);
}
}
}
Die LogikBoard::Setup sieht bei mir dann so aus:
public static void Setup(MainWindow mainWindow)
{
var newAnalysis1 = new NewAnalysis();
var newAnalysis2 = new NewAnalysis2();
newAnalysis1.NextPage = delegate(NavigationService navService)
{
navService.Navigate(newAnalysis2);
};
mainWindow.NavigateToNewAnalysis = delegate(NavigationService navService)
{
navService.Navigate(newAnalysis1);
};
}
Ist das okay? Hat jemand einen besseren Ansatz?
Gruß,
Manuel
Hallo Ralf,
das macht man das doch schon die ganze Zeit in ähnlicher Weise z.B. mit INotifyPropertyChanged, oder findest Du nicht? Einzig der benachrichte muss hier nochmal explizit nach dem Wert fragen, wenn es denn ein Wert ist der ihn interessiert. Irgendwie kann ich aber jetzt noch nicht wirklich etwas neues in dem Code erkennen als dass man dafür nun "ECB" als neues Kunstwort aus der Taufe heben müsste, denn im Prinzip ist das doch nur eine - zugegebenermaßen sehr elegante - Implementierung eines Observer-Pattern... http://msdn.microsoft.com/en-us/library/ee817669.aspx Microsoft nennt seine Variante dann im speziellen Event Pattern. Übersehe ich hier etwa einen markanten Unterschied bis auf nicht benötigte Schnittstellendeklarationen bei der Nutzung von Events (Klassisch halt in Erweiterungen von EventArgs)? Wie gesagt finde ich Deine Art das im Code zu Nutzen elegant aber auch wenn die Art der Implementierung/Nutzung gelungen ist, so sehe ich das hier in naher Zukunft allenfalls ein ein "Snippet" in VS. Schon mal versucht das einfach mit einem UML-Tool zu erschlagen, wenn man schon nach einem Editor sucht?
Vielen Dank für die Inspiration bei der Optimierung von Code und ich hoffe, dass mein Kommentar nicht zu "bissig" rüberkommt... ;-)
Es wäre interessant zu sehen, wie der RequestWithResponsePin im Zusammenhang mit C# 5.0 async/await zusammen spielt und asynchrone Verarbeitung bereit stellt, ohne die Geschäftslogik in Even Handler auseinander zu reißen
Kommentar veröffentlichen