Follow my new blog

Posts mit dem Label Architektur werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Architektur werden angezeigt. Alle Posts anzeigen

Mittwoch, 20. August 2014

Konstruktivistische Softwareentwicklung für mehr Wandelbarkeit

Code sei ein Mittel, um die Realität abzubilden. Zumindest scheint mir das ein wesentlicher Antrieb hinter der Objektorientierung zu sein. Mit Objekten sollte es endlich möglich sein, Dinge und Verhältnisse der Welt möglichst 1:1 in Code zu fassen.

Das ist eine platonische Sichtweise der Welt. Sie geht davon aus, dass es etwas unabhängig von Code gibt, das irgendwie ist. Und dieses So-sein kann man mehr oder weniger getreu in Code abbilden. Und je getreuer es abgebildet wird, desto besser ist das für die Software in irgendeiner Weise.

Ich glaube, dieses Weltbild sollten wir nun hinter uns lassen. Es hat uns zu der Software geführt, die wir haben. Die funktioniert, die skaliert - aber die ist nur schwer wandelbar.

Vom Platonismus sollten wir zum Konstruktivismus wechseln.

Wenn wir konstruktivistisch Software entwickeln, dann ist es nicht mehr wichtig, eine Realität getreu abzubilden.

Bei der platonischen Softwareentwicklung gibt es drei Bereiche, die in Deckung sind: die Realität (Domäne), unsere Wahrnehmung der Realität (Entwickler), Code. Als Entwickler einer Buchhaltungssoftware sehe ich eine Rechnung (Domäne) und lege dafür eine Klasse Rechnung und eine RDBMS-Tabelle Rechnungen an.

Bei der konstruktivistischen Softwareentwicklung hingegen, muss das, was ich als Entwickler in der Realität erkenne, nicht im Code auftauchen. Auch wenn ich eine Rechnung als Gegenstand in der Hand halte, führt das nicht Zwangsläufig zu einer Klasse oder einer einer Tabelle.

Leider muss ich hier von drei Bereichen sprechen, weil Software sich nicht selbst anpassen kann. Wir als Entwickler müssen das als Mittler tun. Deshalb haben wir bisher auch versucht, unsere Sicht der Welt in der Software zu manifestieren.

Aber es geht gar nicht um uns. Wenn wir für unser Überleben Rechnungen, Autos, Katzen, Götter, ein Ich in der Welt sehen wollen, dann ist das unsere Sache. Das ist unsere Konstruktion, die über das gelegt ist, was irgendwie ist.

Der Stoff aus dem die Konstruktionen sind, sind unsere sinnlichen Wahrnehmungen und a priori Grundkonzepte. Die haben wir uns nicht ausgesucht, sondern sie definieren uns. Die Evolution hat dazu geführt, dass wir als Organismen so sind, wie wir sind. Mit unserer Form und unseren Wahrnehmungen haben wir größere Stabilität in einer bestimmten Umwelt erreicht als mit einer anderen Form und anderen Wahrnehmungen.

So ist das mit Evolution. Es geht um größere Stabilität von Strukturen in einer (Um)Welt.

Und so ist es auch mit der Softwareevolution. Es geht darum, wie Software größere Stabilität erreicht in einer stetig im Wandel befindlichen Umwelt.

Wir haben es nun einige Jahrzehnte versucht, die Lebensfähigkeit durch getreuer Abbildung von durch uns wahrgenommener “dinglicher” Realität zu erhöhen. Das hat nicht so geklappt, wie gewünscht, würde ich sagen. Und nur das zählt.

Also sollten wir es anders versuchen. Befreien wir Software vom Abbildungszwang. Das einzige was zählt ist, dass Software als Ganzes ihren Zweck erfüllt (Funktionalität + Qualität) und sich zügig an eine gewandelte Umwelt anpassen lässt (Investitionssicherheit).

Die Struktur von Software muss also nicht zwangsläufig irgendetwas widerspiegeln, was wir als Menschen als Dinge in der Welt erkennen. Wenn wir ein Formular auf dem Tisch liegen haben, dann mag es noch hilfreich sein, das Formular in der Software als geDialog wiederzufinden. Ja, vielleicht ist das so. Vielleicht aber auch nicht. Machen wir uns da mal ganz locker.

Vor allem sollte uns ein Formular auf dem Tisch nicht dazu verleiten, unterhalb der Oberfläche im Code das Formular nochmal zu erschaffen. Und dann ein weiteres Mal auf der Festplatte - weder als Verbund von RDBMS-Tabelle noch als NoSql Dokument.

Damit will ich nicht sagen, dass das nicht so sein darf. Vielleicht ist es hier und da vorteilhaft, das Formular im Code wiederzufinden. Aber wir sollten das sich ergeben lassen und nicht im Sinne eines Weltbildes an den Anfang setzen.

Wenn wir schon mit dem Paradigma unserer Hauptwerkzeuge, den Programmiersprachen, Softwarestrukturen einem platonischen Weltbild unterwerfen, dann schränken wir die Freiheit der Evolution von Software ein. Wir machen es ihr schwer, in einer fluktuierenden Umwelt zu überleben, weil wir sie massiv fixieren.

“Abbildung der Realität” ist aber nicht, worum es geht. Einziger Markstein ist Zufriedenheit des Kunden - zu der gehört, dass Software quasi unsterblich ist, weil sie sich auf ewig anpassen lässt.

Natürlich haben wir wenig Erfahrung mit der Herstellung von Unsterblichen. Wer hat solche Erfahrung schon? ;-) In jedem Fall scheint mir jedoch ein schlechter Ausgangspunkt dafür, die Abbildung von objektbeladener Realität. Denn: Wenn sich diese Objekte in der Realität andauernd ändern, dann muss sich ja auch die Software andauernd ändern. Das umso häufiger und breiter, je tiefer die Objekte der Realität in der Software verankert sind.

Mir scheint, wir brauchen für evolvierbare Softwareentwicklung nicht mehr Objektorientierung, sondern das Gegenteil: Anti-Objektorientierung.

Java und C# und Ruby müssen wir deshalb nicht sofort auf den Müll werfen. Letztlich sind die Sprachen unschuldig. Auf den Müll muss das kontraproduktive platonische Weltbild. Denn das Weltbild steuert, wie wir die Sprachen einsetzen.

Mit einem neuen Weltbild können wir auch mit den überkommenen Werkzeugen durchaus neue, besser passende Strukturen schaffen. Konstruktivistische Softwareentwicklung ist mit C#, Java, Ruby, JavaScript, F# usw. möglich. Mal leichter, mal schwerer.

Hören wir also auf, das Innen der Software so zu strukturieren, wie wir meinen, dass die dingliche Realität aussieht. In Ihrem Gehirn finden Sie den Computer nicht, auf den Sie schauen. Es darin auch nicht den Stuhl, auf dem Sie sitzen. Oder den Raum, in dem Sie sich befinden. Weder die Anatomie des Gehirns noch die Signale zwischen den anatomischen Strukturen haben irgendeine Ähnlichkeit mit der Umwelt. Innen ist nicht wie außen.

Allerdings befähigt Sie der Aufbau Ihres Gehirns (plus Körper), als Ganzes in der Umwelt zu überleben. Nur das ist es, was zählt.

Genau das müssen wir für Software auch erkennen. Ein erster Schritt: EventSourcing und EventStores.

Ein EventStore löst realweltliche Strukturen auf. Sie finden sich in der Software nicht mehr so wieder, wie wir sie als Entwickler in der Domäne gezeigt bekommen. Aber das ist nur der Anfang.

Ein nächster Schritt: Bounded Contexts und Aggregate.

Bounded Contexts und Aggregate lösen die Vorstellung von dem einen Datenmodell, von der einen Datenstrukturrealität auf.

Und noch ein Schritt: Inkrementelle Architektur. Das ist für mich die grundlegende Orientierung von Softwarestruktur an Durchstichen und Nutzen statt an Technologien, Infrastruktur und überkommenen Patterns (z.B. MVC, Layers).

Inkremente lösen die Vorstellung auf, dass es um Dinge ginge bei der Softwareentwicklung. Nicht jedoch das Dokument ist das Wichtigste, sondern der Prozess. Der ändert sich zuerst, ihm folgt ein eventuelles Dokument. Aber der Prozess, die Tätigkeit, das Verhalten sind weniger greifbar, sind keine Dinge. Deshalb tut sich die platonische Softwareentwicklung mit ihnen schwer.

Weitere Schritte werden wir noch erkennen, denke ich. Wenn wir uns darauf einlassen, unser Weltbild bzw. das von Software umzubauen.

In der Psychologie hat der Konstruktivismus “gewonnen”. Ich denke, davon sollten wir lernen als Softwareentwickler.

Was außerhalb von Software existieren mag, ist eine Sache. Eine ganz andere ist es, wie Software intern organisiert ist. Ähnlichkeit muss es zum Außen nicht geben. Nur Überlebenstauglichkeit. Dafür braucht es vor allem… Wandelbarkeit.

Donnerstag, 14. August 2014

Software systematisch wachsen lassen

Mir gefällt die Metapher von der “wachsenden Software”. Ein schöner bildlicher Gegenentwurf zur Hausbaumetapher. Aber bisher hat mir dabei immer etwas gefehlt. Wie funktioniert denn das mit dem Wachsen genau? Software wächst ja nicht von allein, sondern durch unseren Eingriff von außen.

Das Buch “Growing Object-Oriented Software Guided by Tests” (GOOS) hat versucht, diesen Wachstumsprozess, nein, eher die Wachstumstätigkeit zu konkretisieren. Tests sollen das Wachstum antreiben und ausrichten.

