Donnerstag, 2. Juli 2009

CCR Flows - Asynchrone Prozesse mit der CCR verdrahten

Neulich habe ich eine Lanze dafür gebrochen, zwei Probleme der Softwareentwicklung auf einen Streich zu lösen. Das würde Software zukunftsfähiger machen. Denn Abhängigkeiten und Synchronizität sind Behinderungen auf dem Weg in eine glückliche Projektzukunft.

Das Mittel für diese “Wundertat”? Asynchrone Flows, d.h. Funktionseinheiten nicht mehr statisch voneinander abhängig machen und auch nicht mehr synchron miteinander kommunizieren lassen. Stattdessen Verarbeitungsschritte in einem expliziten, getrennten “Bereich” (Separation of Concerns) lose mit eigenständigen Verbindungsgliedern “zusammenstöpseln”.

image

Dazu hatte ich dann ein wenig über einen API spekuliert, der das möglich machen könnte. Damit lag ich – wie sich nun herausgestellt hat – wohl nicht ganz daneben. Denn nach einigen Versuchen habe ich nun so einen Flow API implementiert. Ich nenne ihn CCR Flows, weil er intern auf CCR (Microsoft Concurrency Coordination Runtime) Ports als “Verbindungsglieder” zwischen Prozessschritten setzt.

Die CCR Flows sind jetzt Open Source (sogar inkl. Dokumentation sowie Unit Tests) und liegen bei CodePlex:

http://ccrflows.codeplex.com

Über Feedback und Diskussion dort im Forum würde ich mich freuen. Es gibt natürlich noch etwas daran zu tun. Aber als Einstieg in ein anderes Programmiermodell finde ich den API nicht ganz schlecht. Ein Beispielprogramm in den Sourcen realisiert auch den Beispielprozess meines vorherigen Blogartikels. An dieser Stelle zum Schnuppern aber nur ein kleiner Prozess, der die Worte eines Textes extrahier und dann in zwei Schritten transformiert:

Flow<string>.Do<string>(SplitTextIntoWords).Do<string>(w=>w.ToUpper()).Do<string>(Reverse)

Meine Vermutung, solcher Code lässt sich besser weiterentwickeln, weil schon bei jeder Prozessstufe (stage) viel entkoppelter gedacht wird. Denn diese Stufen kennen ihren Vorgänger und Nachfolger nicht! Sie haben keine Abhängigkeiten.

Wie das genau geht, erklärt die Doku bei CodePlex. Ansonsten fragt mich einfach.

Viel Spaß damit!

Kick It on dotnet-kicks.de

Wie gut ist Ihr Job? – Jetzt Umfrage mit Gewinn [OOP 2009]

Vor einigen Tagen hatte ich darüber spekuliert, wie zufrieden wir Softwareentwickler wohl so mit unseren Jobs sind. Daraufhin gab es einige Einsendungen von Fragebogenergebnissen nach dem DGB-Fragebogen zur Jobzufriedenheit. Das hat mich ermutigt und ich habe mit dem Professional Developer College nun die Aktion etwas erweitert:

Jetzt gibt es eine “echte” Umfrage mit Gewinnen, die unter den Einsendern verlost werden, und einer Veröffentlichung der Ergebnisse in der dotnetpro.

Wäre toll, wenn möglichst viele mitmachten, damit wir ein halbwegs repräsentatives Bild von der Jobzufriedenheit in der Branche bekommen. Der Aufwand ist 5 Minuten, der Nutzen hoch für die Gemeinschaft.

Also, auf geht´s. Klickt hier…

Kick It on dotnet-kicks.de

Mittwoch, 1. Juli 2009

Blinder Fleck Change Tracking

Habe grad ein paar Postings zum Thema Distributed Domain Driven Design (DDDD oder “D4”?) gelesen. Dabei ist mir wieder der Gedanke gekommen, dass wir uns das Leben noch schwerer machen als nötig. Ich glaube nämlich, zu unserem Glück fehlt uns noch eine deutliche Separation of Concerns (SoC).

image Über das DataSet kann man sagen, was man will, es hat eins ganz wunderbar getan: Änderungen verfolgen. DataSet mit Daten füllen, irgendwohin schicken, dort ändern – und dann nur die Änderungen zurückschicken, um sie zu persistieren. Sehr cool!

Und dann kam O/R Mapping – und wir haben für die Änderungsverfolgung (Change Tracking) einen Blinden Fleck entwickelt. Denn entweder haben O/R Mapper kein Change Tracking betrieben, sondern Daten geladen, Objekte befüllt und dann einfach immer komplette Objekte auch wieder gespeichert, wenn man ihnen sagte, dass sich daran etwas geändert hat. Oder sie haben Objekte befüllt, die intern selbst Buch führen über Änderungen an sich. Dann konnte der O/R Mapper später ohne manuelle Meldungen über Änderungen sich selbst geänderte Objekte herauspicken und mit minimalen SQL DML-Statements persistieren.

Klingt bequem, ist bequem. Aber Change Tracking liegt im Blinden Fleck. Wir sehen das nicht. Meistens. Deshalb machen wir uns darüber keine Gedanken. Sollten wir aber. Denn letztlich ist das ein Thema, das immer relevant ist, wenn Mapping ins Spiel kommt.

Deshalb hat mich ja auch D4 wieder darauf gebracht. In verteilten Systemen erzeugen wir nämlich Objekte z.B. in einem Server, die wir zum Client schicken. Ob der Server die aus einer Datenbank befüllt oder nicht, ist egal. Änderungen an diesen Objekten im Client sollen dann wieder zurück zum Server. Aber wie? Geänderte Objekte können in Form “dummer DTOs” eigentlich nur komplett zurückfließen. Aber warum soviel Traffic? Warum können wir nicht nur die Änderungen zurückschicken und in die serverseitigen Instanzen einspielen?

Klar, das geht. Kann man programmieren. Kostet aber Mühe. Event sourcing ist dazu ein Stichwort. Aber warum sollten wir das immer wieder programmieren? Das ist ein so grundsätzliches Problem, dass ich finde, dafür sollte es eine allgemeine Lösung geben.

Und die steckt für mich im Change Tracking, das manche O/R Mapper eh schon tun. Warum wird dieses Change Tracking nicht dort rausgezogen und separat implementiert? Warum gibt es nicht eine Change Tracking Infrastruktur (z.B. auf einem AOP-Fundament), die dann beim O/R Mapping oder auch bei sonstigem Mapping oder Versand von Daten einsetzen kann?

