Follow my new blog

Montag, 3. Mai 2010

Nullonade – Monade statt Null

In der Diskussion um Null als Rückgabewert gab es ein Codeschnippsel, mit dem Thomas Bandt zeigen wollte, dass der TryGetUserById(…)-Vorschlag nicht zwangsläufig zu intuitivem Code führt. Ich formuliere das Beispiel hier mal etwas knapper:

User tmpUser;
bool isAuth = repo.TryGetUserById("987", out tmpUser) &&
              sec.CheckUser("somepassword", tmpUser);

Als Problem sieht Thomas – durchaus zurecht – die Abhängigkeit von CheckUser() von einem Seiteneffekt. Die globale Variable tmpUser muss vor Aufruf gesetzt sein, was innerhalb des logischen Ausdrucks nicht ganz so offensichtlich ist.

Etwas deutlicher wäre die Abhängigkeit der Reihenfolge beider Methodenaufrufe, wenn der Ausdruck traditioneller als Schachtelung formuliert wäre:

bool isAuth;
User tmpUser;
if (repo.TryGetUserById("987", out tmpUser))
    isAuth = sec.CheckUser("somepassword", tmpUser);
else
    isAuth = false;

Doch eigentlich sind Schachtelungen ja “böse” ;-) Sie erhöhen die (zyklomatische) Komplexität des Codes. Also vielleicht lieber so:

User tmpUser;
bool isAuth = repo.TryGetUserById("987", out tmpUser);
isAuth = isAuth && sec.CheckUser("somepassword", tmpUser);

Das mag besser lesbar sein – doch das Grundproblem ist eigentlich nicht gelöst. Das besteht in der Abhängigkeit selbst, also letztlich in der zu beiden Methoden globalen Variablen tmpUser. Die ist nötig, weil Thomas keine Exception werfen möchte, falls es den angeforderten User nicht gibt. Wäre das anders, könnte er nämlich auch ohne diese Variable formulieren:

bool isAuth = sec.CheckUser(“somepassword”,
                            repo.GetUserById(“987”));

Das ist zwar wieder geschachtelter Code, doch irgendwie sind wir sowas ja gewohnt ;-)

Get into the flow

So richtig cool finde ich das aber alles noch nicht. Deshalb schlage ich mal eine weitere Alternative vor. Wäre es nicht am allerschönsten, wenn wir es so schreiben könnten:

var isAuth = repo.GetUserById(“987”) |> sec.CheckUser(“somepassword”);

Das ist natürlich Pseudocode. Aber schön wärs, oder? GetUserById() würde immer noch keine Exception werfen und trotzdem (!) würde das Ganze immer funktionieren.

In F# geht das. Da gibt es so einen Pipe-Operator |> und noch etwas anderes, das nötig ist, einen Option-Typ. Aber in C# sitzen wir erstmal auf dem Trockenen :-(

Zum Glück können wir daran etwas tun. Wir können mit etwas Vorarbeit auch in C# so einen “Flow” von Anweisungen schreiben. Hier meine C#-Version:

bool isAuth = repo.GetUserById("987")
                  .Continue(user => sec.CheckUser("somepassword", user))
                  .WithDefault(false);

Liest sich doch gar nicht schlecht, oder? Es soll ein User geladen werden und danach soll geprüft werden, ob der User ein bestimmtes Passwort hat. Ist das so, dann ist das Ergebnis true, ansonsten false.

Die Sequenz der Verarbeitungsschritte in unserem Kopf findet sich im Code wieder – ohne dass wir eine temporäre Variable bräuchten. Naja, nicht ganz: denn der user-Parameter der Lambda-Funktion ist eine solche temporäre Variable. Doch die ist nicht global, sondern lokal. Es gibt also keine schwer zu überblickenden Abhängigkeiten.

Optionen als Zwischenwerte

Möglich ist so eine Formulierung, wenn als Resultat von GetUserById() kein User (oder gar Null), sondern ein sog. Option-Wert zurückgeliefert wird. Den gibt es in F# gratis; für C# können wir ihn so bauen:

public class Option<T>
{
    public Option()
    {
        this.IsSome = false;
    }

    public Option(T some)
    {
        this.Some = some;
        this.IsSome = true;
    }

    public T Some { get; private set; }

    public bool IsSome { get; private set; }
    public bool IsNone { get { return !IsSome; } }

    …
}

Der Trick an Optionswerten ist, dass es immer eine Instanz gibt. Es geht also nicht um Objekt oder Null, sondern um Options-Objekt gefüllt oder nicht gefüllt. Wenn es gefüllt ist, dann liefert IsSome true, wenn nicht, dann liefert IsSome false bzw. IsNone true. Zugriff auf den Wert gibt die Some-Property.

var optInt = new Option<int>(42);
Console.WriteLine(“{0}, {1}”, optInt.IsSome, optInt.Some);

gibt aus:

true, 42