Das ist sicher richtig, insofern Tests für Anforderungen stehen. Alles beim Wachstum von Software sollte durch konkrete Anforderungen motiviert sein. Doch was tun, nachdem ein Test definiert ist? Die Struktur des Produktionscodes ergibt sich ja nicht von allein, sobald man einen automatisierten Test hat.

Da greift das Buch bzw. die ganze Metapher bisher für mich zu kurz.

Heute morgen jedoch ist mir aufgegangen, wie das, was ich bisher als Softwareuniversum bezeichnet habe, eigentlich eine Anleitung zum Wachstum ist, also sozusagen die DNA für einen Softwareorganismus.

Eine ausführliche Beschreibung des Softwareuniversums finden Sie in meinem Buch The Architect’s Napkin - Der Schummelzettel. Im Weiteren benutze ich die Begriffe aus dem Softwareuniversum ohne ausführliche Erklärung. Ich möchte mich auf ihre Neuordnung konzentrieren.

Anforderungen als Nahrung

Getrieben wird das Wachstum von Software durch Anforderungen. Doch die sollten nicht allen in einen Topf geworfen werden. Eine differenzierte Sichtweise lohnt sich, denn unterschiedliche Arten von Anforderungen müssen beim Wachsen auch unterschiedlich behandelt werden.

Ich unterscheide drei Kategorien von Anforderungen:

image

Funktionale Anforderungen beschreiben, was Software tun soll, z.B. rechnen oder Auktionsangebote zugänglich machen. Qualitätsanforderungen geben vor, wie diese Funktionalität vollbracht werden soll, z.B. wie schnell, wie sicher usw.

Mit diesen Anforderungen werden Sie normalerweise durch den Kunden konfrontiert. Doch es gibt noch eine weitere Kategorie, die dem Kunden wichtig ist, auch wenn er meist dazu schweigt. Er hat nämlich auch noch Anforderungen im Hinblick auf die Investitionssicherheit seiner Software. Er möchte, dass Funktionalität und Qualität sich stets zügig wandeln lässt.

Wenn wir Software wachsen lassen wollen, müssen wir also darauf achten, dass sie in Richtung aller Anforderungen wächst. Und das systematisch. Und auch noch diskutierbar und kommunizierbar.

Wie das nur durch eine “guidance by tests” gehen soll, ist mir schleierhaft. Dem GOOS-Ansatz fehlt schlichtweg jedes Meta-Modell.

Der Softwarebaum

Anders sieht es aus, wenn ich die Dimensionen des Softwareuniversums in Form eines Baumes anordne:

image

Jetzt gibt es einen klaren Weg. Jetzt gibt es ein big picture. Jetzt gibt es “Wachstumsbausteine”.

Der Stamm

Am Anfang des Wachstums steht der Stamm. Hier liegen die Anforderungen unmittelbar an. Der Stamm wird im Dialog mit dem Kunden entwickelt. Das ist wichtig, denn auf ihm ruht am Ende die Krone mit all ihren Ästen.

image

Beim Stamm geht es darum, die Gesamtanforderungen immer feiner zu zerlegen in Inkremente. Das sind kundenrelevante Zuwächse in Form von Durchstichen. Zu denen kann der Kunde klares Feedback geben. Zu denen kann er natürlich auch Akzeptanzkriterien formulieren, die sich (hoffentlich) in automatisierte Tests übersetzen lassen.

Das größte solche Inkrement ist ein Bounded Context, das kleinste eine Interaktion.

Im Bild sehen Sie, dass der Stamm aus mehreren Ebenen besteht. Jede beschreibt ein anderes Abstraktionsniveau und besteht wiederum aus Artefakten der nächsten Ebene.

Der Funktionalitätsast

Aus dem Stamm entwickelt sich als erster Hauptast die Funktionalität. Für jede Interaktion des Stamms wird durch Zerlegung in einer Hierarchie von Datenflüssen bestimmt, wie das gewünschte Verhalten erzielt werden soll.

image

Im Bild steht, dass das zur Codierungszeit geschieht. Das ist insofern richtig, als dass dann mittels des Integration Operation Segregation Principle und des Principle of Mutual Oblivion die Herstellung von Datenflüssen gesichert werden muss. Aber natürlich sollen diese Flüsse vorher (in angemessenem Umfang) entworfen werden.

Die Länge und Verzweigungstiefe des Funktionalitätsastes ist beliebig. Er wird so ausladend, wie es nötig ist. Die Schachtelungstiefe seiner Funktionseinheiten ist unbegrenzt - wenn auch in der Praxis natürlich sehr endlich ;-) Die Knoten in diesem Teilbaum sind Integrationen, die Blätter Operationen.

Der Qualitätsast

Auch wenn Funktionalität das erste ist, was sich aus dem Stamm entwickeln soll, ist es weder das Einzige noch das Wichtigste. Software wird ja nicht für Funktionalität gemacht, sondern für Qualität.

image

Sobald also die Funktionalität soweit heruntergebrochen ist, dass sie sich in dünnen Inkrementen ausliefern lässt (Produktionseffizienz), und auch noch klar ist, was dann intern passieren muss, ist daran zu denken, wie die Qualitätsanforderungen eingehalten werden können. Es geht dann um Laufzeitcharakteristika wie Performance, Skalierbarkeit oder Robustheit.

Dazu sind andere Bausteine in den Blick zu nehmen. Bei der Funktionalität ging es im Wesentlichen um Funktionen, bei der Qualität geht es nun um Threads, Prozesse, Geräte usw. Die wiederkehrende Frage lautet: Wie sollten die Funktionen des funktionalen Entwurfs auf diese so genannten Hosts verteilt werden, um die geforderten Qualitäten herzustellen?

Der Wandelbarkeitsast

Last but not least das Thema Evolvierbarkeit. Auch wenn (oder gerade weil) der Kunde dazu nur eine diffuse Vorstellung hat, müssen Sie sich darum explizit beim Softwarewachstum kümmern.

image

Wenn klar ist, welche Funktionen die Funktionalität herstellen und wie die auf Hosts verteilt werden, ist zu überlegen, zu welchen so genannten Containern sie zusammengeschnürt werden sollen.

Welche Klassen, Bibliotheken, Komponenten, µServices sollte es geben, damit die Wandelbarkeit über lange Zeit erhalten bleibt?

Kohäsion und Kopplung sind die treibenden Kräfte bei der Containerfindung. Sie ist damit ein Thema der Entwurfszeit.

Wachstumsphasen

Der Softwarebaum wächst vom Stamm zu den Ästen. Die Reihenfolge dabei ist jedoch nicht so streng zu sehen, wie ich bisher vielleicht suggeriert habe. Je nach Umfang des zu entwickelnden Softwaresystems kann auf den Stamm auch der Qualitätsast folgen und dann erst der Funktionalitätsast. Der Evolvierbarkeitsast jedoch ist meist der letzte, der sprießt.

Doch auch das geschieht nicht nicht nur einmal, sondern wiederholt. Der Softwarebaum wächst in Phasen: ein bisschen Stamm, dann der Funktionalitätsast, dann der Qualitätsast, dann der Evolvierbarkeitsast - und schließlich geht es wieder von vorne los.

Wachstum ist mithin im Grunde immer überall am Werk. Stamm und Äste wachsen im Umfang und in der Breite.

Dass der Softwareorganismus nicht wie ein Schwamm wächst, sondern strukturierter, differenzierter, steckt im Meta-Modell. Das gilt, so denke ich, grundsätzlich für jedes Softwareprojekt. Denn die drei Anforderungskategorien sind ja universell. Und deren systematischer Bearbeitung dient die anatomische Grundstruktur des Softwarebaums. Keine Anforderung darf unter den Tisch fallen.

Auch wenn die endgültige Architektur einer Software über die Zeit emergieren mag, so sollte das nicht planlos geschehen. Tests können ruhig am Anfang jeder Umsetzung eines Inkrements stehen. Doch wie kommen Sie zu Inkrementen? Sie lassen sie nach dem Schema des Stamms wachsen. Und wie dann weiter, wenn die Tests für ein Inkrement definiert sind? Dann treiben Sie das Wachstum entlang der drei Äste Funktionalität, Qualität und Evolvierbarkeit weiter. Mit klaren Vorstellungen von Bausteinen und Regeln.

Softwareentwicklung ist keine Sache, die aus dem Handgelenk und intuitiv funktioniert. Da braucht es schon etwas mehr Systematik. Die liefert der Softwarebaum, finde ich. Er verbindet Klarheit und Struktur mit organischer Entwicklung.

Dienstag, 3. Juni 2014

Klein ist ökonomisch

Es kommt auf die Größe an – zumindest bei der Wartbarkeit (oder besser: Evolvierbarkeit). Über Helge Nowaks Präsentation bin ich auf diesen Text gestoßen und darüber dann auf einen Video-Vortrag:

Viktigste faktorer for å redusere teknisk gjeld - Dag Sjøberg from Smidigkonferansen on Vimeo.

Keine Angst, Sie müssen nun nicht Ihr Norwegisch abstauben. Ich denke, interessante Einsichten lassen sich auch aus den Vortragsfolien ziehen.

Vorab aber die Geschichte hinter dem Vortrag: Es wurde ein Experiment zu Wartungskosten für Softwaresysteme gemacht. Dazu wurde dasselbe System bei vier Softwarehäusern in Auftrag gegeben. Anschließend wurden alle vier Systeme gleichermaßen genutzt und also mit realen Daten befüllt. Und dann hat man allen Softwarehäusern einen Änderungsauftrag erteilt.