image Für mich sind Change Tracking und Persistenz inzw. ganz klar orthogonale Belange. Sie sollten deshalb auch technologiemäßig ganz klar getrennt werden.  Change Tracking darf nicht unser Blinder Fleck sein.

Wer baut also als erstes eine allgemeine Change Tracking Infrastruktur z.B. auf der Basis von PostSharp? Wer baut einen O/R Mapper, der dann darauf aufsetzt? Das macht dem O/R Mapper-Hersteller weniger Mühe. Und das verschafft dem Change Tracking-Hersteller eine viel größere Zielgruppe. Alle würden profitieren.

Kick It on dotnet-kicks.de

Montag, 29. Juni 2009

Wie gut ist Ihr Job? [OOP 2009]

Mögen Sie eigentlich Ihren Job? Sind Sie zufrieden? Nicht nur mit dem Gehalt, nein, so insgesamt. Immerhin verbringen Sie “auf der Arbeit” ca. 1/3 Ihrer Zeit oder gar knapp 50% Ihrer “Wachphasen”. Da wäre es doch gut, diese Zeit zufrieden zu verbringen, oder?

Also: Wie geht es Ihnen mit Ihrem Job?

Wenn es Ihnen schwer fallen sollte, darüber nachzudenken, dann grämen Sie sich nicht. Das ist ganz natürlich. Ein erstes Gefühl haben Sie sicherlich schnell. Aber im Einzelnen werden Sie unsicher sein. Sie haben es nicht imagegelernt, ihre Arbeit bewusst und detailliert zu reflektieren. Ihr Arbeitgeber ist daran kaum interessiert. Und in Ausbildung oder Schule steht das einfach nicht auf dem Lehrplan.

Zum Glück gibt es aber andere, die sich darüber Gedanken machen. Der DGB zum Beispiel. Deshalb hat der eine Studie zur Arbeitszufriedenheit in Auftrag gegeben. Darüber berichtet gerade die Zeitschrift Psychologie heute (7/09) in ihrem Artikel “Kann man sich in seinen Job (neu) verlieben?”.

15 Kriterien werden darin genannt, anhand derer Sie überprüfen können, ob Sie einen guten oder schlechten Job haben:

    1. Gibt es Qualifizierungsangebote? Lerne ich etwas bei der Arbeit?
    2. Habe ich die Möglichkeit, eigene Ideen einzubringen?
    3. Kann ich im Betrieb aufsteigen?
    4. Habe ich Einfluss auf Planung und Menge meiner Arbeit?
    5. Erhalte ich klare Anforderungen und alle notwendigen Informationen?
    6. Wie gut führen meine Vorgesetzten?
    7. Wie gut ist die Betriebskultur?
    8. Bekomme ich Hilfe von meinen Kollegen?
    9. Ist meine Arbeit nützlich für die Gesellschaft?
    10. Gibt es eine faire und verlässliche Arbeitszeitgestaltung?
    11. Muss ich oft unter Zeitdruck arbeiten?
    12. Werde ich herablassend behandelt? Muss ich meine Gefühle verbergen?
    13. Ist meine Arbeit körperlich schwer oder einseitig?
    14. Habe ich häufig Angst um meine berufliche Zukunft?
    15. Kann ich von meinem Einkommen leben? Entspricht das Einkommen meiner Leistung?

Über diese Kriterien mag man im Einzelnen diskutieren. Fehlt da z.B. die Frage nach der Erfüllung durch Spaß am Metier? Oder entsteht Sinnhaftigkeit vor allem durch gesellschaftliche Relevanz oder nicht vielmehr durch ein Gefühl des “gewollt und gebraucht seins” vor Ort im Unternehmen? Als Ausgangspunkt und vor allem Vergleichsmaßstab sind die Kriterien jedoch nicht schlecht.

Wie steht es also mit Ihnen? Wenn Sie Ihre Arbeit daran messen, wie gut oder schlecht ist sie? Das können Sie leicht herausfinden, indem Sie einen Fragebogen kostenlos online ausfüllen. Das dauert 5 Minuten und sieht im Ergebnis dann z.B. so aus:

image

Hier habe ich mal versucht, mich in die Situation eines “abhängig beschäftigten” Softwareentwicklers zu versetzen. Auf der linken Seite der Zufriedenheitsindex dieses fiktiven Entwicklers, der seinen Job als “knapp vorbei an schlecht” einschätzt mit einem Index von 54. Rechts der Durchschnitt der Befragungen aus der Studien über viele Berufsgruppen hinweg.

Auffällig: Bei der Studie liegen die Werte dichter beieinander. Ich halte also den fiktiven Softwareentwickler in einigen Bereichen für sehr zufrieden (z.B. bei Kollegialität oder auch der Möglichkeit, Kontrolle über seine Arbeit auszuüben), andererseits ist er aber auch sehr unzufrieden (z.B. bei der Weiterbildung, der Arbeitszeit/-intensität oder auch den Aufstiegschancen).

Das sind natürlich nur Annahmen aufgrund meiner Begegnungen mit Softwareentwicklern in vielen Betrieben durch meine Berater- und Trainerarbeit. Sozusagen ein “gefühlter Durchschnitt”.

Aber liege ich damit so falsch? Sagen Sie es mir. Füllen Sie auch den Fragebogen aus und schauen Sie, wo Ihre Zufriedenheit positiv – was zu wünsche wäre – oder negativ abweicht. Motivieren Sie Ihre Kollegen es auch zu tun. Sprechen Sie über Ihre Ergebnisse – auch mit Ihrem Chef. Und: Wenn Sie mögen, schicken Sie mir Ihre Auswertungen als Bilder per Email (inkl. einiger Zusatzangaben: Sind Sie selbstständig/angestellt? Wie groß ist das Team, in dem Sie arbeiten? Welche Position haben Sie? Wie groß ist die relevante Organisationseinheit um Sie herum (Unternehmen, Abteilung)?). Ich verspreche, in jedem Fall Ihre Angaben vertraulich zu behandeln, wenn ich aus den eingegangenen Auswertungen eine kleine Galerie zusammenstelle oder – bei genügend Masse – einen Branchendurchschnitt berechne.

Ich bin gespannt, wie Sie sich mit Ihrem Job fühlen!

 

PS: Da es etwas Überwindung kosten mag, mir das Ergebnis zu schicken und sich damit ein Stück zu öffnen, hier meines sozusagen als “Vorleistung”:

image

