Follow my new blog

Sonntag, 21. April 2013

Warnung vor der funktionalen Abhängigkeit

Immer noch bin ich dem Gefühl auf der Spur, warum funktionale Abhängigkeiten "böse" sind. Sie sind so natürlich, so naheliegend - und doch beschleicht mich immer wieder Unwohlsein. Ich versuche schon lange, sie zu vermeiden. Deshalb betreibe ich Flow-Design. Doch einem Gefühl allein will ich mich da eigentlich nicht hingeben. Es mag als Auslöser für eine Veränderung alter Gewohnheiten sehr nützlich sein, nur ist es schwer, rein aufgrund eines Gefühls diese Veränderung weiter zu tragen. Deshalb will ich noch Mal genauer hinschauen. Gibt es eine rationale Erklärung für mein Gefühl? Ich habe dazu schon Anläufe hier im Blog genommen. Doch jetzt will ich es mehr auf den Punkt bringen.

Funktionale Abhängigkeit

Für mich besteht eine funktionale Abhängigkeit, wenn...
  1. eine Funktion c() wie Client eine Funktion s() wie Service aufruft und
  2. c() selbst die Parameter für s() durch Berechnungen vorbereitet und
  3. c() selbst das Ergebnis von s() selbst in Berechnungen weiterverarbeitet.
Wenn c() in dieser Weise s() benutzt, dann braucht c() s().

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  return ... z ...;
}

c() ist funktional abhängig, weil c() Logik enthält, die auf s() abgestimmt ist. Allemal sind das die Berechnungen, die auf den Aufruf von s() folgen.

Wie gesagt, so zu arbeiten, ist ja total normal. Am Ende können und wollen wir auch nicht ohne. Nur ist mein Gefühl, dass wir zu oft in diesem Stil arbeiten. Zu gedankenlos gehen wir die Probleme ein, die er mit sich bringt. Das Resultat: Code, der sich alsbald nur noch schwer verändern lässt. So werden Brownfields produziert.

Problem #1: Bereitstellungsaufwand

Das erste Problem mit solcher funktionalen Abhängigkeit scheint ein Feature - ist aber ein "Bug". Es besteht darin, dass c() überhaupt auf s() zugreifen muss. Das kann in statischer Weise geschehen: c() bzw. die Klasse, zu der c() gehört, kennt die Klasse, zu der s() gehört. Also wird bei Bedarf eine Instanz der unabhängigen Serviceklasse erzeugt:

int c(int a) {
  U u = new U();
  ...
  int y = u.s(x);
  ...
}

Ok, dass das "böse" ist, wissen wir inzwischen alle. Bestimmt tun Sie das schon lange nicht mehr, sondern folgen dem Prinzip Inversion of Control (IoC) und benutzen Dependency Injection (DI).

class A {
  IU _u;
  public A(IU u) { _u=u; }

  public int c(int a) {
    ...
    int y = _u.s(x);
    ...
  }
}

Die abhängige Klasse A von c() kennt nur eine Abstraktion der unabhängigen Klasse U von s(), z.B. in Form des Interface IU. Jedes A-Objekt bekommt dann ein Objekt irgendeiner Klasse injiziert, die IU implementiert. Das kann eine Instanz von U sein, aber auch eine Attrappe.

Aus der statischen Abhängigkeit ist somit eine dynamische geworden. Die Kopplung zwischen c() und s() ist gesunken. c() ist besser testbar. Wunderbar. Und dennoch...

Das Problem besteht für mich darin, dass wir uns überhaupt um so etwas Gedanken machen müssen. IoC/DI sind nur Symptomkuren. Ohne funktionale Abhängigkeit würde sich die Frage nach statischer oder dynamischer Kopplung nicht einmal stellen.

Wer das Single Responsibility Principle (SRP) ernst nimmt, der muss doch sehen, dass A hier zwei Verantwortlichkeiten hat. Da ist zum einen die Hauptverantwortlichkeit von A, seine Domäne, in der c() eine Funktionalität bereitstellt.

Darüber hinaus leistet A aber auch noch auf eine technischen Ebene etwas. A stellt s() bereit. Auf die eine oder andere Weise. Dass c() überhaupt Zugriff auf s() haben kann, ist eine eigenständige Leistung, die nicht zur "Geschäfts"domäne von A gehört.

Funktionale Abhängigkeit vermischt zwei Aspekte. Das wird noch deutlicher beim zweiten Problem, das ich sehe:

Problem #2: Topologische Kopplung

Die Vermischung der Domäne mit dem "Bereitstellungsaspekt" mag für Sie quasi schon unsichtbar sein. Sie ist total normal und scheint unvermeidbar. Aber das ist noch gar nichts. Es wird noch subtiler.

Nehmen wir an, c() ist funktional abhängig von zwei Funktionen, s1() und s2().

class A {
  IU _u;
  public A(IU u) { _u=u; }

  public int c(int a) {
    ...
    int y1 = _u.s1(x);
    ...
    int y2 = _u.s2(z);
    ...
  }
}

Was passiert, wenn Sie in einem Anfall von SOLIDer Programmierung das Interface Segregation Principle (ISP) anwenden und sich entscheiden, s1() und s2() auf verschiedene Interfaces zu legen?

Sie müssen A ändern. Obwohl sich an der Domäne von A nichts geändert hat. Das ist ein klarer Widerspruch zum SRP, das "once [business] reason to change" zur Einhaltung fordert.

Die gut gemeinte Refaktorisierung nach dem ISP zieht so einiges nach sich für A:

class A {
  IU1 _u1;
  IU2 _u2;
  public A(IU1 u1, IU2 u2) { _u1=u1; _u2=u2; }

  public int c(int a) {
    ...
    int y1 = _u1.s1(x);
    ...
    int y2 = _u2.s2(z);
    ...
  }
}

A muss den Änderungen an der Verteilung der Servicefunktionen auf Interfaces nachgeführt werden, weil A nicht nur von s1() und s2() abhängig ist, sondern auch daran gekoppelt ist, wo (!) diese Funktionen definiert sind. A ist an die Topologie ihrer Umgebung gekoppelt.

Auch das scheint alles ganz normal und unvermeidbar. Dafür gibt es Refaktorisierungswerkzeuge, oder?

Könnte man so sehen. Aber ich finde es besser, sich gar nicht erst in den Fuß zu schießen, als die Wunder der modernen Medizin loben zu müssen.

Interessanterweise wird die topologische Kopplung zu einem umso größeren Problem, je sauberer Sie mit funktionalen Abhängigkeiten programmieren wollen. Denn desto mehr Klassen bekommen Sie (SRP) und desto mehr Interfaces auf diesen Klassen wird es geben (ISP). Der Anteil an "Bereitstellungsinfrastruktur" in den abhängigen Klassen wird immer größer.

Problem #3: Grenzenloses Wachstum

Umfangreiche Funktionen kennen Sie bestimmt auch. Ich habe schon welche mit 10.000 Lines of Code (LOC) gesehen. Sie kennen auch die Empfehlungen, den Funktionsumfang zu begrenzen. Manche sagen, Funktionen sollten nur 10 oder 20 oder 50 LOC enthalten. Andere sagen, sie sollten nicht länger als eine Bildschirmseite sein. Wieder andere beziehen sich nicht auf die LOC, sondern auf die "Komplexität" des Codes in Funktionen (die mit der Zahl der möglichen Ausführungspfade durch ihn zu tun hat).

An guten Ratschlägen mangelt es wahrlich nicht, das uralte Problem der beliebig umfangreichen Funktionen in den Griff zu bekommen. Nützen tun sie aber offensichtlich wenig. Denn bei allem guten Willen wachsen Funktionen auch bei Ihnen wahrscheinlich weiterhin. Sie mögen das Wachstum bewusster verfolgen, es in Code Reviews immer wieder diskutieren, Maßnahmen zur Eindämmung ergreifen (Refaktorisierung)... Doch am Ende ist jede Metrik dehnbar. Ihre besten Absichten können und werden Sie aussetzen, wenn eine andere Kraft nur stark genug ist.

