Follow my new blog

Donnerstag, 22. April 2010

Geshreddert persistent – Gedanken zu einem Datengranulat

Mapping von Daten zwischen Pesistenzmedium und Objektmodell ist nicht zu vermeiden. Es ist keine Last, sondern pure Notwendigkeit, wenn wir mit persistenten Daten “in Zeit und Raum” integrieren wollen. Denn dann müssen persistente Daten eine sehr flexible Struktur haben, die sich immer wieder neuen Anforderungen an in-memory Datenmodelle anpassen lässt. Zu dieser Erkenntnis hat mich eine Diskussion um Ayende Rahiens Dokumentendatenbank RavenDB gebracht.

Mein Gefühl ist nun, dass solch flexible persistente Struktur weder RDBMS noch DocDBs bieten. Ihre Datenmodelle sind zwar flexibler als die von ODBMS, doch ich glaube, das ist noch nicht genug. Zum Beleg hier Ayendes Dokumente für eine Blog-Datenbank:image

Das implizite Schema ist ganz einfach. User, Blog, Post sind Entitäten, die einander referenzieren; ein Post enthält Comments, verweist also nicht weiter auf sie.

Die ist natürlich überschaubarer als ein typisches relationales Schema für das Szenario (ebenfalls von Ayende):

image

Was habe ich nun daran auszusetzen? Das relationale Schema ist vor allem erstmal ein Schema, das geplant werden muss. Solche Planung ist gerade in der Anfangsphase eines Projektes schwierig und tendiert dazu, immer wieder angepasst werden zu müssen. Häufige Schemaänderungen am Persistenzmedium machen aber keinen Spaß.

Dem hält die NoSql-Bewegung schemalose Datenbank entgegen, von denen RavenDB ein Vertreter ist. Ayende konnte die obigen Dokumente einfach so ohne Vorplanung in seine Datenbank “schmeißen”. Keine Schemadefinition ist nötig. Er kann sie dann über ihre Id oder Queries auf ihren Feldern wieder herausholen. Eine NoSql-Datenbank leistet also nicht weniger als ein RDBMS – spart Ihnen jedoch Planungsaufwand. Ändert sich die Struktur eines Dokuments, speichern Sie es einfach neu. RavenDB ist auch tolerant gegenüber dem Hinzufügen/Löschen von Feldern oder Typänderungen. Indexe für schnelle Suche gibt es ebenfalls.

Sind NoSql-Datenbanken also nicht vielleicht die besseren Datenbanken?

Es gibt keine “absolut guten” Datenbanken, die immer eingesetzt werden sollten. Güte/Qualität/Eignung hängt von Ihren Anforderungen ab. Die unausgesprochene Anforderung, die RDBMS lange erfüllt haben und immer noch erfüllen, war/ist die nach hoher Effizienz gepaart mit einiger Flexibilität. Immerhin sind RDBMS Kinder der 1970er, als Software unter starker Ressourcenknappheit (Speicher, Prozessorgeschwindigkeit, Datenübertragungsrate) gelitten hat. Da war Effizienz in dieser Hinsicht ein hohes Gut.

Heute sind diese Ressourcen in vielen Szenarien kein Problem mehr. Andere Anforderungen gewinnen daher an Bedeutung. Die aus meiner Sicht wesentliche Anforderung ist die nach Flexibilität. Wir haben schon genug Mühe mit unseren Objektmodellen, da wollen wir nicht noch aufwändig ein zweites Schema für die persistenten Daten pflegen.

Dass NoSql-Datenbanken jetzt ein Thema sind, finde ich also ganz verständlich. Sie bieten durch Schemalosigkeit mehr Flexibilität. (Dass sie darüber hinaus auch noch andere Anforderungen besser als RDBMS erfüllen, lasse ich hier undiskutiert. Darum geht es mir nicht.)

Doch warum bei Tupeldatenbanken (z.B. Amazon SimpleDb) oder Dokumentendatenbanken (z.B. RavenDB) stehenbleiben? Geht vielleicht noch mehr Flexibilität? Ich glaub schon.

Beziehungsprobleme

Mein Gefühl ist, dass auch Dokumentendatenbanken noch Flexibilitätspotenzial verschenken. Das Problem steckt aus meiner Sicht im Umgang mit Beziehungen:

image

Ein Post-Dokument verweist auf ein Blog-Dokument, ein Post-Dokument enthält Felder und insb. mehrere Comment-Strukturen. Das sind zwei Arten von Beziehungen, explizite und implizite. Einmal wird verwiesen, einmal wird enthalten.

Klingt alles so natürlich – doch ich glaube, da steckt der Teufel im Detail. Wir machen unsere Datenmodelle unflexibel, weil wir beide Arten von Beziehungen vermischen. Ein Dokument wie ein Post ist über den Verweis an das Blog-Dokument gekoppelt. Ein Post ist abhängig vom Blog. Und Abhängigkeiten sind unschön; soviel sollte sich herumgesprochen haben.

Dass wir Einheiten definieren und persistieren wollen, ist unzweifelhaft. Ich habe also nichts gegen Dokumente. Solche Einheiten fassen Daten in Form von Feldern (Name-Wert-Paare) zusammen. Welche Felder sollen in einem Dokument stehen? Felder mit hoher Kohäsion, d.h. solche, die einfach eng zusammen gehören.

Dokumente sind damit sozusagen Einheiten mit einer “Single Responsibility”. Die stehen für genau eine Sache, z.B. ein Artikel in einem Blog oder ein Benutzer usw. Das macht sie auch zu Einheiten hoher Effizienz. Denn so ein Dokument kann man “in einem Rutsch” gut aus einem Persistenzmedium laden.

Und solche Einheiten haben eine Identität, d.h. sie lassen sich referenzieren, statt immer wieder in unterschiedlichen Zusammenhängen gespeichert werden zu müssen. Sie manifestieren das DRY-Prinzip in der Datenhaltung. Konsistenz ist leichter herstellbar.

Ob innerhalb eines Dokuments dann ein Feld nur einen Datenwert hat oder mehrere, halte ich für unerheblich. Im obigen Dokument hat tags z.B. mehrere Werte. Das widerspricht der relationalen 1. Normalform – aber was soll´s ;-) Wir reden über Flexibilität und Dokumentendatenbanken. Ganz pragmatisch gesehen sind multiple Werte kein Problem.

Welche Form ein Wert jedoch hat, das halte ich für bedenkenswert. Sollte ein Wert, der innerhalb eines Dokuments steht, selbst wieder eine Struktur haben, so wie die Comment-Werte? Ich glaube, nein. Felder sollten die Atome eines Dokuments sein, ihre Werte unteilbar. Zeichenketten, Zahlen, Datum/Zeit… das war´s.

Das halte ich aus zwei Gründen für vorteilhaft:

  1. Ob ein Feld einen oder mehrere Werte haben können soll, ist bei der Datenmodellierung meist schnell entschieden. Für die Persistenz als Dokument macht es, wie oben gezeigt, außerdem keinen Unterschied. Auch ob ein Feldwert eine Struktur hat oder nicht, ist meist leicht zu entscheiden. Straße+PLZ+Ort als 3 Felder speichern oder doch besser als eigene Datenstruktur? Kein Problem. Doch dann kommt die knifflige Frage, wenn die Entscheidung für eine Datenstruktur fielt. Soll die in einem Dokument gespeichert werden oder außerhalb nur mit einer Referenz darauf? Darüber kann man lange diskutieren und immer wieder Blicke in die Glaskugel werfen.
    Um das zu vermeiden, möchte ich nahelegen, per se alle strukturierten Werte nicht in einem Dokument abzulegen, sondern darauf zu verweisen. Mein erster Antrieb ist also hohe Produktivität. Nicht lang schnacken ;-)
  2. Doch nicht nur die Produktivität steigt, wenn Sie über Inklusion/Exklusion nicht lange diskutieren müssen. Daten, die für sich, d.h. separat in einem (kleinen) Dokument gespeichert sind, können Sie wiederverwenden. Im Hauptspeicher sind Objekte die Einheiten, die referenziert werden können. In Doumentendatenbanken sind es Dokumente. Felder zu Strukturen zusammenfassen und separat speichern spart also Platz und macht es leichter, Konsistenz herzustellen.
    Das ist erstmal das Potenzial, das in der Auslagerung von Strukturen liegt. Ob Sie es nutzen und einem Adressdokument wirklich eine eigene Identität geben und es wiederverwenden, ist dann nochmal eine zweite Frage. Die Auslagerung, d.h. Entkopplung von Strukturen macht das jedoch erst möglich.
  3. Auslagerung ist Entkopplung. Oder umgekehrt: Einschachtelung ist eine stärkere Form der Kopplung als Referenzieren. Es ist ja gerade der Zweck eines Dokuments, sich eng an seine Felder zu koppeln. Das führt zu Effizienz. Was zusammengehört, soll man nicht auseinander reißen. So eng koppeln wie (aus Effizienzgründen) nötig, so lose koppeln wie möglich. Das ist nicht für in der Softwarearchitektur wichtig, sondern auch bei der Datenmodellierung eine Tugend, denke ich. Wo Sub-Strukturen auftauchen, scheint mir daher eine Grenze überschritten. Bei ihnen sollte die enge Kopplung durch Inklusion enden. Schachtelung ist also nicht nur bei Programmstrukturen “böse”, sondern auch bei Datenstrukturen.

Unterm Strich bin ich also dafür, Daten zu Dokumenten zusammenzufassen. Eine gute Sache. Daten in einen Sack füllen, zubinden, ab in die Datenbank damit.

Diese “Datensäcke”/Dokumente sind für mich das Granulat flexibler persistenter Datenhaltung. Sie sind die aus Anwendungssicht unteilbaren Dateneinheiten. Die hohe Kohäsion ihrer Felder hält sie zusammen. Und die Felder haben wiederum atomare Werte – allerdings können das mehrere sein.

Soweit zur Modellierung der Kommentare in Ayendes Post-Dokument. Was aber ist mit dem Verweis auf das Blog? Ich glaube, dass Felder keine Beziehungen enthalten sollten. Punkt.

Ja, genau, lassen Sie sich das auf der Zunge zergehen: Dokumente selbst referenzieren keine anderen Dokumente. Referenzen wie das blog-Feld in Ayendes Post-Dokument halte ich für eine zu enge Kopplung. Wie oben schon gesagt: Der Verweis macht das Dokument abhängig. Das ist bei Daten so unschön wie bei Funktionseinheiten. Abhängigkeiten sind “böse”.

Warum steckt der Bezug zum Blog im Post? Aus zwei Gründen: Erstens aus einem Mangel an geeigneten Alternativen und zweitens aus Effizienzgründen. Beides halte ich für überwindbare Hindernisse, wenn persistente Datenmodelle flexibel sein sollen. Der Effizienzgrund verschwindet durch den Wunsch nach Flexibilität. Effizienz und Flexibilität widersprechen sich (meistens). Und der Mangel an Alternativen sollte sich beheben lassen. Hier ist “Tooling” gefragt.

Ohne eingeschachtelte Strukturen (comments) und ohne Beziehungen (blog) halte ich dann das Beziehungsproblem für gelöst. Es gibt dann keine enge Kopplung mehr nach innen und keine Kopplung mehr nach außen. Dokumente stehen dann für sich im “Datenraum” als “Granulatkügelchen”. Mein Post-Dokument würde so aussehen:

// posts/1
{
    "title": "RavenDB",
    "content": "... content ...",
    "categories": ["Raven", "NoSQL"]
    "tags" : ["RavenDB", "Announcements"],
}

Beziehungsraum

Wenn Dokumente keine Beziehungen mehr enthalten, wie werden die denn dann aber aufgebaut? Sie sind doch wichtig. Post-Dokumente gehören zu einem Blog und haben Kommentare. Irgendwie muss ein Post-Dokument also auf sein Blog und seine Kommentare verweisen. Doch wo/wie soll das geschehen, wenn es keine Felder mit Referenzen und keine Schachtelung mehr gibt?