Sie sehen, ich fühle mich zufriedener als der fiktive Entwickler. Warum? Weil ich sehr selbstbestimmt arbeiten kann. Zwar knickt die Kurve bei der Arbeitsplatzsicherheit ein, da ich ohne größere Organisation im Rücken wenig Puffer habe, aber ansonsten empfinde ich quasi alles “im grünen Bereich”.

Den Einbruch bei “Sinngehalt” zählt für mich nicht wirklich. Meinen Sinngehalt ziehe ich nicht aus einer größeren gesellschaftlichen Relevanz meiner Arbeit. Das gute Feedback aus der Community und auch der Zuspruch meiner Familie wiegen für mich schwerer.

Manche Fragen sind für mich als Freiberufler natürlich auch nicht passend. Den Führungsstil eines Vorgesetzten kann ich nicht bewerten, weil ich keinen habe, musste aber einen Wert im Fragebogen eintragen.

Insofern ist mein Ergebnis wie auch Ihres immer mit einem Körnchen Salz zu schmecken. Aber das finde ich nicht schlimm. Es geht nicht um einzelne Werte, sondern die Tendenz.

Und nun kommen Sie: hier gehts zum Fragebogen.

Kick It on dotnet-kicks.de

Montag, 22. Juni 2009

Wartung und Evolvierbarkeit [OOP 2009]

Bei meinen Gedanken über die merkwürdige Plötzlichkeit der Unwartbarkeit habe ich eigentlich meinem Credo zuwidergehandelt. Ich habe von Unwartbarkeit geschrieben, obwohl ich immer wieder sage, es gäbe keine Wartung bei Software.

Es gibt keine Softwarewartung

imageDabei bleibe ich auch. Es gibt keine Wartung von Software im üblichen Sinn. Der bezieht sich nämlich auf die Weiterentwicklung nach den ersten Releases. Maschinen können gewartet werden. Software aber nicht, jedenfalls nicht in Bezug auf ihre Funktionalität. Sie enthält keine Teile, die kaputt gehen könnten und die man daher proaktiv austauscht, bevor sie kaputtgehen. Das tut Wartung bei Maschinen.

Wenn eine Maschine kaputt geht, dann wird sie repariert. Um das zu vermeiden, wartet man sie. Man erhält Funktionalität proaktiv. Software hingegen geht nicht kaputt, sondern ist höchstens kaputt - nur man bemerkt es erst später. Ein Bug tritt nicht durch Verschleiß wie bei einer Maschine auf. Reparaturen an Software (bug fixing) beziehen sich also immer auf eine bis dato nur unentdeckte "Kaputtheit". Im Umkehrschluss bedeutet das, Software hat keine Verschleißteile. Also ist Wartung nicht nur unnötig, sondern unsinnig. Funktionalität ist entweder fehlerfrei existent - oder nicht. Software ist insofern binär. Da gibt es keinen schleichenden Übergang, den man mit Wartung verhindern könnte.

Aufmotzen statt warten

Statt von Wartbarkeit spreche ich deshalb lieber von Evolvierbarkeit. Schon allein deshalb, weil in Wartung keine Innovation steckt, keine Weiterentwicklung, sondern nur Erhaltung eines IST-Zustandes.  Software muss gerade deshalb eben nicht wartbar sein, sondern vor allem eines: evolvierbar. Sie muss flexibel sein, sie muss einfach erweiterbar sein. Denn wenn eines gewiss ist bei der Softwareentwicklung, dann ist es der kaum versiegende Anforderungsstrom. Je erfolgreicher eine Software, desto breiter und länger dieser Strom. Das erste Release ist nur der Anfang eines hoffentlich langen Softwarelebens durch viele kleine und große Veränderungen hindurch.

Mit Wartung hat das dann nichts zu tun. Das ist eher Evolution im allgemeinen Sinn des Lateinischen evolvere ("ausrollen, entwickeln, ablaufen"): die Software entwickelt sich weiter, sie rollt und rollt immer weiter... und wird natürlich darin auch irgendwann langsamer. Ihre Lebenszeit durch viele Veränderungen hindurch läuft irgendwann ab. Aber bis dahin... Nein, da wird sie nicht gewartet, sondern aufgemotzt.

image Aufmotzen oder Pimping, das sind für mich passende Begriffe für das, was wir mit Software machen. Wir packen immer mehr und mehr in eine Software rein. So, als würden wir ein Auto aufmotzen. Bei "Pimp My Car" sieht das Ergebnis am Ende auch ganz anders aus, als der Hersteller es bei Auslieferung mal gedacht hatte. Genauso ist es mit Software.

Die Frage ist daher, wie einfach der Hersteller es macht, sein Produkt später aufzumotzen. Autohersteller machen sich darüber keine Gedanken. Weil Autos aber keine so komplexen Maschinen sind, kann man sie trotzdem noch recht gut aufmotzen.

image Bei Software ist das anders. Wenn wir das Aufmotzen da nicht voraussehen und einplanen, dann wird es schwer. Wie man an vielen Projekten sieht. Evolvierbarkeit bzw. Aufmotzbarkeit ist für mich daher das passende Wort für eine wesentliche Eigenschaft von Software. Und ich glaube, dass wir erst wirklich verlässlich evolvierbare Software herstellen werden, wenn wir den Unterschied zwischen Wartbarkeit und Evolvierbarkeit ganz bewusst wahrnehmen.

Und doch gibt es Softwarewartung

Soweit der Blick auf die Funktionalität von Software. In Bezug auf sie gibt es keine Wartung, kein proaktives Handeln zu ihrer Erhaltung. Wenn der Kunde eine neue Funktionalität möchte, dann wird sie reaktiv eingebaut. Voraussetzung dafür ist Evolvierbarkeit.

Die übliche Wartung sichert proaktiv Funktionalität zu. Allgemeiner formuliert ist Wartung jedoch eine Tätigkeit, die proaktiv irgendeine Qualität erhält. Funktionalität ist nur eine sehr naheliegende Qualität, die sich bei Maschinen mit Wartung erhalten lässt und daher Wartung für Software nahelegt.

Welche anderen Qualitäten hat Software denn aber noch? Vielleicht lässt sich ja der Wartungsbegriff für sie retten. Performance oder Skalierbarkeit oder Sicherheit oder Robustheit sehe ich auf einer Stufe mit Funktionalität. Entweder ist Software so performant oder robust, wie sie sein soll - oder sie ist es nicht. Da gibt es keinen schleichenden Übergang. Bei der Hard-/Software-Infrastruktur mag das anders sein. Dort kann man Teile proaktiv austauschen, um eine Qualität zu erhalten. Das fällt für mich aber unter Administration statt Softwareentwicklung.