Das ist so. Darüber sollten wir uns nicht grämen. Es bleibt nämlich so. Unser Fleisch ist schwach. Der Nahkampf mit dem Code im Tagesgeschäft ist dreckig.

Trotzdem, nein, deshalb lohnt eine Frage nach der Ursache solchen grenzenlosen Wachstums. Wie kann es dazu überhaupt kommen? Das passiert ja nicht einfach. Wir tun es. Aber warum?

Wie lassen Funktionen grenzenlos wachsen, weil wir es können.

So einfach ist das.

Und das, was es uns möglich macht und motiviert, Funktionen wachsen zu lassen, das ist die funktionale Abhängigkeit.

Ja, so einfach ist das, glaube ich.

Alles beginnt mit etwas Logik in c():

int c(int a) {
  int x = ... a ...;
  return ... x ...;
}

Dann kommt eine funktionale Abhängigkeit dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  return ... x ...;
}

Und dann kommt noch eine dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  int k = t(z);
  return ... k ...;
}

Und dann kommt noch eine dazu:

int c(int a) {
  int x = ... a ...;
  int y = s(x);
  int z = ... y ...;
  int k = t(z);
  int l = ... k ...;
  int m = v(l);
  return ... m ...;
}

Das geht so weiter, weil nach dem Funktionsaufruf vor dem Funktionsaufruf ist.

Die eigentliche Logik von c() kann beliebig fortgeführt werden, weil ja bei Bedarf immer wieder die Berechnung von Teilergebnissen durch Delegation (funktionale Abhängigkeit) eingeschoben werden kann.

Klingt normal. Dafür sind Funktionsaufrufe ja da. Das möchte ich ganz grundsätzlich auch nicht missen.

Aber wir müssen erkennen, dass genau diese Möglichkeit dazu führt, dass abhängige Funktionen grenzenlos wachsen.

Dem können wir dann zwar mit SOLIDem Denken und Refaktorisierungen entgegenwirken - doch das sind eben Maßnahmen, die angewandt werden können oder auch nicht. Ob das SRP noch eingehalten ist, ob eine Methode schon zu lang ist und refaktorisiert werden sollte... das entsteht im Auge des Betrachters. Darüber kann man sich trefflich streiten in einem Code Review. Und im Zweifelsfall lässt man die Finger von der Veränderung laufenden Codes.

Problem #4: Syntaktische Kopplung

Wenn c() die Funktion s() aufruft, dann muss c() wissen, wie genau das zu geschehen hat. c() muss die Form, die Syntax von s() kennen. Wenn s() einen int-Parameter brauch und sein Ergebnis als int zurückliefert, dann muss c() sich darauf einstellen.

Was aber, wenn die Form von s() sich ändert? Parameter könnten zusammengefasst werden, Parameter könnten Typ oder Reihenfolge ändern... Dann muss c() sich dieser Änderung beugen.

c() muss sich mithin also ebenfalls ändern, wenn sich "nur" an der Form von s() etwas ändert. Und das hat mit der Domäne, der single responsibility von c() nicht unbedingt etwas zu tun.

Problem #5: Semantische Kopplung

Wenn c() die Funktion s() braucht, um anschließend mit deren Ergebnissen weiter zu rechnen, dann ist c() von der Semantik von s() abhängig.

Wenn s() z.B. heute eine Liste von sortierten Daten produziert, die c() weiterverarbeitet, dann kann es eine für c() relevante semantische Veränderung sein, dass s() morgen diese Liste nicht mehr sortiert zurückliefert.

Syntax und Semantik sind orthogonal. An der Syntax kann sich etwas verändern ohne Semantikveränderung. Genauso kann sich die Semantik ohne Syntaxveränderung verschieben. In jedem Fall hat c() nicht die Hoheit über seine innere Ausprägung, sondern muss sich s() beugen. Weil s() "in c() eingebettet ist".

Fazit

Das scheinen mir fünf sehr rationale Gründe für mein Unwohlsein in Bezug auf funktionale Abhängigkeiten. Das ist alles nichts Neues, würde ich sagen. Damit leben wir täglich. Und wenn die Probleme uns nicht so explizit bewusst sind, so leiden wir dennoch an ihnen. Sie verursachen uns Schmerzen beim Ändern oder Verständnis von Code.

Jedes einzelne Problem erscheint vielleicht klein. Und die Auswirkungen sind ja auch unterschiedlich nach Domäne von c() und Form und Semantik von s1(), s2() usw. In Summe jedoch machen diese Probleme aus scheinbar simplen funktionalen Abhängigkeiten eine große Last, glaube ich.

Wir werden nicht ohne funktionale Abhängigkeiten auskommen. Ich möchte die Möglichkeit, sie einzugehen, auch nicht abschaffen. Sie sind ein Mittel zur effizienten Lösung von Problemen. Das wollen wir in unserem Programmierhandwerkszeugkasten haben.

Dieses Mittel darf nur kein Selbstzweck sein. Und es darf sich nicht als alternativlos darstellen. Programmierung mit funktionalen Abhängigkeiten gleichzusetzen, würde sie auf ein unverdientes Podest heben.

Mir geht es deshalb um eine Bewusstmachung und eine Begrenzung. Bindungen bewusst eingehen und immer wieder fragen, ob sie noch ihren Zweck erfüllen, ist wichtig. Je größer die potenziellen Probleme durch Bindungen, desto mehr sollte auch geprüft werden, ob es Alternativen gibt. Denn oftmals werden Bindungen enger geschnürt als nötig. Weil man nicht weiß, wie es anders gehen könnte oder weil man die Notwendigkeit größerer Nähe überschätzt (Premature Optimization).

Mit diesen fünf guten Gründen habe ich also jetzt zumindest für mich geklärt, was mein Gefühl ausmacht, dass funktionale Abhängigkeiten "böse" sind. Ich halte sie für ausreichend, um meine Praxis der Minimierung funktionaler Abhängigkeiten fortzuführen. Denn es gibt Möglichkeiten, sie zu vermeiden.

Mittwoch, 3. April 2013

Clean Code Development anders lernen

Ich hatte einen Traum. Den erfülle ich mir nun. Zusammen mit Stefan Lieser eröffne ich die Clean Code Developer School (CCDS):

image

Mein Traum war schon vor Jahren, Softwareentwicklung entspannter und nachhaltiger für die Teilnehmer zu unterrichten. Zunächst hatten Stefan und ich das mit den CCD Camps versucht, deren Trainings bis zu 14 Tage gedauert haben.

Da hat uns dann aber die Realität schnell eingeholt. So lange mögen Unternehmen ihre Mitarbeiter nicht “von der Leine lassen”. Auch die Aufteilung in kleinere Blöcke mit Pausen dazwischen hat die Attraktivität solch soliden, ausführlichen Lernens nicht erhöht.

Die Konsequenz: Inzwischen sind sind runter auf die üblichen 2, 3 oder maximal 5 Tage Trainingseinheiten. Wir versuchen, die Clean Code Developer Inhalte so zu schneiden, dass Teilnehmer in so kurzer Zeit wirklich etwas mitnehmen.

Leider zeigt sich, dass schon 5 Tage am Stück zu lernen, eine starke Belastung für die Aufmerksamkeitsspanne ist. Ab Tag 4 ist deutlich zu spüren, wie die Luft bei den Teilnehmern ausgeht. Der Lernstoff muss einfach mal sacken, statt unter weiterem begraben zu werden.

Doch die übliche Ökonomie von Trainings drängt darauf, soviel wie möglich in kurze Trainings zu stecken.

Darauf habe ich jetzt aber keine Lust mehr. Und Stefan Lieser auch nicht.

Wir sehen einfach, dass oft das Geld für die üblichen Trainings rausgeschmissen ist. Zuviel Lernstoff in zu kurzer Zeit führt dazu, dass am Ende nichts richtig geübt wurde. Aber vor allem fallen die Teilnehmer nach dem Training in ein Loch. Zurück am Arbeitsplatz fehlt ihnen die Zeit und die Anleitung, um den Stoff weiter zu üben und dann auf ihre Situation zu transferieren.