Mit so einem Typen kann GetUserById() nun umformuliert werden zu:

Option<User> GetUserById(string id) {…}

Die Methode liefert immer ein Option-Objekt zurück und die Umgebung muss prüfen, ob etwas drin steckt oder nicht:

var optUser = repo.GetUserById(“987”);
if (optUser.IsSome)
    …

Das ist fast so wie eine Prüfung auf Null – aber besser. Denn mit einem Option-Objekt kann man immer weiterarbeiten.

Logik verstecken in einer Monade

Das Ziel ist immer noch der “Flow”, d.h. globale Variablen und Prüflogik auf unerwünschte Ergebnisse zu verstecken. Weil nun immer ein Option-Objekt vorliegt, können wir darauf immer eine Methode aufrufen

public class Option<T>
{
    …

    public IEnumerable<Option<TOutput>> Continue<TOutput>(Func<T, TOutput> processor)
    {
        return new[] {this}.Continue(processor);
    }
}

und auch noch Erweiterungsmethoden in Anschlag bringen

public static class OptionExtensions
{
    public static IEnumerable<Option<TOutput>> Continue<TInput, TOutput>(
                                     this IEnumerable<Option<TInput>> values,
                                     Func<TInput, TOutput> processor)
    {
        return values.Where(v => v.IsSome)
                     .Select(v => new Option<TOutput>(processor(v.Some)));
    }

    public static TInput WithDefault<TInput>(
                                     this IEnumerable<Option<TInput>> values,
                                     TInput defaultInCaseOfNone)
    {
        return (values.FirstOrDefault() ??
                     new Option<TInput>(defaultInCaseOfNone)).Some;
    }
}

Ich denke, damit habe ich eine Monade definiert, d.h. ein “Dings”, mit dem sich quasi unsichtbar ein Kontext um mehrere Anweisungen wickeln lassen kann.

Dass ich dafür auf IEnumerable<> zurückgegriffen habe, ist lediglich meiner Faulheit geschuldet ;-) (Mit IEnumerable<> kriegen ist nämlich die Verkettung geschenkt, weil der Typ selbst schon eine Monade ist.) Ebenso die Methode Continue() der Option-Klasse. Hätte ich mir mehr Mühe gegeben, hätte ich statt IEnumerable<> eine spezielle Monadenklasse gebastelt. Das wäre sauberer gewesen.

Für den Punkt, den ich hier rüberbringen wollte, geht es aber auch so, würd ich sagen. Ich möchte einfach einen weiteren Weg aufzeigen, ohne Null leben zu können – und dadurch lesbaren Code zu bekommen. Die Verkettung könnte noch weiter gehen, falls mehr “Transformationsschritte” nötig wären, die immer wieder Output erzeugen, der Input für den nächsten sind. Die Leistung der Monade besteht darin, die Schritte abzubrechen, falls kein Ergebnis geliefert wird (der frühere Null-Fall). Die verbirgt die Logik der Prüfung, ob ein Zwischenergebnis vorliegt und es noch weitergehen soll.

Die C#-Mittel bleiben dabei immer etwas hinter denen von F# zurück. Doch mir scheint “das Denken in Option-Werten” durchaus den einen oder anderen Versuch wert. Wie gesagt, mit etwas mehr einmalig aufgewandter Zeit, ist das auch noch eleganter ;-)

Sonntag, 2. Mai 2010

Es hilft nichts, dass es darauf ankommt – nicht nur beim Null-Problemo [OOP 2010]

Erst ein Feiertag, jetzt ein Sonntag und die Diskussion um “Null oder nicht Null” geht weiter. Als hätten diese Softwareentwickler nichts anderes zu tun… ;-) Ilker hat versucht, einen ausbalancierenden Schlussstrich unter die Diskussion zu ziehen, um uns wenigstens ein Rest schönes Wetter genießen lassen zu können. Doch mich wurm da noch etwas. Und weil der Stein, an dem ich mich da anstoße, bei ihm so ausdrücklich in seinem Blogartikel zu lesen ist, greife ich auch am Sonntag Morgen nochmal in die Tasten:

Woran ich mich in dieser Diskussion – die allerdings nur stellvertretend für viele andere ist – störe, das sind Aussagen wie

“Es kommt auf den Anwendungsfall an. Die beliebte ‘It-Depends’-Weisheit streckt das Thema mit geballter Kraft nieder.”

und

“So oder so – beides ist machbar und beides hat seine Berechtigung.”

Ich kann das einfach nicht mehr hören. (Sorry, Ilker, ist nix Persönliches. Diese Formulierungen sind vielmehr ein Anti-Pattern unserer Branche.) Kommt irgendeiner mit ‘ner Frage, was er tun soll – diese oder jene Technologie oder dieses oder jenes Sprachfeature oder diesen oder jenen Lösungs weg? –, dann antwortet die Branche unisono: “It depends!”, “ Kommt darauf an… “, “Im Prinzip ist alles möglich.”