Mit dem Experiment sollte herausgefunden werden, welche Merkmale von Software mit guter/schlechter Wartbarkeit korrelieren. Deshalb hat man auch noch Softwarequalitätsexperten befragt, wie sie die Wartbarkeit der Systeme einschätzen.

Das spannende Ergebnis: Die Experten lagen daneben mit ihren Metriken. Vorhersagekraft hatte allein die Zahl der Codezeilen (Lines of Code, LOC).

image

Verblüffend, oder?

Denn dem stehen diese Vorhersagen gegenüber:

image

Systeme B und D sollten die beste Wartbarkeit aufweisen, A und C die schlechteste.

Nun könnten Sie sagen, der Aufwand für B und D lag nicht soviel über dem von A wie C. Für B mit dem besten Vorhersagewert war nur ca. 50% mehr Aufwand als für A nötig. Für D nur knapp 100% mehr. Das sind doch keine großen Missweisungen wenn man an das Verschätzen bei der Softwareentwicklung im Allgemeinen denkt.

Gegen diese Interpretation spricht für mich aber zweierlei: Zum einen ist der Vorhersagewert von A der schlechteste und auch noch für sich genommen grottig. Zum anderen steht hinter den schlechteren Wartungsaufwänden bzw. besseren Vorhersagewerten ein deutlich höherer Anfangsaufwand:

image

Meine Norwegisch-Kenntnisse geben mir ein, hier wird gefragt, ob wirklich mehr Aufwand zu geringeren technischen Schulden (lies: mehr Evolvierbarkeit) geführt hat. Leider muss die Antwort Nein lauten. Die laut Vorhersage besser wartbaren Systeme haben zwar knapp 100% bzw. 250% der Aufwands erfordert, der in den Letztplatzierten gegangen ist. Doch trotzdem hat der sie am Ende deutlich geschlagen.

Bessere Metrikwerte waren also teuer – und haben es nicht gebracht. Das ist bitter, oder?

Das am besten wartbare System hat pro Person am wenigsten Zeit in der Entwicklung gekostet und am wenigsten LOC enthalten.

Ist das eine triviale Aussage? Hm… ich glaube nicht. Denn sonst hätten die Experten ja besser geurteilt.

Schlussfolgerungen

Ich denke, es lassen sich aus dem Ergebnis einige Schlussfolgerungen für die Praxis ziehen. Wenn weniger LOC bessere Evolvierbarkeit bedeuten, dann müssen wir alles (naja, zumindest vieles) daransetzen, weniger LOC herzustellen. Und das geht so:

1. YAGNI

Weniger LOC beginnen nicht so sehr beim Entwickler, sondern eher beim Kunden bzw. beim PO. Er sollte noch schärfer darüber nachdenken, was wirklich, wirklich an Features benötigt wird. Und er sollte in noch dünneren Inkrementen codieren lassen, um schneller sagen zu können “Good enough!”. Ich bleibe also dabei, das Spinning eine Kernpraxis jeder Softwareentwicklung sein sollte.

Code erst gar nicht zu schreiben, ist das beste Mittel, um LOC zu reduzieren.

Aber das geht natürlich nicht nur den PO etwas an, sondern auch die Entwickler. Gern wird auf Vorrat implementiert, weil man ja soviel Erfahrung hat, was da noch alles kommen mag. Dann wird vorhergesehen und flexibilisiert, was das Zeug hält. Wiederverwendbarkeit ist ganz beliebt als herzustellendes Merkmal – auch wenn niemand weiß, was wie viel wiederverwendet wird.

2. KISS

Was dann implementiert wird, sollte so einfach wie möglich sein. Da verschwimmt manchmal die Grenze zwischen YAGNI und KISS, aber ich denke, jeder kennt Beispiele, wo etwas kompliziert gelöst wurde, wo es einfacher gegangen wäre. Der Grund: Unkenntnis, vermeintlicher Zeitmangel.

Auch für Code gilt Pascals (oder Goethes?) Ausspruch: Wenn ich mehr Zeit gehabt hätte, hätte ich mich kürzer gefasst.

Wie das Experiment zeigt, ist Zeitaufwand gesteckt in weniger LOC aber gut investierte Zeit. Die macht die später unabsehbaren “Wartungsarbeiten” kostengünstiger.

Natürlich gilt es da eine Balance zu finden. Mancher elegante Einzeiler ist am Ende schwerer zu evolvieren als etwas gesprächigerer Code.

3. Knappe Programmiersprache

Die LOC-Frage wirft eine zweite, womöglich schon zu den Akten gelegte auf: Welche Programmiersprache sollte benutzt werden, um das Nötige simpel zu codieren?

Früher ging es um Compiler- und Ausführungsgeschwindigkeiten. Heute ist das nicht mehr so ein großes Thema. Stattdessen sollte es um Knappheit (terseness) gehen. Natürlich Knappheit, die zur Lesbarkeit beiträgt. Das kann man von PERL oder APL eher nicht sagen, aber wohl z.B. von F#.

Welche Sprache bietet gute Abstraktionen, welche Sprache bemüht sich um “Rauschunterdrückung”? Java hat da heute für mich z.B. eher einen hinteren Platz. C# scheint mir im Mittelfeld. Und wie steht es mit Modernem wie Go, Exlixir, Swift?

Einerlei. Ich will hier für keine Sprache werben, sondern nur auf ein Mittel zur Reduktion der LOC für mehr Evolvierbarkeit aufmerksam machen. Die Sprachfrage sollte nicht leichtfertig abgetan werden mit dem Verweis aus die Tradition: “Wir haben halt immer schon in X entwickelt.”

4. Granulare Abstraktion

Eine Programmiersprache bietet mehr oder weniger Abstraktion. Mehr bedeutet gewöhnlich weniger LOC. Noch mehr bedeutet noch weniger LOC – aber ab einem gewissen Punkt geht das auf Kosten der Universalität. Beispiel SQL: damit lassen sich sehr knapp Anweisungen ausdrücken, aber SQL ist nicht computationally complete.

Durch die Wahl einer 3GL lassen sich die LOC also nur auf ein gewisses Maß reduzieren. Weiter muss es dann mit DSLs (Domain Specific Languages) gehen. Die gibt es nur nicht für alle Problemdomänen. Und es ist nicht zu erwarten, dass sich das grundlegend ändert. Einzelne Teams haben es schon schwer genug, Bibliotheken mit vernünftigen Abstraktionen zu entwickeln, da sind eigene DSLs in weiter Ferne. Sie haben ihren Platz – aber ihr Beitrag zur LOC-Reduktion für den Mainstream wird gering bleiben.

Wichtiger finde ich es daher, mit den Mitteln der gewählten 3GL eigene Abstraktionen auf unterschiedlichem Niveau zu schaffen. Die Mittel sind Container wie Funktion, Klasse/Modul, Komponente, (micro)Service.

Mein Annahme ist, dass sich das Versuchsergebnis auf verschiedene Größenordnungen übertragen lässt. Zwei Funktionen, die dasselbe leisten, sind je nach LOC unterschiedlich evolvierbar. Zwei Klassen, die dasselbe leisten, sind je nach LOC unterschiedlich evolvierbar. Usw. usf.

Kleine Funktionen, kleine Klassen, kleine Komponenten, kleine Services sind also anzustreben. Funktionen mit 3000 LOC, Klassen mit 100.000 LOC, die Abwesenheit von Komponenten und Services… das alles sind Zeichen der Unwartbarkeit.

Wer Evolvierbarkeit will, der sollte sich also stetig um kleine Container bemühen und um Container auf weiteren Abstraktionsebenen oberhalb von Klassen. Wo es nur Funktion, Klasse, Softwaresystem und das auch noch ohne LOC-Begrenzung gibt, ist der Monolith vorprogrammiert.

Wie kommen Sie denn aber zu kleinen Funktionen und Klassen? Die Forderung ist ja auch schon älter, ohne dass sie einen spürbaren Effekt gehabt hätte. Manche sagen, Funktionen sollten nur 7 oder 20 Zeilen haben. Andere sagen, sie sollten nicht länger als eine Bildschirmseite sein (aber mit welcher Auflösung).

Ich glaube, die Forderung nach einem bestimmten max. Zeilenzahl bringt uns nicht weiter. Wir brauchen einen Ansatz des Softwareentwurfs, der ganz natürlich zu kleinen Funktionen und Klassen führt. Die bisherige Objektorientierung leistet das nicht. Wir müssen nachbessern.

In meiner Codierungspraxis gibt es inzwischen keine Funktion mehr, die länger als 50 Zeilen ist (und mehr als Triviales tut). Nicht, weil ich mich durch StyleCop warnen lasse, sondern weil ich Software mit Flow-Design nur so entwerfen kann, dass alle Funktionen klein sind. Wenn Sie wissen wollen, wie das funktioniert, folgen Sie doch dem “Book in Progress” von Stefan Lieser und mir: The Architect´s Napkin Kata für Kata.

Eine größere Zahl von Funktionen und Klassen braucht dann natürlich wieder Container. Das Softwaresystem als Ganzes ist zu groß. Deshalb gehören für mich Komponenten- und Serviceorientierung ganz natürlich zu den Maßnahmen, die LOC pro Container zu reduzieren. Die einzuführen ist aber natürlich nochmal eine andere Nummer als die LOC-Reduktion bei den schon in Gebrauch befindlichen Containern Funktion und Klasse.

Aber es lohnt sich, denke ich. Durch Komponenten und Services wird es nicht nur mit LOC besser pro Container, sondern auch mit der Produktionseffizienz. Denn durch die expliziten Kontrakte kann an Komponenten und Services viel besser arbeitsteilig parallel gearbeitet werden. Darüber hinaus bieten Services die Chance, im selben Softwaresystem mehrere Runtimes/3GLs einzusetzen. Plattformneutrale Kontrakte machen es möglich. Ausführlicher dazu in The Architect´s Napkin – Der Schummelzettel.