Das ist für uns als engagierte Trainer frustrierend zu sehen.

Deshalb machen wir es jetzt anders.

Einstweilen geben wir unsere üblichen Trainings zum Beispiel im Rahmen der Clean Code Developer Akademie noch nicht auf. Aber zumindest setzen wir einen Fuß in eine neue Richtung mit der Clean Code Developer School.

Dort findet Lernen nämlich nicht in tagelange Kursen ab und an im Jahr statt, sondern jede Woche in Blöcken zu 4 Stunden.

Wir meinen das mit dem “regelmäßig lernen” ernst. Ohne geht es einfach nicht. Nur findet es am Arbeitsplatz nicht statt. Deshalb machen wir mal ein ganz anderes, quasi ein unwiderstehliches Angebot. Wir kommen mit der CCDS den Lerninteressierten im doppelten Sinn so weit entgegen, dass kontinuierliches Lernen machbar wird.

Natürlich sind wir uns bewusst, dass ein solches Angebot schlechter skaliert als übliche Trainings. Ob und wie es sich für uns rechnen wird, müssen wir sehen. Aber egal. Wir starten das Experiment, weil wir das Richtige im Sinne eines nachhaltigen Lernens für die Teilnehmer tun wollen.

Details zum Was, Wie, Wo, Wann finden Sie auf der Seite der Clean Code Developer School. Hier will ich nicht mehr verraten ;-)

Falls Sie in der Nähe eines Schulungsortes der CCDS sein sollten, schauen Sie doch zumindest mal für einen Schnupperunterricht rein.

Wenn ich Sie für diese Idee begeistern konnte, dann folgen Sie der CCDS doch ab jetzt über ihren RSS-Feed oder bei Twitter: @ccd_school.

Freitag, 29. März 2013

Ab auf die Walz – Software Craftsmanship ernst genommen

Jetzt will ich es endlich tun. Ich will auf die Walz gehen. Schon länger hatte ich mit dem Gedanken gespielt – doch mehr als der prio walk mit Stefan Lieser von München nach Nürnberg ist bisher nicht draus geworden.

Nun möchte ich des Rest aber nachholen. Gewandert bin ich genug. Jetzt möchte ich mitmachen. An verschiedenen Stationen möchte ich Teamkulturen kennenlernen, indem ich für eine begrenzte Zeit “ganz normal” mitarbeite. Als Trainer und Berater sehe ich zwar viele Teams, doch ist da mein Auftrag ihnen bei Veränderungen zu helfen. Deshalb erfahre ich nicht unbedingt, wie sie heute leben und weben. Das möchte ich ändern, indem ich mich ein Teil von ihnen werde. Mit geht es um den Ist-Zustand.

Den Artikel zu dieser Idee für Frühjahr und Sommer 2013 finden Sie in der dotnetpro 5/2013 – oder hier zum bequemen sofortigen Lesen:

Ich würde mich freuen, wenn Sie mir eine Station für meine Walz anbieten würden. Meine Erfahrungen kommen am Ende der Community zugute.

Dienstag, 26. März 2013

Entwerfen fürs Schreiben

Gerade merke ich es am eigenen Leibe, wie wichtig Entwurf ist. Nein, Software entwickle ich gerade nicht. Ich schreibe Texte. Aber ich erlaube mir, meine Erfahrungen dort auf die Softwareentwicklung zu übertragen.

Dass ich viel schreibe, ist ja nichts Neues. Warum fällt mir das Thema Entwurf dazu gerade jetzt ein? Weil ich anders schreibe als sonst.

Normalerweise sind meine Texte bis 20-25 A4 Seiten lang, inklusive Code, Abbildungen, Tabellen. Und das Thema kenne ich gut. Solche Texte kann ich leicht runterschreiben. Thema überlegen – es reicht eine grobe Überschrift oder die Idee für eine Anwendung – und dann losschreiben. Der Rest ergibt sich von selbst. Der Text gliedern sich beim Schreiben von allein. Manchmal unterbreche ich das Schreiben, um zuerst etwas zu codieren, danach geht es dann umso flotter weiter. Alles kein Problem.

Aber jetzt sitze ich an zwei Texten, die sind anders. Da kann ich nicht einfach so drauflosschreiben. Meine Schreiberfahrung und Themenkenntnis gliedert den Text nicht just-in-time. Deshalb muss ich einen Gang zurückschalten. Nachdenken ist angesagt. Entwerfen.

Das Drehbuch

Der eine Text ist ein Drehbuch. Das schreibe ich mit einer Freundin zusammen. Trotzdem ist es nicht einfach. Form und Genre sind für mich neu. Zum Inhalt nur soviel: Es ist eine Liebeskomödie. (Ja, ja, ich höre schon die Lacher. Aber mir macht es Spaß, mal etwas ganz anderes auszuprobieren. Hat sich einfach so ergeben. Ob das mal verfilmt wird? Vielleicht. Erstmal interessiert mich der Prozess.)

Bei dem Dreh konnten wir nicht einfach drauf los schreiben. Wir hatten zwar eine Idee – doch für 90 oder 100 Minuten Film ist das nicht genug. Da muss man sich an den Text ranrobben. Das ist Entwurfsarbeit at its best. Dazu gibt es eine Menge Literatur. Ein Klassiker ist Syd Fields “Das Drehbuch”. Den hatte ich schon vor vielen Jahren mal gelesen… und jetzt ziehe ich endlich Nutzen daraus. Die Zeit musste einfach reif werden. (Es hilft natürlich auch, dass ich nun eine Co-Autorin mit Erfahrung in dem Metier habe.)

Und was tun wir? Wir strukturieren. Kein Satz “Drehbuchcode” ist bisher geschrieben. Stattdessen hangeln wir uns top-down heran.

  • Ebene 1: Die Idee. Wir überlegen ganz grob, worum es geht. Was ist der Kern der Story? Welche Entwicklung sollen die Protagonisten durchlaufen? Haben wir eine “Moral von der Geschicht”? Das ganze passt auf 1 A4 Seite.
  • Ebene 2: Wir verfeinern die Geschichte. Ein paar Highlights kommen auf den Zettel. Wir überlegen, was Wendepunkte sein könnten. Woraus könnten sich Konflikte speisen? Erste Charakterstudien für die wichtigsten Protagonisten. Das passt auf vielleicht 10 A4 Seiten.
  • Ebene 3.1: Nächste Verfeinerungsstufe. Jetzt überlegen wir uns einen Handlungsverlauf, der für bestimmte Entwicklungen/Phasen schon Minutenzahlen zugeordnet bekommt. Dafür gibt es sogar eine Entwurfsskizze. Das ist schon alles ziemlich detailliert. Insgesamt kommen am Ende 30-40 Seiten Fließtext heraus, das sog. Treatment. Die Geschichte wird grob in Szenen eingeteilt erzählt.

image

  • Ebene 3.2: Gleichzeitig entwickeln sich die Charaktere. Wir erfahren immer mehr über ihre Motive und ihr Vorleben. Manchmal geben wir ihnen bewusst gewisse Züge, um in der Geschichte etwas zu bewirken; aber manchmal ergeben die sich von allein. Als hätten die Figuren ein Eigenleben.
  • Ebene 4: Das Drehbuch wird geschrieben. So richtig mit Dialogen und Einstellungsbeschreibungen. Der Umfang wird ca. 90-100 Seiten haben. Aber da sind wir noch nicht.

Dass es so am besten funktioniert, habe ich immer gewusst. Aber ich habe nie selbst Zugang zu diesem Vorgehen gefunden. Insofern kann ich jetzt besser mit Ihnen fühlen, falls Sie es schwierig finden, sich auf Softwareentwurf einzulassen.

Mit ein wenig Anleitung durch meine Co-Autorin hab ich jetzt aber den Sprung in dieses Vorgehen geschafft – und es macht Spaß. Wir haben auch eine schöne Rollenverteilung gefunden. Ich bin eher der Mann fürs Grobe :-) Sie feilt dann die Details aus. Auch beim Drehbuch konzentriere ich mich also eher auf den Entwurf, das Big Picture, den Rahmen. Das liegt mir irgendwie.