Ich schlage einen “Beziehungsraum” vor, einen Bereich/Container, der nur die Beziehungen zwischen Dokumenten verwaltet. Das wäre eine klare Separation of Concerns (SoC). Dokumente enthalten Daten. Der Beziehungsraum enthält Beziehungen. Zusammen ergeben sie das persistente Datengeflecht.

Im Beziehungsraum wären jedem Dokument die Dokumente zugeordnet, mit denen es in Beziehung steht. Der Beziehungsraum bestünde also quasi nur aus Identitäten. Der Identität post/1 wären dann z.B. die Identitäten blog/1 und comment/1, comment/2 zugeordnet.

image

Etwas weniger bildhaft könnte das so aussehen:

image

Das wäre dann ein normaler Graph. Die Beziehungen sind bewusst ohne Pfeile, weil ich denke, dass alle Beziehungen grundsätzlich bidirektional persistiert werden sollten. Denn: Wer weiß, ob man nicht irgendwann mal auch “rückwärts” traversieren will? Wer heute ein Blog herauszieht aus der Datenbank, um darüber Postings zu erreichen, der will es morgen vielleicht umgekehrt tun. Immer schön flexibel bleiben.

Ein Graph legt natürlich nahe, eine Graphendatenbank wie Neo4J (http://neo4j.org/) oder InfoGrid (http://infogrid.org/) für das Datengranulat einzusetzen. Leider gibt es anscheinend bisher (Stand April 2010) keine ähnlich leistungsfähigen Angebote auf der .NET Plattform, doch das mag sich ja ändern. Dokumentendatenbanken schießen aus dem Boden, warum nicht auch Graphendatenbanken? Müssen ja nicht gleich Enterprise-Qualität haben. Erstmal geht es auch kleiner, embedded, um mal mit dem Paradigma warm zu werden.

Oder sind Graphendatenbanken es auch noch nicht? Denn lt. deren API sieht es noch so aus, als würden Beziehungen mit den Knoten gespeichert. (Aber vielleicht ist das nur einem einfachen API geschuldet und im Persistenzmedium sieht es dann anders aus.) Denn meine Idee ist ja eben, die Knoten (“Datensäcke”) frei von Beziehungen zu halten. Das Bild dafür sähe eher so aus:

image

“Irgendwas” zwischen den “Datensäcken”, den Granulatkügelchen setzt sie in Beziehung.

Während ein generischer Graphendatenbank-API Beziehungen so aufbauen würde:

Node post = new Node(…);

Node blog = new Node(…);

post.Relate(blog);

oder

blog.Relate(post);

Wäre ein Granulat-API ehrlich, wenn er es so täte:

RelationSpace rs = …;

rs.Relate(post, blog);

Aber das könnte natürlich auch in einer Node.Relate() Methode versteckt sein. Hm… Ich bin noch unsicher, wie ich es gern hätte.

In jedem Fall scheint mir die deutliche Unterscheidung zwischen Daten und Beziehungen wichtig. So wird das persistente Datenmodell sauber (im Sinne des SoC) und flexibel. Zusätzlich wären Beziehungen immer first class citizens und könnten ebenfalls mit Attributen versehen werden. Sie könnten z.B. den Beziehungsenden “Rollen” zuweisen:

rs.Relate(post, “partOf/contains”, blog);

Aus Sicht eines Posting wäre das Blog das Ganze dessen Teil (“partOf”) es ist. Für ein Posting könnte damit gezielt erfragt werden, wovon es denn Teil ist:

IEnumerable<Node> blogs = rs.Traverse(post, “partOf”);

Oder ein Blog könnte nach seinen Postings befragt werden:

IEnumerable<Node> postings = rs.Traverse(blog, “contains”);

Aber vielleicht ist der Graph-API doch etwas intuitiver?

… = post.Traverse(“partOf”);

Um einem Node Zugriff auf den “Beziehungsbaum” zu geben, müsste er dafür allerdings damit initialisiert worden sein. Hm… warum nicht? Es geht hier ja nicht um einen API/ein Datenmodell, dass in allen Anwendungsschichten und über Remoting-Grenzen hinweg funktionieren soll. Mapping ist notwendig. Die über den API zugänglichen Granulatkügelchen und Beziehungen müssen damit nur gefüllt werden können. Danach kann das objektorientierte Datenmodell allein stehen.

Semantikfreiheit

Das bringt mich zu einem letzten Punkt: Ich glaube immer mehr, dass im Persistenzmedium keine Semantik stecken sollte. Die ist ein Concern, der nicht dahin gehört. Ein Persistenzmedium ist ein Persistenzmedium ist ein Persistenzmedium. Es speichert Daten dauerhaft, so dass aus ihnen später wieder “Werkstücke” gefertigt werden können. Dafür sind in einem Datengranulat “Kügelchen” und Beziehungen festzuhalten. Fertig.

An einem Blog hängen viele Postings. Ein Posting hat viele Kommentare. Dieser Zusammenhang ist zu persistieren. Ob ein Blog-Objekt in einem bestimmten Kontext seine Postings in einer IList<> oder einem Dictionary<,> oder in einem Array halten will, ist nicht Sache des Persistenzmediums. Ein Mapping sorgt dafür, dass die vielen Postings in die kontextspezifische Collection eines Blogs kommen.

Das Persistenzmedium kennt nur atomare Granulatkügelchen mit einem oder mehreren atomaren Werten je Feld. Und es kennt bidirektionale benannte Beziehungen zwischen den Granulatkügelchen.

Das scheint mir die Grundanforderung an ein Datengranulat-Modell. Was in einem Objektmodell für die Verarbeitung “irgendwie verwoben ist”, wird für die Persistenz gründlich geshreddert. Nur so ist es möglich, es später für die Verarbeitung in anderen Kontexten möglichst einfach immer wieder anders zusammenzusetzen.

image

Der Unterschied zum relationalen Datenmodell besteht dabei darin, dass das relationalen Datenmodell kontextspezifische Schemata nahelegt. Datengranulat hingegen sieht immer gleich aus. Die Granulatkügelchen mögen unterschiedliche “Farben” und Größen haben, doch sie sind in einem immer gleich: es sind Atome ohne inhärente Beziehungen. Beziehungen sind extern zu ihnen und sehen ebenfalls immer gleich aus.

Jetzt die Frage: Wo ist eine Datengranulat-Persistenztechnologie? Dokumentendatenbanken sind es nicht. In ihnen können “Datensäcke” zwar hübsch gespeichert werden, doch für die Beziehungen sind Dokumente untauglich, glaube ich.

RDBMS wiederum könnten für Beziehungen ordentlich sein, aber schemalose “Datensäcke” sind nicht ihr Ding.

Ein ODBMS könnte hier hingegen passen. Das Granulat-Datenmodell ist ja universell und ändert sich nicht. Flexibilität ist nicht nötig. Da ODBMS für starre Datenmodelle geeignet sind und direkte Beziehungen effizient verwalten, könnte darauf eine Granulatdatenbank aufgesetzt werden.

Oder doch besser eine Graphendatenbank? Hm… Ich glaube, damit würde ich am ehesten mal spielen wollen.

Oder selbst bauen? Mal mit F# Infrastruktur basteln? Ein Entwicklertraum würde wahr… :-) Na, mal schauen. Erstmal drüber schlafen. Am Ende gehts ja zunächst nur um einen API. Wie es darunter aussieht, ist egal.

Mittwoch, 21. April 2010

Datengranulat oder: Mapping als Lust statt Last

Anlässlich einer Diskussion um NoSql in Ayende Rahiens Blog mache ich mir Gedanken darum, was wir eigentlich von einem Persistenzmedium wollen (sollten). Lange haben wir es jetzt als gesetzt angesehen, dass Daten von Geschäftsanwendungen in relationalen Datenbanken gespeichert werden. Man tut das einfach so. Das ist der Default. Die Gründe dafür sind vielfältig. Oft ist schon eine relationale Datenbank vorhanden und es ist einfacher, eine neue Anwendung oder auf die aufzusetzen. Oder das RDBMS XYZ ist Unternehmensstandard, so dass auch bei der nächsten Anwendung XYZ zum Einsatz kommen muss. Oder Entwickler sind schlicht vertraut mit dem relationalen Datenmodell. Oder die sonstigen Tools um das relationale Datenmodell erlauben einfache Eingriffe in persistierte Daten an Anwendungen vorbei.

Gehupft wie gesprungen, Daten werden immer noch vor allem mit RDBMS verwaltet. Ist so. Aber ist das auch gut so?

image Die Diskussion um NoSql zeigt, dass es eine wachsende Gemeinde von Entwicklern gibt, die sagt, dass sei nicht gut so. RDBMS haben ihren Wert – doch der sollte von Anwendung zu Anwendung immer wieder neu festgestellt werden. Für manche Anwendungen sind RDBMS die passende Wahl, für andere nicht. So geht es der NoSql-Bewegung um Alternativen, nicht um Ersatz. RDBMS sollen und werden nicht einfach durch Dokumentendatenbanken o.ä. ersetzt. NoSql will nur dafür sorgen, dass Entwickler mehr als eine Option für die Persistenz haben. Das mag dann mal wie eine Rebellion gegen die RDBMS-Hersteller aussehen… am Ende ist´s aber nicht mehr als eine Ausbalancierung mit Gegengewichten.

Ayende Rahien bastelt nun auch an einer NoSql-Datenbank: RavenDB. Ein interessantes Projekt. Ich bin gespannt. Eine Dokumentendatenbank à la CouchDb oder MongoDb, aber in C# implementiert, tut der .NET-Plattform gut.

Nichtsdestotrotz hat mich sein Posting zu der Frage geführt: Was wollen wir denn eigentlich von einer Datenbank, von einem Persistenzmedium? Ein RDBMS ist nicht per se “gut”. Eine DocDB ist auch nicht per se “gut”. Dasselbe gilt für ODBMS, Graphendatenbanken, assoziative Datenbanken usw.

Warum sollten wir also das eine persistente Datenmodell dem anderen vorziehen? Zur Laufzeit gehen wir in jedem Fall nicht mit den persistenten Daten in ihrem Ursprungsformat um, sondern wünschen uns immer wieder Objekte. Nicht umsonst arbeiten wir uns seit Jahren am Thema O/R Mapping ab. Im Hauptspeicher wollen wir durch Objektgraphen navigieren. Wie die Daten auf der Platte liegen, ist uns letztlich egal.

Als das relationale Datenmodell entwickelt wurde, gab es keine Objektorientierung. Deshalb hat man gleich zum Datenmodell eine passende Sprache erfunden (SQL), mit der die Daten manipuliert/abgefragt werden können. Datenmodell und Sprache haben zueinander gepasst. Heute ist das anders: C# passt nicht zum relationalen Datenmodell. Deshalb gibt es O/R Mapper.

image Passt C# nun aber zum Dokumentendatenmodell? Wenn ich den Code in Ayendes Posting anschaue, dann lautet die Antwort für mich: nein. C# mag hier besser passen, aber es ist keine 100%ige Passgenauigkeit. RavenDB bietet zwar gleich einen objektorientierten API, der durchaus Ähnlichkeiten mit dem meines Lounge Repository hat. Doch wenn Sie genau hinschauen, dann gibt es merkwürdige invertierte Beziehungen: da wird einem Posting ein Blog zugewiesen, obwohl der naheliegende Gedanke doch wohl eher ist, dass ein Blog Postings enthält. Warum Ayende es so macht, können Sie in den Kommentaren zum Blogartikel lesen. Das ist schon verständlich. Doch es zeigt, dass auch zwischen Dokumentendatenbanken und Objektorientierung ein Impedanzunterschied ist, der durch Mapping überwunden werden will. Denn ein Objektmodell wie das im Artikel wollen Sie nicht einfach so in der Anwendung allerorten verwenden, denke ich. Dafür ist es zuwenig intuitiv mit seinem “Beziehungssystem”, das der Skalierbarkeit geschuldet sind.

Mapping ist unvermeidbar

Wie es aussieht, ist das objektorientiere Datenmodell immer irgendwie anders als ein persistentes. Immer existiert ein Impedanzunterschied. Oder? ODBMS versprechen, dass das nicht so sei. Stimmt auch. Mit db4o können Sie Objekte mit ihren Beziehungen “as is” speichern. Kein Mapping ist nötig. Ist das nicht der heilige Gral? Merkwürdig nur, dass die Branche den bisher nicht erkannt hat. Vielleicht ist er gar keiner. Oder liegt das nur an der Übermacht böser RDBMS-Hersteller, so dass das Wahreguteschöne keine Chance hat? Die Antwort ist eine Mischung aus allem, denke ich.

An dieser Stelle möchte ich mich aber auf einen Aspekt konzentrieren: die Abwesenheit eines Mappings. Die scheint mir nämlich ein Problem zu sein.

Ja, genau: das Problem, das ODBMS lösen, vereitelt ihren breiten Erfolg, glaube ich. ODBMS lösen das Mapping-Problem und graben sich damit das Wasser ab. Wie kann das sein? Eine Antwort ist nur möglich, wenn wir erneut die Frage stellen, wozu denn überhaupt Persistenz heute nötig sei?

Früher bzw. dort, wo Daten vor allem mit SQL verarbeitet werden, ist das persistente Datenmodell das Datenmodell, das einzige, das zentrale. Persistenz ist dann kaum zu unterscheiden von Verarbeitung. Mit RDBMS, SQL und Store Procs schlägt man also quasi zwei Fliegen mit einer Klappe.

Heute ist das nicht mehr nötig oder wir wollen es einfach nicht mehr. Das persistente Datenmodell verliert damit einen Zweck. Es muss nicht mehr der Verarbeitung dienen. Damit bricht im Grunde die mathematische Basis von RDBMS weg. Dass RDBMS mit ihrem Datenmodell so wunderbar dem relationalen Kalkül entsprechen, ist uninteressant. Datenbanken sollen heute vor allem eines: Daten persistieren. Fertig, aus, Applaus. Verarbeiten tun wir die Daten in unseren objektorientierten 3GL Hochsprachen. Und dabei sollte uns das Persistenzmedium am besten nicht im Wege stehen.

Jetzt der Trick: Wie wir Daten im Hauptspeicher halten, das ändert sich alle Nase lang. Oder aus unterschiedlichen Perspektiven (Stichwort Bounded Context) sehen dieselben Daten unterschiedlich aus. Wenn also ein ODBMS so wunderbar ein Objektmodell impedanzunterschiedlos gespeichert hat, dann ist das schön für den Augenblick und für den speichernden Kontext. Aber was ist morgen? Was macht ein anderer Kontext damit, der die Daten anders sehen möchte?

ODBMS fixieren ein Datenmodell im Persistenzmedium. Und das ist gut, wenn man das denn will. Wer sicher ist, dass sein Datenmodell so bleibt, wie es ist, der ist mit einem ODBMS wunderbar bedient. Ich möchte ODBMS also nicht abschaffen. Sie sollen zur Artenvielfalt der Persistenzmedien beitragen. Aber sind eben nicht der heilige Gral, sondern haben auch nur einen Bereich von Szenarien, in dem sie mit Gewinn eingesetzt werden sollten.

Durch den Frust, den der Impedanzunterschied zwischen OO-Datenmodell und relationalem in der Branche erzeugt hat, haben wir jedoch falsch eingeschätzt, was ganz breit das wahre Problem heutzutage mit persistenten Daten ist. Deshalb scheinen/schienen ODBMS so vorteilhaft. Ist nicht Mapping das Problem, von dem wir erlöst werden müssen?

Nein.

Mapping ist kein Problem, sondern unvermeidbar, gar eine Tugend.

Nicht immer, aber in vielen, vielen Fällen.

Denn das Problem, von dem wir erlöst werden müssen, ist das Problem des inflexiblen persistenten Datenmodells. Anwendungen leiden heute darunter, dass sie sich frühzeitig auf ein persistentes relationales Datenmodell festlegen müssen, dass einem objektorientierten entspricht. Dann verändert sich das objektorientierte Datenmodell und das relationale muss nachgeführt werden. O/R Mapper sollen das verbergen helfen… aber das klappt nicht so recht. Also pflegen wir zwei Datenmodelle und ein Mapping.

Bei ODBMS muss zwar nur ein Datenmodell gepflegt werden, aber die persistenten Daten sind sehr star. Sie enthalten ja direkte Objektbeziehungen, die morgen anders sein können.

Wir leiden also entweder unter zwei Datenmodellen + Mapping oder einem sehr starren Datenmodell.

NoSql tritt zwar an, mit seinen schemalosen Datenbanken das persistente Datenmodell etwas weniger starr zu machen. Doch ich bin im Zweifel, ob das schon reicht. Derzeit ist meine Vorstellung, dass wir noch radikaler denken sollten. Nicht in allen Persistenzszenarien natürlich; aber in denen, wo Datenmodelle im Fluss sind. Und das ist viel öfter der Fall, als wir wahr haben wollen.

ODBMS sind effizient. Wir sparen uns das Mapping. Der Preis dafür ist Inflexibilität. Das bedeutet für mich im Umkehrschluss: Wenn wir wahrhaft Flexibilität haben wollen, dann kommen wir ums Mapping nicht herum. Wir müssen sogar noch mehr und häufiger mappen als bisher.

Daten als Granulat

Kennen Sie Fabbing? Beim Fabbing wird – zumindest bei einem Verfahren – aus einem Granulat ein Gegenstand “gedruckt”. Fabbing ist “Drucken in 3D”. Wie der “Ausdruck” aussehen soll, definiert ein Modell. Das Material liegt als Granulat vor. Im Grunde ist das ähnlich wie beim Laserdrucker: Da liegt der Ausdruck als Text vor und wird auch Tonerstaubpartikeln “hergestellt”.

Tonerstaub und Fabbing-Granulat sind das Material, aus dem “Ausdrucke” nach immer wieder neuen Modellen produziert werden können. Sie sind die ultimativ flexible Grundsubstanz für “Ausdrucke”.

Wenn wir in unseren Anwendungen nun immer wieder die in-memory Datenmodelle verändern oder neue definieren, also Datenstrukturen ganz unterschiedlicher Form aus den selben Daten herstellen wollen, dann sollten wir anfangen, unsere persistenten Daten als Grundsubstanz anzusehen, denen wir auch eine ultimativ flexible Struktur geben müssen.

Wir brauchen eine Art persistentes Datengranulat, wenn wir hochflexibel bei unseren Laufzeitdatenmodellen sein wollen. Dessen persistentes Datenmodell ist dann natürlich ganz weit weg vom Laufzeitdatenmodell. Noch weiter als relationales oder dokumentenorientiertes. Zwischen Datengranulat und OO-Datenmodell müsste also noch mehr Mapping betrieben werden.

Doch das macht nichts. Das ist vielmehr der Trick an der Sache. Dass wir bereit sind für mehr Mapping ist die Bedingung für die Möglichkeit, eines hochflexiblen persistenten Datenmodells, ein Datengranulat.

Wie das aussehen soll? Keine Ahnung. Wie das dann auch noch skaliert? Keine Ahnung. Daran müsste man wohl mal forschen. Dass RDBMS oder NoSql heute ein solches Datengranulat aber schon bieten, bezweifle ich.

Sicher ist für mich auch, dass so ein Datengranulat einen Preis über das Mapping hinaus hat. Es wird wohl weniger performant und weniger skalierbar sein bzw. weitere Technologieentwicklung wie Mehrkernprozessoren oder Hochgeschwindigkeitsleitungen oder mehr Speicher einfach “aufsaugen”. Der Gewinn, der winkt, ist jedoch – glaube ich – enorm. Höchste Flexibilität und damit eine Befreiung von vergeblicher Planungssicherheit.

Bottom line: Solange wir Mapping zwischen persistentem Datenmodell und objektorientiertem Laufzeitdatenmodell noch als Problem ansehen, suchen wir die Lösung für die Datenpersistenz noch unter der Laterne statt dort, wo sie wirklich liegt. Die Notwendigkeit zum Mapping ist kein Bug. Sie liegt in der Natur der Sache, dass persistente Daten langlebig und weitreichend sind. Deshalb müssen sie flexibel sein. Sie müssen unterschiedlichen Herren dienen können. RDBMS mit dem relationalen Datenmodell haben das schon lange irgendwie geboten. Aber wie NoSql zeigt, ist das nicht genug. Entwickler wollen mehr Flexibilität. Also sollten wir über einen wirklichen Sprung in puncto Flexibilität nach vorn nachdenken. Wie könnte ein Datengranulat als Grundstoff für alle möglichen Datenmodell in Anwendungen aussehen?

Freitag, 16. April 2010

Kanalisiert – Event-Based Components im Dialog

Die Pins von Event-Based Components (EBC) sind bisher ganz einfach: Output-Pins sind Delegaten, Input-Pins sind Methoden. Beide haben 1 Parameter und liefern kein Resultat. Damit können Sie alle Kommunikationswünsche erfüllen. Irgendwie. Manchmal ist es jedoch etwas umständlich. Die Simplizität und Regelmäßigkeit der Beschränkung auf nur eine Signatur hat das für mich jedoch bisher wett gemacht.

class Client
{
    public Action<string> pingEvent;
    …

class Service
{
    public void DumpText(string text)
    {
        Console.WriteLine("text received: {0}", text);
    }
    …

Ein Artikel über Axum in der dotnetpro hat mich nun allerdings nochmal nachdenken lassen. Ich bin weiterhin sicher, dass Pins keine beliebige Form haben sollten. Eine überschaubare Menge an Pin-“Formen” macht ein Tooling einfacher.  Andererseits wäre es schön, Kommunikationsmuster nicht immer wieder selbst mit speziellen Nachrichtentypen modellieren zu müssen.

Beispielhaft habe ich für eine Request/Response Kommunikation hier demonstriert und auch schon versucht, den Aufwand hinter einer Erweiterungsmethode zu verstecken. Für diese einmalige bi-direktionale Kommunikation ist das ok. Was aber, wenn zwei EBCs einen Plausch halten wollen? Ich skizziere das mal mit Standardtypen:

EBC A sagt… EBC X antwortet…
string  
  int
double  
  DateTime
bool  

Der Plausch beginnt, indem A an X einen string sendet. (Dass A nichts von X weiß, sondern einen string-Event feuert, lasse ich einmal außen vor. Auf einer Platine sind A und X mit einer Leiterbahn verbunden. Die Formulierung “sendet an” ist schlicht leichter zu verstehen.) Daraufhin antwortet X mit einem int-Wert. Den nimmt A auf und führt den Plausch mit einem double-Wert an X weiter. Den beantwortet X mit einem DateTime-Objekt und A beendet den Plausch mit einem bool-Wert.

So eine Konversation können Sie mit den bisherigen Pin-Standardformen implementieren, doch das ist umständlich. Der Gesprächsfluss ist in den Methoden bei A und X nur schwer zu erkennen. Probieren Sie es aus.

Dies und anderes wird aber einfacher, wenn Sie EBC-Pins nicht mehr “durch blankes Metall” und quasi direkt verbunden sehen, sondern durch eine sichtbare Leitung. Darauf hat mich der Axum-Artikel gebracht. Dort läuft die Kommunikation zwischen Axum Agenten nämlich durch Channels.

Mit diesem Konzept habe ich nun ein wenig in der EBC-Welt gespielt. Herausgekommen ist das Folgende.

EinwegKommunikation

Wenn ein EBC nur einen Event feuern will, ohne ein Ergebnis im weiteren Verlauf der Methode zu erwarten, dann ist das eine Einwegkommunikation. Die wird ganz natürlich durch einen Delegaten als Output-Pin beschrieben. Das kann auch so bleiben. Eine Modellierung mittels Channels sollte auch hier jedoch schon möglich sein. Die sähe z.B. so aus:

class Client
{
    public TriggerChannel noDataChannel;
    public OneWayChannel<string> pingChannel;
    …

Auf die Input-Pins haben Channels nur wenig Einfluss. Die werden weiterhin als Methoden implementiert:

class Service
{
    public void Reactor()
    {
        Console.WriteLine("reacting");
    }

    public void DumpText(string text)
    {
        Console.WriteLine("text received: {0}", text);
    }
    …

Bitte beachten Sie: Ich erlaube jetzt auch parameterlose Methoden! Das scheint mit durch Einführung der Channels eine angebrachte Lockerung der Regelmäßigkeit. Events ohne Nachrichten tauchen häufig in Architekturen auf. Für die einen Dummy-Nachrichtentyp einzusetzen, erhöht die Lesbarkeit dann nicht.

Verbunden werden Output- und Input-Pin mit Channel-Objekten:

Client c = new Client();
Service s = new Service();

c.noDataChannel = new TriggerChannel(s.Reactor);
c.pingChannel = new OneWayChannel<string>(s.DumpText);

Der OneWayChannel<> bringt hier zwar noch keinen Vorteil gegenüber den bisherigen Events. Dennoch scheint er mit angebracht, um alles, was bisher auch ohne Channels möglich war, konsequent mit Channel modellieren zu können.

Events feuert der Client nun allerdings nicht mehr so offensichtlich. Seine Output-Pins sind ja Channels und über die werden Nachrichten versandt:

this.noDataChannel.Trigger();
this.pingChannel.Send(“hello, world!”);

Für das grundsätzliche Prinzip der Kopplung von EBCs macht das keinen Unterschied. Alle Vorteile von EBCs gegenüber Injection-Based Components (IBC) bleiben erhalten. Und weitere kommen hinzu…

ZweiwegeKommunikation

Durch Channels auch Events ohne Parameter feuern zu können, ist nett. Noch netter ist es allerdings, eine saubere Abbildung für die Request/Response-Kommunikation zu haben. Der Vorschlag eines speziellen Request<,>-Nachrichtentyps, den ein Tooling natürlich auch erkennen könnte, war praktikabel – doch irgendwie hat er das saubere Paradigma “verdreckt”.

Die Kommunikation grundsätzlich über Channels laufen zu lassen und dann für Request/Response einen speziellen Channel zu haben, scheint mir sauberer. Aber sehen Sie selbst:

class Client
{
    public RequestResponseChannel<string, string> echo;
    …

Passende Input-Pins können verschiedene Formen haben:

class Service
{
    public string EchoFunc(string text)
    {
        return text.ToUpper();
    }

    public void EchoAction(string text, Action<string> reply)
    {
        reply(text.ToUpper());
    }
    …

Endlich können Sie in der Kommunikation zwischen Komponenten wieder Funktionen verwenden :-) Ist das nicht ein Gewinn. Die Regelmäßigkeit des Einsatzes von Channels erlaubt auch hier wieder etwas Öffnung für das Korsett der Input-Pin-Signaturen. Funktionen sind “serverseitig” die natürliche Form für Query-Prozessoren.

Alternativ können Sie aber auch Antworten über einen reply-Pin zurückreichen. Das bietet sich an, wenn für eine Anfrage mehr als ein Ergebnis erzeugt wird – und diese Pin-Form hat Ausbaupotenzial. Aber davon ein andermal mehr.

Die Bindung von Output- an Input-Pin ist denkbar einfach:

c.echo = new RequestResponseChannel(s.EchoFunc);

oder

c.echo = new RequestResponseChannel(s.EchoAction);

Beim Versand von Nachrichten fangen Channels an, ihren Vorteil gegenüber den bisherigen “blanken Drähten” auszuspielen. Sie können entweder ein Resultat so direkt am Ort der Anfrage verarbeiten, wie es eine nachrichtenorientierte Kommunikation erlaubt:

Console.WriteLine("echo result: {0}",
                  echo.Request("hello, echo")
                     
.Receive());

Das ist zwar immer noch ein zweischrittiger Prozess, doch der ist einem Funktionsaufruf noch recht nahe.

Oder Sie verlagern die Verarbeitung in eine Continuation:

echo.Request("hello, echo")
    .Receive(t => Console.WriteLine("echo result: {0}", t));

Das sind dann nicht nur nachrichtenorientiert, sondern sogar schon asynchron aus. Ist es mit EBCs zunächst jedoch nicht.

Die hier beschriebene Request<,>-Nachricht bot zwar auch schon so ein Programmiermodell, doch das war ein Sonderfall. Es musste speziell auf einem Nachrichtentyp implementiert werden. Mit Channels unterscheiden sich Einweg- und Zweiweg-Kommunikation hingegen nicht mehr so sehr. Sie laufen zwar über verschiedene Channels, doch das Programmiermodell ist gleich: in beiden Fällen erfolgt der Versand von Nachrichten mittels Channel-Methoden. Das halte ich für einen Gewinn an Regelmäßigkeit in der Verständlichkeit.

Mehrwegkommunikation

Regelmäßige Kommunikation via Channel macht es dann im nächsten Schritt möglich, auch Komplizierteres zu modellieren. Ich komme zurück auf den obigen Plausch zwischen zwei EBCs. Lassen Sie mich am besten dessen Implementation zunächst zeigen. Der Initiator sieht so aus:

public void A()
{
    using(var x = chat.Open())
    {
        x.Send("hello");
        var i = x.Receive<int>();
        Console.WriteLine("  {0}", i);
        x.Send(i/3.14);
        Console.WriteLine("  {0}", x.Receive<DateTime>());
        x.Send<bool>(true);
    }
}

und sein Gesprächspartner so:

public IEnumerable<ConversationMessage> Chat(IProducerConversation a)
{
    yield return a.WaitFor<string>();
    Console.WriteLine(a.Received<string>());
    conv.Reply(a.Received<string>().Length);
    yield return a.WaitFor<double>();
    Console.WriteLine(a.Received<double>());
    a.Reply(DateTime.Now);
    yield return a.WaitFor<bool>();
    Console.WriteLine(a.Received<bool>());
}

Für beide Konversationspartner macht der Channel es möglich, dass sie trotz Nachrichtenorientierung ziemlich “natürlich” aussehen, d.h. die Sequenz der “Sprechakte” ist klar erkennbar. Sie stehen einfach untereinander in den jeweiligen EBC-Methoden.

Für den Initiator A wäre das auch auf einem Weg wie bei Request/Response möglich gewesen. Aber nicht für seinen Gegenpart. Der wäre ohne Continuations mit einfachen Events nicht möglich. Und das wäre schlecht zu lesen.

Indem X – ich habe die Seite des Gesprächs “Producer” genannt, A ist der “Consumer” oder “Initiator” – jedoch als Iterator ausgelegt ist, kann auch er sequenziell notiert werden. Das ist ein Kleinwenig umständlicher als beim Initiator, doch ich denke, das ist zu verschmerzen.

Dieses Vorgehen habe ich mir bei der Concurrency Coordination Runtime abgeschaut. Funktioniert, wie Sie sehen, aber nicht nur für asynchrone Komponenten gut ;-) Eine Konversation ist dadurch zwar asymmetrisch, doch das finde ich auch nicht schlimm. Axum kann das zwar besser – Sie müssten dafür nur den Preis einer neuen Programmiersprache bezahlen. Angesichts dessen ziehe ich es vor, etwas Umständlichkeit in Kauf zu nehmen.

Den Input-Pin stellt die Methode Chat() dar. Der Output-Pin sieht so aus:

class Client
{
    public ConversationChannel<Conversation> chat;
    …

Indem Sie dem Channel einen Typ für die Kommunikation anhängen, haben Sie die Möglichkeit, eigenen Sende/Empfangsroutinen und weitere Funktionalität darauf zu definieren. Darüber hinaus sehe ich hier auch die Chance für die Definition eines Protokolls; Ihr Conversation-Typ könnte z.B. bestimmen, dass auf eine string-Nachricht vom Initiator ein double folgen muss. Sendet er etwas anderes, liegt ein Fehler vor. Dafür habe ich mir aber noch keine weitere Notation ausgedacht. Später einmal mehr dazu.

Die Verbindung der Pins ist wie bei den anderen Channels ganz einfach:

c.chat = new ConversationChannel<Conversation>(s.Chat);

Wenn eine Konversation umfangreicher wird, können Sie natürlich auch überlegen, ob Sie sie mit einer IBC lösen; ein Interface mag eine natürlichere Ausdrucksform dafür sein. Allerdings verlieren Sie dann den recht einfachen Migrationspfad zur Asynchronizität. Ich ich vermute, dass es dann auch nicht mehr so einfach ist, den Austausch über ein Protokoll zu regeln.

Fazit

Für mich fühlt sich die Einführung von Channels in die EBC-Kommunikation stimmig an. Sie schließt die bisherigen Events nicht aus und führt die Eventhandler ein Stück aus ihren Beschränkungen. Indem Channels Methoden anbieten, ermöglichen Sie auf der Basis einer erweiterten Regelmäßigkeit deutlich mehr Flexibilität für die Gestaltung der Kommunikation.

Channels erweitern das EBC-Paradigma ganz natürlich.

Donnerstag, 15. April 2010

Gemeine Kenntnislücken

In der dotnetpro 5/2010 habe ich in meiner Sandbox-Kolumne beklagt, dass viele .NET-Entwickler heute nicht vertraut sind mit Lambda-Funktionen oder der Funktionsweise von yield return (C# Iteratoren). Daraufhin bekam ich folgende Zuschrift eines Lesers:

“[I]n deiner neusten Sandbox ziehst du ja ganz schön über uns Entwickler her! Ich bin der Meinung, dass das ziemlich gemein ist.”

Das hat mich schon ein wenig getroffen. Denn “gemein” möchte ich ja nicht sein. Gemein findet der Leser es, dass ich die Unkenntnis von Jahre alten und grundlegenden Features des Hauptwerkzeugs von .NET-Entwicklern kritisiere, denn…

“Wenn ich meine tägliche Arbeit als Entwickler reflektiere, bin ich der Meinung das [m]an fast keine Change hat sich in alle neuen Technologien einzuarbeiten. Der tägliche Projektablauf ist halt mehr auf Codegenerierung ausgelegt. Es gibt halt von Seiten des Managements oft keine Toleranz für Weiterbildung oder den Einsatz von neuen Technologien.”

Leider liegt hier ein Missverständnis vor. Ich habe eben nicht gesagt, dass “man” (also jeder Entwickler) sich in “alle neuen Technologien” einarbeiten muss. Ich habe vielmehr geschrieben: “Zugegeben, niemand muss alles beherrschen. Und niemand kann alles beherrschen.” Und selbstverständlich habe ich Verständnis dafür, wenn im Projektdauerstress Veränderungen an Konzepten und Technologien nicht sofort nachvollzogen werden. Das kennen wir ja auch alles schon. Ja, ich weiß, lieber Leser:

“Es gibt halt von Seiten des Managements oft keine Toleranz für Weiterbildung oder den Einsatz von neuen Technologien.”

Deshalb habe ich ja eben auch nicht über die neueste Mode gesprochen und schon gar nicht über VS2010/.NET 4. Es ging mir um Features, die Jahre (!) alt sind. Es ging mir um Features des Haupt(!)werkzeugs. Nix Esoterisches.

Eine Analogie mag helfen: Wer einen Klempner trifft, in dessen übervollen Werkzeugkasten eine Bohrmaschine liegt, dessen Knöpfe er nicht erklären kann, der wird sicherlich Zweifel an der Professionalität des Klempners haben. Eine Bohrmaschine gehört zu dessen wichtigsten Werkzeugen (nehme ich an ;-). Die muss er auf dem Effeff beherrschen. Ist es dann gemein, wenn diese Unkenntnis hervorhebt und beklagt?

Nein. Natürlich ist das nicht gemein. Ich kann alles mögliche Verständnis dafür haben, warum der Klempner in den vergangenen 5 Jahren keine Zeit hatte, die Bohrmaschinenknöpfe kennenzulernen. Immer soviel zu tun. (Oder eher sowenig?) Ja, ja, schon schlimm die Auftragslage.

Doch das ändert nichts daran, dass er sein Werkzeug nicht beherrscht. Und damit frage ich mich, ob er für mich einen guten Job machen kann. Kann er die Löcher in bester Weise bohren, wenn er die Schlagbohrfunktion nicht kennt? Hm… wahrscheinlich nicht immer.

Jetzt zurück zur Softwareentwicklung. Hier ein Beispiel für die Arbeit eines Entwicklers, der nicht firm in der Verwendung von Lambda-Funktionen ist:

image

Ist das gut lesbar? Kaum.

Hier der Code lesbarer gemacht:

image

Und jetzt überlege man einmal, wie viel es ein Unternehmen jedes Mal kostet, wenn Entwickler Änderungen an schwer lesbarem Code vornehmen sollen. Dauert das 5% länger oder 10% oder gar 50%? In jedem Fall dauert es jedes, jedes Mal messbar länger.

Natürlich kostet es auch Zeit, sich mit Lambda-Funktionen auseinander zu setzen. Vielleicht 1-2 Tage. Das ist allerdings Zeit, die nur einmal anfällt. Danach stehen die Kenntnisse zur Verfügung und können helfen, Code z.B. lesbarer zu machen.

Stehen Sie nicht zur Verfügung, dann ist es ein Fass ohne Boden, wie oft Mehraufwand zu treiben ist. Das misst zwar keiner – aber es ist viel, viel teurer. Und es wird beklagt. Keine Sorge. “Warum dauert das schon wieder so lange mit dem neuen Feature?” ist (allermeistens) eine indirekte Klage darüber, dass Code unstrukturiert oder unlesbar ist. Dem, der klagt, fehlt zwar das Verständnis für den Kausalzusammenhang. Aber die Ursache ist dennoch dieselbe: mangelnde Werkzeugkenntnis. Ist das gemein? Ja, ich würde sagen, das ist gemein. Denn der, der klagt, ist wahrscheinlich gleichzeitig der (Mit)Verursacher der Unkenntnis.

Und nochmal: Ich rede nicht über exotische Features der Workflow Foundation. Ich rede über “Brot und Butter” Features des Hauptwerkzeuges jedes Programmierers: seine Programmiersprache. Man werfe mir also bitte nicht Gemeinheit vor, wenn ich darüber (schon wieder) den Kopf schüttele.

Eigentlich kann es mir ja sogar egal sein. Wenn ein Entwickler 5 Jahre oder mehr

“[…] Erkenntnisse  nicht in der Praxis einsetzen kann/darf hat man” und so “ irgendwann nur noch ein "Halbwissen"” hat,

dann ist es ja nicht mein Problem, wenn der Code schwerer als nötig lesbar ist oder er einen Job wie diesen nicht annehmen könnte:

image

Denn wenn dort schon TDD und Scrum im Titel stehen, dann wird man erwarten, dass der Bewerber sein Hauptwerkzeug selbstverständlich kennt. Denn TDD ohne Lambda-Funktionen ist kaum denkbar, wenn man moderne Mock-Frameworks einsetzen will.

Wer “irgendwann nur noch ein Halbwissen” hat und als Grund die mangelnde “Toleranz für Weiterbildung” beim Management anführt, der begibt sich in gefährliche Abhängigkeit. Denn wenn sich das Management in ähnlicher Weisheit wie in Bezug auf die Fortbildung entschließt, es einmal ohne den halbwissenden Entwickler zu probieren, dann wird der nächste Job kaum besser.

Gegen das Halbwissen hilft kein Händeringen und kein hoffender Blick zum Management. Halbwissen ist – das klingt hart, ist aber deshalb nicht minder wahr – im Wesentlichen selbstverschuldet. Heute noch mehr als gestern. Denn die Zeiten der verlässlichen Institutionen, die für uns die Lebensplanung übernehmen, ist vorbei. Unwiederbringlich. Verlässlichkeit und weise, gütige, wissende Lehrer, Beamte, Manager, Firmenpatriarchen oder was auch immer finden sich eher nur noch im Geschichtsbuch.

Die wahre Gemeinheit besteht also in den Kenntnislücken, die Entwicklern nicht nur das Entwickeln schwerer als nötig machen, sondern ihnen auch Freiheitsgrade bei der Gestaltung ihres Lebens nehmen.

Was dagegen tun? Am Ball bleiben. Braucht es dafür immer Freizeit? Nein. Einfach unter der Woche in der Arbeitszeit jeden Tag 30 Minuten hinsetzen und lesen. Oder etwas in Visual Studio ausprobieren. Wer soll denn da kommen und das verbieten? Der unverständige Manager? “Hey, was programmieren Sie denn da? Gehört das zum Feature X?” Und selbst wenn er es fragt, dann lautet die Antwort eben “Ja, das gehört dazu. Ich muss etwas ausprobieren.” Oder die Antwort auf einen Kommentar zum Lesen währen der Arbeit: “Ich muss etwas nachlesen, damit ich die Aufgabe besser erfülle.”

In welchem Land leben wir denn, wo nicht nur private Emails während der Arbeitszeit untersagt werden, sondern auch noch die Fortbildung? Wie viel Mikrokontrolle will denn Management ausüben?

Also, lieber Leser, es hilft nichts. Die Gemeinheit liegt nicht bei mir. Einen Missstand hervor zu heben, ist nicht gemein. Gemein ist der, der ihn herstellt – vor allem, wenn er womöglich weiß, dass er “das Personal dumm hält.”

16 Stunden in stickiger Luft und mit krummem Rücken müssen wir nicht mehr arbeiten. Gut so. Aber in einer der schnellsten Branchen zu arbeiten ohne Chance, sich durch kontinuierlichen Wissenserwerb unabhängig von einem Arbeitgeber zu halten, ist kaum besser. Das physische Leid ist vergangen. Präsent ist die aber anscheinend noch die existenzielle Abhängigkeit. Und das in einer Zeit, wo der, von dem man sich abhängig macht, überhaupt keine Garantie mehr geben kann, dass er für den Abhängigen auch angemessen sorgen kann.

Davon, dass der sinkende Wissenspegel (im Verhältnis zum Relevanten) nicht nur dem Entwickler, sondern sogar dem ignoranten Management schadet, mal ganz zu schweigen. Wo z.B. 19.000 Zeilen Code unwidersprochen in einer Klasse stehen können – ja, das habe ich neulich gesehen –, da kann mir niemand erzählen, dass irgendeinen Vorteile hätte. Das hat nur Nachteile, die viel, viel Geld kosten. Und das, weil der, der den Code geschrieben hat, mit “Halbwissen” zufrieden war. Das ist schlicht unprofessionell – und nicht nur auf Seiten des Entwicklers. Da hat das Management auch seine “Aufsichtspflicht” verletzt.

Und jetzt? Ran an den Ball. VS2010 und .NET 4 sind draußen. Auch ich muss mir das für mich Relevante jetzt drauf schaffen. Manchmal würde ich lieber mit dem Altbekannten weiter mokeln. Doch ich bin ja auch Teil der Branche und kann mich daher nicht vom Lernen ausnehmen. Wir sitzen alle im selben Boot.

Schaffe ich das mit dem Lernen übrigens komplett in dem, was ein Angestellter als seine Arbeitszeit ansehen würde? Nein. Aber es hilft nichts. Wenn ich gerade als Freiberufler weiter gefragt sein will, dann darf ich da nicht so genau sein. Zum Glück macht mir das Lernen allerdings Spaß. Ich fühle mich in dieser Branche gut aufgehoben.

PS: Bevor ich´s vergesse: Als Kritiker des allgemein nicht so rosigen Standes der Branche stehe ich nicht allein. Man lese mal hier bei SEMAT:

Software engineering is gravely hampered today by immature practices.

“Immature practices” – darum geht es. Das bezieht sich genauso auf das (oft abwesende) Vorgehensmodell wie auf die Abwesenheit von Fortbildungsplanung wie auf die Unkenntnis von zentralen Konzepten der Informatik (denn nichts anderes sind Iteratoren und Lambda-Funktionen), die präsent sind im Hauptwerkzeug eines Entwicklers.

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.

Sonntag, 4. April 2010

Geschichtet – Event-Based Components abstrahieren und kontrollieren

Das übliche Schichtenmodell der Architektur packt Code bildlich übereinander. Aber was bedeutet das? Sind damit Abstraktionsebenen gemeint wie beim OSI-Schichtenmodell? Oder ist damit eine Kontrollhierarchie gemeint, bei der das Presentation Layer das Business Logic Layer kontrolliert usw.? Eher wohl letzteres, denn das Presentation Layer ist abhängig vom Busines Logic Layer.

Vom Abstraktionsgrad sind im Schichtenmodell daher alle Schichten gleich. Ein Presentation Layer verbirgt nichts, macht nichts einfacher verständlich. Eine so strukturierte Anwendung verstehen Sie nur im Durchstich von der obersten bis zur untersten Schicht.

Das Schichtenmodell hat sich entschieden: Es steht für eine Schichtung entlang der Dimension “Kontrolle”. Mit ihm können Sie Abhängigkeiten innerhalb einer Ebene hübsch ausrichten. Das ist auch sehr wichtig, wenn Sie mit Injection-Based Components (IBC), d.h. “traditionellen” Komponenten arbeiten.

Mit Event-Based Components hingegen ist das anders. Damit können Sie ebenfalls Schichtenarchitekturen definieren – aber Sie können das in zwei Dimensionen. Indem Sie Platinen (boards) definieren und aufeinander stecken, beschreiben Sie ein Softwaresystem auf unterschiedlichen Abstraktionsniveaus:

image

Innerhalb einer Platine können Sie dann aber auch noch die üblichen Schichten bilden:

image

Beide Sichtweisen manifestieren sich mit EBCs natürlich im Code. Platinen dienen der Definition von Abstraktionsniveaus und die Verdrahtung auf einem Board sagt etwas über die Kontrollhierarchie innerhalb eines Boards aus.

EBCs trennen damit deutlich, was im Schichtenmodell oder allgemeiner im IBC-Paradigma vermischt ist: Abstraktion und Kontrolle. Abstraktion hat etwas mit Überblick zu tun. Kontrolle mit Einblick. Bei der Abstraktion geht es um den Betrachter, bei den Abhängigkeiten “den Prozessor”.

Damit sind wir beim nächsten Problem von Abhängigkeitsdiagrammen. Wenn es denn schon dabei um “den Prozessor” bzw. einen Prozess geht, dann wird der nicht verständlich dargestellt. Denn Abhängigkeiten sind statisch, da rührt sich nichts mehr. Ein Prozess jedoch “lebt”, da ist was in Bewegung. Daten wollen verarbeitet werden.

Auf einer Platine eines EBC-Diagramms geht es nun genau darum: um den Prozess. Die Verdrahtung von EBCs gibt an, wie Daten fließen. Abhängigkeiten existieren dafür zwischen den EBCs nicht. Nur der Prozess, d.h. die Platine ist von ihren Komponenten abhängig. Doch das ist keine Linie wert; diese Abhängigkeit ist natürlich und implizit. Im Vordergrund steht also der Prozess.

Gleichzeitig dienen EBC-Diagramme dem Betrachter bzw. Planer, weil sie ihm erlauben, ein System auf dem Level zu betrachten, das ihm gerade am meisten dient. Er bewegt sich mit einem frei gewählten Maßstab durch die Diagramme als Landkarten der Applikation. Die Regeln sind dabei – wie könnte es anders sein – auf jeder “Zoomstufe” gleich. Immer sind Komponenten mit Komponenten im Sinne eines Prozesses verdrahtet. Mal ist der Prozess sehr grob beschrieben, mal ist ein kleiner Ausschnitt des Prozesses sehr detailreich dargestellt.

Freitag, 2. April 2010

Gedanken zu Gedanken zum TDD

Nach einer TDD-Demonstration bei der NUG Franken hat sich eine lebhafte Diskussion um einen kritischen Blog-Artikel von René Drescher-Hackel entsponnen. Das ist ein schöner Effekt der Demonstration und gut fürs Thema.

Zu einigen Punkten finde ich das Diskussionsergebnis allerdings noch unbefriedigend.

Darf´s ein bisschen mehr sein als Visual Studio? Aber sicher doch!

René hat sich über den scheinbaren Zwang von ReSharper mokiert. Ich stehe da auf einem anderen Standpunkt und finde nichts dabei, Unternehmen den Kauf von Tools zu empfehlen, auch wenn das Investitionen von 1.000, 5.000 oder gar 10.000 EUR bedeuten sollte. Unsere Branche ist so privilegiert und deshalb verwöhnt, was die Werkzeugkosten angeht im Vergleich zu Tischlern, Klempnern, Kirmesschaustellern, Ärzten usw., dass ich über solche Beträge nicht zweimal nachdenke.

Einen Entwickler ein Jahr lang jeden Arbeitstag auch nur 15 Minuten länger an einer Aufgabe sitzen zu lassen als nötig, ist so teuer, dass ein Tool wie ReSharper oder TestDriven.Net usw. sich schnell bezahlt macht. (Beispielrechnung: 60 EUR kostet der Entwickler/Stunde, er arbeitet 200 Tage im Jahr, dann entstehen 15 EUR*200=3.000 EUR Verlust pro Jahr dadurch, dass ihm ein Tool oder auch nur Kenntnise fehlen, die seine Arbeit um nur ca. 3% beschleunigen.)

René sagt:

Viele Unternehmen, die Software entwickeln sind Microsoft-Partner. Was dies wirtschaftlich für diese Unternehmen bedeutet, brauche ich hier nicht zu erklären. Du kannst aber nicht in einem Unternehmen auflaufen und TDD propagieren und gleichzeitig sagen, "wenn wir gescheit arbeiten wollen, dann brauchen wir...". Die Grunderwartung ist: was können wir mit den gegebenen Möglichkeiten tun?

image Das kann ich nachvollziehen. Das ist eine “Grunderwartung” vieler Programmierbuden. Aber nur weil die Erwartung so ist, ist sie nicht “richtig” oder realistisch. Kinder haben auch viele Erwartungen, was die Weihnachtsgeschenke angeht – leider sind viele davon auch nicht realistisch.

Softwareentwicklung ist eben keine Sache mehr, die man mit Notepad und einem Kommandozeilencompiler bewältigen könnte. Das mag 1984 noch gegangen sein – aber heute nicht mehr. Der Arzt hat heute nicht mehr nur ein Messer oder einen Haken, um einen grauen Star zu stechen. Sein Gerät ist deutlich umfangreicher und teurer. Damit musste sich die Ärztezunft auch abfinden. Wir als Patienten freuen uns darüber und wollen es nicht mehr anders.

Wer meint, mit Visual Studio allein professionelle Softwareentwicklung betreiben zu können, der ist schon lange auf dem Holzweg. Sich auf Microsoft und Visual Studio zu verlassen, ist bequem. Es macht die Toolkosten planbar, die vor allem in einer MSDN Subscription bestehen oder in einem Update alle 2-3 Jahre. Doch seit wann lassen sich Fachleute von Controllern vorschreiben, was ihre Werkzeuge sein und kosten sollten?

Für mich ist also das, was René zu ReSharper gesagt hat viel weitgreifender. ReSharper war nur ein Stein des Anstoßes. Der Punkt ist jedoch allgemeiner. Es besteht eine merkwürdige Vorstellung davon, wie Produktivität in der Softwareentwicklung entsteht. Aber ich schweife ab… ;-)

Apokalyptischer Reiter Resignation

René sagt:

Dass der Softwareentwickler die Tests für sich im Kleinen macht, ist eher Wunschdenken bzw. der Idealfall. In den Firmen/Projekten, wo ich bislang tätig war, war dafür kein Platz. Und glaube mir Thomas, irgendwann gewöhnst du es dir ab, mit deinem Projektleiter/Vorgesetzen darüber zu diskutieren - so ist das ebend.

Für das, was richtig ist, ist kein Platz. Und wenn das so ist, dann nimmt man es hin.

Verständlich mag das sein. Aber traurig ist es, nein, tragisch und vor allem kontraproduktiv. Niemand hat etwas davon – außer demjenigen, der sich in die Resignation zurückzieht. Und der auch nur kurzzeitig, denn Resignation führt auf Dauer zu Schlimmerem.

Ich halte Resignation für einen Apokalyptischen Reiter für Softwareprojekte. Wo Resignation aufkeimt, erblühen und sich verbreiten kann, da scheitern Projekte. Resignation ist das Gegenteil von Motivation.

Was kann man dagegen tun?

Masterfrage: Braucht TDD (oder eine andere Veränderung) “Platz”? Ich würde sagen, nein. Oft braucht eine Veränderung keinen “Platz” im Sinne von “Darüber müssen wir alle einig sein” oder “Dafür muss extra Zeit veranschlagt werden”. Viele Veränderungen können nämlich einfach so vom Einzelnen vorgenommen werden. Man tut schlicht etwas anderes als bisher.

Ob ich nach einer Besprechung sofort an die Tastatur hechte und den Code-Colt ziehe oder neuerdings erstmal mit einem Block auf dem Tisch nachdenke… darüber muss ich mit niemandem reden.

Ob ich mit dem Code-Colt sofort aufs Ziel schieße oder erstmal einen Spike mache oder einen Test schreibe… darüber muss ich mit niemandem reden.

Ob ich die Pfadfinderregel befolge, meine Klassen nach SRP, SoC, LSP usw. anders schneide als früher… darüber muss ich mit niemandem reden.

image Wer in seiner persönlichen Begeisterung gleich andere zu Proselyten machen will, der darf sich nicht wundern, wenn er auf Gegenwehr stößt. Resignation ist dann jedoch nicht die einzig mögliche Reaktion. Man kann zunächst auch einfach den Missionsdrang runterfahren und soviel wie möglich ohne Übereinstimmung mit anderen tun. Einfach unter dem Radar der Projektleitung fliegen.

Und wenn dann einer fragt, “Solltest du nicht lieber mal programmieren, statt wieder über deinem Block zu hocken?”, dann kann man ruhig auch mal die Vertrauensfrage stellen: “Vertrauen Sie mir nicht, wenn ich erst nachdenke und dann programmiere? Ist meine Leistung schlechter geworden?” (Und wenn darauf unwahrscheinlicherweise die Antwort “Ja!” kommt, dann kann man auch mal um die Messdaten bitten, die das belegen. )

Darüber, was es bedeutet, wenn man immer wieder gegen Wände läuft mit Vorschlägen für Veränderungen, will ich mich hier nicht weiter auslassen. Jeder muss für sich selbst entscheiden, ob das ein lohnenswertes Arbeitsumfeld ist, immer wieder in seiner Motivation, Vorgehen und Ergebnisse zu verbessern, frustriert zu werden. Ob längerfristige oder gar zunehmende Resignation gesundheitszuträglich sind, sollte man dann mal einen Arzt oder Apotheker fragen ;-)

Besser wird es schon, wenn man bei sich beginnt

René sagt:

Ich bin überzeugt davon, dass wenn du testgetrieben entwickeln willst, es nur wirklich sinnvoll ist, wenn im Team alle mitziehen - andernfalls sehe ich es als Kampf gegen Windmühlen. Die gleiche Situation hast mit den Fragen zum CCD.

Warum ist testgetriebene Entwicklung nur sinnvoll, wenn alle im Team mitziehen? Natürlich wäre das besser für das Produkt, weil dann mehr Code so entwickelt würde. Aber für mich als Entwickler ist das nicht wichtig. Ich kann meine Vorteile aus TDD schon ziehen, wenn ich allein damit anfange. Und der Kunde hat auch schon etwas davon, wenn nur ich so arbeite. Denn dann ist zumindest mein Code von höherer Qualität.

Darauf zu warten, dass eine Veränderung erst allgemein akzeptiert ist, um sie selbst vorzunehmen, ist frustrierend und unnötig. Für manche ist es nicht anders möglich, z.B. die Einführung einer verbindlichen Versionskontrolle für alle oder eine Continuous Integration. TDD gehört jedoch nicht in diese Gruppe. Wer etwas anderes behauptet betreibt Prokrastination durch “Schuldverschiebung”: “Die anderen sind Schuld, dass ich nicht nach TDD arbeiten kann.”

TDD kostet kein Geld, da kein Tool gekauft werden muss. NUnit ist gratis und reicht aus. TDD braucht keinen Konsens, da ich an den Codeteilen, die ich bearbeitet, nach TDD vorgehen kann.

Wenn andere sich darüber lustig machen, dann ist das deren Problem.

Entwickler sind keine Arbeitsbienen

René sagt:

Test sollten immer vom Architekten der vorgegeben werden. Der Entwickler (die Arbeitsbine also ;-) ) setzt die Software anhand des Tests um. So ist letztlich sichergestellt, dass die Software so funktioniert, wie der Architekt die Anforderung auch umgesetzt hat. Das ist meines Erachtens auch der tiefere Sinn der TDD.

Oha, das ist starker Tobak. Und das mir als Nichtraucher… ;-) Architekten sollen Tests vorgeben? Entwickler sind Arbeitsbienen?

Architekten sollen zwar die Nützlichkeit von Software sicherstellen worin ihre Korrektheit eingeschlossen ist. Doch dass Architekten deshalb Tests vorgeben sollten… nein, das halte ich für eine irrige Vorstellung.

image Architekten sollen Entwickler und/oder Kunde helfen, angemessene Testszenarien zu formulieren. Sie sollen dem Kunden klar machen, wie wichtig es ist, dass er klare Vorstellungen von Korrektheit entwickelt und dokumentiert. Am besten in Form maschinenlesbarer Dokumente, die Testläufe treiben. Tools wie Fitness machen das möglich.

Und Architekten sollen den Entwickler im Rahmen eines expliziten Entwicklungsprozesses immer wieder über sein Verständnis der Anforderungen reflektieren lassen, aus dem heraus der dann weitere Testfälle formulieren kann. Dazu gehört auch, dass der Architekt TDD im Team einführt, falls noch nicht etabliert.

Doch Tests definiert der Architekt selbst nicht. Allerdings kann er vertreten, dass ausdrückliche semantische Kontrakte für bestimmte Funktionseinheiten formuliert werden, insbesondere wenn deren Entwicklung outsourcet ist.

Der “tiefere Sinn” von TDD ist eben nicht, dass Tests von irgendwoher kommen und der Entwickler nur krampfhaft versucht, die auf grün zu bringen. Nein, dem Entwickler dienen TDD-Tests über eine Korrektheitsprüfung hinaus zu…

  • Ermittlung von Schwachstellen im Verständnis von Anforderungen und der Planung von Code
  • angemessener Strukturierung nach SLA und SRP
  • hoher Testabdeckung
  • Dokumentation

TDD ist ein zentrales und unverzichtbares Entwicklungs- und Denkwerkzeug.

Und jetzt noch zur “Arbeitsbiene”. Das hat René nicht ganz ernst gemeint, aber ein wenig schon. Sonst hätte er nicht die Grenze zwischen Architekt und Entwickler so gezogen.

Dass solche Titulierung nicht zum Selbstbild von uns Softwareentwicklern gehört, ist klar. Ein solches Fremdbild mag es jedoch hier und da geben. “Ach, die klopfen doch nur Code ein.” Vielleicht ist es in manchen Projekten auch so. Und für Anfänger sind die Vorgaben sicherlich auch enger als für erfahrene Entwickler.

Am Ende könnte das Bild der “Arbeitsbiene”, d.h. eines Entwicklers, der in engen Vorgaben nur noch “runtercodiert”, nicht weiter von der Notwendigkeit entfernt sein. Entwickler dürfen keine “Arbeitsbienen” sein. Denn sobald sie das werden, bricht Software unter der Komplexitätslast zusammen.

Software ist so komplex, dass sie nicht einer vordenken kann. Weder im Hinblick auf die Umsetzung von Anforderungen, noch in Bezug auf Lösungsdetails in Algorithmen und Technologieeinsatz.

Softwareentwicklung braucht kooperative autonome Entwickler. Keine “Bienenköniginnen” oder gar “Drohnen” und keine “Arbeitsbienen”. Softwareentwickler sollten vielmehr innerhalb einem recht weiten architektonischen Rahmens viel Freiheit besitzen. Sie müssen fähig sein, lokale Probleme kreativ zu lösen. Und sie müssen fähig sein zu kommunizieren, was sie allein nicht lösen können, um es gemeinsam auf höherer Ebene zu lösen.

Donnerstag, 1. April 2010

Holarchie – Event-Based Components für natürliche Softwaremodellierung

Software ist eine Holarchie, d.h. eine Hierarchie von Teilen, die gleichzeitig Ganzes sind. Das hatte ich während der Modellierung von Software mit “traditionellen” Komponenten schon fast wieder vergessen. Aber neulich schrieb mich jemand zu meiner Blogserie über das Softwareuniversum an, weil er Parallelen zu seiner Arbeit sah. Da habe dann versucht, meinen derzeitigen Ansatz der Event-Based Components (EBC) die damaligen Gedanken anzuschließen.

So sah 2005 eines meiner Diagramme für die grundsätzliche Struktur von Software aus:

image

Die Frage heute: Bin ich diesem “Ideal” nun mit EBCs ein Stück näher gekommen?  Ich will die Antwort anhand eines Beispiels versuchen, für das ich zwei Architekturen entwickle. Eine mit “traditionellen” Komponenten, eine mit EBCs.

Das Szenario ist simpel: Der Kunde wünscht sich einen Funktionsplotter. Er möchte also ein kleines Programm haben, in das er eine Formel eingeben kann – z.B. x^2+2x-3 oder Sqrt(Sin(x)*Cos(x)) –, für die dann ein Graph am Bildschirm geplottet wird.

Traditionelle Komponentenarchitektur

Wie hätte ich eine Architektur für den Funktionsplotter vor einigen Monaten geplant? Ich kann mich kaum erinnern ;-) EBCs haben sich schon so in meinem Denken breit gemacht, dass ich kaum noch ohne kann. Aber ich versuche es einmal.

Am Anfang stünde eine Softwarezellen bzw. ein System-Umwelt-Diagramm:

image

Eine Anwenderrolle greift auf die ganze Anwendung zu. Der Anwender ist abhängig von der Anwendung. Das System-Umwelt-Diagramm ist also ein Abhängigkeitsdiagramm.

Im nächsten Schritt würde ich die Softwarezelle zerlegen in Funktionseineiten. Nach dem Separation of Concerns Prinzip fielen mir da mindestens zwei ein: eine Funktionseinheit für die Interaktion mit der Benutzerrolle, ein Portal, und eine für den Rest, d.h. die Domänenlogik.

image

Bei genauerem Nachdenken würde ich dann natürlich das Portal weiter zerlegen. Eine eigene Funktionseinheit für das Plotten schiene mir angezeigt. Und auch die Logik zerfiele in mindestens zwei Funktionseinheiten:

image

Sollte ich es dabei bewenden lassen? Im Frontend ja. Aber die Logik… Nein, die ist noch zu grob. Ein Formelübersetzer ist – so nehme ich mal für dieses Beispiel an – keine so einfache Sache. Die will sauber in drei Pässe gegliedert sein. Es geht ja um nicht weniger als die Übersetzung von Quellcode (Formel) in Code (ausführbare Funktion). Da soll auch die kleine Plotteranwendung in puncto Systematik nicht hinter einem richtigen Compiler zurückstehen.

Und auch die vorgeschaltete Funktionseinheit scheint nicht sauber. Sie hat noch ein “und” in der Bezeichnung, sie tut also noch zweierlei. Zum einen veranlasst sie etwas (Übersetzung), zum anderen tut sie etwas (Funktionswerte berechnen). Das ist im Sinne des Single Responsibility Principle nicht schön.

Wenn ich diese Gedanken in die Architektur einfließen ließe, würde sie sich so weiter entwickeln:

image

Zwei Funktionseinheiten stechen darin heraus, das Rechenwerk und der Compiler. Beide haben eigentlich keine richtige Aufgabe im Sinne der Geschäftslogik. Sie sind nur Fassaden, die Details rechts von ihnen gegenüber ihren Clients verbergen. Allenfalls koordinieren sie noch die Arbeit der Funktionseinheiten, von denen sie abhängig sind. Das Rechenwerk lässt erst den Compiler die Formel übersetzen und übergibt sie dann an die Funktionswertberechnung. Der Compiler fordert zuerst den Scanner auf, die Formel in Symbole zu zerlegen. Die reicht er weiter an den Parser. Der erzeugt einen abstrakten Syntaxbaum, den der Codegenerator übersetzt.

Im Rahmen “traditioneller” Komponentenorientierung ist das völlig normal. Auf Rechenwerk und Compiler zu verzichten, würde nämlich zu ganz unschönen Abhängigkeiten führen, z.B.

image

Rechenwerk und Funktionswertberechnung hingen nun zusammen und würden nur den Parser ansteuern. Der lieferte ultimativ auch die übersetzte Funktion – aber natürlich nur unter Zuhilfenahme des Parsers, der wiederum den Codegenerator brauchte.

Nein, das sieht ungut aus. Warum soll ein Scanner den Parser kennen und am Ende sogar etwas mit dem generierten Code zu tun haben? Und warum sollte eine Funktionsberechnung einen Scanner kennen? Der ist ein Detail einer bestimmten Compilerimplementation.

Die Funktionseinheiten Rechenwerk und Compiler wären nötig in einer “traditionellen” Komponentenorientierung.

Und wo sind die Komponenten nun? Ich würde sagen, alle Funktionseinheiten verdienen es, als Komponenten realisiert zu werden. Z.B. Compiler, Scanner, Parser und Codegenerator in einer Komponente zusammen zu fassen, würde die Möglichkeit zur Parallelentwicklung begrenzen. Denn neben klaren Grenzen zwischen Funktionseinheiten geht es darum bei der Komponentenorientierung: hohe Produktivität, durch gleichzeitige Arbeit an Funktionseinheiten. Rechenwerk und Compiler sind dann zwar recht simple Komponenten, weil sie nur andere integrieren. Doch das macht nichts. Wenn sich z.B. der Ansatz zur Compilation ändert, dann kann wieder mehr Verantwortung in den Compiler wandern. Ihn als Verantwortlichkeit ausdrücklich zu definieren, trägt zur Evolvierbarkeit bei.

Fazit “traditionelle” Komponentenarchitektur

Was gibt es hervorzuheben an einer “traditionellen” komponentenorientierten Architektur? Was gibt es womöglich zu kritisieren? Das ist gar nicht so leicht zu sagen, wenn man tief drin steckt in dem Denken. Denn dann ist vieles ganz natürlich – was eigentlich Ballast ist. Und was fehlt, erkennt man nicht so genau.

In meinem ersten Posting zu EBCs habe ich fünf Aspekte “traditioneller” Komponenten genannt, mit denen ich mich schwer tue. Für das obige Beispiel möchte ich drei davon herausgreifen:

  • Komponenten haben Abhängigkeiten. Abhängigkeiten machen das Entwicklerleben schwer. Wo Abhängigkeiten ins Spiel kommen, wird allemal das Testen schwieriger.
  • Spezifikationen sind nicht kompakt. Die Spezifikation des Compilers besteht z.B. aus seinem eigenen exportierten Kontrakt sowie den importierten Kontrakten der drei Komponenten, von denen er abhängig ist.
  • Die Schachtelung von Komponenten ist schwierig oder gar unmöglich.

Insbesondere die letzten beiden Punkte machen mich bei der obigen Architektur unglücklich. Ich finde es sehr unhandlich, einen Compiler nur implementieren zu können, wenn ich insgesamt vier Kontrakte zur Hand habe. Und warum sollte eine Client-Komponente den ganzen Kontrakt einer Service-Komponente kennen, wenn sie selbst davon nur einen Ausschnitt selbst braucht? Wer ist schon so konsequent und betreibt Interface Segregation, um jedem “Kunden” einer Service-Komponente nur den für ihn passenden Ausschnitt zur Verfügung zu stellen?

Viel schlimmer aber noch ist, dass die finale Architektur nur noch eine Abstraktionsebene im Code enthält. Die Komponenten liegen alle auf demselben Niveau. Ein Betrachter des Codes hat damit keine Möglichkeit, unterschiedliche Betrachtungsabstände einzunehmen, um sich Überblick zu verschaffen oder Details zu betrachten.

Zwar habe ich mich in mehreren Schritten an diese “Blattebene” herangeschlichen. Doch die Funktionseinheiten auf darüber liegenden Abstraktionsniveaus sind nicht mehr sichtbar. Sie existieren nur noch auf einem Blatt Papier – und das ist bekanntlich geduldig.

Aber vielleicht sehen Sie es auch anders und meinen, die Architektur würde immer noch mehrere Abstraktionsebenen enthalten. Scanner, Parser und Codegenerator gehören dann zur untersten Ebene, Compiler liegt darüber und mit der Funktionswertberechnung gleich auf. Darüber dann das Rechenwerk, darüber das GUI. Ja, so könnte man das sehen:

image

Aber ist das wirklich plausibel und hilfreich? Liegt ein GUI auf einem höheren Abstraktionsniveau als ein Compiler? Ist das Abstraktionsniveau von einem Aufgabenbereich oder gar Belang abhängig? Nein, nein, damit kann ich mich gar nicht anfreunden. Außerdem würde das bedeuten, dass Kommunikation nie zwischen Funktionseinheiten auf demselben Abstraktionsniveau abliefe, sondern immer über Niveaus hinweg: der Compiler auf Level n spräche mit dem Scanner auf Level n+1 usw.

image

Das fühlt sich gar nicht gut an.

Abhängigkeiten definieren einen Baum. Entlang der Äste verläuft die Kommunikation. Schwer verständliche Schachtelung ist damit das Fundament der “traditionellen” Komponentenorientierung. Grauslich! Da hilft dann auch keine Dependency Injection.

Das führt übrigens auch dazu, dass so sinnlose Komponenten wie der Compiler oder das Rechenwerk auftauchen. Die tun nichts anderes, als die Kommunikation zwischen anderen Komponenten zu ermöglichen. Vom Standpunkt der Domänenlogik aus gesehen ist das nicht nötig. Aber die vertikalen Abhängigkeiten erzwingen das. Scanner, Parser und Compiler liegen auf demselben Abstraktionsniveau – und kennen sich daher nicht. Widersinnig, oder? Und wenn sie sich kennen wie in der verworfenen Architektur, dann ist das auch nicht glücklich. Denn warum sollte ein Scanner von einem Parser abhängen? Oder von mir aus auch umgekehrt: Warum sollte ein Codegenerator von einem Parser abhängen und der von einem Scanner?

Ein Codegenerator leistet etwas auf einem Input: er verwandelt einen Abstrakten Syntaxbaum (AST) in lauffähigen Code. Diese Leistung hat nichts, aber auch gar nichts mit der Erzeugung eines solchen AST zu tun. Warum sollte also ein Codegenerator die anstoßen?

Also muss ein übergeordneter Compiler her. Der tut nichts anderes, als die Kommunikation zwischen Scanner, Parser und Codegenerator angemessen herzustellen. Aber er ist eine Komponente mit vielen Abhängigkeiten. Er ist vergleichsweise schwierig zu testen und dabei so gar nicht durch die Problemdomäne motiviert. Eine unbefriedigende Situation. Koordinationskomponenten verrauschen das Architekturbild.

Ein Abhängigkeitsdiagramm ist einfach nicht genug, um eine Architektur zu beschreiben. Es ist nötig, um die vielen Kontrakte klar zu kriegen und die Abhängigkeiten sauber zu definieren. Aber wie die Kommunikation läuft, ist daraus nicht ersichtlich. Dafür brauchen wir mindestens einen zweiten Diagrammtyp. Z.B. ein Aktivitätsdiagramm oder ein Datenflussdiagramm müssen die Lücke füllen.

Meine bottom line: Eine unbefriedigende Situation. “Traditionelle” Komponenten sind besser als gar keine Komponenten. Sie befördern die Produktivität, die Flexibilität und die Korrektheit. Doch es bleibt ein Nachgeschmack.

Event-Based Component Architektur

Wie kann es mit der Architektur besser werden als mit “traditionellen” Komponenten? Ich versuche dasselbe Szenario mal mit Event-Based Components (EBC) zu beschreiben. Am Anfang steht wieder ein Systen-Umwelt Diagramm:

image

Das sieht der “traditionellen” Komponentenorientierung noch sehr ähnlich. Allerdings: Beachten Sie, dass auch hier schon von Nachrichten gesprochen wird und keine Abhängigkeit vorhanden sind. Die Anwendung als ganzes wird als Komponente gesehen, die asynchron gegenüber ihrer Umgebung läuft.

Das System-Umwelt Diagramm ist die höchste Abstraktionsstufe. Es besteht ja nur aus einer Funktionseinheit. Diese Funktionseinheit ist natürlich zu groß, um sie einfach so zu implementieren. Also muss ich sie zerlegen.

image

Wenn ich die Applikation “aufmache”, dann finde ich zwei Funktionseinheiten: ein Frontend für die Interaktion mit dem Benutzer und ein Rechenwerk, dass die Formel im angegebenen Wertebereich berechnet. Das ist immer noch eine Ebene, die ein Laie versteht. Eine Benutzeroberfläche ist etwas anderes als die Funktionalität, die Formeln berechnet.

Im Hintergrund habe ich noch die darüber liegende Hierarchieebene “durchscheinen lassen”. Sie soll uns erinnern, dass die beiden Funktionseinheiten Verfeinerungen sind.

Jetzt zur nächsten Ebene hinab steigen:

image

Jede Funktionseinheit der vorherigen Ebene ist verfeinert worden, d.h. zerlegt in mehrere Funktionseinheiten mit spezifischeren Verantwortungen. Diese Subfunktionseinheiten sind den übergeordneten eingeschrieben. Funktionseinheiten können also Container sein. Nicht physische Container, sondern logische, sozusagen “Verantwortungscontainer”.

Das ist ein Aspekt, der mir wichtig geworden ist. Mit EBCs ist echte Schachtelung möglich. Aber das ist keine physische Schachtelung. Wie Implementierungen von Funktionseinheiten physisch geschachtelt sind, halte ich inzwischen für einen ganz anderen Concern. Darüber muss man nachdenken – aber es hat nichts mit der Funktionalität einer Anwendung zu tun. Wie Funktionseinheiten physisch versammelt und geschachtelt werden, ist eine Sache des Entwicklungsprozesses. Darauf wirken vor allem Fragen der Parallelität von Entwicklung, von Risiko, Unsicherheit oder Komplexität/Entwicklungsdauer ein.

Aber nun weiter mit der Modellierung. Das Frontend ist abgeschlossen, doch beim Rechenwerk muss der Compiler noch zerlegt werden:

image 

In dieser Abbildung sind nun alle Abstraktionsebenen ganz natürlich ineinander geschachtelt zu sehen. Je dunkler eine Komponente, desto niedriger ihr Abstraktionsniveau. Echte Domänenfunktionalität enthalten nur die Blätter, d.h. die Funktionseinheiten mit dem niedrigsten Abstraktionsniveau.

Der Schachtelungsbaum dazu sieht so aus:

image

Abhängigkeiten verlaufen in der Vertikalen. Sie sind im Architekturdiagramm implizit. Wenn Komponenten andere enthalten, dann sind sie von ihnen abhängig. Wichtiger ist jedoch, dass die Kommunikation hier horizontal verläuft. Das Architekturdiagramm legt den Schwerpunkt nicht auf Abhängigkeiten, sondern darauf, wie die Funktionseinheiten miteinander kommunizieren.

Wo bei “traditionellen” Komponenten zwei Diagramme nötig sind, reicht bei EBCs ein Diagramm.

Fazit Event-Based Component Architektur

Vergleichen Sie selbst die Diagramme für die “traditionelle” Komponentenarchitektur und die EBC-Architektur. Welches finden Sie aussagekräftiger, verständlicher?

Die Vorteile der EBC-Architektur scheinen mir auf der Hand zu liegen:

  • Der Fokus liegt hier ganz eindeutig auf den Blättern. In denen “spielt die Musik”, dort steckt die Domänenlogik, sie mögen komplex sein. Aber sie haben keine Abhängigkeiten. Deshalb sind sie gut zu testen. Und deshalb lassen sie sich auch vergleichsweise gut wiederverwenden.
  • Abhängig sind “Zwischenebenen” oder “Platinen”. Sie sind von den eingeschachtelten Funktionseinheiten abhängig. Aber das macht nichts. Diese Abhängigkeiten sind sehr einfach. “Platinen” lassen sich aus der Beschreibung der Verbindungen zwischen ihren “Bauteilen” generieren. Das macht ihre Tests sehr einfach.
  • Die Abhängigkeiten sind implizit. Sie verwirren nicht das Verständnis. Die wichtigeren Kommunikationswege stehen im Vordergrund. Die Kommunikation ist horizontal zwischen Funktionseinheiten innerhalb eines “Containers” auf derselben Abstraktionsebene.
  • Last but not least: Die Ebenen, die eine schrittweise Verfeinerung durchläuft, bleiben in der Architektur physisch, d.h. in Form von Artefakten (“Platinen”) erhalten. Das trägt sehr zum Verständnis der Architektur bei. Zoom-in/out ist nicht nur möglich, sondern bezieht sich auf realen Code und nicht nur “Ideen”.

EBC-Architekturen manifestieren für mich die konzeptionelle Holarchie des ersten Bildes oben. Software ist ein System mit beliebig vielen Ebenen. Auf jeder Ebene arbeiten beliebig viele Funktionseinheiten zusammen. Aus Input produzieren sie Output. Sie kennen einander nicht und sind ganz regelmäßig aufgebaut. Das erleichtert ihre Produktion (“Platinen” können generiert werden, Blätter werden parallel handcodiert). Das erleichtert die Komposition von Architektur mit ihnen. Und das ermöglicht den Einsatz von Standardkomponenten.

Nach meinem Empfinden sind wir mit EBCs der lang erprobten Arbeitsweise von Elektrotechnikern und Maschinenbauern so nah wie nie zuvor. Software ist natürlich immer noch anders als physische Bauteile; aber warum soll sie nicht von denen lernen?

Ich jedenfalls kann quasi schon gar nicht mehr anders denken als in EBCs. Und mit jeder Architektur, die ich so entwerfe, wird es einfacher.

Mit EBCs sehe ich Software als Sammlung von Prozessen. Einfachen und komplizierten. Die bestehen aus Verantwortlichkeiten, die zusammen etwas verarbeiten und produzieren. Diese Verantwortlichkeiten und ihre Kooperation beschreiben EBCs.

Objekte und “traditionelle” Komponenten werden dadurch nicht überflüssig. Sie bekommen aber einen anderen Platz. Auch das macht die Architektur einfacher.

Verfeinerung der EBC-Architektur

Zum Schluss noch ein Detail, über das Sie sich keine Gedanken gemacht haben mögen. In der EBC-Architektur ist noch eine kleine Unstimmigkeit, die ich der Einfachheit halber bisher überspielt habe. In das Rechenwerk kommt eine Berechnungsanforderung rein. Die besteht aus einer Formel und einem Wertebereich, auf den die Formel angewandt werden soll, z.B. Formel “2*x” und Wertebereich 0 bis 10 in 100 Schritten.

Durch die Blätter des Rechenwerks fließt bisher nur ein Datenstrom. Das bedeutet, jede Funktionseinheit muss den Wertebereich bis zur Funktionswertberechnung durch schleifen. Was aber soll z.B. ein Scanner mit diesem Wertebereich?

Die Architektur für das Rechenwerk sähe daher besser wie folgt aus:

image

Split und Join sind Standard-EBCs. Sie werden in den Nachrichtenstrom eingesetzt, um Nachrichten zu transformieren und damit passend für folgende Funktionseinheiten zu machen. Das erhöht die composability (Komponierbarkeit) von Architekturen. Funktionseinheiten können leichter wiederverwendet werden, wenn sie ihren Kontrakt nicht an ihre Umgebung anpassen müssen, sondern umgekehrt kleine Standardadapter die Umgebung an eine Komponente anpassen.

Hier noch zwei Ideen, wie solche Standardkomponenten helfen könnten:

image

Zunächst: Bemerken Sie, wie einfach es ist, die Flughöhe zu wechseln? Im vorhergehenden Bild waren wir im Rechenwerk. Jetzt ist das Rechenwerk eine Black Box; wir fliegen höher, sehen ein bigger picture. Das Abstraktionsniveau ist gestiegen.

Auf diesem Level habe ich nun Standardkomponenten eingefügt, die die Performance und Reaktionsfähigkeit des Systems steigern sollen. Ein Cache ist zwischen Frontend und Rechenwerk geschaltet, um zu vermeiden, dass Übersetzungen und Berechnungen, die schon gelaufen sind, nochmal zeitaufwändig ausgeführt werden. Für das Frontend macht dieser Einschub keinen Unterschied. Es ist ja unabhängig von anderen Komponenten. Und auch das Rechenwerk merk davon nichts. Das ist AOP at its best ;-)

Dasselbe gilt für die Asynchronizität. Das Rechenwerk läuft nun auf einem anderen Thread als das Frontend. Berechnungen finden dem gegenüber also im Hintergrund und asynchron statt. Der Benutzer kann im Frontend andere Funktionen aufrufen, während das Rechenwerk seine Arbeit leistet. Das Frontend friert nicht ein.

Falls im Hintergrund ein Fehler auftritt, wird der als eigene Nachricht auf dem Frontend-Thread zurück gemeldet. Und Ergebnisse kommen auch auf dessen Thread an, weil sie ein Synchronization Context Switch aus dem Hintergrund in den Vordergrund holt.

Fühlen Sie die Eleganz des EBC-Ansatzes? Keine der Komponenten kennt/braucht eine andere. Die Verbesserung der nicht funktionalen Eigenschaften des Systems waren deklarativ ohne Veränderung auch nur einer existierenden Komponente möglich.

Und falls sich herausstellt, dass die Kombination Cache+Async öfter nützlich ist, kann sie ganz einfach mit einer “Platine” für die Wiederverwendung zu einer neuen Komponente zusammengeschnürt werden:

image

Wieder würde sich für die anderen Komponenten nichts ändern. Nur die Verdrahtung wäre betroffen, d.h. die “Platine”, die die Komponenten verbindet. Mit einem Architekturtool wäre das aber wohl nur eine Kleinigkeit. Eine simple und typische Refaktorisierung, z.B. “Komponenten zu Platine zusammenfassen”.

Im Sinne des SLA-Prinzips würde ich dann jedoch noch einen Schritt weitergehen:

image

Jetzt ist wieder alles auf demselben Abstraktionsniveau: ein Frontend ist verbunden mit einem Rechenwerk. Wie das genau funktioniert, ist egal. Wen das interessiert, der zoomt in das asynchrone Rechenwerk hinein.

Irgendwie geht das alles natürlich auch mit “traditionellen” Komponenten. Aber elegant finde ich es nicht mehr. Warum sollte ich mir auch nur eine Sekunde lang Gedanken über Abhängigkeiten machen? Warum sollte ich mir eine Sekunde lang Gedanken über die Größe von Interfaces machen?

Ob eine EBC-Komponente “zu groß” ist, sehe ich in einer EBC-Architektur viel leichter als in einer “traditionellen”. Ich muss nur die Input-/Output-Pins zählen. Die geben viel detailliertere und sichtbarere Auskunft über die Verantwortlichkeitsbreite einer Komponente, als ein simpler Pfeil in einem Abhängigkeitsdiagramm.

So, nun aber genug für heute. Lassen Sie sich die Diagramme einmal auf der Zunge zergehen. Ich hoffe, Sie schmecken es, wieviel einfacher, regelmäßiger, verständlicher echt holarchische Software mit EBCs sein kann.