Ziel ist in jedem Fall, mit den vielen kleinen Containern ein eigenes Domänenvokabular, gar eigene “Domänensätze” zu formulieren. Mit dem können Sie dann immer neue Geschichten (user stories) “schreiben”.

5. Geschichten trennen

Container sind ein technisches Granulat. Ich denke, es gibt aber auch aus Sicht von Kunde/Benutzer die Möglichkeit, granularer zu entwickeln, d.h. mehr Funktionseinheiten mit jeweils weniger LOC zu bilden.

Das Problem beginnt hier wieder beim Kunden. Der will oft “alles unter einem Hut”. Der sucht die “one size fits all” Anwendung. Die wächst und wächst dann und enthält alle LOC.

Was aber, wenn die Anforderungen nicht durch nur eine Anwendung, sondern durch viele umgesetzt würden. Nicht ein Icon auf dem Desktop, nicht eine URL im Intranet für alles, sondern viele.

Die Smartphones machen es vor. Statt einem Boliden wie Outlook vertrauen wir uns einem Schwarm aus kleinen Apps für Email, Kalender, Aufgaben, Notizen an. Viele Spezialisten statt ein Generalist. Das sind viele kleine Zweige unabhängiger Evolutionsmöglichkeit.

Jede App ist auf eine Domäne spezialisiert, erzählt sozusagen eine eigene Geschichte. Warum nicht dasselbe tun mit CRM, ERP, Buchhaltung, Qualitätsmanagement, Vertragsverwaltung usw. usf.? Das gesamte Softwaresystem enthält dann immer noch 100% aller LOC – nur ist es unterteilt in Apps mit jeweils einem Bruchteil der LOCs. Die lassen sich für sich leichter evolvieren.

Und innerhalb von Apps können Sie weiter Geschichten trennen. Apps müssen keine Monolithen sein, sondern können aus Modulen bestehen.

Am Ende geht es also nicht um ein Softwaresystem, sondern um einen bunten Strauß ein Modulen in Apps zusammengefasst. Das ist dann nicht eine monolithische Codebasis. Vielmehr gibt es viele schmale Schnitte durch die Anforderungen: Durchstiche. Jeder macht für sich Sinn. Jeder kann getrennt von anderen evolvieren. Änderungsaufträge werden sich meist auf einzelne Module beziehen. Die gesamte Codebasis steht also gar nicht mehr zur Debatte.

Fazit

Ich denke, das Ergebnis des Experiments bestätigt ein Bauchgefühl, das wir immer hatten. Zeit, dass wir es ernst nehmen. Packen wir die LOC bei den Hörnern und reduzieren, wo wir können.

Wo der LOC-Haufen schon groß ist, geht es nur mit Refaktorisierungen. Wo immer wir aber neuen Code schreiben, sollten wir uns mit den beschriebenen Mitteln bemühen, die LOC gering zu halten. Prevention over refactoring ;-)

Kleiner Code, Code mit weniger LOC ist unterm Strich einfach ökonomischer. Das zählt für unsere Kunden.

Sonntag, 18. Mai 2014

Zuschauen beim Softwareentwurf

Konzepte, Notationen, Methoden auf Papier sind geduldig. Darüber zu lesen, ist eine Sache. Sie dann aber anzuwenden, auch mit dem besten Willen, eine andere. Was im Artikel oder im Buch noch verständlich war und einfach aussah, erweist sich in der Praxis schnell als knifflig. Der Transfer fällt schwer – und dann lässt man es einfach sein und macht weiter wie bisher.

Dem möchten Stefan Lieser und ich nun etwas entgegenstellen. Wir sind überzeugt, dass leichtgewichtiger Softwareentwurf möglich und wünschenswert ist. Wir finden, dass er mit Flow-Design und Softwarezellen vor dem Hintergrund des Softwareuniversums wirksam ist. Und wir wollen das nun auch live demonstrieren.

imageBisher haben wir dazu viel geschrieben, in diesem Blog, in der dotnetpro und anderen Zeitschriften und schließlich auch in einem Buch: The Architect´s Napkin – Der Schummelzettel.

Was in den Artikeln über Jahre durch eine Evolution gegangen ist, fasst das Buch zusammen. Es ist eine Referenz unserer Konzepte und Notationselemente. Wir benutzen es als Schulungsunterlage in unseren Clean Code Developer Trainings z.B. bei der CCD Akademie. Es ist bei leanpub.com und sogar bei amazon erhältlich.

Wir wir erleben, braucht es einige Übung, um vom traditionellen schwergewichtigen Entwurfsdenken – das deshalb oft vermieden wird – auf diesen Ansatz umzuschalten. Bei aller Leichtgewichtigkeit ist er doch sehr anders.

In Trainings können wir das anleiten und begleiten. Was aber danach?

Für die Zeit danach bzw. zwischen Trainingseinheiten, für das Selbststudium haben wir nun ein gemeinsames Buchprojekt gestartet:

image

In diesem Buch wenden wir unsere Methode an. Wir lösen mit ihr kleine und große Aufgaben aus dem Coding Dojo der CCD School.

Das tun wir jedoch nicht wieder nur im Text, sondern auch per Video. Und die Resultate stehen in einem öffentlichen Git-Repository.

Zu jeder Kata führen wir den Entwurf live durch. Wir treffen uns online ohne weitere Vorbereitung und gehen die Anforderungen gemeinsam an. Hier als Beispiel der Entwurf zur ersten Kata im Buch. Die ist natürlich noch klein und einfach, doch sie zeigt den grundsätzlichen Ansatz:

In dieser Weise geht es weiter mit den anderen Katas bis zu Application und Architecture Katas, die wir dann verteilt implementieren.

Das Buch dient der Zusammenfassung von Video und Code. Es destilliert das Wichtigste nochmal zum Nachlesen heraus. Denn in Text lässt sich leichter navigieren als in einem Video. Und es kommentiert und ergänzt den live Entwurf.

Wir hoffen, in dieser Weise einem stiefmütterlich behandelten und zu Unrecht als trocken oder gar irrelevant angesehenem Thema neues Leben einzuhauchen. Softwareentwurf ist ein systematischer Prozess unter Anwendung einer Methode. Dabei kann man nun zuschauen. Das zeichnet ein realistisches Bild des Vorgehens. Das nimmt hoffentlich Unsicherheit.

So wie wir Software entwickeln, so arbeiten wir auch an dem Buch: agil. Es ist nicht fertig, sondern wächst in Inkrementen. Das erste Inkrement veröffentlichen wir heute. Weitere Releases folgen in den nächsten Wochen.

So können wir aus unserer Erfahrung bei der Videoaufzeichnung, beim Schreiben, beim Publizieren lernen. Und Feedback aus der Community können wir auch einfließen lassen.

Ich würde mich freuen, wenn Sie Lust hätten, uns beim Softwareentwurf zuzuschauen. Über neue Releases erfahren Sie bei Twitter hier und hier.

Montag, 22. Juli 2013

Zustand ist eine armselige Optimierung

Das Denken des Softwareentwicklers kreist ständig um Daten. Wer auf Anforderungen schaut, sucht zuerst nach Daten. Wer an Infrastruktur denkt, denkt zuerst an Persistenztechnologien. Und wenn nicht immer, dann zumindest oft.

Nicht umsonst gibt es die Mittel der Objektorientierung als Weiterentwicklung von Sprachen wie C und Pascal: um Daten nicht nur zusammenzufassen (class), sondern auch noch funktional zu kapseln (interface).

Was dann in Objekten steckt, ist Zustand. Das sind nicht nur einfach Daten, sondern Daten in einer aus vielen Veränderungen hervorgegangenen Konstellation.

Hier ein simples Beispiel:

image

Mit jedem Aufruf von Sum() berechnet ein Aggregator die Summe einer Anzahl von Werten. Zu diesem Zweck führt das Objekt einen Zustand. Der ist das Ergebnis einer Reihe von Veränderungen:

image

Er stellt also einen Schnappschuss dar. Wie es zu ihm gekommen ist, liegt im Dunkeln.

Das macht für diesen Zweck auch nichts. Ich meine nicht, dass man eine solche Aggregationsfunktionalität dringend anders realisieren müsste. Dennoch kann sie helfen, deutlich zu machen, dass solcher Umgang mit Daten nur eine Option von mehreren ist. Und das deshalb ihr Entscheidungen zugrundeliegen, die ungünstig sein können, wenn wir sie nicht kennen oder leichtfertig treffen.

Wie könnte es anders gehen? Die Funktionale Programmierung (FP) würde vielleicht sagen, der Zustand in der Klasse sei unschön. Sum() ist keine reine Funktion, eben weil sie von unsichtbarem Zustand abhängt. Für einen gegebenen Input - z.B. (2) - lässt sich nicht vorhersagen, welchen Output sie erzeugt.

Als Antwort könnte die Summierung so umgebaut werden:

image

Es gibt immer noch Zustand im obigen Sinne, doch der wird irgendwo gehalten. Er ist nicht mehr Sache der nun reinen Funktion Sum(). Für einen gegebenen Input - z.B. (2, 1) - ist deren Output immer gleich - z.B. (3).

Ist dadurch etwas gewonnen? FP sagt, ja. Über reine Funktionen lässt sich besser nachdenken, die composability ist höher, die Testbarkeit besser. Und dann die Daten auch noch immutable sind… Besser geht´s nimmer ;-)