Natürlich geht es am Ende wie bei Software nicht ohne Details. Die dreckige Dialogrealität will ausgefleischt werden. Aber das geht viel, viel einfacher auf der Basis eines mehrstufigen Entwurfs.

Der ist auch auf jeder Ebene ein Durchstich. Die ganze Geschichte wird immer wieder erzählt – nur eben mit unterschiedlichem Detailierungsgrad.

Das läuft natürlich nicht sauber in Wasserfallmanier von oben nach unten ab. Es ist eher Jo-Jo-Schreiben. Mal ganz oben, dann wieder kurz durchfallen auf die unterste Ebene. Wir wissen schon ein paar Einstellungen und Sätze für Dialoge, auch wenn wir eigentlich noch beim Treatment sind. Und manchmal gehen wir auch wieder auf eine höhere Ebene und drehen den Fluss der Geschichte in eine andere Richtung.

Das Fachbuch

Das Drehbuch ist Hobby. Arbeit ist das Buch über Flow-Design, an das ich mich nun endlich doch mal gemacht habe. Die Nachfrage ist gestiegen – und ich empfinde das Thema als inzwischen so stabil, dass sich das Aufschreiben lohnt.

Hier bin ich nun voll in meinem Metier. Trotzdem kann ich nicht einfach losschreiben. Dafür ist der Scope zu groß. Ein Blogposting wie dieses oder einen Artikel von 10 Heftseiten für die dotnetpro sind überschaubar. Da kann ich die Text-IDE Word aufklappen und losschreiben. Aber ein ganzes Buch… Nein, das geht nicht. Das würde zwar am Ende auch irgendwie alle Details enthalten – nur in einer nicht sehr leserfreundlichen Weise. Einen so großen Text beim Schreiben entstehen zu lassen, würde zuviel Refaktorisierungsaufwand bedeuten. Das ist wie beim Coden.

Also denke ich vorher über den Text nach. Auch auf mehreren Abstraktionsebenen.

  • Ebene 1: Grobe Gliederung. Ich unterteile das Thema in Bände. Jeden einzelnen will ich als eBook veröffentlichen, sobald er fertig ist. Es kommt also nicht alles erst am Ende als ein Buch heraus.

image

  • Ebene 2: Gliederung eines Bandes. Zu jedem Band überlege ich mir eine Kapiteleinteilung für den groben Erklärungsfluss. Das habe ich aber bisher nur für Band 1 getan. Ich möchte mich konzentrieren auf ein Inkrement. Denn nichts anderes sind die Bände. Jeder für sich bietet schon einen Nutzen.
    Ob es am Ende wirklich soviele Bände oder genau die werden? Keine Ahnung. Das ist auch nicht wichtig. Bisher ist ja kein Aufwand in Band 2 bis 5 geflossen.

image

  • Ebene 3: Ich schreibe mir zu jedem Kapitel Stichpunkte auf. Eine Sammlung von Material, das ich in den Kapitel behandeln will. Das habe ich für Band 1 auch für alle Kapitel gemacht. Hier nur ein Auszug für ein Kapitel:

image

  • Ebene 4: Ich schreibe jedes Kapitel in einem eigenen Word-Dokument. Das mache ich vor allem, um mich besser fokussieren zu können. Aber ich will damit auch technischen Problemen vorbeugen. Wer weiß, wie Word reagiert, wenn am Ende hunderte Bilder in einem Dokument sind und dessen Größe mehrere hundert MB umfasst?
    Das ist jetzt “Codieren”: Ich schreibe den Text, den Sie später lesen sollen. Aber beim Schreiben gliedere ich weiter. Das ist sozusagen situativer Entwurf. Vorher war strategischer und taktischer Entwurf. Hier die dabei entstandene Gliederung für ein Kapitel:

image

Auch dieser Prozess ist natürlich jo-jo-iterativ. Ich kann vom Schreiben jederzeit wieder in den Entwurf. Und ich kann jederzeit zwischen den Entwurfsebenen wechseln.

Der Entwurf gibt mir einen Überblick und definiert Ösen, in die ich dann Fließtext einhängen kann. Ich kann jederzeit an jeder Stelle arbeiten, weil ich weiß, wie der Zusammenhang ist. Aber ich ziehe es vor, Inkremente zu produzieren. Zuviel Springen ist auch nicht gut. Außerdem habe ich zu bestimmten Themenblöcken, die weiter hinten im Buch liegen, noch keine rechte Idee, wie sie konkreter aussehen sollten. Das mache ich auch abhängig von dem, was ich weiter vorne an Text produziere oder was mir bis dahin noch für Gedanken kommen mögen. Ich will keine Entwurfshalde auftürmen.

Auch beim Fachbuch erfahre ich also, wie wichtig es ist, vor dem Codieren, äh, Schreiben mal nachzudenken, einen Rahmen aufzuziehen, ein Big Picture zu entwickeln. Ein Plan ist gut – und ich erwarte nicht, dass der 100% eingehalten wird. Trotzdem ist er besser als keiner.

Beim Entwurf geht es nicht darum, alles komplett vorwegzunehmen. Dann wäre es kein Entwurf mehr, sondern schon Schreiben bzw. Codieren. Ein Entwurf ordnet vielmehr grob. Sein Ergebnis sind handhabbare Blöcke. Ein Kapitel ist so groß wie ein Artikel. Das kann ich ad hoc formulieren und strukturieren. Ein ganzes Buch jedoch, das ist zu groß. Dito bei Software. Eine ganze Software ist zu groß, um einfach mit dem Codieren anzufangen. Aber eine Klasse oder eine Methode, die durch einen guten Entwurf entstehen, sind überschaubar.

Es braucht daher für Software wie für Texte eine Entwurfsmethode. Das ist mir jetzt durch diese Schreiberfahrungen nochmal klar geworden.

Donnerstag, 21. März 2013

Softwareentwurf als ökonomische Notwendigkeit

Softwareentwurf ist selbstverständlich kein neues Thema. Früher war Softwareentwurf als der Codierung vorgelagerte Tätigkeit sogar ein zentraler Punkt jeder Informatikerausbildung. Das war dem Mangel an Prozessorkapazität und Speicher geschuldet. Die Turnaround-Zeiten bei der Codierung waren so lang, dass man sich besser gut überlegte, was man schreibt und dann der Übersetzung übergibt und dann laufen lässt. Das Motto lautete geradezu Think before coding. Fehler waren teuer. Ausdrücklicher Softwareentwurf war also ein ökonomisches Gebot.

Ab der zweiten Hälfte der 1980er begannen sich die Verhältnisse jedoch umzukehren, glaube ich. Der Preis für Fehler beim Codieren fiel plötzlich stark. Zuerst wurde die Hardware besser und verfügbarer (Stichwort PC), dann wurden die Codierungswerkzeuge besser (Stichwort RAD).

Der Engpass „Werkzeug“ wurde bald so weit, dass er durch einen anderen abgelöst wurde. Code war nicht länger das Problem, sondern Codierer. Es kam einfach nicht soviel Funktionalität auf die Straße, wie Kunden sich das wünschten. Das hatte mehrere Folgen:

  • Codierer konzentrierten sich mehr und mehr aufs Codieren. Der Softwareentwurf fiel aus der Gnade, da er scheinbar wertvolle Codierkapazität raubte. Man fühlte sich von der Knechtschaft durch die Maschine befreit und schüttelte daher gern damit bisher verbundene Notwendigkeiten ab.
  • Die Ausbildung der Codierer wurde „entformalisiert“, so dass sich schneller mehr von ihnen auf den Markt bringen ließen. Softwareentwurf war Ballast im Curriculum.
  • Man sann auf Mittel, sich das Codieren überhaupt zu sparen. „Wiederverwendbarkeit herstellen!“ wurde zum Schlachtruf der Softwareentwicklung.