Eine andere Qualität jedoch scheint mir durchaus ein Fall für die Wartung. Es ist die Evolvierbarkeit, mit der ich oben die Wartung vom Thron gestoßen habe. Denn eben diese Evolvierbarkeit ist nicht entweder vorhanden oder nicht, sondern verfällt über die Zeit. Sie stellt sich sozusagen durch ihren Zweck selbst ein Bein. Indem sie es leicht machen soll, Software zu verändern, sie wachsen zu lassen, zehrt sie sich selbst auf. Jede funktionale Veränderung von Software lässt ihre Evolvierbarkeit ein kleinwenig erodieren. Evolution verschleißt die Evolierbarkeit.

Wartung auf der Meta-Ebene

Damit ist Evolvierbarkeit selbst ein Kandidat für Wartung. Von der konkreten Funktionalitätsebene wandert Wartung sozusagen auf die Meta-Ebene. Software warten bedeutet also nicht wie bisher, Funktionalität zu erhalten und schon gar nicht, neue Funktionalität herzustellen, sondern die Voraussetzungen für neue Funktionalität zu erhalten.

Softwarewartung ist die Bedingung für die Möglichkeit von Veränderung. Bisher war Softwarewartung eben diese Veränderung selbst. Jetzt ist sie ihre Voraussetzung. Das ist ein Unterschied, finde ich. Ein erheblicher Unterschied.

Clean Code Developer wird damit zu einem Werkzeugkasten für die Softwarewartung. Das hatte ich bisher nicht so gesehen. Eine solche Assoziation fühlte sich falsch an. In Bezug auf den bisherigen, falschen Gebrauch des Begriffs Wartung war sie es auch. Mit der hiesigen neuen Definition jedoch wird die Assoziation korrekt.

So bin ich nun im Reinen mit dem Begriff Wartung. Ich muss nicht länger darauf bestehen, dass es keine Softwarewartung gibt. Sie hat vielmehr eine andere Aufgabe als bisher. Evolvierbarkeit ersetzt nicht Wartbarkeit. Wartbarkeit bezeichnet jetzt nur einen Zustand, in dem Evolvierbarkeit immer wieder leicht herzustellen ist. Wartbarkeit ist sozusagen Meta-Evolvierbarkeit ;-)

Evolution ist die Reaktion auf Veränderungen in der Umwelt. Wartung ist die proaktive Erhaltung von Evolvierbarkeit.

Kick It on dotnet-kicks.de

Warum überrascht Unwartbarkeit? [OOP 2009]

Warum sind Projekte immer wieder überrascht, dass sich ihr Code so schlecht warten lässt? Fehler zu beheben wird immer schwieriger. Neue Kundenwünsche einzubauen dauert immer länger. Mir scheint, viele fühlen sich damit so plötzlich konfrontiert wie mit Weihnachten.

image Am Anfang ist diese Situation noch in weiter Ferne; man hat von ihr gehört und will auch rechtzeitig darauf achten. Aber es ist ja noch so lang bis dahin. Dann vergeht die Zeit mit Tagesgeschäftalltagsstress... und - zack! - steht die Unwartbarkeit vor der Tür und man hat eben doch vergessen, sich frühzeitig darum zu kümmern.

Warum ist das so? Immer wieder. Es mangelt ja nicht an Kenntnis, dass es Unwartbarkeit gibt. Irgendwie liegt sie meistens jedoch in der Zukunft und betrifft ohnehin eher die anderen.

Bei Weihnachten rührt die gefühlte Plötzlichkeit des Auftretens sicherlich vom Stellenwert her. Weihnachten ist nicht so wichtig wie die Dinge des Tagesgeschäftes. Außerdem scheinen die Weihnachtseinkäufe so leicht zu erledigen, dass dafür immer noch irgendwie Zeit ist. Das ist ja auch durchaus wahr. Selbst am heiligen Morgen lassen sich noch Geschenke kaufen - und das wird genutzt, wie die vollen Geschäfte zeigen.

In Kauf genommen wird dabei jedoch, dass man dann damit zufrieden sein muss, was noch übrig ist. Die Qualität leidet also darunter, dass Geschenke auch noch bis kurz vor knapp gekauft werden können. Man ist eher bereit, sich mit dem, was übrig ist zufriedenzugeben, als rechtzeitig einzukaufen.

Aber das ist natürlich unkritisch im Verhältnis zu dem, was in Projekten passiert, wenn sie sich von der Unwartbarkeit überraschen lassen. Deshalb lohnt die Frage doppelt, woher denn dort diese Überraschung kommt?

Abgesehen einmal von einer allgemeinen Überschätzung oder gar Überheblichkeit, die das Problem entweder weit in der Zukunft, eher bei anderen oder als eigentlich nicht so schlimm einstuft, scheint mir hier die Natur der Sache der Kern des Problems. Das ist die Struktur von Software. Deren Komplexität macht nämlich die Wart- oder Unwartbarkeit aus.

image Mir scheint nun, dass die Sorglosigkeit um Umgang mit der Wartbarkeit das Ergebnis einer Fehleinschätzung des Wachstums eben dieser Komplexität ist. Ich kann mir die Überraschung über Unwartbarkeit nur vorstellen als Überraschung über ein schnelleres Wachstum als erwartet. Man weiß, dass die Komplexität über die Zeit zunimmt, man weiß, dass damit die Gefahr von Unwartbarkeit droht. Soweit sind sich alle einig. Aber wer im Projekt ist, glaubt daran, dass die Komplexität überschaubar wächst. Dass sich die Unwartbarkeit langsam ankündigt und dann immer noch gegengesteuert werden kann. Und genau das sieht mir wie ein großes Missverständnis aus!

 

image

 

Komplexität wächst nicht linear! Doppelt soviel Code bedeutet nicht doppelt soviel Komplexität und damit nur etwas verringerte Wartbarkeit!

Komplexität wächst vielmehr exponenziell! Und damit wächst auch die Unwartbarkeit exponentziell. Das bedeutet, mit der Unwartbarkeit ist es wie mit den Seerosen. Ein Beispiel aus der 10. Klasse: "Wenn die Seerosen auf einem Teich im Verlauf von 3 Monaten (90 Tage) jeden Tag ihre Zahl verdoppeln, wann bedecken sie den Teich zur Hälfte? Nach a) 10 Tagen, b) 45 Tagen, c) 60 Tagen, d) 89 Tagen?"