Ich möchte jedoch noch ein Stückchen weitergehen. Selbst wenn der reingereichte Zustand immutable ist, juckt mich etwas.

Warum gibt es in diesem Beispiel die Summe als Zustand? Ich glaube, der Grund ist ganz schlicht und einfach, aus Platz- und Performancegründen.

Denn es ginge ja auch anders. Der Zustand als punktuelle Konstellation hervorgegangen aus einer Linie von Veränderungen, kann immer durch diese Linie plus eine Reihe von Transformationen ersetzt werden:

image

Sum() hat jetzt überhaupt keinen Zustand mehr, weder internen noch externen. Stattdessen sieht die Funktion immer nur einen Liste von Veränderungen bestehend aus vielen historischen und einer aktuellen. Daraus berechnet sie das aktuelle Resultat - und schreibt die Liste der historischen Veränderungen fort.

Das Ergebnis ist dasselbe wie in den Varianten vorher. Nur wird jetzt mehr Platz und Zeit gebraucht.

Hat das einen Vorteil? Ja, ich denke, schon. Vorteil #1: Die Entwicklung des Ergebnisses ist immer komplett nachvollziehbar. Das hilft bei der Fehlersuche. Vorteil #2: Wenn eine Korrektur der Funktionalität nötig ist, können Ergebnisse nachträglich angepasst werden, falls das sinnvoll ist.

Diese Vorteile stechen in diesem Kleinstbeispiel nicht so ins Auge. Aber wenn Sie das Aggregat mal als persistent und etwas umfangreicher ansehen, dann werden sie relevant. Dann ist es interessant, diese Option denken zu können.

Ob die Liste der Veränderungen in die Funktion reingereicht wird oder aus einer globalen Quelle beschafft werden kann, finde ich gerade nicht so wichtig.

image

Hier geht es mir um den Wechsel der Sichtweise: vom Zustand zum Veränderungsstrom. Dass es schlicht möglich ist, auf Zustand zu verzichten. Dass seine Herstellung eine selbstverständliche und intuitive Optimierung ist - die wir als solche erkennen sollten.

Über die Optimierung in Hinsicht auf Platz und Zeit steckt darin sogar noch eine: eine Optimierung im Hinblick auf Form.

Wenn Sie Zustand denken - sei das eine Summe wie oben oder eine Datenklasse wie Person, Auktion oder Produkt -, dann denken Sie auch immer sofort an eine konkrete Struktur. Sie überlegen sehr sorgfältig, wie die aussehen soll. Davon hängen Klassen-, Datei- und Datenbankstrukturen ab. Bei solchen Strukturen darf Ihnen kein Fehler unterlaufen. Davon hängt ja viel ab. Sie zu ändern, allemal, wenn sie persistent sind, macht viel Aufwand.

Was aber, wenn es nicht mehr um Zustand geht? Dann müssen Sie nicht mehr über die eine beste Struktur für Daten nachdenken. Bei der obigen Summe ist diese Struktur trivial. Aber nehmen wir eine Software für ein Ortsamt: Wie sollte darin am besten die Klasse oder Tabelle oder das Dokument für einen Einwohner strukturiert sei?

Wie lange sollte man darüber nachdenken? Wieviele Anforderungen sollte man dafür analysieren? Nach wievielen Jahren Entwicklung und Änderungen an der Software kann man sich sicher sein?

Ich weiß es nicht. Und deshalb ist für mich jede Festlegung ohne Erfahrung und Mustererkennung eine vorzeitige Optimierung.

Die Lösung besteht für mich darin, das harte Nachdenken aufzugeben. Schluss mit Zustand! Stattdessen einfach nur Veränderungen speichern. So, wie sie in einem Inkrement gerade anfallen.

Aus diesen Veränderungen kann dann jederzeit eine Datenstruktur nach aktuellem Bedarf gegossen werden. Oben ist das zunächst nur eine Summe. Doch jederzeit könnte der auch noch ein Mittelwert zugesellt werden:

image

Hinter dem steckt nicht nur eine andere Logik, sondern auch eine andere Datenstruktur (double).

Zustand statt Veränderungen (events) zu speichern, ist mithin eine Optimierung, die unsere Optionen reduziert. Sie lässt uns langfristig verarmen - für kurzfristige Gewinne.

Wir glauben, wir hätten weder Platz noch Zeit, um Zustände bei Bedarf aus den ursprünglichen Veränderungen herzustellen. Deshalb treiben wir viel Aufwand in Bezug auf universelle in-memory wie persistente Datenstrukturen. Doch letztlich wissen wir nicht, ob wir mit dieser Befürchtung recht haben. Wir probieren ja nie die Alternative aus.

Dabei gäbe es viel zu gewinnen an Flexibilität. Und die brauchen wir dringend, wenn Software lange leben soll.

Freitag, 19. Juli 2013

Was ist eigentlich Kopplung?

Funktionseinheiten von Software sollen lose gekoppelt sein. Das ist ein Prinzip, dass wir alle runterbeten können - aber was bedeutet das denn konkret?

Irgendwie scheint es auch schwer einzuhalten, denn die Mehrheit der Softwaresysteme, die ich sehe, folgt ihm eher nicht. Monolithen sind das Gegenteil von loser Kopplung. Da hilft auch kein Einsatz von Interfaces.

Also: Was ist (lose) Kopplung und warum ist sie so schwer herzustellen?

Ein Blick in Wikipedia fördert leider nicht wirklich Hilfreiches zu Tage. Danach ist Kopplung…

die Verknüpfung von verschiedenen Systemen, Anwendungen, oder Softwaremodulen, sowie ein Maß, das die Stärke dieser Verknüpfung bzw. der daraus resultierenden Abhängigkeit beschreibt.

Und danach findet sich eine Liste Kopplungsarten, die ich, hm…, akademisch finde. Nicht falsch, aber eben nicht praxistauglich. Zumindest nicht für den Einstieg.

Der Eintrag zu loser Kopplung bringt auch nichts, würde ich sagen. Da steht z.B.

Eine lose Kopplung bedeutet in der Softwarearchitektur, dass Komponenten einer Software nur über wenige Schnittstellen mit anderen Komponenten kommunizieren bzw. von anderen Komponenten abhängig sind.

Wenn es nicht weit her ist mit der losen Kopplung in den heutigen Softwaresystemen, dann ist das kein Wunder, scheint mir. Aus solcher Schwurbelei lassen sich konkrete Maßnahmen nur schwer ableiten.

Ich versuche deshalb mal, den Begriff alltagstauglich zu fassen.

Kopplung konkret

Kopplung ist nötig, wenn Kooperation im weitesten Sinn gewünscht ist. Daran geht nichts vorbei. Sollen zwei Funktionseinheiten - Funktionen, Klassen, Bibliotheken, Services, Anwendungen, Softwaresysteme - miteinander arbeiten, dann müssen sie gekoppelt werden.

Das ist wie bei einer Lokomotive und Waggons. Wenn die Lokomotive Waggons ziehen soll, müssen die an die Lokomotive und untereinander gekoppelt sein, damit sich die Zugkraft überträgt.

Und auch hier ist die Stärke der Kopplung schon ein Thema. Sie braucht ein rechtes Maß: Ist sie zu lose, fällt der Zug [sic!] leicht auseinander. Ist sie zu fest, zerbricht es den Zug z.B. in Kurven.

Die erste Frage an eine Kopplung ist also die nach dem Zweck. Das Fragewort ist Wozu. Bei Lokomotive und Waggons geht es um Kraftübertragung zum Zwecke der Bewegung. Die Lokomotive ist Zugkraftdienstleister für die Waggons.

Die Kopplung im Sinne eines Zwecks ist immer eng, würde ich sagen. Weil das der Sinn einer Kopplung ist.

In allen anderen Belangen jedoch lässt sich die Stärke von Kopplung variieren von eng bis lose.

Hier meine Liste von solchen Belangen. Ich versehe sie alle mit einem Fragewort, um deutlich zu machen, dass wir uns immer wieder fragen sollten, wie stark die einzelnen Kopplungen denn wirklich sein müssen.

  • Wer: Wer ist es, an den eine Funktionseinheit gekoppelt ist? Ist eine Funktionseinheit an eine konkrete Instanz oder Identität einer anderen gekoppelt? Das Prinzip IoC und die Diskussion um Zustand in Servern drehen sich um diesen Kopplungsbelang.
  • Wo: Ist einer Funktionseinheit der “Ort” der anderen bekannt - egal, wie eng sie an eine Instanz gekoppelt ist. “Orte” sind Speicherbereiche, Prozesse oder Geräte. Dieser Belang hat jedoch zwei Seiten: Adresse und Entfernung. Die Adresse bezeichnet einen Platz in einem Adressraum, das kann ein physikalischer Speicher sein oder ein virtueller Raum wie das Internet. Die Diskussion um DI Container, Warteschlangen und Servicebusse drehen sich um diesen Kopplungsbelang. Die Entfernung hingegen sagt etwas darüber aus, wie leicht/schnell die Kommunikation zwischen zwei Orten stattfinden kann. Wenn Kopplungspartner sich nicht über ihre Hauptspeicheradresse kennen, geht es hier um Transportmedien, Protokolle, Kompression oder Caches.
  • Wohin: Wie ist die Kopplung zwischen zwei Funktionseinheiten gerichtet? Ist sie unidirektional oder bidirektional? Wer muss wieviel vom anderen wissen? Die Diskussion um die Vermeidung zirkulärer Referenzen adressiert diesen Belang. Aber auch bei der reaktiven Programmierung bzw. asynchroner Verarbeitung geht es darum.
  • Wann: Wann erwartet eine Funktionseinheit das Ergebnis einer anderen? Oder ist eine Funktionseinheit daran gebunden, wann ihr Kopplungspartner existiert? Darum geht es bei asynchroner Verarbeitung, Event-Driven Architecture und Cloud-Diensten wie ironWorker.
  • Womit: Wie sind die Daten (Parameter, Resultat, globale Daten), aber auch die Dienste strukturiert, auf denen Kopplungspartner arbeiten? Inhaltlich, also in Bezug auf den Zweck (Wozu) gibt es da immer eine enge Kopplung (logische Abhängigkeit). Aber bei der Form gibt es Spielraum. Darum geht es u.a. bei dynamischen Programmiersprachen oder NoSql.
  • Wie: Wieviel weiß eine Funktionseinheit darüber, wie es in ihrem Kopplungspartner aussieht? Kann sie algorithmische oder datenbezogene Details kennen - oder nutzt sie sie gar? Ist sie nicht nur an eine Leistung, sondern auch an die Plattform, auf der die erbracht wird, gekoppelt? Wenn über Entkopplung gesprochen wird, dann meist in dieser Hinsicht. Objektorientierung, Information Hiding, Interfaces: das sind Begriffe, die bei der Diskussion über diesen Belang fallen.