Während viele Jahrzehnte lang (die 1950er bis 1980er) die Softwareentwicklung sich im Grunde um sich selbst drehen musste aufgrund einer eklatanten Mangelsituation bei der Hardware, verschob sich ab den 1990ern endlich der Fokus auf den Kunden. Und der ist seitdem unersättlich. Der Hunger nach Software ist ungebrochen, nein, sogar im stetigen Wachsen begriffen. Mehr Software und länger lebende Software sind gefragt. Berge an funktionalen und nicht-funktionalen Anforderungen soll die Softwareentwicklung erfüllen. Und zwar ASAP!

Software überhaupt auf die Straße zu bekommen, ist seitdem wichtiger, als alles andere. Nachfrage nicht zu bedienen kommt teurer als fehlerhafte Software. Nur in wenigen Bereichen ist die ökonomische Priorität bisher anders geblieben: da wo es um Menschenleben geht.

Doch dieses Bild verschiebt sich. Der Engpass hat sich wieder verschoben. Codierer sind nicht mehr das Problem, auch wenn weiterhin Softwareentwickler fehlen. Die grundsätzliche Kapazität von Codierern ist heute durch Hardware, Softwareinfrastruktur, Tools, Bibliotheken sowie Entwicklungs- und laufzeitplattformen so hoch... dass sie sich selbst durch ihre Produktionsweise behindern.

Der Engpass ist von Ressourcen – zuerst Hardware, dann Softwarewerkzeuge und Menschen – zu einem Grundsatz gewandert. Die Theory of Constraints nennt das einen Policy Constraint. Der lautet salopp formuliert: „Wir hauen den Code raus so schnell die Finger tippen!“

Dieser Grundsatz limitiert heute den Durchsatz der Softwareentwicklung. Die kommt nicht ins Stocken, weil sie unter Hardwaremängeln leidet oder softwaretechnisch irgendetwas nicht umsetzbar wäre. Nein, sie stockt, weil ihr Grundsatz einer überholten Ökonomie angehört.

Der Preis dafür, Nachfrage nicht zu bedienen, ist nämlich gesunken. Oder genauer: Der Preis dafür, Nachfrage heute nicht zu bedienen, ist gesunken. Er ist gesunken gegenüber dem Preis für die Auslieferung fehlerhafter Software. Vor allem wird er aber immer kleiner im Vergleich zu dem, was es kostet, morgen Nachfrage nicht mehr bedienen zu können.

Modern ausgedrückt ist der langjährige Grundsatz nicht nachhaltig.

Das war früher kein Problem. Genauso wie es früher kein Problem war, wenn eine Handvoll Menschen in einem großen Wald Bäume für den Hüttenbau und das Herdfeuer abholzte.

Für große, hungrige Zivilisationen funktioniert derselbe Umgang mit dem Wald jedoch nicht.

Wenn bei naiver Nutzung der Ressourcenbedarf über der „Ressourcenauffrischung“ liegt, dann ist das kein nachhaltiger Umgang mit Ressourcen. Dann kann man heute daraus Nutzen ziehen – aber morgen nicht mehr.

So ist das auch bei „naiver“ Softwareentwicklung. Die funktioniert solange auf grüner Wiese codiert wird. Das war viele Jahrzehnte der Fall. Der Markt war grün, die Software war grün. Es gab keine Altlasten. Und der Hunger nach Funktionalität war so groß, dass alles andere nur wenig zählte. Da konnte man überall seine „braunen Flecken“ hinterlassen. (Von drastischerem Vokabular sehe ich hier einmal ab.)

Doch das hat sich in den letzten 10-15 Jahren verändert. Die Kunden werden anspruchsvoller, sie tolerieren immer weniger Bugs. Vor allem aber muss heute Software schneller und enger am Kunden entwickelt werden. Die Budgets sind gesunken. Und Software ist umfangreicher denn je, muss also länger leben. Auch das ist eine Budgetfrage.

Die grüne Wiese ist bis zum Horizont ein braunes Feld geworden.

Deshalb ist nachhaltige Softwareentwicklung das Gebot der Stunde. Deshalb ist der bisherige Grundsatz der neue Engpass als Policy Constraint.

Daraus folgt für mich aber nicht nur, dass mehr Qualitätssicherung und automatisierte Tests und agile Vorgehensmodelle sein müssen. Es folgt auch, dass Softwareentwurf wieder einen festen Platz in der Softwareentwicklung bekommen muss. Das scheint mir eine ökonomische Notwendigkeit im Rahmen der historischen Bewegungen unserer Disziplin. Denn ausdrücklicher Softwareentwurf macht sich Gedanken über das Big Picture und das Morgen. Wer ständig über dem Code hängt und dort lokale Optimierung betreibt, der fliegt am Ziel Nachhaltigkeit vorbei.

Auszug aus dem Buch “Flow-Design – Pragmatisch agiler Softwareentwurf”, an dem ich nun endlich doch arbeite. Jeden Tag 4 Stunden schreiben… Puh… Demnächst mehr dazu hier im Blog.

PS: Gerade stolpere ich über eine Aussage von Dijkstra:

“[The major cause of the software crisis is] that the machines have become several orders of magnitude more powerful! To put it quite bluntly: as long as there were no machines, programming was no problem at all; when we had a few weak computers, programming became a mild problem, and now we have gigantic computers, programming has become an equally gigantic problem.”

Das entspricht ganz meinem Gefühl – auch wenn das, was er 1972 mit “gigantic computers” gemeint hat, heute lächerlich wirkt.

Mittwoch, 13. März 2013

Wider den sprachlichen Plattform Lock-In

One platform to rule them all – so könnte wohl das Motto vieler Softwareunternehmen/-abteilungen lauten. Man ist ein Java-Shop oder eine .NET-Bude oder eine Ruby-Schmiede usw. Hier und da machen einige Entwickler vielleicht einen Ausflug in andere Gefilde, doch das Hauptprodukt wird auf einer Entwicklungsplattform hergestellt.

Das ist kein Zufall. Das ist gewollt. Das will das Management so, weil Vereinheitlichung eine gute Sache ist. Dadurch kann man immer Geld sparen, oder? Und das wollen die Entwickler auch so, weil das weniger verwirrend ist. Es würde sich doch keiner mehr auskennen, wenn ein bisschen Code in Java, ein bisschen in C#, ein bisschen in Ruby und noch ein bisschen in JavaScript oder Clojure oder C++ vorläge. Oder?

Da ist natürlich etwas dran. Eine gewisse Vereinheitlichung, ein Fokus ist ökonomisch. Aber nur bis zu einem gewissen Grad. Darüber hinaus kippt Vereinheitlichung, d.h. Homogenität um in Monokultur mit all ihren Nachteilen.

Aus Plattform-Fokus wird dann Plattform Lock-In. Man kann dann nicht mehr anders. Die Plattform ist dann eine systemrelevante Größe. An der ist nicht mehr zu rütteln.

Das hat negative Auswirkungen auf die Evolution der Software. Die kann dann nämlich nicht mehr mit der Zeit gehen. Entwicklungen auf anderen Plattformen oder gar Entwicklungen ganz neuer Plattformen ziehen an ihr vorbei. Und oft genug sind es sogar Entwicklungen der Plattform selbst, die man nicht nutzen kann. Gerade neulich war ich wieder bei einem Team, das sich noch an .NET 2.0 bindet. Erweiterungsmethoden, Lambda-Ausdrücke, yield return, Linq, dynamische Typen, aync/await und mehr sind dort kein Thema.

Zu den negativen Auswirkungen auf die Software kommen negative Auswirkungen auf das Team. Teams verkalken, wenn sie Jahre lang auf eine Plattform starren. Sie werden immer mehr zu einer Echokammer, durch die dieselben alten Glaubenssätze und derselbe alte Code hallen. Sie werden damit immer unattraktiver für andere Entwickler.

Bei allem Verständnis für den Vereinheitlichungsreflex halte ich diese Nachteile für so offensichtlich, dass ich mich frage, warum der Plattform-Fokus landauf, landab so groß ist. Mir scheint es eine Lücke in der Argumentation dafür zu geben.