image Wenn wir die Wartbarkeit von Software mal mit der Schiffbarkeit des Teiches in diesem Beispiel gleichsetzen und Schiffbarkeit (mit Ruderbooten) definieren als "Max. 25% des Teiches sind von Seerosen bedeckt", wann tritt dann Unschiffbarkeit ein? Am 88. Tag. Ganz plötzlich.

Am 87. Tag ist der Teich nur zu einem 1/8 mit Seerosen bedeckt. Am 86. Tag ist es nur 1/16 oder knapp 6% usw. Das heißt, 90% der Zeit von 3 Monaten sind die Seerosen auf dem Teich kaum sichtbar; es sind hübsche Inselchen die weit weniger als 1% der Teichfläche bedecken. Dann jedoch, im Verlauf von 5 Tagen explodiert ihre Zahl. Wer am 87. Tag knapp 10% Bedeckungsfläche wahrnimmt und denkt, man solle gelegentlich mal überlegen, die Seerosen zurückzuschneiden, der muss nur ein Wochenende nicht hinschauen und hat keine Chance mehr zur Reaktion. Dann ist der Teich nämlich plötzlich komplett bedeckt.

Genau so scheint es mir mit der Unwartbarkeit. Man kennt ihre grundsätzliche Möglichkeit (Analogie: man weiß, Seerosen können sich bis zur Unschiffbarkeit ausdehnen), man will auch darauf achten, dass sie nicht eintritt (Analogie: man schaut gelegentlich auf den Teich, was denn die Seerosen so machen) - aber schließlich wird man von ihr überrollt (Analogie: wenn man die Seerosen als signifikante Fläche wahrnimmt, dann ist es schon zu spät).

 

image 

Das ist ja auch in anderen Bereichen ein oft beobachtetes Verhalten. Vorsorge in der Gesundheit oder im Umgang mit der Umwelt ist schwer "zu verkaufen", weil in beiden Bereichen vieles lange gut geht. Aber wenn es dann schlecht wird, dann oft sehr schnell. Dann kippt das Biotop um, dann ist der Herzinfarkt da - auch wenn kurz vorher noch alles ziemlich in Ordnung schien.

In der Softwareentwicklung liegen die negativen Effekte von mangelnder Vorsorge und Voraussicht jedoch nicht so weit in der Zukunft. Hier reden wir nicht über Jahrzehnte, sondern über wenige Jahre oder gar nur Monate. Da sollte es doch möglich sein, über die Schwierigkeit von uns Menschen, mit exponenziellen Entwicklungen klarzukommen, einfach mal hinwegzuspringen. Im Vergleich zum Weltklima oder der Globalisierung ist doch ein Softwareprojekt eine simple Angelegenheit. Da sollte es uns leicht fallen, Wartbarkeit nicht so auf die leichte Schulter zu nehmen. Lassen wir uns also nicht länger von Unwartbarkeit überraschen. Wenn wir die Plötzlichkeit lieben, haben wir ja auch immer noch Gruselfilme oder Weihnachten ;-)

Kick It on dotnet-kicks.de

Samstag, 20. Juni 2009

Wider die Geißeln zukunftsfähiger Software: Abhängigkeiten und Synchronizität

Was macht Software so schwer zu evolvieren? Abhängigkeiten. Was beschränkt den Nutzen morgiger Prozessorgenerationen für heutige Software: Synchronizität.

Funktionseinheiten, die von anderen abhängig sind, die insofern einen bestimmten Kontext voraussetzen, lassen sich mühsamer weiterentwickeln als solche, die frei und unabhängig sind. Möglichkeiten zur Abhängigkeit gibt es natürlich viele und nicht alle lassen sich immer kappen. Aber das Streben nach immer geringerer Kopplung lohnt sich - wenn Evolvierbarkeit gefragt ist. Wo hingegen zweifelsfrei Effizienz nötig ist, da müssen womöglich Kopplungen eng bleiben oder gar enger werden. Im Zweifelsfall bin ich jedoch der Meinung, dass unsere Software heute eher unter zu engen, als zu losen Kopplungen leidet.

Funktionseinheiten, die synchron arbeiten, arbeiten notwendig auch sequenziell. Von einer steigenden Zahl an Prozessorkernen können sie nicht profitieren. Die nützen ja nur, wenn es auch etwas parallel auszuführen gibt.

Abhängigkeiten und Synchronizität stehen uns also im Weg bei unserer Reise in eine glücklichere Softwarezukunft. Was tun? Ich beschreibe mal ein paar Gedanken anhand eines Beispiels. Hier etwas synchroner und abhängiger Code:

class MyBusinessLogik : IBusinessLogik
{
    IDatenquelle dq;
    IDatensenke ds;
    IValidator v;

    public BusinessLogik(IDatenquelle dq, IDatensenke ds, IValidator v)
    {
        this.dq = dq;
        this.ds = ds;
        this.v = v;
    }

    public int Aktualisiere(Parameter p)
    {
        v.Validiere(p);
        Datencontainer dc = this.datenquelle.LadeDaten(p.Query);
        int n = AktualisiereDaten(dc, p.Request);
        this.datensenke.SpeichereDaten(dc);
        return n;
    }

   private int AktualisiereDaten(Datencontainer dc, Aktualisierungsanfrage req) { ... }
}

Statische und dynamische Abhängigkeiten

Der Code ist statisch und dynamisch abhängig von anderen Funktionseinheiten:

image

Diese statischen Abhängigkeiten der Businesslogik sind statisch in der Businesslogik-Implementation verdrahtet. Das ist ein Punkt, den es deutlich herauszustellen gilt. Ausdruck der statischen Abhängigkeiten sind die Felder der Klasse MyBusinessLogik und die Aufrufe von Methoden auf deren Instanzen.

Die dynamischen Abhängigkeiten hingegen, sind nicht in der Businesslogik-Implementation zu finden. Sie kennt nur abstrakte Dienstleister wie IDatenSenke oder IValidator. Die zur Laufzeit relevanten Implementationen, werden ihre hingegen über den Ctor injiziert.

In puncto Abhängigkeiten hat also schon eine gewisse Separation of Concerns (SoC) stattgefunden: die laufzeitrelevante Bindung übernimmt eine andere Codeeinheit.