Was soll denn das? Ja, ja, ich mache so einen Spruch auch mal. Am Ende sind diese Aussagen aber doch ein Armutszeugnis. Vor allem aber: Sie helfen nicht. Niemandem. Vor allem nicht dem, der gefragt hat.

“Es kommt darauf an…” ist eine triviale Antwort. Sie ist trivial und nutzlos, weil sie quasi immer wahr ist. Es kommt immer im Leben darauf an. Auf irgendwas. Wo Wahlmöglichkeiten bestehen, da kommt es immer auf den einen oder anderen Faktor an, ob die eine oder andere Möglichkeit gewählt werden sollte. Diese Faktoren mögen subjektiv sein, also rein persönlich, oder objektiv, d.h. irgendwie aus der Problenstellung bzw. der angestrebten Lösung abgeleitet.

Also: Leute, ihr habt/wir haben immer Recht, wenn wir sagen “Es kommt darauf an…”. Super. Lasst uns darauf einen trinken. Und dann lasst uns endlich damit aufhören. Wer “Es kommt darauf an…” sagt (oder etwas Sinngleiches), der zahlt in Zukunft 5 EUR in das Phrasenschwein. Wie wäre das? Wir kommen einfach nicht voran mit solchen Sprüchen. Sie dienen nie-man-dem. Wir bringen weder den einzelnen Frager noch “unsere Kunst” damit voran.

Ok, so ein Spruch geht also nicht mehr. Was aber dann? Wir sollten, wann immer wie reflexartig “Es kommt darauf an…” sagen wollen, innehalten. Mund zu, Denkmaschine anwerfen… Das halte ich für den ersten Schritt. Nachdenken, ob denn aus einer Problembeschreibung sich nicht doch irgendwie ein Hinweis auf eine vorzuziehende Lösungsmöglichkeit ergibt.

Ja, Nachdenken verbraucht auch Kalorien, aber wir sollten uns diesen Sport gönnen. Der bringt uns selbst und “unsere Kunst” nämlich voran. Die Ergebnisse sind nämlich immer positiv: Schärfung der Wahrnehmung, Reflexion des eigenen Tuns, Ausarbeitung von Regeln.

Eine Regel hat sich ja auch in dieser Diskussion um das Null-Problemo herauskristallisiert:

Null-Regel #1: Man darf Null benutzen und auch als Wert zurückgeben – aber Vorsicht! Erst prüfen, ob es nicht auch anders geht.

Das ist doch schon mal ne schöne Sache, oder? Darin sind wir uns einig. Prost!

Nur verstehe ich nicht, warum Ilker dann schreibt

“Für mich gibt es beim Umgang und bei der Anwendung von Null keine Faustregel.”

Denn diese Regel ist doch eine Faustregel. Sie lautet: Setze Null eher nicht ein. Punkt. Warum sollten wir das runter spielen.

image Mein Gefühl ist, dass “die Branche” da an einem kollektiven mangelnden Selbstvertrauen leidet. “Man” traut sich nicht, einen Pflock in den Boden zu hauen. Denn wer Pflöcke in den Boden haut und die dann auch  noch mit Latten zu einem Areal verbindet, der läuft Gefahr, dass andere einen Streit von diesem Zaun brechen. Und das will “man” doch nicht. Ne, Streit ist doof.

“Wer Regeln formuliert oder verbreitet, wird mit Stirnrunzeln oder gar flame comments bestraft.” – so lautet das unausgesprochene Gesetz.

Regeln stören den Seelenfrieden dessen, dem sie “aufgedrückt” werden. Oder sie stören womöglich sogar den Seelenfrieden dessen, der sie aufstellt. Denn wer Regeln formuliert, der muss sich ja auch daran halten. Will “man” das aber? Wer weiß, vielleicht findet “man” es morgen bequemer, es anders zu tun – und dann wäre die Regel hinfällig. Aaargh… Also besser mal keine Regel aufstellen. Und schon gar nicht anderen die Befolgung einer Regel nahelegen. Denn was auch, wenn die damit nicht zum Erfolg kommen? Dann sind sie sauer auf “einen”.

Das sind natürlich sehr verständliche Regungen. Aber: Leute, Leute, so kommen wir nicht voran. Niemand behauptet, dass “alles” reglementiert werden sollte oder gar könnte. Angst, in der “künstlerischen Programmiererfreiheit” eingeschränkt zu werden, ist völlig (!) unbegründet. Maler, Bildhauer, Schauspieler, Musiker… alle sind vielmehr erst Künstler durch die Regeln ihres Metiers. Ohne Regeln keine Kunst. Denn Kunst ist sozusagen “mit und in den Regeln schaffen und spielen”. Ohne Regeln ist alles keine Kunst. Wir schätzen einen Meister seines Metiers dafür, dass er “gar künstlich” (d.h. soviel wir “nach den Regeln der Kunst” oder “virtuos”) etwas (er)schafft.