Die meine ich nun gefunden zu haben. Ich glaube, ich kenne jetzt das fehlende Argument der Entwickler für die eine Plattform. Und dieses Argumente ist keines, das sich auf einen Vorteil der einen oder anderen Plattform bezieht. Es geht vielmehr um die Kompensation eines Mangels.

Teams mangelt es an einer anderen Sprache für die Beschreibung von Software außerhalb ihrer Programmiersprache.

Oder anders herum: Ich glaube, Teams fühlen sich zu der einen Plattform hingezogen, weil deren Programmiersprache für sie das einzig verlässliche Kommunikationsmittel über Software darstellt.

Es geht weniger um technische Vorteile einer bestimmten Plattform. Es geht auch weniger um technische Vorteile durch Konzentration auf nur eine Plattform. Es geht auch nicht um personelle Vorteile durch eine Vereinheitlichung [1].

Nein, die eine Plattform dient auch als die eine Sprache. Denn wie ein Blick auf aktuelle Konferenzprogramme zeigt, gibt es keine Sprache, um über Code zu reden. Es gibt nur Code und Technologien/Frameworks. Auf der Meta-Ebene findet im Grunde nichts mehr statt.

Früher gab es die ehrwürdige Disziplin des Softwareentwurfs. Die hatte den Anspruch, eine Lösung sprach-/plattformunabhängig zu beschreiben. Zugegeben, das ist manchmal ein bisschen weit getrieben worden. Doch der Grundgedanke war richtig, finde ich. Code war “nur” ein notwendiges und nicht triviales Implementationsdetail.

Diese Disziplin hat eigene Hochsprachen entwickelt. EBNF, RegEx, Zustandsdiagramm, Struktogramm, UML-Diagramme, sogar noch Blockdiagramme mit dem Schichtendiagramm als Sonderfall gehören dazu.

Doch das ist heute alles hohles Geschwätz. Wer nicht codiert, verschwendet Zeit an Flüchtiges. Diagramme haben keinen Bestand, sie veralten in dem Moment, da man sie implementiert. Also gar nicht erst damit anfangen. Jedenfalls nicht offiziell.

Ich kenne kein Tutorial, das eine Vorgehensweise zur Entwicklung von Software von Anforderungen bis zum Code beschreibt, in dem nicht schnellstmöglich zur Tastatur gegriffen wird. Code ist Trumpf! Code ist die einzige Wahrheit! Das wird mantrahaft wiederholt. Und alles andere wird entweder gar nicht thematisiert oder rein textuell abgehandelt. Und so wird aus einem Text ein anderer. Keine Abstraktion, keine Visualisierung. Diagramme sind selten und unsystematisch [2].

Code und Infrastruktur: das ist der Inhalt des Entwicklerlebens.

Und weil das so ist, müssen Code und Infrastruktur vereinheitlicht sein. Denn sie stellen die einzige Sprache dar, auf die Verlass ist.

Was aber nun, wenn Entwickler sich üben würden, ihre Software auch anders ausdrücken zu können. UML hat dazu einen Vorschlag gemacht – leider einen zu schwerfälligen. Vielleicht geht es ja aber auch einfacher. Ich denke, die Suche nach einer leichtgewichtigen Weise zur Beschreibung von Software, ist lohnend [3]. Dabei geht es ja nicht um einen Ersatz für Programmiersprachen. Dann wäre nichts gewonnen. Leichtgewichtigkeit bedeutet Entlastung von den ganzen Details, die sich beim und durch Codieren ergeben. So wie das Codieren in heutigen Hochsprachen auch eine Entlastung ist von ekeligen Details, wie sie bei der Assembler- und Maschinencodeprogrammierung wichtig sind. Speichermanagement, Registerallokation, Kontrollstrukturenbau und vieles mehr liegt hinter uns.

Der Gewinn einer plattformunabhängigen, vom Code abstrahierenden Sprache läge für mich nicht nur in verbesserter Möglichkeit, allein oder im Team über Codestrukturen nachzudenken. Softwareentwurf könnte damit also besser skalieren. Er würde von einer persönlichen Angelegenheit, die beim TDD-Codieren passiert, zu einer Angelegenheit des Teams. Die Collective Software Ownership würde steigen.

Der Gewinn würde außerdem in einer Befreiung aus der Umklammerung einer Plattform bestehen. Endlich könnten Entwickler verschiedener Plattformen an einem Tisch sitzen und gemeinsam über Softwarestrukturen nachdenken. Die würden sie unterschiedlich implementieren, je nach Plattform. Aber sie hätten zunächst einmal eine gemeinsame Vorstellung vom bigger picture.

Eine leichtgewichtige Sprache, um über und (weitgehend) unabhängig von Code zu “reden”, würde die Softwareherstellung also aus dem Plattform Lock-In befreien. Endlich könnte Software leichter aus Code auf verschiedenen Plattformen zusammengesetzt werden. Und das würde die Evolvierbarkeit von Codebasis und Team beflügeln.

Expliziter Entwurf in einer Entwurfssprache ist kein Selbstzweck. Mir scheint er vielmehr eine Bedingung für die Möglichkeit nachhaltiger Softwareentwicklung.

Medizin, Soziologie, Psychologie, Land- und Forstwirtschaft und Politik haben erkannt, dass Monokulturen mittelfristig schädlich sind. Überall da, wo es um Leben, um Entwicklung geht, fördert Vielfalt dieses Ziel. Nur bei der Software-Entwicklung glaubt man noch Gegenteiliges. Da setzt man noch auf Homogenisierung, Monokultur, Vereinheitlichung, Konsolidierung.

Damit sollten wir aufhören. Dafür ist aber eine praktikable Entwurfssprache und –methode nötig. Daran sollten wir arbeiten. Dazu brauchen wir mehr Ideen [4].

Endnoten

[1] Eine Plattformvielfalt kann personell nur von Vorteil sein. Nicht nur würde sie Diversität im Denken fördern, was inzwischen allgemein anerkannt einen immer größerer Überlebensvorteil für Unternehmen darstellt. Noch konkreter bedeutet Plattformvielfalt eine viel größere Auswahl an Entwicklern.

Wer nur Java oder C# macht, der kann nur aus der Java- oder C#-Community rekrutieren. Wer aber Java und C# und Python und JavaScript und Ruby macht, der kann in all diesen Communities nach neuen Teammitgliedern suchen.

Wo Plattformvielfalt Programm ist, gibt es keine zähneknirschende Einstellung plattformfremder Entwickler mehr, die dann mühsam auf die eigene Plattform umgehoben werden müssen. Dann kann jeder mit seiner Plattform zum Gelingen der Software viel schneller beitragen. Das spart bares Geld!

[2] Hier uns da tauchen mal UML-Diagramme auf: ein Klassendiagramm, ein Sequenzdiagramm. Auch Blockdiagramme gibt es hier und da. Doch auch wenn deren Syntax definiert sein mag, ihre Nutzung ist unsystematisch. Sie ist punktuell, steht in keinem Zusammenhang, ist nicht Teil einer Methode oder übergreifenden Sprache oder Kultur.

Diagramme werden nicht als gleichberechtigtes Mittel neben Code angesehen. Sie sind eher Notlösungen, wenn man grad nicht anders kann.

[3] Natürlich wäre es auch gut, wenn Entwickler sich mit mehr Programmiersprachen beschäftigen würden. Dann wären andere Plattformen ihnen nicht so fremd; sie wären offener, für manche Funktionalität auch mal etwas anderes auszuprobieren.

Dass der wahrhaft polyglotte Programmierer jedoch bald Realität würde, sehe ich nicht. Genauso wenig wie Menschen, die mehr als zwei Fremdsprachen sprechen, selten sind. Bei den natürlichen Sprachen hat sich deshalb eine Universalsprache zur kulturübergreifenden Verständigung durchgesetzt: Englisch.

Allerdings ist das kein Englisch, das alle Feinheiten der englischen Sprache enthält, sondern eine Verflachung. Allemal das gebrochene gesprochene Englisch. Doch das reicht für die meisten Situationen, in denen viele Details, Feinheiten, Nuancen ausgelassen werden können.