Auf alle W-Fragen zu Belangen der Kopplung kann die Antwort mehr oder weniger spezifisch sein. Je spezifischer aber, desto enger die Kopplung.

Eine bestimmte Instanz an einer bestimmten Adresse in bestimmter Entfernung, die zu einem bestimmten Zeitpunkt vorhanden sein muss und zeitlich in bestimmter Spanne mit ganz bestimmten Strukturen in einer ganz bestimmten Struktur in genau bestimmter Weise arbeitet… an die ist die Kopplung eben sehr, sehr eng.

Oder eben umgekehrt ist die Kopplung lose, wenn die Antworten unbestimmter ausfallen. Wenn Instanzen egal sind, wenn nicht genau gewusst werden muss, wo und in welcher Entfernung sind sie befinden. Wenn der Zeitpunkt ihrer Existenz und ihrer Antwort nicht festgeschrieben sein müssen. Wenn die Datenstrukturen und Aufrufstrukturen nicht in Beton gegossen sind. Und wenn es egal ist, wie im Detail die Leistung erbracht wird.

Wenn Sie dem Prinzip der losen Kopplung folgen wollen, dann stellen Sie sich am besten immer wieder diese Fragen. Bemühen Sie sich, die Antworten möglichst allgemein, offen, locker zu halten. Als Gewinn winken höhere Evolvierbarkeit und bessere Testbarkeit.

Aber auch ein Preis ist zu bezahlen. Lose Kopplung ist eine Form von Flexibilität. Das sehen Sie an der Verbindung zwischen Waggons. Flexibilität jedoch steht im Gegensatz zu Effizienz. Wo Sie lose koppeln, leidet also irgendeine Form von Effizienz. Das könnte die Entwicklungsgeschwindigkeit sein oder die Performance der Software. Das ist nicht zu vermeiden.

Lose Kopplung gibt es also nicht umsonst, sondern nur in einem Trade-off mit anderen Werten. Seien Sie sich derer also bewusst. Und denken Sie nicht nur an heute, sondern auch an morgen und übermorgen.

Deshalb ist für mich der default, im Zweifelsfall eher in lose Kopplung zu investieren, also in Flexibilität, denn in Effizienz. Das scheint mir nachhaltiger.

Montag, 1. Juli 2013

Eine Black Box für Software

CQRS hat mich jetzt gepackt. Auf der DWX Konferenz hatte ich Gelegenheit, mich darüber länger mit Jan Fellien auszutauschen. Den Moment, wo ich innerlich “Aha!” und “Wow!” ausrief war, als ich erkannte, was die Aufgabe einer Event Source ist.

Nicht nur ist Event Sourcing für mich die Antwort auf meine Frage nach einem Datengranulat. Denn aus den “Daten-Kügelchen” vieler kleiner Events kann man sich größere Strukturen in immer neuer Weise “gießen”. Es gibt für mich nicht mehr die Frage, ob “das eine Datenschema” für eine Anwendung relational oder dokumentenorientiert oder sonstwie sein sollte. Stattdessen gibt es soviele Schemata und Datenbanken wie man braucht. Und alle werden aus der einen Quelle gespeist: aus der Event Source.

Daten für einen Zweck in einem bestimmten Schema bereitzustellen, kann immer dynamisch aus der Event Source geschehen. On demand. In dem Augenblick, wenn sie benötigt werden. Wem das zu langsam ist, der muss halt die Daten in dem Schema cachen. Doch das ist dann eine bewusste Optimierung.

Mit einer Event Source kann man diese Optimierung dann vornehmen, wenn man sie braucht. Bis dahin ist man frei von lästigen Überlegungen, wie denn ein Schema am besten aussehen sollte. Welche Erleichterung!

Aber dieser Gedanke hatte mich nicht überfallen auf der DWX. Den hatte ich schon vorher. Im Gespräch mit Jan kam mir vielmehr ein sehr mächtiges Bild für die Event Source in den Sinn.

Die Event Source ist die Black Box einer Software.

Ich meine das im Sinne der Flugschreiber, die auch als Black Box bezeichnet werden. Die zeichnen Ereignisse während des Fluges auf. Wenn etwas schief geht, kann man durch Abspielen der Aufzeichnung versuchen, die Ursache zu finden.

Das leistet für mich nun auch eine Event Source bzw. ein Event Store für Software. Alle Domänenevents werden doch gespeichert (record). So kann man den Zustand eines Programms zu jeder Zeit rekonstruieren. Was an Zustand in-memory ist nur eine Optimierung; Zustand in einem Read-Model ist auch nur eine Optimierung. Maßgeblich ist einzig das, was in der Black Box steht.

image

Wenn also ein Programm oder auch nur ein Teil abstürzt, kann es neu gestartet und aus der Event Source auf den letzten Stand gebracht werden (replay). Die Speicherung auch kleinster Veränderungen des Zustands ist ja kein Problem, weil die Events als “Zustandsdifferenzen” ganz simpel und schnell persistiert werden können.

Events kommen aus der Domäne. Da spielt die Anwendungsmusik. Dafür wird Software gemacht.

Die Domäne spielt immer wieder aber auch Events ab, da sie ja nicht ihren kompletten Zustand in-memory halten will.

Andere Konsumenten von Events werden über sie per Notifikation informiert. Die sind also von der Eventquelle entkoppelt. Falls sie jedoch offline waren, können sie sich “verpasste” Events wieder vorspielen lassen, um sich auf den aktuellen Stand zu bringen.

Bei CQRS ist ein typischer Event-Konsument natürlich das Read Model. Es fertigt aus einzelnen Events fixe größere Strukturen, die auf unterschiedliche Abfragemuster zugeschnitten sind.

Ich finde das Bild der Event Source als Black Box sehr eingängig und motivierend. Damit werde ich mich jetzt mal intensiver beschäftigen…

Donnerstag, 30. Mai 2013

Daten als systemrelevante Größe behandeln

Systemrelevante Größen sind in der Softwareentwicklung wie auch sonst im Leben zu vermeiden. Dieser Meinung bin ich immer noch und sogar immer mehr.

Doch auch wenn wir sie vermeiden sollen, heißt das nicht, dass wir das immer können. Manches ist einfach systemrelevant, weil es die fundamentale Wahl für ein System repräsentiert. Es muss konstant bleiben - ansonsten handelt es sich um ein ganz anderes System. Für die westliche Welt sind das z.B. eine Demokratie oder Marktwirtschaft.

Für die Softwareentwicklung gibt es das auch. Da gibt es einen Aspekt, der ist so zentral, dass wir nicht um ihn herum kommen. Er ist bestimmend. Es ist unveränderlich.

Damit meine ich nicht, ein bestimmtes Betriebssystem oder eine Entwicklungsplattform. Im Gegenteil! Die müssen ständig disponibel.

Nein, ich meine die Daten. Die Daten einer Software bzw. eines Unternehmens sind aus meiner Sicht eindeutig systemrelevant. Sie sind förmlich das Fundament. Sie gilt es zu hegen, zu pflegen, zu erhalten. Nicht umsonst heißt es: data outlives application.

Anwendungen kommen und gehen über Jahre und gar Jahrzehnte. Doch die Daten bleiben. Sie wachsen nur. Allemal in Zeiten scheinbar unbegrenzten Speicherplatzes scheint es töricht, Daten zu löschen. Wer weiß, wann sie einmal nützlich werden könnten für eine Big Data Auswertung?

Wenn das aber nun so ist, dass Daten unvermeidbar systemrelevant sind, was bedeutet das für den Umgang mit ihnen?

Ich denke, systemrelevante Größen brauchen eine spezielle Behandlung. Sie stehen ja quasi außerhalb des Gesetzes. Wir sind von ihnen abhängig. Deshalb müssen wir darauf achten, dass sie uns keine Diktatur aufzwingen. Wenn eine Größe systemrelevant ist, dann ist sie ein Herrscher. Mit einem "guten König" lässt es sich da leben; aber einen unberechenbaren Nero wollen wir nicht über uns haben.

Letztlich sollte auch die systemrelevante Größe an einem guten Verhältnis mit ihren Untertanen interessiert sein. Denn die Abhängigkeit besteht letztlich in beide Richtungen. Ohne ein System, ist die Größe nichts.

Also, was bedeutet es für die Softwareentwicklung oder genauer: für die Softwarearchitektur, dass Daten eine unvermeidlich systemrelevante Größe sind?

Wie im richtigen Leben folgt daraus die Notwendigkeit zu Transparenz und Flüssigkeit, würde ich sagen.