image Wer am leeren Tisch sitzt und hört, “Schaffe mal ein Kunstwerk”, der hat es schwer, nicht leicht. Sitzt er aber vor einer Staffelei mit Ölfarbenpallette und Pinsel in der Hand und bekommt den Auftrag “Schaffe ein impressionistisches Landschaftsbild”, der kann loslegen. Öl, Pinsel, Leinwand, Stilrichtung, Motiv: das sind Begrenzungen mit impliziten Regeln, die erst die Möglichkeit bieten, Künstler zu sein.

(Ja, ja, es gibt immer mal wieder auch Künstler, die dadurch groß werden, dass sie (scheinbar) regellos oder gegen die Regeln arbeiten. Aber Achtung: Erstens können sie sich nur profilieren im Kontrast zu etablierten Regeln der Kunst, zweitens schaffen diese Künstler durch ihr Tun (bewusst oder unbewusst) wieder neue Regeln, und drittens sind solche Regelwerk sprengenden bzw. ausweitenden Künstler die Ausnahme. Wer mein, ohne Regeln auszukommen, läuft also Gefahr, sich zu überschätzen.)

Bottom line: Ohne Regeln geht es nicht. Und jeder, der in einer Situation sitzt, wo er nicht recht weiter weiß, der sehnt sich nach einer Regel, die ihn leitet. Warum also diese branchenweite Ambivalenz? Einerseits Regeln suchen für die eigene Anwendung, andererseits aber keine Regeln formulieren wollen, wenn “man” gefragt wird.

imageOhne Regeln ist das Verhalten ein Rumgeeiere wie hier von Ilker trefflich beschrieben:

“Ich gebe in einigen Methoden NULL zurück. Meistens genau dann, wenn ich wirklich damit ausdrücken möchte, dass etwas katastrophales passiert ist. Ich finde es sehr schwerwiegend, NULL als Zuweisung oder Rückgabe stehen zu lassen – aber ich mache es manchmal ganz bewusst.”

Ja, was denn nun? Keine Faustregel, aber “manchmal ganz bewusst”? Wie geht das? Bewusstsein erfordert (!) einen Raum, in dem es sich eben bewusst verortet durch Entscheidungen. Und die Wände dieses Raumes bestehen (zumindest zum Teil) aus… Regeln. Die mögen sehr speziell sein, aber immer noch sind es Regeln, die man dann auch äußern kann, um sie zur Diskussion zu stellen.

Thomas hatte das in seinem ursprünglichen Blogartikel getan:

“Außerdem erwarte ich von jeder Methode null als Rückgabewert für den Fall, dass keine Daten in der Datenbank gefunden wurden.”

Jede Methode (seines Repositories) liefert bei Erfolg ein Objekt und bei Misserfolg Null. So war seine Regeln. Ob die gut oder schlecht ist, sei mal dahin gestellt. Das wurde ja auch ausführlich diskutiert. Zumindest war das eine Regel basierend auf einer bewussten Entscheidung. Die hat er geäußert. Nur so konnte darüber ein Diskurs beginnen. Wunderbar.