Warum nicht genauso in der Softwareentwicklung? Womit natürlich keine verflachte 3GL gemeint ist, sondern eine echte Abstraktion.

[4] Wie gesagt, die UML hat einmal versucht, so etwas zu sein. Doch sie ist gescheitert. Ich würde sagen, an Hybris. Sie hat sich nicht beschränken wollen in Breite und Tiefe. Oder es waren zu viele Köche an diesem Brei beteiligt?

Das bedeutet nicht, dass die UML nichts verwertbares enthielte. Ich sehe aber nicht UML 3.0 als Lösung, sondern eher etwas anderes, das hier und da aus der UML etwas entlehnt, ansonsten aber eigenständig ist.

Mittwoch, 6. März 2013

Software als Web of Services

Für mehr Evolvierbarkeit von Software wie Team müssen wir Software grundsätzlich anders aufgebaut denken, glaube ich. Derzeit stehen Objekte im Vordergrund. Das halte ich für eine überholte Sichtweise. Sie entstammt einer Zeit, da Software weniger umfangreich war und nur mit knappen Ressourcen betrieben wurde.

Historisch hat alles mit einem bunten Gemisch von Anweisungen und Daten innerhalb eines Betriebssystemprozesses angefangen. Der Prozess spannt die Laufzeitumgebung für den Code auf.

image

Unterprogramme entstanden erst, als Programme umfangreicher wurden. Sie sind ein erster Schritt zur Kapselung von Code. Die Zahl der Anweisungen konnte durch sie weiter steigen, ohne dass die Übersichtlichkeit drastisch gesunken wäre.

image

Als nächstes entwickelte man Container für Daten. Sie sollten zu größeren Einheiten zusammengefasst werden. Die Sprache C spiegelt diesen Entwicklungsstand wider: sie bietet Unterprogramme (Prozeduren und Funktionen) sowie Strukturen.

image

Anschließend gab es einen letzte Schritt: die Objektorientierung. Sie hat Strukturen und Unterprogramme zu Klassen zusammengefasst. Dadurch wurde Software nochmal etwas grobgranularer, so dass sich mehr Anweisungen innerhalb eines Prozesses übersichtlich handhaben ließen.

image

Parallel gab es eine Entwicklung von Bibliotheken. Zuerst wurden in ihnen Unterprogramme und Strukturen zusammengefasst, später Klassen.

imageimage

Doch auch wenn Bibliotheken schon lange existieren, sind sie aus meiner Sicht nie zu einem gleichwertigen Container neben Klassen aufgestiegen. Sie werden nicht konsequent zur Strukturierung und Entkopplung im Sinne der Evolvierbarkeit eingesetzt. Ihr Hauptzweck ist es, Wiederverwendung zu ermöglichen. Sie sind die Auslieferungseinheiten für Code, der in unterschiedlichen Zusammenhängen genutzt werden können soll.

Die hauptsächliche Strukturierung von Software befindet sich mithin auf dem Niveau der 1990er, als die Objektorientierung mit C++, Delphi und dann Java ihren Siegeszug angetreten hat.

Die Bausteine, aus denen Software zusammengesetzt wird, sind unverändert feingranular; es sind die Klassen. Der Umfang von Software ist seitdem jedoch kontinuierlich gestiegen. Mehr Hauptspeicher macht es möglich. Früher waren es 640 KB oder vielleicht wenige MB; heute sind es 4 GB und mehr.

Nur eine weitere Veränderung hat sich noch ergeben. Die Zahl der Prozesse pro Anwendung ist gewachsen, weil die Zahl der beteiligten Maschinen gewachsen ist. Das Ideal ist zwar immer noch, so wenige Prozesse wie möglich laufen zu haben, doch Mehrbenutzeranwendungen erfordern für Performance und Skalierbarkeit oft mindestens zwei Prozesse, einen für den Client und einen für den Server.

image

Im Vergleich zur Zahl der in einer Anwendung involvierten Bibliotheken und vor allem der Klassen, ist die Zahl der Prozesse jedoch verschwindend klein. Prozesse sind heute keine Mittel zur Strukturierung von Software im Sinne der Evolvierbarkeit. Sie werden als notwendige Übel betrachtet, um andere nicht-funktionale Anforderungen zu erfüllen.

Komponenten für industrielle Softwareentwicklung

Bibliotheken sind Konglomerate von Klassen. Die gehören inhaltlich zwar zusammen, nur ist ihre Leistung nicht sehr klar definiert. Ihr Angebot, der Kontrakt, ist implizit.

Das ändert sich erst mit Komponenten. Komponenten sind Bibliotheken mit explizitem und separatem Kontrakt.

Mit Komponenten ist es möglich, Software industriell zu entwickeln. An allen Komponenten kann gleichzeitig gearbeitet werden, auch wenn sie gegenseitig Leistungen nutzen. Die Abhängigkeit zur Entwicklungszeit bezieht sich ja nur auf den Kontrakt.

image

Idealerweise würde Software also mit Komponenten statt Bibliotheken strukturiert. Erstens ist die Kopplung zwischen Komponenten durch den expliziten Kontrakt geringer als zwischen Bibliotheken. Implementationen lassen sich dadurch leichter austauschen [1]. Zweitens steigt durch Komponenten die Produktionseffizienz.

image

Komponenten verändern gegenüber Bibliotheken allerdings nicht die Granularität von Software. Sie ziehen eher kein neues Abstraktionsniveau ein, wie es Bibliotheken in Bezug auf Klassen und Klassen in Bezug auf Methoden getan haben.

Außerdem verlangen Komponenten wie Bibliotheken und Klassen für eine Zusammenarbeit Übereinstimmung in der Plattform. .NET-Komponenten können .NET-Komponenten nutzen und Java-Komponenten können Java-Komponenten nutzen. Aus meiner Sicht gibt es keine breit praktikable Lösung, um universell plattformübergreifend Komponenten zu koppeln.

Das soll den Wert von Komponenten nicht schmälern. Sie sind sehr hilfreich und weit unterschätzt. Dennoch sollten wir auch noch einen Schritt weiter denken.

Services für Flexibilität

Der Betriebssystemprozess ist heute immer noch die Funktionseinheit, mit der wir am natürlichsten und einfachsten Code entkoppeln. Er spannt einen eigenen Adress- und Sicherheitsraum auf. Und er läuft autonom auf mindestens einem eigenen Thread.

Darüber hinaus ist der Prozess aber auch die Funktionseinheit, die wir mit einer Entwicklungsplattform assoziieren. Ob der Code eines Prozesses mit C, C++, C# oder Ruby oder Python oder Perl oder in Assembler geschrieben wurde, sehen wir dem Prozess nicht an. Von Prozess zu Prozess kann die Plattform wechseln.

Damit wird der Prozess für mich interessant im Sinne der Evolvierbarkeit. Denn die ist begrenzt, solange die Plattform eine systemrelevante Größe ist. Wer sein ganzes Team auf unabsehbare Zeit an eine Entwicklungsplattform bindet, wer den Plattformwechsel also nicht vom ersten Tag an als (wünschenswerte) Möglichkeit im Auge behält, der begibt sich auf einen unglücklichen Pfad von Co-Evolution von Software und Team: beide erstarren im Verlauf von 4-8 Jahren. Nicht nur wird es immer schwieriger, neue Funktionalität in die Codebasis einzupflegen; es wird auch immer schwieriger, echte Innovationen denken zu können und neue Leute ins Team zu holen.

Am Ende wacht dann ein Team nach Jahren auf und findet sich abgehängt. Die Kunst hat sich weiterentwickelt – nur kann man davon nicht profitieren. Weder hat sich das Team kontinuierlich Kenntnisse angeeignet, noch konnten Kenntnisse in die Codebasis einfließen.

Ohne Komponenten gibt es kaum Codeeinheiten, an denen etwas hinter einer Barriere einfach mal ausprobiert werden könnte, wenn die Plattform neue Features hergibt.