Vollständig ist die SoC allerdings nicht. Denn - wie gesagt - die BusinessLogik hat neben ihrer funktionalen Aufgabe auch noch eine nicht-funktionale: die statische Bindung. Ihr dienen die Felder und die Aktualisiere()-Methode. Ihre funktionale Aufgabe erfüllt AktualisiereDaten().

Während der Code also durch Injektion konkreter Abhängigkeiten gegenüber der früheren Praktik evolvierbarer geworden ist, weil er nicht mehr an Implementationen gebunden ist. So ist die Kopplung noch nicht wirklich lose. Die statischen Abhängigkeiten halten ihn starr. Das schränkt seine Evolvierbarkeit ein.

Synchronizität und Sequenzialität

Dass die BusinessLogik synchron und sequenziell ist, liegt auf der Hand. Ohne Hilfsmittel kann sie in C# nicht definiert werden. Ein Aufrufer von Aktualisiere() muss also auf das Ergebnis warten. Und während Aktualisiere() läuft, kann ein Prozessor nichts anderes tun; andere Aufgaben, andere Programme müssen auch warten. (Preemptives Multitasking lasse ich hier außen vor. Damit kann zwar doch quasi-parallel weiteres geschehen, aber jede zusätzliche Aufgabe verlangsamt alle schon laufenden.)

Auf innerhalb der BusinessLogik gibt es keine Parallelität. Mehrere Prozessorkerne bringen hier überhaupt keinen Nutzen. Es könnte ja sein, dass Laden, Verarbeiten und Speichern der Daten im Rahmen der Aktualisierung zumindest überlappen dürfen. Während noch Daten geladen werden, kann die Verarbeitung schon beginnen. Und während die Verarbeitung noch läuft, können erste Ergebnisse schon gespeichert werden.

Die synchrone Notation einer Sprache wie C# lässt das jedoch ohne Hilfsmittel nicht zu. Und weil wir letztlich "in C# denken", kommen wir auch nicht so recht auf die Idee, dass es auch anders sein könnte. Die beschränkten Ausdrucksmttel von C# stehen für uns zu sehr im Vordergrund. Das objektorientierte, synchrone Programmierparadigma ist wie ein Korsett um unsere Vorstellungen.

Das schränkt den Nutzen ein, den die Software, zu der die BusinessLogik gehört, aus der zukünftig wachsenden Zahl an Prozessorkernen ziehen kann.

Ausweg Asynchronizität

Ich glaube nun, dass uns ein anderes Paradigma einen Weg aus dieser beschränkten Zukunftsfähigkeit weisen kann. Durch asynchrone Programmstrukturen können wir beide Fliegen mit einer Klappe schlagen. Und das geht so:

1. Abhängigkeiten beseitigen

Im ersten Schritt schütteln wir auch noch die statischen Abhängigkeiten ab. Oder genauer: Wir separieren den Aufbau von Abhängigkeiten komplett von der problemdomänenorientierten Funktionalität. Dazu führe ich mal den Begriff "Prozessdefinition" ein. Ich trenne die Abfolge von Arbeitsschritten ganz klar von den Arbeitsschritten selbst. Alle Arbeitsschritte werden damit frei von Abhängigkeiten:

image

Die BusinessLogik v2 enthält jetzt nur noch die Funktionalität von AktualisiereDaten()! Aufgabe der Prozessdefinition ist es nun, die Arbeitsschritte ohne Abhängigkeiten in eine nützliche Reihenfolge zu bringen. Dass sie selbst viele Abhängigkeiten enthält, ist nicht schlimm. Sie ist im Vergleich zu den Arbeitsschritten trivial.

Dynamische und statische Abhängigkeiten stehen damit auf derselben Stufe: sie sind separierte Concerns.

image

Die Konsequenz solcher Beseitigung von Abhängigkeiten ist, dass Funktionalität nicht mehr geschachtelt ist. Funktionalität der Problemdomäne wie die BusinessLogik oder auch Infrastrukturfunktionalität wie eine Datenquelle sind Blätter im Abhängigkeitsbaum. Ergebnisse reichen sie also nicht tiefer hinunter in einem Aufrufbaum. Unter ihnen gibt es ja keine Ebene mehr. Stattdessen sind Ergebnisse immer Rückgabewerte in irgendeiner Form. Nur wer die Funktionalität aufruf bzw. sie zusammengesteckt hat, weiß ja, was weiter mit Ergebnisse geschehen soll. Die Wiederverwendbarkeit ist damit gestiegen.

2. Vom Callstack zum Fluss

Wenn nun alle Funktionalitäten ohne Abhängigkeiten sind, dann sind wir plötzlich sehr frei, was ihre Verschaltung angeht. Müssen wir sie denn wirklich noch synchron "ineinanderstecken"?

Hier aber zunächst ein erster Schritt. Ich nehme die refaktorisierten Funktionalitäten 1:1 und füge sie zu einem Prozess zusammen. Eine Funktion scheint mir da der passende Ausdruck für einen Prozess. In den geht etwas hinein und am Ende kommt etwas heraus.

class Prozessdefinition
{
    public Func<Parameter, int> Prozess { get; private set; }

    public Prozessdefinition(IDatenquelle dq, IDatensenke ds, IValidator v, IBusinessLogikV2 blv2)
    {
        this.Prozess = new Func<Parameter, int>(p =>
           {
               v.Validiere(p);
               Datencontainer dc = dc.LadeDaten(p.Query);
               int n = blv2.AktualisiereDaten(dc, p.Request);
               ds.SpeichereDaten(dc);
               return n;
           });
    }
}

Soweit das synchrone Programmierparadigma. Um weiter zu kommen, ist nun eine Richtungsänderung nötig. Asynchrones Denken und codieren ist nötig. Dafür zunächst eine etwas andere Darstellung der Funktionseinheiten:

image

Die Funktionseinheiten haben immer noch keine Abhängigkeiten untereinander. Aber es ist ihnen nun deutlich anzusehen, was reingeht und was rauskommt. Eine implementationsunabhängige Darstellung im Sinne von EVA (Eingabe-Verarbeitung-Ausgabe).

Das bisherige Abhängigkeitsdiagramm war nicht so detailliert. Darin war nur zu sehen, ob eine Funktionalität von einer anderen abhängig ist. Indem nun die Funktionalitäten aber darstellen, wie man von ihnen abhängig sein kann, ist Klarheit gewonnen. (Dass ich Ausgaben bei Validator und Datensenke eingeführt habe, ist hier vernachlässigbar. Sie machen den späteren asynchronen Prozess etwas einfacher.)