Systemrelevante Größen müssen besonders offen und verständlich sein. Weil sie systemrelevant sind, können sie ja nicht ersetzt werden. Wenn man sie nicht mehr versteht, gibt es keine Alternative.

Systemrelevante Größen müssen besonders flüssig sein. Ihre Form muss sich verändern lassen. Sie dürfen sich nicht aus innerer Trägheit/Festigkeit dem Wandel widersetzen. Denn Wandel - soviel sollte gewiss sein - wird nötig sein. Systemrelevante Größen haben eine lange Lebensdauer, so dass Veränderungen in der Umwelt und deshalb Anpassung unvermeidbar sind.

Alles, was die Transparenz/Verständlichkeit und die Flüssigkeit beeinträchtigt, ist also zu vermeiden.

Was bedeutet das für Daten konkret?

Daten müssen in einer Weise gehalten werden, die transparent und verständlich ist. Klingt vielleicht einleuchtend - wird aber oft nicht beachtet. Einkaufspolitik, Tradition, Performance und andere Kräfte ziehen Daten oft in eine Form, die Transparent und Verständlichkeit reduzieren. So weit reduzieren, bis nur noch eine kleine Gruppe von Personen die Daten versteht. Die bestimmt dann über das Schicksal einer Organisation - ob sie will oder nicht.

Was Transparenz im konkreten Fall bedeutet, weiß ich nicht. Das ist von Fall zu Fall, von Datenbasis zu Datenbasis verschieden. Das eine große Unternehmensdatenmodell mit 1468 eng verwobenen Tabelle in einer Oracle Monsterserver ist aber sicherlich keine Lösung.

Eher sind es Daten in unterschiedlicher Organisationsform in verschiedenen Bounded Contexts, die transparent sind.

Daten müssen darüber hinaus auch in einer Weise gehalten werden, die es leicht macht, ihre Form immer wieder zu verändern. Formate und Technologien, die es schwer machen, zu exportieren und zu importieren, sind zu vermeiden. Lock-In jeder Art ist eine Gefahr für das System. Wer sagt, "Wir sind eine XYZ-Company!" ist schon auf dem falschen Weg. (Setzen Sie für XYZ ein Persistenzprodukt oder eine Technologie oder ein Paradigma Ihrer Wahl ein.)

ETL ist deshalb auch here to stay und wird weiter an Bedeutung gewinnen. Weil die Vielfalt der Optionen, der Anwendungen und Bounded Contexts wächst, mit/in denen Daten gesammelt und verarbeitet werden.

Event Sourcing halte ich aus diesem Grund auch für zeitgemäß. Denn damit werden Daten im Grunde von jedem Format befreit. Das macht sie maximal flüssig. Events sind quasi ein Granulat, das in immer neue Form je nach Bedarf gegossen werden kann.

Ja, ich denke, Transparenz und Flüssigkeit sind die beiden zentralen universellen nicht-funktionalen Anforderungen an die Datenhaltung. Sonst ist sie nicht nachhaltig. Sonst kommt es zu hemmendem Lock-In der einen oder anderen Art - und das kostet Geld.

Daten sind eben ein oder vielleicht sogar die einzige wirklich systemrelevante Größe in der Softwareentwicklung. Deshalb müssen wir mit ihnen in besonderer Weise umgehen.

Dienstag, 28. Mai 2013

Zeitgemäße Architekturkompetenz

Die Frage, was denn die Aufgabe eines Softwarearchitekten sei, ist nicht tot zu kriegen. Eine Konferenz, die etwas auf sich hält, hat dazu mindestens eine Podiumsdiskussion.

Ich habe da jetzt keine Lust mehr drauf. Das ewige Diskutieren hält uns davon ab, das Wichtige zu tun, nämlich Softwarearchitektur zu betreiben.

Im Sinne des Cult of Done beende ich deshalb jetzt für mich die Arbeit am Begriff "Softwarearchitektur". I´m done.

Hier die Eckpfeiler der Definition, was Softwarearchitektur aus meiner Sicht bedeutet:

  1. Softwarearchitektur dreht sich nur um nicht-funktionale Aspekte von Software. Ihre Ergebnisse spannen den Rahmen für Funktionalität auf.
  2. Softwarearchitektur hat das Ganze im Blick. Ihre Aufgabe ist es, ein globales Optimum herzustellen und lokale Optima zu vermeiden [1]. Softwarearchitektur ist damit eine ökonomische Disziplin.
  3. Da es nicht so einfach "das Ganze" gibt, sondern immer nur verschiedene Blickwinkel und unterschiedliche Werte, Anforderungen, Bedürfnisse, die sich auch noch über die Zeit verändern, kann Softwarearchitektur nicht einmal ein Optimum einstellen. Architektur ist daher ein Prozess, ihr Ergebnis ein dynamisches Gleichgewicht.

Die Invariante der Softwarearchitektur ist mithin der Wandel. Alles kann sich über die Lebensdauer einer Software verändern.

Deshalb ist für mich die Evolvierbarkeit die zentrale nicht-funktionale Anforderung, um die sich die Softwarearchitektur kümmern muss.

Natürlich beantwortet Softwarearchitektur Fragen zu...

  • Produkten, z.B. "Soll Sql Server oder Oracle eingekauft werden?"
  • Technologien, z.B. "Soll ein RDBMS oder eine NoSql Datenbank zum Einsatz kommen?"
  • Paradigmen, z.B. "Sollen die Daten relational strukturiert werden oder dokumentenorientiert?"

Und viele weitere Entscheidungen trifft die Softwarearchitektur auch noch. Ständig muss sie dabei abwägen, um keine nicht-funktionale Anforderung aus dem Blick zu verlieren. Kann sie das eigentlich? Wer hat die Kompetenz, aus einer wachsenden Zahl von Optionen, die richtigen auszuwählen? Es scheint mir zunehmend schwieriger.

Dass 1 Person dies als Architekt dauerhaft kann, glaube ich nicht. Deshalb ist für mich zeitgemäße Architekturkompetenz eine Teamangelegenheit [2]. Dabei mag der eine mehr, der andere weniger Spaß daran haben - ohne dass am Ende jedoch alle zumindest verstanden und zugestimmt haben, sehe ich keinen hohen buy-in. Und ohne hohen buy-in, droht von vornherein Erosion der Architektur(vision).

Also: Es gibt viel zu beachten für die Softwarearchitektur. Kräfte ziehen sie in verschiedene Richtungen, Anforderungen sind im Wandel, Optionen entwickeln sich... Mir scheint es daher illusorisch, von der Softwarearchitektur Entscheidungen von Dauer zu verlangen.

Natürlich wünscht sich jeder, dass dies oder jenes endlich ein für alle mal entschieden wird: Hardware, Plattform, Paradigma, Technologie, Produkt, Methode... Damit Ruhe reinkommt, damit die Entwicklung planbarer wird, damit der Einkauf gute Konditionen aushandeln kann.

Doch hier scheint mir Software fundamental anders als alle anderen Produkte. Für Software lassen sich endgültige Entscheidungen immer weniger treffen. Nicht nur, weil Kunden notorisch schlecht sagen können, was sie eigentlich wollen, sondern weil die Branche sich mit ihrem rasanten Fortschritt selbst immer wieder Knüppel zwischen die Beine wirft. Es kommt einfach keine Ruhe rein. Vorgestern Desktop, gestern Web, heute Mobile. Gestern RDBMS, heute NoSql, morgen NoDb. Vorgestern Java Applets, gestern Silverlight, heute HTML5/JS, morgen... Und was sonst noch alles. Vor allem aber: Eigentlich stirbt kein Paradigma, keine Technologie aus.

So ergibt sich für mich als Schluss für die Architekturkompetenz eines als Kernaufgabe. Sie ist das zeitgemäße Fundament für die obigen Architekturpfeiler:

Die vornehmste Aufgabe für die Softwarearchitektur ist es, möglichst viele Entscheidungen reversibel zu halten [3].

Softwarearchitekten sind sozusagen eine Kartellbehörde, die ständig darauf achtet, dass keine Monopole entstehen, d.h. systemrelevante Größen, die alles dominieren. Keine Plattform, kein Paradigma, kein Hersteller, keine Technologie sollte so groß und alles durchdringend werden, dass sich der Rest nur noch demütig danach richten kann.

Nur wenn Softwarearchitektur ständig dafür sorgt, dass Entscheidungen im Grunde jederzeit nach neuer Erkenntnislage verändert werden können, kann sie angstfrei und zügig voranschreiten.

Solange noch die Angst regiert, ist Softwarearchitektur nicht frei. Solange muss umfänglich geprüft, statt geliefert werden. Solange muss verhandelt, statt Feedback generiert werden. Solange muss noch einer seinen Arsch absichern, statt Wert für den Kunden bzw. das Ganze zu schaffen.

Früher... ja, früher war das noch anders. Da bestand Architekturkompetenz in einmaligen Entscheidungen. Die Zahl der Optionen war deutlich geringer. Und der Mangel an quasi allem hat deutliche Grenzen gesetzt. Entscheidungen fanden im Konkreten statt. Heute tun sie das auch noch, klar. Einer muss sagen, ob z.B. Notifications via Pubnub oder XMPP verschickt werden sollen. Doch diese Entscheidungen sind nicht von solcher Dauer wie früher. Deshalb wandert zeitgemäße Architekturkompetenz auf die Meta-Ebene. Sie trifft Entscheidungen über Entscheidungen, nämlich wie die konkreten möglichst unbegrenzt vorläufig gehalten werden können.