Und selbst mit Komponenten gibt es keine Codeeinheiten, an denen eine vielversprechende Plattform ausprobiert werden könnte. Oder ein vielversprechender Bewerber, dessen Plattformkenntnisse leider nicht so tief sind – der aber Experte auf einer anderen Plattform ist.

Der Code ganzer Prozesse ist heute gewöhnlich zu groß, um solche Veränderung zu erlauben. Auf der anderen Seite sind Prozesse die Codeeinheiten, mit denen solche Veränderungen auf Plattformebene einzig möglich sind.

Deshalb plädiere ich dafür, Software aus mehr, viel mehr Prozessen zusammenzusetzen als heute üblich.

Der Zweck dieser Prozesse ist aber ein anderer als der heutige. Heute dienen sie vor allem kundenrelevanten nicht-funktionalen Anforderungen wie Skalierbarkeit oder Sicherheit. Ich möchte sie nun ja aber zu einem Mittel der Flexibilität, der Evolvierbarkeit von Code und Team erheben. Deshalb nenne ich die Prozesse, um die es mir geht, Services.

Ein Service ist für mich ein Prozess mit einem expliziten und separaten und vor allem plattformneutralen Kontrakt.

image

Ein Service ist mithin eine Kombination aus Komponente und Betriebssystemprozess. Er definiert ganz genau und unabhängig von jeder Implementation, was er leistet. Und er ist autonom, d.h. ausgestattet mit eigenem Adressraum und asynchron gegenüber anderen Services.

Das macht einen Service zu einer vergleichsweise schwergewichtigen Funktionseinheit. Aber ich behaupte, das macht nichts. Es lassen sich in jedem Prozess jeder Anwendung Aspekte finden, die in Services verpackt mit einander arbeiten können, ohne dass dadurch ein Performanceproblem entstünde.

Mein Bild von Software sieht damit so aus:

image

Die Services innerhalb eines Prozesses können in einer Hierarchie zu einander stehen. Eher sehe ist sie aber auf derselben Ebene, verbunden in einem Netzwerk: a web of services.

image

Jenachdem, wie der plattformneutrale Kontrakt eines Services aussieht, kann er auch einfach von einem Prozess in einen anderen bewegt werden. Am Ende sind die umschließenden Prozesse nur Hosts für Services, leisten aber nichts selbst für die Domäne. Sie werden quasi unsichtbar.

Software besteht damit aus Services. Und Services können zusammenarbeiten, auch wenn sie auf verschiedenen Entwicklungsplattformen implementiert sind. Innerhalb eines Host-Prozesses können Services geschrieben in C#, Java, Ruby und Python kooperieren.

image

Solche Vielfalt muss nicht sein. Aber sie kann sein! Es ist möglich, eine Software, deren Services zunächst mit nur einer Plattform implementiert sind, schrittweise zu überführen. Service für Services wird einfach neu geschrieben. Das ist bezahlbar, solange ein Service keine systemrelevante Größe hat. Wenn man es sich nicht leisten kann, einen Service (oder zunächst einmal eine Komponente) neu zu schreiben, falls es schwierig wird mit Veränderungen und Refactoring… dann hat man sich in die Ecke gepinselt. Dann ist die Software wahrlich zum Monolithen erstarrt.

Sie mögen nun einwänden, die Kommunikation zwischen Services sei doch viel zu langsam. Aber wissen Sie das? Haben Sie es ausprobiert, ob Sie höhere Geschwindigkeit brauchen? Ein Roundtrip mit TCP braucht ca. 0,125msec, einer über die I/O-Streams 0,1msec, einer über einen embedded Web-Server wie Nancy im schlechtesten Fall 0,55msec – und ein direkter Aufruf nur 0,00065msec, d.h. er ist rund 500 Mal schneller.

Ja, so ist das. Aber diese im Vergleich schlechteren Werte für cross-service Kommunikation im Vergleich zu cross-component Kommunikation sagt nichts darüber aus, ob cross-service für Ihren konkreten Fall wirklich zu langsam wäre. Haben Sie das überhaupt schon einmal in Erwägung gezogen und ausprobiert? Wahrscheinlich nicht.

Genau dazu möchte ich Sie aber ermuntern. Ich möchte Sie einladen, ein web of services auszuprobieren. Experimentieren Sie damit – denn es gibt etwas zu gewinnen. Sie gewinnen all die Vorteile, die das Internet für seine Evolution und die der an ihm beteiligten Services schon nutzt.

Falls Sie glauben, ich sei der einzige mit solch einer spinnerten Idee, dann schauen Sie einmal bei infoq.com vorbei: Der Vortrag “Micro Services: Java, the Unix Way” bricht ebenfalls eine Lanze für diese Denkweise.

Ich bin überzeugt, wir müssen diesen evolutionären Schritt gehen. Wir müssen ein nächstes Abstraktionsniveau für Funktionseinheiten erschließen. SOA ist mir da schon zuviel. Aber Actors sind mir zuwenig.

Ich glaube mit “einfacheren” Services, die keinen weiteren Anspruch haben als einen separaten plattformneutralen Kontrakt für Funktionalität in einem eigenen Prozess, kann das funktionieren. Technisch ist das kein Hexenwerk. Jeder kann ein web of services aufziehen. REST sehe ich in dieser Hinsicht nicht als hübschen Ansatz für Web-Anwendungen, sondern als grundsätzliches Paradigma der Servicekopplung auch im Kleinen. Für REST braucht es keinen Web-Server; Sie können REST auch mit Service implementieren, der Requests per Standard-Input entgegen nimmt und Responses auf dem Standard-Output liefert.

Es ist vielmehr eine Frage des Willens, so zu denken. Es ist eine Frage Ihres Wertesystems: Was schätzen Sie höher ein? Garantiert bessere Evolvierbarkeit durch Plattformflexibilität – oder vermeintlich benötigte höhere Kommunikationsgeschwindigkeit?

Das ist für mich der Weg für die effizientere Herstellung von evolvierbarer Software: Funktionalität in Komponenten kapseln für bessere Entkopplung innerhalb einer Plattform; Funktionalität in Services kapseln für bessere Entkopplung in plattformneutraler Weise.

Und wenn Sie dadurch nebenbei auch noch Ihre Prozessorkerne besser ausnutzen und die Verteilbarkeit von Code steigt und Sie auf einen größeren Pool von Entwicklern zurückgreifen können bei Neueinstellungen… dann ist das auch nicht schlecht, oder?

Endnoten

[1] Es gibt mindestens zwei Gründe, Funktionseinheiten durch einen expliziten Kontrakt leicht komplett austauschbar zu halten.

Der erste ist leichtere Testbarkeit. Auch wenn es nur eine Implementation des Kontraktes für Produktionszwecke je geben wird, kann es für Tests sehr hilfreich sein, ganz einfach eine Attrappe hinter dem Kontrakt anfertigen zu können.

Der zweite Grund ist die Möglichkeit zur Neuentwicklung. Hinter einem Kontrakt kann die gesamte Implementation ausgetauscht werden, ohne dass Kontraktnutzer davon tangiert würden.

Beispiel: Wenn eine Komponente den Datenbankzugriff kapselt, dann wollen Sie in Tests von abhängigen Komponenten, nicht immer auch tatsächlich auf die Datenbank zugreifen. Also entwickeln sie z.B. eine Attrappe, die Datenbankzugriffe durch Zugriffe auf das Dateisystem simuliert.

Und auch wenn Sie nie beabsichtigen, ein anderes Datenbanksystem als MySQL zu benutzen, kann es Ihnen helfen, die gesamte Implementation der Datenbankzugriffskomponente einfach wegzuschmeißen und neu zu schreiben. Denn Refactoring kann sich irgendwann als zu aufwändig herausstellen.

Ganz zu schweigen davon, dass es mit einer Komponente für den Datenbankzugriff einfach ist, von MySQL umzusteigen auf Oracle oder SQL Server alternativ zu MySQL anzubieten. Doch diese Art der Austauschbarkeit sowie die Möglichkeit zur Wiederverwendung sind für mich eher die geringeren Gründe, mit Komponenten zu arbeiten.