Die Prozessdefinition ist nun nicht mehr in einer Black Box eingeschlossen, sondern kann im Grunde das trivial gewordene Abhängigkeitsdiagramm ersetzen:

image

Diese Grafik zeigt nicht nur die grundsätzlichen Zusammenhänge der Funktionsbausteine, sondern auch den Zweck, zu dem sie zusammenhängen: die Abarbeitungen eines Prozesses. Und bitte im Hinterkopf behalten: Die Funktionsbausteine haben keine statischen Abhängigkeiten mehr. Ihre dynamische Zusammenarbeit im Prozess sieht man ihnen selbst nicht an. Die ist Sache des Prozesses.

Jetzt der Trick: Wenn der Prozess erstmal so dargestellt ist und eben nicht sofort als Code wie in der obigen ersten Prozessdefinition, dann... ja, dann ist es leicht, von einer synchronen Kopplung abzusehen. Warum sollte ich dies:

image

übersetzen in jenes:

if (v.Validiere(p))
{
    Datencontainer dc = dq.LadeDaten(p.Query);
    ...

Das Prozessdiagramm ist aber nicht zu verwechseln mit einem simplen Flowchart, das auch ein Kind des synchronen Programmierparadigmas ist. Es ist allgemeiner als Fluss zu verstehen: Die Funktionseinheiten sind verbunden zu einem Fluss, auf dem Daten zwischen ihnen als Verarbeitungsstationen fließen.

3. Asynchrone Flüsse

Die Darstellung eines Prozesses bietet nun die Chance, das Paradigma zu wechseln. Die Frage ist nur, wie kann solch bisher synchroner, sequenzieller Code in asynchronen, sequenziellen oder parallelen Code gewandelt werden?

Der Schlüssel liegt in expliziten Verbindungsstücken!

image Bei der üblichen synchronen Programmierung sind die Funktionsbausteine quasi aneinandergeschweißt. Die ursprüngliche BusinessLogik war untrennbar verbunden mit einem Validator usw. Sie bildeten eine Einheit - auch wenn die konkreten Abhängigkeiten erst zur Laufzeit dynamisch eingespritzt wurden.

Das ist zwar effizient - aber eben auch inflexibel. Und es ist synchron. Damit steht solche Verschweißung der Evolvierbarkeit im Wege.

imageGanz anders das Bild, wenn die Funktionsbausteine nicht verschweißt, sondern verschraubt sind. Mit expliziten Verbindungsstücken - wieder eine Separation of Concerns - können Beziehungen viel flexibler aufgebaut werden.

Ein Mittel dafür sind die Ports der Microsoft Concurrency Coordination Runtime (CCR). Mit ihnen ließe sich ein Prozess ganz anders beschreiben. Aus den bisherigen synchronen Methoden

interface IValidator
{
    bool Validiere(Parameter p);
}

interface IDatenquelle
{
    Datencontainer LadeDaten(string query);
}

könnten solche werden:

void Validiere(Parameter p, Port<bool> output) {...}

void LadeDaten(string query, Port<Datencontainer> output) {...}

Ich habe sie aus zwei Gründen nicht in eine Klasse oder ein Interface eingetragen: Zum einen möchte ich an dieser Stelle keine bestimmte Lösung jenseits explizit verbundener Funktionsbausteine vorschlagen. Auch die CCR Ports sind nur eine mögliche Variante für explizite "Schraubverbindungen". Zum anderen sollen die beiden freigestellten Routinen unterstreichen, dass Funktionale Programmierung - also die Konzentration auf Funktionen statt Klassen - einen Beitrag leisten kann, um Software zukunftsfähig zu machen.

Die Übersetzung einer bisher synchronen Methode könnte mit Ports also geradlinig sein: Eingabeparameter bleiben, Rückgabewerte werden ersetzt durch einen Output-Port. Der ist mit der nächsten Verarbeitungsstation verbunden. Zu jeder Methode gehört also ein Port für die Eingabe. Für die obige Methode Validiere() könnte das in vereinfachter und roher Form so aussehen:

Port<bool> pOutput = ...;

var pValidiere = new Port<Parameter>();
pValidiere.ReceiveSequentially(p => Validiere(p, pOutput));

Der Port für die Ausgabe, der gehört dann zum nächsten Arbeitsschritt im Prozessfluss.

Die Methode ReceiveSequentially() ist eine Extension Method für Ports, die ich mir ausgedacht habe. Sie bindet einen Eventhandler so an einen Port, dass immer nur ein Element zur Zeit verarbeitet wird. Das sichert eine sequenzielle, allerdings asynchrone Verarbeitung durch die Stationen in einem Prozessfluss zu. Bei Bedarf können Stationen aber natürlich auch parallel verarbeiten. Ihre vielen Ergebnisse müssen dann nur auch wieder eingesammelt werden. Map-Reduce ist dafür ein berühmtes Beispiel.

Viel wichtiger als Parallelität ist jedoch die Asynchronizität. Dadurch, dass Arbeitsschritte jetzt explizit über puffernde Ports gekoppelt sind, geben sie immer wieder ihre Prozessorressource (Thread auf einem Kern) frei. Das skaliert wunderbar. Es ist kooperatives Multitasking, das alle Kerne oder potenziell auch viele Maschinen überspannt.

Damit ist das eingangs beschriebene Ziel im Grunde erreicht: Die Abhängigkeiten sind minimiert, der Umgang mit ihnen ist herausfaktorisiert. Und die Asynchronizität ist eingeführt. Damit ist der Code zukunftsfähiger geworden:

  • Viele Flüsse können in dieser Weise gut skalierbar abgehandelt werden und nutzen dabei alle Prozessorkerne. (Und falls es mal nicht viele Flüsse durch die Last auf einem Rechner zu bearbeiten geben sollte, dann zeige ich in einem späteren Blog-Posting, wie so ein unterforderter Rechner anderen seine Leistung anbieten kann.)
  • Abhängigkeitsfreie Funktionseinheiten lassen sich viel einfacher weiterentwickeln und neu kombinieren.
Technische Umsetzung mit Notationsschwierigkeiten

Jetzt aber noch kurz konkret zur Umsetzung des obigen Prozesses:

  • Es kommen Parameter an, die in mehreren sequenziellen Schritten durch einen Prozess laufen sollen.
  • Am Anfang werden die Parameter validiert. Nur wenn die Validation erfolgreich ist, werden sie zum Laden von Daten weitergeschickt. Ein Fork-Station spaltet die Daten dafür auf: sie werden gleichzeitig zur Validation und zu zwei Joins weitergeleitet.
  • Der erste Join führt das Ergebnis der Validation und die Query in den Parametern zusammen und leitet die Query weiter an die Datenbeschaffung - aber natürlich nur, wenn die Validation erfolgreich war. Validation und Datenbeschaffung laufen also nicht parallel, sondern immer noch sequenziell.
  • Nach der Datenbeschaffung führt ein weiterer Join deren Ergebnis (Datencontainer) und die Parameter zusammen und leitet beide weiter an die eigentliche Geschäftslogik.
  • Die Geschäftslogik verändert die Daten und reicht sie weiter zum Speichern. Gleichzeitig erzeugt sie ein Ergebnis (z.B. Anzahl veränderter Datensätze), das jedoch nur am Ende aus dem Prozess herauskommt, wenn auch die Datenspeicherung erfolgreich war.

Dieses Szenarion mit Ports zu bauen, ist nicht schwierig. Es ist nur etwas umständlich. Eine wirklich gute textuelle Notation oder ein Fluent Interface, dass gerade diese Fork-Join-Kombinationen plastisch macht, ist mir noch nicht eingefallen.

Ohne Fork-Join oder auch Scatter-Gather oder Select/Choice und andere Muster, bei denen mehrere Ports beteiligt sind, wäre es einfach. Mit Pipes könnte ein vereinfachter Fluss ohne Fork-Join so aussehen:

Validator | Datenquelle | BusinessLogikv2 | Datensenke

Jede Station würde ihre Ergebnisse einfach nur weiterschieben an die nächste. Allerdings müsste z.B. der Validator alle Parameter weitergeben und nicht nur sein boolean-Resultat, weil ja nachfolgende Stationen davon mehr oder weniger für ihre Arbeit brauchen.

Etwas realistischer ließe sich so ein Fluss auch mit einem Fluent Interface beschreiben, z.B.

new Stage<Parameter>(Validiere)
    .Stage<Parameter, Tupel<Datencontainer, Parameter>>(LadeDaten)
    .Stage<Tupel<Datencontainer, Parameter>, Tupel<Datencontainer, int>>(Aktualisieren)
    .Stage<Tupel<Datencontainer, int>, int>(SpeichereDaten);

Dabei stehen die generischen Typparameter von Stage<TIn, TOut> für den Typ des Input- und des Output-Ports. Als Verarbeitungsschritt wird dann Action<TIn, TOut> erwartet.

Die Schwierigkeit der Notation - nicht der Technologie! - beginnt jedoch, wenn Flüsse sich teilen und wieder zusammenfließen.

Die Fork am Anfang des obigen Prozesses ließe sich so formulieren:

var fork = new Fork<Parameter, Parameter, string, Parameter>(
    (i, o0, o1, o2) => {oo.Post(i); o1.Post(i.Query); o2.Post(i);});

Der erste Typparameter definiert den Input-Typ, die weiteren die Typen für die Output-Ports, d.h. die Input-Ports der nachfolgenden Schritte. Die Aufgabe des Lambda-Funktion ist also die Aufspaltung oder Verteilung des Input auf die Outputs.

Die einzelnen Verarbeitungsschritte sind ebenfalls für sich genommen leicht zu formulieren:

var val = new Stage<Parameter, bool>(Validiere);
var laden = new Stage<string, Datencontainer>(LadeDaten);
var akt = new Stage<Tupel<Datencontainer, Parameter>,
                                          Datencontainer, int>(Aktualisieren);
var speichern = new Stage<Tupel<Datencontainer, int>, int>(SpeichereDaten);

Und wie die Joins formulieren, die Zusammenführungen von mehreren Flussarmen?

var joinFürLaden = new Join<bool, string, Datencontainer>((i0, i1, o0) => oo.Post(i1));

var joinFürLogik = new Join<Datencontainer, Parameter, Tupel<Datencontainer, Parameter>>(
    (i0, i1, oo) => oo.Post(new Tupel<Datencontainer, Parameter>(i0, i1)));

var joinFürEnde = new Join<bool, int, int>((i0, i1, oo) => oo.Post(i1));

Zum Schluss noch die Prozessschritte "zusammenstöpseln":

fork.Output[0] = val;
fork.Output[1] = joinFürLaden.Input[1];
fork.Output[2] = joinFürLogik.Input[1];

val.Output = joinFürLaden.Input[0];

joinFürLaden.Output = laden;

laden.Output = joinFürLogik.Input[0];

joinFürLogik.Output = akt;

akt.Output[0] = speichern;
akt.Output[1] = joinFürEnde.Input[1];

speichern.Output = joinFürEnde.Input[0];

Der Input geht dann in den Prozess bei fork.Input hinein und das Ergebnis kommt bei joinFürEnde.Output heraus. Das ist technologisch nicht kompliziert - aber die vorstehende Formulierung ist sicher nicht so gut zu lesen wie die synchrone Variante ganz am Anfang.

Was tun? Hier ist wahrscheinlich eine DSL angezeigt. Eine textuelle könnte schon ein wenig Erleichterung bringen; am Ende geht es aber wohl nicht ohne Diagramme. Ja, das wäre doch mal was: Eine grafische DSL mit einem hübschen Designer, die aus solchen Prozessdiagrammen eine Assembly produziert, die wir nur noch mit Prozessschritten parametrisieren müssen, wenn das nicht schon der Designer getan hat, weil wir ihm Referenzen auf Prozessschritte übergeben haben.

Ist das dann nicht aber die Microsoft Workflow Foundation neu erfunden? Nein. Die Prozesse, von denen ich hier geschrieben habe, sind leichtgewichtiger. Sind sollen nicht lange laufen. WF scheint mir da Overkill.

Und vielleicht... findet sich ja doch auch noch eine textuelle Beschreibung z.B. in Form eines Fluent Interface? Das würde mir sehr gefallen.

Aber all dessen ungeachtet ist mir an dieser Stelle wichtig gezeigt zu haben (oder zumindest laut gedacht zu haben), dass asynchrone explizite Kopplung mit soetwas wie CCR Ports hilft, Software zukunftsfähig in zweierlei Hinsicht zu machen. Wer asynchrone Prozesse denkt, der geht anders mit Abhängigkeiten um und macht Software fit für Mehrkernprozessoren. Da müssen wir gar nicht erst auf Axum & Co warten. Das geht hier und heute.

Kick It on dotnet-kicks.de