Ilker hingegen sagt, er sei bewusst – eine Regel (außer der obigen Null-Regeln #1) mag er aber eben nicht benennen. Das finde ich sehr, sehr schade. Denn so – ich wiederhole mich – kommen wir eben nicht voran.

Oder? Hoppla, jetzt hab ich mich doch durch Ilkers Statement, er hätte keine (Faust)Regel verwirren lassen. Natürlich hat er welche. Die Regel #1 und auch noch seine persönliche Regel:

Null-Regel #2 (von Ilker; noch zu diskutieren): In Katastophenfällen darf Null zurükgegeben werden.

Das ist doch mal eine Aussage. Darüber kann man diskutieren. Aber nicht hier.

@Ilker: Vielleicht magst du ja in einem weiteren Blogartikel ausführen, was Katastrophenfälle sind und warum dann Null als Rückgabewert erlaubt ist und wie zwischen Null und Exceptions als Mittel zur Anzeige von Katastophenfällen entschieden werden sollte. Darüber würde ich gern mehr hören.

Ilker hat also Regeln (mindestens zwei), zieht sich aber darauf zurück, dass “die beliebte ‘It-Depends’ Weisheit” im Falle Thomas “alles niederstreckt”. Warum? Warum keine klare Aussage, keine Empfehlung mit Verweis auf eine seiner Regeln? Weiß Ilker nicht mehr weiter? Das glaube ich nicht.

Ich möchte ihm stellvertretend für uns alle zurufen: Nicht verzagen! Mach doch eine klare Aussage. Habe eine Präferenz. Zeige Kontur. Mach dich sichtbar – ja, und auch angreifbar. Haue einen Pflock in den Boden.

Denn niemand wird dich oder irgendwen für soetwas verhaften. Entweder ist die Empfehlung für den konkreten oder auch allgemeinen Fall richtig/praktikabel – oder es stellt sich im Diskurs heraus, dass sie es nicht ist. Dadurch können doch dann aber alle nur gewinnen. Und niemand steht deshalb schlechter da. Auch nicht der, der die Regeln/Empfehlung ausgesprochen hat.

Niemand muss ja auch seine Regel/Empfehlung kampflos ausgeben. Nur weil ein anderer damit nicht übereinstimmt, heißt das nicht, das man falsch liegt. Solange wir einen Diskurs führen, also sachlich bleiben, solange kommen wir voran – und können am Ende auch mal unterschiedliche Meinungen behalten. In der Malerei gibt es Schulen, in der Programmierung kann es die auch geben. Da könnte es z.B. die “Null geht gar nicht, nie, nimmer!”-Schule geben oder die “Null ist überhaupt kein Problem”-Schule oder die “Null mit Vorsicht”-Schule oder die “Null nur mit Vorsicht und mit folgenden Regeln”-Schule.

image Ich gehöre – soviel kann ich sagen – zur letzteren. Deshalb hier mein Pflock im Boden:

Null-Regel #3 (Ralfs Vorschlag): Null darf nur Rückgabewert sein, wenn Null zur selben Kategorie gehört wie ein Nicht-Null Rückgabewert.

Darüber können wir gern diskutieren. Einen Anfang habe ich hier gemacht. Also: Klare Aussage von mir, kein Rumgeeiere. Und jetzt die Argumente dagegen. Aus der Community. (Zur Regel #2 sage ich etwas, wenn Ilker sie begründet. Hier nur soviel vorab: Ich bin dagegen ;-)

Und jetzt noch ein letzter Schwenk zurück zum Allgemeinen. Wir brauchen also Regeln. Wir sollten uns der Reflexäußerung “Es kommt darauf an…” enthalten. (Auch das ist übrigens eine Regel ;-) Noch ein Pflock von mir im Boden.) Besser, wir schalten in solchen Situationen erstmal die Denkmaschine an. Analyse hilft.

Ebenfalls hilfreich ist allerdings auch: Fragen und Zuhören! Denn so mancher, der sagt “It depends”, leidet womöglich nicht unter mangelndem Selbstvertrauen oder Denkfaulheit, sondern schlicht unter Unwissen. Er weiß nicht genug, um eine Entscheidung zwischen den Alternativen klar treffen zu können.

Dem kann dann durch Nachfragen und genaues Hinhören abgeholfen werden. Wenn man sich denn als Befragter überhaupt interessiert. Womit wir bei einem weiteren Grund für “Es kommt darauf an…”-Antworten sind: Desinteresse. “Was interessiert mich das Problem des anderen? Ich hab schon selber genug Probleme. Wenn mir doch mal einer helfen würde…” usw.

Hieraus ergibt sich auch eine Empfehlung für Hilfesuchende: Wer fragt und “It depends” als Antwort bekommt, der sollte nicht zufrieden sein. “It depends” ist trivial und beliebig. Wer fragt, sollte die Antwort als Aufforderung verstehen, mehr über das Problem zu erzählen oder überhaupt mal festzustellen, ob der Befragte überhaupt ein Interesse an einer Antwort hat. (Von einer Kompetenz zur Antwort sehe ich hier mal ab. Jeder ist kompetent zumindest insofern, als dass er dem Fragenden ein Spiegel sein kann. Auch das hilft oft.)

Fazit: Es hilft also nichts, dass wir die Hände in die Luft werfen und sagen “Es kommt darauf an…” Diese Phrase können wir uns in Diskussionen sparen. Sie macht nur Sinn als Replik auf eine ebenso sinnlose allgemeine Frage. Beispielfrage: “Sollte man die Daten mit einem RDBMS oder einem ODBMS speichern?”, Antwort: “Es kommt darauf an…”

In solche einem Fall mag die allgemeine Frage allgemein mit einer Phrase beantwortet werden, um dadurch aufzuzeigen, dass schon die Frage schlecht formuliert war. In dem Fall ist “Es kommt darauf an…” aber sozusagen ein Stilmittel – dass sofort als solches aufgedeckt werden sollte: “Es kommt darauf an… Das sage ich, weil Ihre Frage zu allgemein ist. Erklären Sie mir bitte genauer, worum es geht.”

“Es kommt darauf an…” ist quasi immer richtig und hilft deshalb nicht. Wir müssen weg von dieser Phrase hin zu mehr Nachdenken und mehr Zuhören. Und daraus müssen sich mehr konkrete Regeln ergeben, die auf konkrete Fälle angewandt werden können. Ob das wenige oder viele, einfache oder komplizierte Regeln sind, ist erstmal egal. Wir brauchen sie als Fundament für “unsere Kunst”.

Niemand behauptet auch, dass die so bewusst erarbeiteten Regeln starr sein sollten. Im Gegenteil. Sie sollen lebendig bleiben im Rahmen eines Diskurses. Zwischendurch mal Ruhe ist auch gut. Einfach mal die Regeln lernen und anwenden, bevor man sie hinterfragt. Aber dann auch gern immer wieder Infragestellung. Die Zeiten ändern sich, also dürfen sich auch die Regeln ändern. Manche langsamer, manche schneller.

Unzweifelhaft ist jedoch, dass wir sie brauchen. Ich glaube sogar, dass wir als Branche unterspezifiziert sind mit Regeln. Deshalb habe ich auch www.clean-code-developer.de (CCD) mit gegründet. Und der Erfolg der Initiative gibt meiner Vermutung recht, würde ich sagen. Denn CCD ist eine Sammlung von Regeln und darauf stehen offensichtlich mehrere Tausend Entwickler, wenn ich mal die Mitgliedszahlen in den Foren und die vergebenen Armbänder zusammenrechne.

So, nach diesem Ausflug ins Grundsätzliche gern wieder zurück zum Konkreten: Da es beim Null-Problemo genauso “darauf ankommt” wie sonst im Leben, stellt sich immer noch die Frage: Worauf kommt es denn an? Was sind die Regeln für die Null-Verwendung? Was sind damit die Eigenschaften eines Szenarios, die geklärt werden müssen, um zu entscheiden, welche Regel zum Einsatz kommt?

Samstag, 1. Mai 2010

Sollte Architektur wirklich emergieren? [OOP 2010]

In Objektspektrum 3/2010 steht ein Artikel auf der Höhe der Zeit: “Emergente Architektur: Der machtlose Architekt”. Denn davon spricht man auch hier oder hier. Und woanders mag es auch Emergent Design heißen.

“Emergenz” scheint ein neues Zauberwort zu sein. Es bedeutet soviel wie Entstehung von etwas Ganzem, ohne dass dieses Ganze so geplant worden wäre. Eine Termitenbau entsteht auf diese Weise oder geordnete Menschenströme in einer Einkaufsstraße oder sogar die erfolgreiche Jagd eines Löwenrudels.

Klingt gut, oder? Wäre doch toll, wenn Softwareentwicklung nicht länger krampfhaft und bewusst versuchen müsste, der ewig nörgelnden Forderung nach einer sauberen Architektur nachzukommen, sondern sie irgendwie “wie von selbst” entstehen lassen, sie emergieren lassen könnte.

Zugegeben, eine schöne Vorstellung. Aber glauben tue ich an sie genauso wenig wie an die Rettung der Welt durch CASE, SOA oder O/R Mapper. Trotz aller Erkenntnisse der Komplexitätsforschung sehe ich weiterhin die Notwendigkeit zur Planung von Architektur. Denn:

  • Emergenz ist trivial: Wenn Systeme genügend unbekannt oder genügend groß in Bezug auf den Kenntnishorizont eines Teams sind, dann kann ihre Architektur nicht “auf einmal” geplant werden. Sie entwickelt sich vielmehr über die Zeit in Reaktion auf neue Erkenntnisse und Anforderungen. Jeder Entwicklungsschritt folgt dabei (hoffentlich) den lange bekannten Prinzipien “guter Architektur”. Jeder einzelne Architekt hat dann über die Zeit nur einen Teil zum großen Ganzen beigetragen.
    Emergenz ist letztlich also kein neues Phänomen, sondern nur ein modischer Begriff für Evolution. Weniger Rede von Emergenz und mehr von Evolution könnte helfen, nicht schon wieder nach einer neuen Silberkugel zu suchen.
  • Emergenz ist unvermeidbar: Auch wenn die Kenntnis über das zu realisierende System groß ist, endet die Verantwortung eines Architekten irgendwo. Er ist ja Architekt und nicht Entwickler. Und Planungspapiere sind kein Code. Bei der Umsetzung der Architekturplanung kann daher immer noch einiges passieren und passiert auch. Architekturvorstellungen werden selten 1:1 umgesetzt. Die Architektur eines Systems ist damit immer mehr oder weniger ungeplant und Resultat vieler kleiner Entscheidungen vor Ort während der Implementation.
  • Emergenz ist schwer planbar: Soll ein makroskopisches Ergebnis (z.B. Architektur) emergent entstehen, sind passende microskopische, einfache Regeln zu definieren. Solche Regeln a posteriori durch Analyse von Ergebnissen zu finden, ist eine Sache. Sie jedoch a priori zu postulieren, eine andere. Das makroskopische, am Ende recht simple Verhalten von Schwärmen hat schon Forscherjahre in der Analyse gekostet, auch wenn nur 3 Kernregeln dabei gerausgekommen sind. Was ist dann für so etwas komplexes wie Softwarearchitektur zu erwarten? Ich bin sehr im Zweifel, ob die Softwarekunst darauf schon eine Antwort hat. Entwickler mit TDD vor sich hinwerkeln zu lassen, mit S.O.L.I.D kräftig voran zu schreiten und darauf zu vertrauen, dass Featureteams es schon richten werden, halte ich für zuwenig. Damit sind zwar Richtschnüre auf der Microebene gespannt – doch die führen nicht so unausweichlich wie die Regeln eines Sardinenschwarms zu einem evolvierbaren großen Ganzen.

Architektur braucht, so glaube ich, auf unabsehbare Zeit immer noch ein gerüttelt Maß an expliziter Planung “in der Mitte”. BDUF ist out, soviel ist klar. Gesamtplanungen sind womöglich nicht mal machbar. Microplanung ist ebenfalls kontraproduktiv; sie zerstört Motivation und Effizienz. Doch “in der Mitte” gibt es Raum und Notwendigkeit für explizite Architekturplanung. Denken und Planen hilft, da bin ich mir sicher. Immer noch. Ameisen oder Sardinen mögen nicht planen müssen. Aber “große Maschinen”, die sich auch noch ständig ändern sollen, die deshalb auf der Metaebene konstant änderbar/evolvierbar sein müssen, solche “Maschinene” “sich ergeben zu lassen” durch lokale Anwendung überschaubarer Regeln… das halte ich für zumindest eine verfrühte Vorstellung.

Denn wahre Emergenz" bedeutet eben keine Planung des großen Ganzen. Niemand malt ein Architekturdiagramm. Niemand hat eine Vorstellung von der Form des Ganzen – sondern immer nur von seinem Verhältnis zur Umwelt. Viele kleinste Entscheidungen auf Microebene ergeben – oh, Wunder! – ein wahrhaft elegantes und zweckmäßiges Ganzes. (Wobei Zweckmäßigkeit von Software immer einschließt, dass die nicht nur heute vorhanden ist, sondern auch für morgige Anforderungen leicht hergestellt werden kann.)

Damit komme ich sozusagen zum philosophischen Teil meiner Position:

Keine Ameise versteht das Ganze ihres Staates. Kein Mensch versteht mehr das Ganze einer Volkswirtschaft. Dasselbe mag für ein genügend großes Softwaresystem gelten. Das Internet könnte dafür ein Beispiel sein. So ist das halt, wenn etwas sehr komplex wird.

Das bedeutet im Umkehrschluss jedoch, dass das, was da so groß und komplex ist, was über die Köpfe der Individuen hinaus gewachsen ist, keine (oder nur wenige) von den Individuen planbare Eigenschaften hat. Wir das an der Finanzkrise, wir sehen das an den Entwicklungen im und um das Internet und die neuen Medien.

Wie können womöglich noch etwas Emergiertes erkennen. Wir können das Ganze beobachten. Aber wir können ihm als “Rädchen im Getriebe” keine spezifischen Eigenschaften mehr verordnen. Klar, wir schrauben am Ganzen herum. Jede unserer Handlungen verändert das Ganze. Aber es in eine funktionale Richtung zu bewegen oder auf der Metaebene es Flexibel zu machen… das sind geradezu titanische Aufgaben, deren Lösung unabsehbar ist.

Was bedeutet das für Software, wenn wir uns für sie wünschen, dass ihre Architektur emergieren möge? Mir scheint es, dass Emergenz nur um den Preis eines Kontrollverlustes zu haben ist. Aber wer möchte den schon erleiden? Software soll sehr konkrete Zwecke erfüllen, Architektur ist dafür die Basis. Wenn Architektur dann jedoch “nur” emergiert, wenn Architekten machtlos sind und das Ganze “sich ergibt” aus regelhaften Einzelaktionen… wie stellt ein Team dann sicher, dass der durch den Kunden vorgegebene Zweck zeitig wie nachhaltig erfüllt wird?

image Ergo: Solange wir noch Ziele ansteuern, können wir uns nicht auf Emergenz verlassen. Softwareentwicklung ist kein Selbstzweck. Also kann ihr Ergebnis nicht komplett einer Emergenz anheim gestellt werden.

Das bedeutet nicht, dass wir nicht immer wieder prüfen sollten, wieviel Planung/Kontrolle wann angemessen ist. Wir sollen nach Microregeln suchen, die helfen, Makroeigenschaften “einfach so” herzustellen. Doch um explizite Architekturentscheidungen kommen wir nicht herum. Architekten werden nicht überflüssig und degenerieren auch nicht zu reinen Prozessmoderatoren.

Ein wenig Bescheidenheit mag allerdings angezeigt sein. Eine Haltung der Akzeptanz der Vorläufigkeit hilft. Etwas weniger Kontrolle haben wollen, schadet nicht. Kollektive Intelligenz fördern, ist sicherlich ein guter Ansatz. Aber Planen und Denken bleiben Kernaufgaben von Architekten.

Null oder nicht Null, das ist hier die Frage

Sollte man Null als Ergebnis einer Abfrage zurückgeben oder nicht? Diese Frage ergab sich anlässlich eines Blogartikels von Thomas Bandt, der in ein Problem gelaufen war, seiner Gewohnheit folgen zu können, eine Query-Methoden eines Repositories Null zurückliefern zu lassen:

“[Ich] erwarte […] von jeder Methode null als Rückgabewert für den Fall, dass keine Daten in der Datenbank gefunden wurden. Nun ist mir aber beim Testen meiner Repositories […] aufgefallen, dass Folgendes niemals null zurückliefert: […] return […].Select(…).ToList(); […]”

Letztlich hat Thomas zwar eine Lösung für sein Problem gefunden, doch ein Nachgeschmack bleibt. Ist das vielleicht nur eine Symptomkur? Ich würde sagen, ja.

Ob man nie Null zurückgeben darf oder nicht, will ich nicht entscheiden. Solche Allaussagen sind auch selten hilfreich. Soweit stimme ich mit einigen Kommentatoren des Blogartikels überein. Wie steht es allerdings in diesem Fall? Lässt sich im Konkreten doch vielleicht eine saubere Entscheidung treffen?

Für mich steht außer Zweifel, dass im Falle von Thomas´ Repositories die Entscheidung eindeutig für den Verzicht auf Null als Rückgabewert ausfallen sollte. Als Erklärung vier unterschiedliche Ansätze:

  • Philosophisch/Logischer-Blickwinkel: Eine Liste mit Resultaten und Null gehören unterschiedlichen Kategorien an. Eine Liste ist ein Container, Null ist… nichts oder nur ein generischer Wert, jedenfalls kein Container. Dass aber eine Methode Ergebnisse aus zwei Kategorien zurückliefert, halte ich für kategorisch falsch.
    Eine Liste mit Resultaten und eine Liste ohne Resultate (leere Liste) hingegen gehören beide derselben Kategorie Container an.
    In diesem Fall ist Null ganz kategorisch also nicht erlaubt. Anders mag das sein, wo eine Funktion object als Resultattyp hat. Der ist so allgemein, dass eine Zeichenkette, eine Zahl, eine Liste oder eben auch Null in seiner Hinsicht derselben Kategorie Wert angehörten.
  • Praktikscher Blickwinkel: Wer Thomas´ Repository-Query-Methoden benutzt, muss im Code eine Fallunterscheidung treffen. Falls eine Abfragebedingung keine Ergebnisse liefert, muss es zwangsläufig anders weitergehen als mit Resultaten, auch wenn vielleicht nur die Resultate gelistet werden sollen und eine leere Liste am Bildschirm für den Benutzer völlig ok wäre. Thomas´ Muster für Query-Rückgabewerte macht also im Zweifelsfall mehr Aufwand – ohne einen Gewinn zu bringen. Falls leere Liste und gefüllte Liste unterschiedlich behandelt werden sollten, ist die Prüfung auf Null und die auf Länge 0 gleich aufwändig.
  • Prinzipieller Blickwinkel: Ich würde sagen, ein Null-Ergebnis widerspricht dem Principle of Least Astonishment. Denn wenn ich eine Signatur sehe, die eine Liste als Rückgabewert definiert und dann kommt keine Liste, sondern Null zurück, dann bin ich erstmal ziemlich überrascht. Nicht, dass der Gedanke dahinter kompliziert wäre, nicht dass Thomas das nicht in einem XML-Kommentar erklären könnte – aber eine Überraschung bleibt es und deshalb auch ein zusätzlicher Dokumentationsaufwand.
    Auch ist eine Abfrage mit leerer Ergebnismenge keine Besonderheit. Sie rechtfertigt aufgrund ihrer Erwartbarkeit keine Sonderbehandlung durch Null oder gar eine Exception.
  • Analogischer Blickwinkel: Wenn ein Räuber mich zur Übergabe meines Geldes auffordert, dann gebe ich ihm besser mein Portemonais. Falls im Portemonais kein Geld ist, mag er zwar frustriert sein, doch er kann mich zumindest keines Widerstandes bezichtigen. Hätte ich hingegen geantwortet “Ich habe kein Geld.”, dann würde die Szene in jedem Fall sehr unschön werden, denke ich. Allemal würde er mir ohnehin nicht glauben.
    Die Moral von der Geschicht: Versuche der Aufforderung eines Räubers nicht differenziert nachzukommen. Antworte platt so, wie er es sich wünscht und du gleichzeitig klar machst, dass deine Antwort ehrlich ist. Er soll dann selbst aus dem Ergebnis etwas machen.

Soweit mal meine im Grunde doch einfache Lösung des Null-oder-nicht-Null-Problems. Im Zweifelsfall eher auf Null verzichten. Null ist eher “böse” so wie Goto “böse” ist. Und wer das nicht glauben mag, weil Null doch so einfach zu gebrauchen ist, der höre dazu den Erfinder von Null, Tony Hoare, mit seinem Vortrag “Null References: The Billion Dollar Mistake”.

image

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.