Soweit meine Definition für Softwarearchitektur. Ich finde sie einfach verständlich und klar umrissen. Nach ihr zu handeln jedoch, ist nicht so einfach. Es erfordert viel Bewusstheit. Ich würde sogar sagen, es geht mehr um Bewusstheit als um technische Kompetenz.

Endnoten

[1] Dass Architekten mit allen möglichen Stakeholdern zu tun haben, ist damit selbstverständlich. Sonst können sie keinen "Bedürfnisausgleich" mit Blick auf ein globales Optimum herstellen.

[2] Über die Frage, ob "der Softwarearchitekt" codieren soll, kann ich mich nicht ereifern. Mir ist das egal. Erstens gibt es für mich nicht (mehr) denen einen Softwarearchitekten. Zweitens kann kein Teammitglied heutzutage mehr alle nötige Kompetenz für die ganzen nötigen Architekturentscheidungen auf sich vereinigen. Architekten als Universalgelehrte gibt es nicht mehr. Drittens müssen die, die Architekturentscheidungen fällen, nur irgendwie zu ihrer Kompetenz kommen. Wie sie das erreichen...? Wahrscheinlich, indem sie auch mehr oder weniger lange in den Codiergräben gekämpft haben. Aber wie lange, ob sie das heute noch kontinuierlich müssen... Das lasse ich dahingestellt. Wichtiger ist mir, dass die, die Architektur betreiben, sich ihrer Kompetenzgrenzen bewusst sind. Da können sie nämlich ggf. etwas dagegen tun.

[3] Reversibilität bedeutet hier natürlich, mit angemessenem Aufwand reversibel zu sein.

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.

Donnerstag, 21. Februar 2013

Vorzeitige Optimierung durch Monolithen

Softwareentwicklung ist historisch ein Geschäft der knappen Ressourcen. Speicher war knapp, Speicher war langsam, die Prozessorgeschwindigkeit war gering, die Prozessorverfügbarkeit war niedrig und die Kommunikation zwischen Prozessoren/Maschinen war unmöglich bis schwierig.

Ich glaube, diese jahrzehntelange Not ist zum Bestandteil der Kultur der Softwareentwicklung geworden. So wie die Not der (Vor)Kriegszeit zur Kultur unserer Eltern und Großeltern gehört.

Der Teller wird leer gegessen. Man schmeißt keine Lebensmittel weg. Mit dem Essen spielt man nicht. Kerzenstummel und andere Reste werden gehortet. Socken müssen gestopft werden. Wer kennt solche Ermahnungen und Verhaltensweisen nicht von Eltern und Großeltern? Sie entstammen einer tief liegende Angst, dass das, was heute verfügbar ist, morgen wieder weg sein könnte. In der heutigen Zeit des Überflusses sind es allerdings Anachronismen [1].

Ähnlich kommen mir nun viele Reflexe von Entwicklern vor. Sie mahnen, nicht verschwenderisch mit Speicher umzugehen; sie bedenken zu allererst die Performance; sie misstrauen jeder Ressource und jeder Interaktion [2].

Dabei leben auch wir inzwischen in Zeiten des Überflusses. 48 KB oder 640 KB Hauptspeicher oder 5 MB Festplatten sind heute nicht mehr Stand der Dinge. Für viele (oder gar die meisten?) Anwendungsszenarien ist Hardware heute so groß, schnell und billig, dass sie keine Begrenzung mehr darstellt.

Komplette Unternehmensdatenbanken können wir heute im Hauptspeicher halten. Prozessoren haben mehrere Kerne. Festplatten sind schnell und groß. Rechner sind in Gigabit-Netzwerke eingebunden. Und quasi unendlich viele Rechen- und Speicherressourcen stehen auf Abruf weltweit zur Verfügung.

Doch die Mentalität ist immer noch eine der Knappheit. Das kann ich irgendwie verstehen; ich bin ja selbst in Zeiten der Hardwaremängel mit der Programmierung groß geworden. Jetzt sollten wir aber mal diese Haltung langsam ändern. Solange wir uns ihrer nämlich nicht bewusst sind und sie versuchen zu überwinden, verschwenden wir unsere Zeit und geistige Kapazität. Die sind nämlich tatsächlich noch genauso begrenzt wie früher.

In vielen Bereichen ist die Mangelkultur ein Anachronismus. Wir verhärten uns mit ihr gegen nützliche Veränderungen. Denn dass sich einiges verändern muss, ist unzweifelhaft. Beispiel: Softwareentwurf. Wir müssen Software anders entwerfen, damit die Strukturen evolvierbarer werden. Das heutige Legacy Code Problem darf sich nicht ungebremst so fortsetzen.

Für mich ist es ein Problem des Mangeldenkens, weil die uns so drückenden Softwaremonolithen aus meiner Sicht Kinder des Mangeldenkens sind. Denn warum sollte man Code zu einem Monolithen zusammenschweißen, wenn nicht aus Angst vor Performance- und Speicherproblemen?

Unter Monolith verstehe ich hier Software, die all ihre Aufgaben in möglichst wenigen Prozessen erledigt. Am besten ist das nur einer, eine Desktopanwendung (bzw. ein Batch) oder eine Web-Anwendung. Ein Betriebssystemprozess hostet dabei die gesamte Funktionalität. Das ist das Ideal.

Das ist einfach zu deployen. Das ist vermeintlich einfach zu entwickeln, da man alles in ein Visual Studio Projekt stecken kann. Das ist schnell, weil die gesamte Kommunikation innerhalb des Adressraums eines Prozesses stattfindet. Das ist technisch simpel, weil man sich nicht mit Infrastruktur herumschlagen muss.

Natürlich verstehe ich all diese Beweggründe. Ich habe auch so gedacht. Doch inzwischen glaube ich, dass wir uns damit keinen Gefallen tun, wenn wir so auf diese Mängel stieren. Das Deploymentproblem ist heute nicht mehr so groß wie noch vor 10 Jahren; die Kommunikationsgeschwindigkeit ist mit heutigen Prozessoren, Hauptspeichern und Netzwerkverbindungen auch zwischen Prozessen oft ausreichend. Und die Infrastruktur müssen wir nicht mehr in dem Maße selbst programmieren wie noch vor 10-20 Jahren [3]. Wer heute einen Monolithen entwickelt, der betreibt also vorzeitige Optimierung. Er optimiert für Mängel, deren Existenz nicht gesichert ist im Hinblick auf eine konkrete Domäne.

Unsere Kultur verhindert jedoch, dass wir dieses Potenzial voll ausschöpfen. Wo es nicht anders geht, da wird es getan. amazon, Twitter, Facebook, eBay usw. können nicht anders, als an die heutigen Grenzen zu gehen.

Wer allerdings heute ein CRM oder sonstige Intranet/Desktop-Software programmiert, der verschenkt Potenzial. Der schnürt sich ein in ein Korsett, dass man geradezu masochistische Neigungen vermuten könnte.

Wenn es denn sonst keine Probleme gäbe, wäre das ja nicht schlimm. Jedem seine Neigung ;-) Das Evolvierbarkeitsproblem halte ich jedoch für so groß, dass wir uns “Neigungsprogrammierung” in dieser Hinsicht nicht mehr erlauben können. Die mittel- bis langfristige Gesundheit unserer Software leidet darunter. Und auch die unserer Teams.

Wir müssen bessere Wege finden, um Software über die Jahre nicht nur zu retten, sondern entspannt weiterentwickeln zu können. Dazu gehört für mich immer mehr auch die Neuentwicklung. Ja, genau, Code wegschmeißen und neu machen.

Dass das nicht für komplette Anwendungen funktioniert, ist mir klar. Deshalb müssen wir aufhören, Monolithen zu bauen. Die können wir nämlich nur komplett ersetzen – was unökonomisch ist. Oder wir müssen sie zu Tode refaktorisieren.

Mit geht es um einen Mittelweg. In “Form Follows Feasibility” hatte ich schon darüber nachgedacht. Seitdem bin ich nur sicherer geworden, dass solch ein Mittelweg nötig und gangbar ist – wenn wir uns aus dem Mangeldenken befreien.

Die bewusste Strukturierung auch von Deskptop- oder Web-Anwendungen in mehrere Services, d.h. Komponenten mit plattformunabhängigem Kontrakt, ist mir sogar noch wichtiger geworden.

Erstens kann man sich mit plattformunabhängigen Kontrakten weniger in die Tasche lügen, was die Entkopplung von Code angeht. Zweitens bieten nur solche Kontrakte die Chance, sich vor der drohenden Systemrelevanz der Programmierplattform mit einhergehender Verkalkung des Teams zu schützen.

Jede Software als “web of services” zu denken, sollte der Default werden, glaube ich. Und erst wenn sich das handfest als suboptimal herausstellt, sollte mit einer Optimierung begonnen werden.

Endnoten

[1] Die Motive Respekt und Dankbarkeit hinter solcher Haltung will ich nicht ausschließen. Schön, wenn sie denn mitschwingt. Auch im Überfluss tun wir sicherlich gut daran, Bescheidenheit und Respekt unserer Umwelt entgegen zu bringen.

[2] Schizophren ist, dass diese Haltung komplett umkippen kann. Denn der Umgang mit Objekten in verteilten Anwendungen war (und ist?) von großer Naivität, gar Sorglosigkeit geprägt. Da spielten Skalierbarkeit, Robustheit und Performance lange so gar keine Rolle. Anders sind CORBA, EJB und DCOM und auch noch Teile von .NET Remoting nicht zu erklären.

[3] Eine Kommunikation zwischen Prozessen, die über die Welt verteilt sind, ist via Internet und Cloud API wie Pubnub oder iron.io heute eine Sache weniger Zeilen Code.