Follow my new blog

Montag, 9. Januar 2012

Functions considered harmful

Bisher habe ich die "traditionelle" konzeptionelle Objektorientierung als eine der Ursachen für die heutigen Probleme mit der Wartbarkeit von Software gesehen. Inzwischen regt sich jedoch in mir der Verdacht, dass das Wurzelproblem tiefer liegt.

Womöglich ist die Objektorientierung sogar zu loben, weil sie das irgendwie verstanden hatte und versucht zu helfen. Leider ist das nicht so geglückt, wie man es sich erhoffte. Warum? Weil das Wurzelproblem eben nicht behoben, sondern nur kaschiert wurde. Man hat sich nicht getraut, so tief nach unten zu graben, um es auszureißen.

Denn das Wurzelproblem scheint mir... der Funktionsaufruf.

Ja, genau, der gute alte, unscheinbare und für imperative Sprachen so fundamentale Funktionsaufruf.

Dass wir schreiben können

y = f(x)

ist der Keim vieler Wartbarkeitsübel.

Wie kann das sein?

Problem #1: Unbegrenzte Länge

Durch Funktionen gibt es keine Grenze für das, was eine Codeeinheit tun kann. Ungezügeltes Wachstum von Codeeinheiten wird durch Funktionen ermöglicht. Denn nach einem Funktionsaufruf kann es ja weitergehen im aufrufenden Code.

Funktionen kombinieren Request und Response. Damit kombinieren sie vorbereitenden Code und nachbereitenden Code im Aufrufer. Und selbstverständlich kann nachbereitender Code gleichzeitig vorbereitender Code für den nächsten Funktionsaufruf sein. Nach dem Funktionsaufruf ist vor dem Funktionsaufruf.

Wann sollte also aufrufender Code beendet sein? Wenn er inhaltlich eine Verantwortlichkeit erfüllt. Klar. Aber das ist ein Kriterium, dessen Erfüllung im Auge des Betrachters entsteht. Der eine mag es, Verantwortlichkeiten in höchstens 20 Zeilen zu formulieren, der nächste hat kein Problem, wenn es dafür 100 Zeilen braucht und wieder einem anderen sind auch 10.000 Zeilen recht.

Durch Funktionsaufrufe gibt es kein Halten in Bezug auf den Umfang des aufrufenden Codes. Die Existenz von Regeln, die versuchen, diesen Umfang direkt oder indirekt zu begrenzen, ist der beste Beweis dafür.

Und wie wäre es, wenn es keine Funktionen gäbe? Dann würde eine Codeeinheit immer nur aus soviel Code bestehen wie nötig ist, um einen Request vorzubereiten, der am Ende dann abgeschickt wird. Ohne Funktionen gäbe es keinen Response, den die vorbereitende Codeeinheit nachbereiten könnte, also wäre ihre Aufgabe mit Versand des Request abgeschlossen. Der Response würde von einer anderen Codeeinheit weiterverarbeitet.

Ohne Funktionen ist es aber natürlich sinnlos, von Request und Response zu sprechen. Einen Request gibt es nur, wenn man auch einen Response erwartet. Clients übergeben Requests an Services, die mit Responses antworten.

Ohne Funktionen gibt es nur Daten, die produziert und konsumiert werden. Producer senden Daten an Consumer. Und immer so weiter. Aus Client-Code wie diesem, der sich potenziell unendlich fortsetzt…

Client:
  Vorbereitender Code erzeugt X
  Y= f(X)
  Nachbereitender Code verarbeitet Y

wird Producer-Consumer-Code:

Producer:
  Vorbereitender Code erzeugt X
  Versenden von X

f als Prosumer:
  X nach Y transformieren
  Versenden von Y

Consumer:
  Nachbereiten von Y

Sie sehen, der Umfang jeder Codeeinheit ist ganz natürlich sehr begrenzt, wenn es keine Funktionen gibt. Es lässt sich einfach nur wenig tun, bis wieder Arbeit an eine andere Codeeinheit delegiert werden muss.

Die grenzenlose Kopplung von Vorbereitung und Nachbereitung ist das offensichtliche Problem, zu dem Funktionsaufrufe führen. Sie bläht aufrufende Codeeinheiten auf. Das erschwert schnell die Verständlichkeit und leistet einer Vermischung von Verantwortlichkeiten im Sinne des SRP Vorschub.

Problem #2: Unbegrenzte Tiefe

Unterhalb des Request/Response-Problems liegt leider noch ein weiteres, noch fundamentaleres. Das wird allerdings erst sichtbar, wenn Software weiter wächst. Es ist das Problem des Aufrufs schlechthin.

Zur Erinnerung: Aufrufe von Code sind eigentlich als Mittel zur Platzersparnis erfunden worden. Früher war Speicher eben sehr knapp. Da war jedes Mittel recht, um Bytes zu sparen. Also hat man das CALL/RET Maschinenbefehlpaar erfunden. Aus Code wie:

A
S
T
U
B
S
T
U
C
S
T
U

konnte nun werden:

A
CALL F
B
CALL F
C
CALL F

F: S
T
U
RET

6 Anweisungen statt 10. Das ist selbst mit diesem Pseudocode eine Reduktion um 40%.

Platzersparnis, nicht Wiederverwendbarkeit ist die Motivation hinter Unterprogrammen als Verallgemeinerung von Funktionen. Für Wiederverwendbarkeit wären Macros ausreichend gewesen.

Wie alles im Leben hat natürlich auch die Platzersparnis ihren Preis. Der besteht in der Trennung von Client-Kontext und Service-Code. Solange im obigen Beispiel STU zwischen A und B textuell steht, ist klar, was die Aufgabe von STU ist. STU steht im Nutzungskontext. Der Entwickler sieht zur Entwicklungszeit, was vorher passiert, was zwischendurch passiert und was nachher passiert.

Die Einführung des Unterprogramms F zerstört diese verständliche Einheit. Was zwischendurch passiert, steht nun irgendwo. Wer nun liest

A
CALL F
B

der ist darauf angewiesen, dass F ein ausdrucksstarker Name ist, um zu verstehen, was da passiert. Die vorherige Einheit zur Entwicklungszeit existiert erst wieder zur Laufzeit.

Das bedeutet: Funktionen machen den Aufrufort schwerer lesbar, weil sie dort Code durch einen Namen ersetzen. So eine Ersetzung ist sehr verlust- bzw. missverständnisgefährdet.

Und Funktionen machen Code insgesamt schwerer lesbar, weil sie einen natürlichen Zusammenhang wie

A
S
T
U
B

über die Codebasis verteilen. Auch da gibt es ja kein Halten. Die Aufruf-Schachtelung von Funktionen ist beliebig tief. Aus

A
S
T
U
B

wird zuerst

A
CALL F
B

F: S
T
U
RET

dann vielleicht

A
CALL F
B

F: S
CALL G
U
RET

G: T
X
Y
RET

und dann vielleicht

A
CALL H

H: CALL F
B
RET

F: S
CALL G
U
RET

G: T
X
Y
RET

uns so weiter…

Zur Laufzeit macht das alles keinen Unterschied. Zur Entwicklungszeit jedoch wird es immer schwieriger zu verstehen, was da eigentlich passiert. Inhaltliche Sequenzen werden aufgelöst. Es entsteht ein Granulat an Unterprogrammen, dessen Aufrufhierarchie zur Laufzeit keine formale Entsprechung zur Entwicklungszeit hat. Funktionen führen mithin zu einem fundamentalen Impedance Mismatch zwischen Entwicklungszeit und Laufzeit.

Gut, inzwischen gibt es IDEs, mit denen man die Aufrufhierarchie durchwandern kann. Aber seien wir ehrlich: das ist umständlich. Eine hierarchische Sicht von Funktionsaufrufen ist kein First Class Citizen in den populären IDEs wie eine Projektdateiansicht oder eine Klassenansicht. Und in der Tradition von C gibt es keine geschachtelten Funktionen in C++, Java, C#.

Die Schachtelung von Funktionsaufrufen ist so tief in unser aller Programmiererstammhirn eingebrannt, dass es nicht einmal eine Metrik dafür gibt. Man macht sich Gedanken über LOC pro Funktion oder die Schachtelungstiefe von Kontrollanweisungen in Funktionen. Die Tiefe der Aufrufhierarchie von Funktionen zur Entwicklungszeit hingegen, scheint niemanden zu interessieren. Dabei ist sie es, die den logischen Zusammenhang von Code auseinanderreißt.

Aus unbegrenzter Aufrufschachtelung folgt, dass auch die Problemlösung beliebig über die Tiefe der Aufrufhierarchie verteilt werden kann. Im obigen Beispiel können ja auf jeder Ebene – Aufrufwurzel, H, F oder G – Anteile von “Geschäftslogik” stehen.

  1. Das bedeutet erstens, es bedarf zusätzlichen Aufwands, um zu entscheiden, auf welcher Ebene Geschäftslogik angesiedelt werden sollte. Das ist aber natürlich Aufwand, der nicht zur Lösung des Problems beiträgt. Also scheut man ihn, wo es geht. Das Ergebnis sind flache Hierarchien mit sehr langen Funktionen (s. Problem #1).
  2. Das bedeutet zweitens, Funktionen, die nicht Blätter in der Aufrufhierarchie sind, machen zusätzlichen Aufwand beim Testen. Es muss ja nicht nur ihr Beitrag zur Problemlösung überprüft werden, sondern es müssen auch noch die aufgerufenen Funktionen ersetzt werden (Attrappen).
  3. Und drittens sind Aufrufhierarchien ständig gefährdet, durch Veränderungswellen erschüttert zu werden. Veränderungen breiten sich entlang von Abhängigkeiten aus:
    Wenn C wie Client von S wie Service abhängig ist, dann kann es bei Änderungen an C nötig sein, S nachzuführen. Und falls sich S ändert, kann es nötig sein, C nachzuführen. Je breiter und tiefer Aufrufhierarchien sind, desto unüberschaubarer die Ausbreitung von Änderungen, die immer irgendwo nötig sind. Das ist umso schlimmer, da ja diese Hierarchien nur schwer zu übersehen sind. Sie existieren nicht textuell und auch kaum in anderer Ansicht in den IDEs.

Fazit

Funktionen (oder allgemeiner: Unterprogramme) sind aus meiner Sicht eine der Hauptursachen für die Undurchschaubarkeit von Code. Sie machen seine Ausdehnung in Breite und Tiefe grenzenlos.

Dagegen helfen dann auch keine Ermahnungen und Metriken. Denn die sind sehr geduldig. Wenn es eng wird, hört man nicht hin und setzt sie aus. Immer mit dem Verweis, dass gerade anderes wichtiger sei – und es ja auch ohne ginge.

So entstehen unwartbare Codekonvolute einen Funktionsaufruf nach dem anderen.

*Dagegen hilft nur, Funktionsaufrufe klaren Auges als Gefahr zu sehen und ihre Nutzung zu rigoros zu begrenzen.*

Wir müssen diese Altlast aus den Anfangstagen der Programmierung abwerfen (oder zumindest mit weniger Schädlichem integrieren). Funktionen sind ein Erbe der Nähe zur Mathematik. Das hat uns weit gebracht – aber nun haben wir eine Grenze erreicht, da der Schaden größer als der Nutzen ist.

Mittwoch, 4. Januar 2012

Bei häufigem Kontakt mit Gift refaktorisieren

images/refactoring-toxicity.pngGraham Brooks bricht eine Lanze dafür, Code nicht mit der Gießkanne zu refaktorisieren, sondern den Aufwand dort zu treiben, wo es gerade besonders nutzt. Das hört sich sinnig an.

Noch sinniger wird es, wenn Graham als Nützlichkeitsindikatoren Giftigkeit (Toxicity) und Unbeständigkeit (Volatility) angibt.

Giftig (als Steigerung von schmutzig) ist der Code dort, wo bestimmte Metriken besonders schlimme Werte annehmen. Unbeständig ist er dort, wo häufig an ihm gearbeitet wird.

In Kombination ergibt sich eine Giftigkeit x Unbeständigkeit Matrix, die klar sagt: Refaktorisiere, wo häufiger Veränderungskontakt mit hoher Giftigkeit besteht; aber lass Code stabilen (relativ) ungiftigen Code in Ruhe. Das hört sich sinnig an.

image
Quelle: Graham Brooks

Nun fragt Jens Schauder in einem Tweet aber zurecht: Und wie misst man nun Giftigkeit? (Die Unbeständigkeit ist kein Problem; sie lässt sich aus einem VCS Log herauslesen.)

Graham sagt nichts dazu. Er verweist vielmehr auf diesen Artikel von Erik Dörnenburg. Der zeigt dann ein konkreteres Giftigkeitsdiagramm und gibt konkrete Metriken an. Auch sehr sinnig.

Wenn Sie nun aber einmal diese Grafik genauer anschauen,…

image
Quelle: Erik Dörnenburg

…dann fällt Ihnen sicher auf, dass sich im Grunde nur bei vielleicht 10 (7,5%) von 132 Dateien die Giftigkeit aus mehr als Zyklomatischer Komplexität (ZK) und Methodenlänge (ML) ergibt.

Die Werte anderer Metriken spielen diesen beiden gegenüber kaum eine Rolle - oder sie korrelieren mit ihnen positiv: wo ZK+ML groß ist, da ist z.B. auch der Fan-Out (efferente Kopplung) groß.

Ich frage mich deshalb: Was soll dieser ganze komplizierte Metrikenkram? Wenn man sonst nichts zu tun hat, kann man sich damit einen schönen Tag machen und wichtig aussehen. Für die Entscheidung an der Praxisfront jedoch reichen einfachere Heuristiken. Und um nicht mehr als Heuristiken geht es ohnehin. Denn auch die exaktesten Berechnungen von Metriken sollten nicht darüber hinweg täuschen, dass Giftigkeit vor allem im Auge des Betrachters entsteht.

Ich mache es mir in der Codebeurteilungspraxis einfacher. Mein Fokus liegt auf nur zwei Metriken:

  • LOC: Je länger eine Codeeinheit, desto schmutziger und schließlich giftiger ist sie. (Oder genauer: Es geht eigentlich um Anweisungen und nicht um Lines. Aber LOC hat sich einfach eingebürgert als Begriff.)
    Wieviele LOC eine Codeeinheit hat, kann man sehen.
  • Zyklomatische Komplexität: Je höher die ZK, desto schmutziger und schließlich giftiger ist eine Codeeinheit.
    Wie die ZK einer Codeeinheit ist, kann man abschätzen nach der Zahl der Kontrollanweisungen (Fallunterscheidungen, Schleifen) und ihrer Schachtelungstiefe.

Auch hier gibt es natürlich eine gewisse positive Korrelation: bei wenigen LOC wird die ZK eher gering sein, bei vielen LOC wird sie tendenziell auch steigen.

Dennoch möchte ich auf keine der Metriken verzichten. Auch wenn sie korrelieren, repräsentieren sie unterschiedliche Aspekte des Codes. LOC steht für mich eher im Zusammenhang mit dem SRP. Und ZK steht für mich eher im Zusammenhang mit Testbarkeit.

Ab welcher Länge oder Komplexität wird Code nun aber nicht nur schmutzig oder giftig? Meine persönlichen Alarmglocken gehen an, wenn eine Methode länger als eine Bildschirmseite ist, also bei LOC > 20-30, und/oder mehr als 2-3 Kontrollanweisungen enthält und/oder die Schachtelungstiefe > 2 ist.

Letztlich geht es aber für das Refaktorisieren weniger um absolute Zahlen. Refaktorisiert werden sollte dort, wo die Giftigkeit “nur” größer ist als woanders. Das gilt für Methoden mit 10.000 LOC und ZK 20 vs 8.000 LOC mit ZK 15 genauso wie für Methoden mit 100 LOC und ZK 10 vs 80 LOC mit ZK 6.

Und dieses Verhältnis sollte dann auch noch im Lichte der Unbeständigkeit betrachtet werden. Da stimme ich Graham Brooks zu.

Darüber hinaus interessiert mich allerdings noch die afferente Kopplung (AK, Fan-In). Denn wenn Codeeinheiten von vielen anderen referenziert werden, haben sie eine große Strahlkraft. Änderungen/Fehler an/in ihnen wirken sich leicht auf weite Teile der Software aus. Das bedeutet, Codeeinheiten mit hoher afferenter Kopplung verdienen ein besonderes Augenmerk.

Wenn ich eine Liste mit Codeeinheiten und ihren Metriken wie die folgende hätte

Codeeinheit(Name, LOC, ZK, AK, Unbeständigkeit)

würde ich sie so sortieren:

  1. Zuerst Codeeinheiten mit hoher Unbeständigkeit...
  2. ...dann solche mit hoher afferenter Kopplung...
  3. ...dann solche mit hoher LOC...
  4. ...dann solche mit hoher ZK.

Komplizierter muss die Entscheidungsgrundlage für die Refaktorisierung nicht sein, finde ich. Es sind keine komplizierten Schwellenwerte nötig, keine weiteren Metriken. LOC und ZK kann man sogar mit dem unbewaffneten Auge recht gut beurteilen. Für Unbeständigkeit und AK ist Werkzeugunterstützung nützlich.

Mittwoch, 2. November 2011

Get real, Kanban!

Schöne Sache das Networked Kanban wie es Jurgen Appelo in seinem Blogbeitrag beschreibt - nur was ist denn daran wirklich neu? Jeder nicht triviale Produktionsprozess ist mehr als eine "Eimerkette". Und jeder Produktionsschritt arbeitet immer aus einer Warteschlange. Die ringelt sich auf dem Schreibtisch in Form einer Inbox, die besteht aus Post-It Notes am Monitor oder die zischelt in Excel oder einem anderen Tool im Intranet.

Die "Erfindung" von Kanban kann also nicht in Warteschlangen bestehen. Und wenn Kanban die Softwareproduktion als "Eimerkette" sieht, dann ist das eine sehr vereinfachende Sichtweise.

Aber zum Glück bietet Kanban mehr. Es geht darum, die ohnehin immer existierenden Warteschlangen zur Steuerung zu nutzen. Der bewusste Umgang mit Warteschlangen ist der Trick bei Kanban. Weg von der lokalen ad-hoc Optimierung hin zu einer globalen Optimierung.

Guter Ansatz - nur fehlt mir immer wieder dabei etwas. Daran rührt Jurgen Appelo, finde ich. Es fehlt die Frage, wie denn überhaupt der Produktionsprozess aussieht?

Die Kanban-Boards, die ich bisher gesehen habe, sind aus meiner Sicht so einfach, dass sie der Realität des Softwareproduktionsprozesses nicht wirklich gerecht werden, finde ich. Man freut sich, dass man ein Board aufhängt und nun Warteschlangen beobachtet. Das Vorhandensein eines Terrariums auf dem Gang wird als Meilenstein betrachtet.

Doch was ist es, das man dort beobachtet? Ist das wirklich genug? Ist das die Realität oder nur eine mühelose Abstraktion?

Jurgen Appelo  kommt der Wirklichkeit näher mit dem Netzwerk. So kann Kanban skalieren. (Ob Kanban dafür dann ein guter Name ist, lasse ich mal dahingestellt. Er ist zumindest eingebürgert.)

In Jurgens Beitrag kann man ahnen, dass Softwareproduktion erstens ein Netzwerk von Produktionsschritten ist und zweitens auch noch eine Holarchie: jeder Produktionsschritt auf einer Abstraktionsebene kann potenziell wieder ein Produktionsprozess bestehend aus mehreren Schritten sein. Ich fühle mich erinnert an Staged Event Driven Architecture, SEDA:

image

Die Masterfrage jedoch lässt auch Jurgen offen: Wie sieht denn überhaupt der Produktionsprozess im Ganzen aus? Wieviele Ebenen hat er, aus welchen Schritten besteht er?

Board-Beispiele wie dies

image

finde ich viel zu starr. Das Leben und die Produktion von Software finden nicht im Schwimmbad auf klaren Bahnen statt.

Indem Kanban aber immer wieder nur so beschrieben wird, macht man es Teams schwer, ihre Realität wirklich zu finden - und zu optimieren. Denn es kann ja nicht nur darum gehen, einen simplifizierten Prozess zu optimieren. Auch das führt nur zu Reibungen - über die man sich wundert, weil man doch lokal alles optimal gemacht zu haben meint.

Jurgens Beitrag sollte daher ein Appell sein, sich mehr Gedanken über den Produktionsprozess von Software zu machen. Wenn dabei dann erstmal soetwas herauskommt ist das auch ok:

image

So ist halt das Leben. Gut, dass man das weiß. Dann kann man an die Sache wirklich bewusst herangehen. (Im Bild ist zum Beispiel zu sehen, dass nicht die Softwareentwicklung das Problem ist. Die mit Kanban zu optimieren, brächte nichts. Das Problem steckt in den Schritten davor (rot).)

Kanban-Kärtchen schieben, ist auch wieder keine Silberkugel für die Softwareentwicklung. Genauso wenig wie jeden morgen 15 Minuten Standup-Meeting und alle 3 Wochen ein Release rausschieben nach Scrum.

Die Realität ist unordentlicher und schmutziger. Sie muss zuerst erkannt werden. Teams müssen sich das IST bewusst machen, bevor sie auf den einen oder anderen Zug aufspringen. Das, würde ich sagen, hat auch Jurgen in gewisser Weise gemerkt. Sein Beitrag hilft damit, realistischer in eine Veränderung des Vorgehens bei der Softwareentwicklung einzusteigen. Analyse und Nachdenken helfen halt doch.

Am Ende zählt nicht, ob man Scrum oder Kanban “nach dem Buch macht”. Es zählt, dass die Softwareproduktion besser ist als vorher.

Mittwoch, 12. Oktober 2011

Komponentenorientierung fürs Unternehmen

Kann Unternehmensorganisation von der Softwareentwicklung etwas lernen? Dass umgekehrt Softwareenwicklung von der Unternehmensorganisation lernen kann, zeigt ja in der letzten Zeit sehr schön Kanban. Das Vorgehensmodell überträgt erfolgreiche Organisationsmuster aus warenproduzierenden Unternehmen in die Programmierung

Aber auch umgekehrt ist Lernen möglich. Die Komponentenorientierung ist ein Beispiel, wo Unternehmen der Softwareentwicklung etwas abschauen können.

Komponenten in der Softwareentwicklung

In der Softwareentwicklung stehen Komponenten zunächst für Wiederverwendbarkeit, d.h. den unveränderten Einsatz von Code in verschiedenen Anwendungen. Die Motivation dahinter ist die Erhöhung des ROI: Wenn schon in Code investieren, dann am besten in einer Weise, dass der möglichst häufig zum Einsatz kommen kann.

Beispiel Rechtschreibprüfung: Wenn ein Softwareentwicklungsteam einen Blog-Editor entwickeln soll und dafür eine Rechtschreibprüfung braucht, dann tut es gut daran, diese Funktionalität so zu formen und zu verpacken, dass sie nicht nur im Blog-Editor zum Einsatz kommen kann, sondern auch in einem zukünftigen Twitter- oder Google+-Client oder auch noch in einem Chat-Client oder Email-Client.

Die Entwicklungskosten mögen für eine solch allgemeinere Rechtschreibprüfung etwas höher sein als für eine Blog-Editor-spezifische - doch das wird kompensiert durch zukünftige Ersparnisse. Bei den anderen Produkten fallen keine weiteren Kosten für die Rechtschreibprüfung mehr an.

Programmcode wiederverwendbar auszulegen, d.h. Komponenten zu entwickeln, spart also Geld. Noch mehr Geld spart jedoch, Programmcode gar nicht mehr zu entwickeln, sondern einzukaufen. Warum sollte das Softwarehaus überhaupt eine Rechtschreibprüfung entwickeln? Rechtschreibprüfung ist eine so grundlegende Aufgabe für viele Programme, dass sicherlich schon andere vor dem Softwarehaus dafür Lösungen entwickelt haben. Warum also nicht eine solche Lösung einkaufen? Das kostet zwar Geld, aber der enorme Aufwand, den Entwicklung und Pflege einer eigenen Rechtschreibprüfung bedeuten würden, steht gewöhnlich in keinem Verhältnis zum Kaufpreis einer Komponente oder gar eventueller Lizenzgebühren.

Wo die Aufgabe einer Software sich standardisieren lässt, sollte also nach einer existierenden Komponente Ausschau gehalten werden. Das ist eine Entscheidung für buy statt build, für den Einkauf statt den Selbstbau.

Die Entwicklung von allgemeinen Komponenten statt immer wieder spezieller Softwarebausteine spart also Kosten, wenn zu erwarten ist dass sie wiederholt zum Einsatz kommen.

Der Einkauf von Komponenten statt ihrer Entwicklung spart darüber hinaus Kosten, selbst wenn kein wiederholter Einsatz im eigenen Haus zu erwarten ist.

Software aus existierenden Komponenten zusammenzusetzen spart Geld; doch selbst wenn die Kosten genauso hoch wären wie die einer Eigenentwicklung oder vielleicht sogar etwas höher, wäre der Einkauf existierender Komponenten der Eigenentwicklung vorzuziehen.
Der Nutzen eingekaufter Komponenten liegt nämlich nicht nur in einer wahrscheinlichen Kostenersparnis, sondern in der Freisetzung von Ressourcen. Wo Funktionalität nicht selbst entwickelt werden muss, kann man seine Aufmerksamkeit etwas anderem widmen.

Der Einkauf von Komponenten, die für Standardfunktionalität existieren, konzentriert die Arbeitskraft auf das, was nicht standardisierbar ist. Und genau dort liegt die Aufgabe eines Teams.

Der Job eines Teams, das einen Blog-Editor entwickeln soll, ist es vor allem, sich auf das "Blog-Editor-hafte" zu konzentrieren. Was macht einen Blog-Editor aus? Wie unterscheidet er sich von einer allgemeinen Textverarbeitung, wie unterscheidet er sich von einem Twitter-Client? Alles, was ein Blog-Editor mit anderen Programmen gemein hat, sollte das Team nicht selbst programmieren müssen, sondern einkaufen.

Damit erreicht es, dass es mit dem, worum es ihm eigentlich geht, schnellstmöglich voran kommt.

Standardfunktionalität selbst zu entwickeln ist ein Luxus, den Teams sich leisten können, wenn sie durch das Mehr an Kontrolle über diesen Softwareteil, einen deutlichen Vorteil erlangen. [1] Das ist allemal in frühen Phasen eines Softwareprojektes eher selten der Fall. Projekte werden ja nicht aufgesetzt, um etwas Existierendes zu duplizieren, sondern einen Unterschied zu machen. Auf diesen Unterschied müssen deshalb alle Kräfte ausgerichtet sein.

Neben der durch Wiederverwendbarkeit ermöglichten Konzentration auf das Wesentlich bieten Komponenten aber noch einen weiteren Vorteil: Austauschbarkeit. Komponenten sind Black Boxes, deren Aufgabe durch einen separaten Kontrakt definiert ist. Wie welche Daten wann zwischen ihnen und der Umgebung fließen, das ist unabhängig davon beschrieben, wie konkret sie ihre Aufgabe erfüllen, d.h. wie sie implementiert sind. Kontrakte verbergen Details, mit denen sich Komponentennutzer nicht herumschlagen wollen und sollten.

Kontrakte für Standardfunktionalität sind Standardkontrakte. Das heißt, nicht nur eine bestimmte Komponente kann sie erfüllen, sondern viele verschiedene Varianten oder Modelle einer Komponente. Das Blog-Editor Team, das komponentenorientiert denkt und entwickelt, ist also nicht darauf angewiesen, die Rechtschreibkomponente von Hersteller X zu verwenden. Wenn es damit Erfahrung gesammelt hat und unzufrieden ist, kann es zur Rechtschreibkomponente von Hersteller Y wechseln. Das ist selbst dann der Fall, wenn es zunächst in eine eigene Rechtschreibkomponente investiert haben sollte (out-sourcing, buy) - oder umgekehrt, wenn es später von einer eingekauften Standardkomponente zu einer Eigenentwicklung wechseln will (in-sourcing, build).

Komponentenorientierung reduziert die Gefahr eines lock-in, d.h. der Festlegung auf eine ganz bestimmte Implementation von Funktionalität. Das ist günstig, falls die Implemenation in puncto funktionaler oder nicht-funktionaler Anforderungen nicht mehr genügt oder der Anbieter sich als unvorteilhaft erweist.

Es gibt noch weitere Vorteile der Komponentenorientierung für die Softwareentwicklung. Doch schon die aufgezeigten können der Unternehmensorganisation Impulse geben.

Die Vorteile von Komponenten nochmal auf den Punkt:

  • Komponenten setzen Ressourcen frei
  • Komponenten entkoppeln

Komponentenorientierung in der Unternehmensorganisation

Unternehmen haben über "Geld verdienen heute und in Zukunft" eine inhaltliche Mission. Sie bieten einen Haarschnitt, Möbel, Hausbau, Entrümpelung und Millionen andere Leistungen. Ziel jedes Unternehmers, insbesondere jedes Gründers, sollte daher sein, die besonders knappe Ressource Aufmerksamkeit auf seine Mission zu fokussieren.

Beispiel Ad-hoc Buchdruck: Die Mission des Unternehmens ist, Lesern von elektronischen Dokumenten - seien es PDFs oder Web-Seiten - die Möglichkeit zu geben, daraus Bücher zu machen. Beliebige Seiten beliebiger Dokumente sollen ad-hoc zusammengefasst werden können. Und diese Zusammenfassungen sollen dann on-demand in beliebiger Auflagenhöhe, d.h. beginnend mit Auflage = 1, als Buch gedruckt und innerhalb von 2 Tagen geliefert werden.
Was kann der Entrepreneur, der eine solche Leistung aufbauen möchte, nun von der Komponentenorientierung lernen?

1. Out-sourcing vs in-sourcing

Der Entrepreneur sollte sich fokussieren auf das, was seine Idee, sein Geschäft ausmacht. Für das Beispiel ist das die einfache individuelle Zusammenstellung von Inhalten zu Büchern, der Druck von Einzelexemplaren und die schnelle Lieferung.

Print-on-demand Dienste wie epubli oder lulu.com bieten das heute auch schon grundsätzlich - nur nicht in dieser Einfachheit und Geschwindigkeit. Die nicht-funktionalen Aspekte machen mithin die Geschäftsidee aus: individuelle Bücher aus existierenden Inhalten in Auflage 1 drucken und übermorgen liefern. Darauf muss sich der Entrepreneur fokussieren.

Das bedeutet im Umkehrschluss: Alles, was nicht im Fokus seiner Mission ist, sollte der Entrepreneur nicht selbst leisten. Stattdessen sollte er nach Komponenten Ausschau halten, die es für ihn leisten.

Sollte der Entrepreneur seine Buchhaltung selbst machen? Nein. Das gehört nicht zu seiner Mission.

Sollte er selbst drucken? Nein, jedenfalls nicht, wenn es sich vermeiden lässt. Drucken ist zwar Teil der Idee, doch deren Unterschied zu existierenden Angeboten besteht nicht in Funktionalität (Buchdruck), sondern in einer Qualität der Funktionalität (Geschwindigkeit). Die Aufgabe des Entrepreneurs ist es daher, Buchdruck nicht zu ermöglichen, sondern zu beschleunigen. Wenn er das kann, ohne selbst zu drucken, dann ist das der einzuschlagende Weg.

Sollte er selbst das Marketing machen? Nein. Das gehört nicht zu seiner Mission. Der Entrepreneur hat die Idee von einem Ganzen. Dieses Ganze aus Teilen zusammenzufügen, das ist seine vornehme Aufgabe. Er ist Integrator und Koordinator, nicht Executor.
Sollte der Entrepreneur selbst die Homepage seines Unternehmens erstellen? Nein. Das gehört nicht zu seiner Mission.

"Selbst machen" bezieht sich bei diesen Fragen nicht nur darauf, ob der Entrepreneur als Person, sondern ob sein Unternehmen eine Aufgabe selbst erledigt. In-sourcing vs out-sourcing ist die Frage.

Und die Antwort der Komponentenorientierung ist: out-sourcing soweit möglich, in-sourcing soweit nötig.

Dem out-sourcing kann natürlich das Budget entgegenstehen. Wo das Geld knapp/unsicher ist, tendieren Unternehmensgründer daher zum in-sourcen. Lieber die Buchhaltung im eigenen Haus, lieber die das Sekretariat im eigenen Haus, lieber Lager und Auslieferung im eigenen Haus usw.

Wo es vergleichbare Leistungen nicht am Markt einzukaufen gibt, stellt sich die Frage nach dem out-sourcing nicht. Immer mehr Leistungen lassen sich heute jedoch einkaufen. Der Gründer hat also die Alternative. Und er sollte sich im Zweifel für das out-sourcing entscheiden.
In-sourcing kostet auch Geld, manchmal vielleicht sogar weniger. In-sourcing hat aber mehrere Nachteile, die sich gerade ein Gründer bewusst machen sollte:

  • In-sourcing erfordert Führung. Out-sourcing erfordert nur Koordination der zugekauften Komponente mit anderen; aber in-sourcing braucht darüber auch noch Führung der "Implementation" einer Leistung. Sie ist keine Black Box. Der Entrepreneur muss sich um die einzelnen Mitarbeiter kümmern.
  • In-sourcing bindet. Ein Dienstleister kann ohne moralische Bedenken gewechselt werden; eigenen Mitarbeitern kann und/oder will man nicht so leicht kündigen.
  • In-sourcing erzeugt Fixkosten. Zugekaufte Leistungen werden meist nach Aufwand/Stück abgerechnet. Hat das Geschäft keinen Bedarf, fallen keine Kosten an. Interne Leistungen kosten jedoch immer Geld, auch wenn es nichts zu tun gibt. Gehälter und Mieten laufen weiter.
    Der Leitstern des Unternehmers sollte immer der Fokus sein. Bei jeder Entscheidung über in- vs out-sourcing sollte die Frage lauten: Wie kann ich mich mehr auf die eigentliche Mission konzentrieren?

Für Gründer kommt dann noch als zweite Frage hinzu: Wie kann ich flexibel bleiben, falls ich den Kurs des Unternehmens ändern muss?

2. Austauschbarkeit durch Kontrakte

imageEgal, ob eine Leistung selbst erbracht oder eingekauft wird, sie sollte klar kontouriert werden. Die Schnittstellen zwischen den Komponenten - ob interne oder externe - sind klar zu definieren. Kontrakte sollte so ausgelegt werden, dass die Kopplung zwischen Komponenten so lose wie möglich ist.

Wie eng Leistungserbringer - Abteilungen oder externe Dienstleister - gekoppelt sind, hängt davon ab, wie stark Änderungen auf der einen Seite des Kontraktes Änderungen auf der anderen Seite hervorrufen können.

Wenn der ad-hoc Buchdruck zum Beispiel den Versand selbst übernimmt, dann haben personelle Änderungen darin größere Auswirkungen, als bei Einbindung einer externen "Versandkomponente". Eine "Versandkomponente" ist für den Entrepreneur eine Black Box. Wie sie ihren Kontrakt erfüllt, ist ihm egal. Sollte es dort personelle Änderungen geben, darf das laut Vertrag keinen Einfluss auf die Leistungsqualität haben.

Intern lässt sich solche Entkopplung jedoch kaum herstellen. Und der Austausch einer internen "Versandkomponente" fällt auch nicht leicht.

Externe Komponenten erhöhen mithin die Flexibilität eines Unternehmens - zumindest ganz allgemein. Das ist besonders für Gründer wichtig. Die sind ja für ihre Mission noch auf der Suche nach einem Erfolgsweg. Da sollten sie in der Lage sein, Leistungen einfach zu verändern und auszutauschen. Bei internen Leistungen fällt das jedoch schwerer als bei externen. Mindestens die Kontrakte sind weniger explizit.

Deshalb sollte ein Unternehmen Wert darauf legen, nach Identifikation seiner Komponenten, deren Kontrakte möglichst ausdrücklich zu formulieren, ganz unabhängig davon, ob nun in- oder out-sourcing stattfindet.

Dazu gehört aber nicht nur die Form der Schnittstelle, also die Oberfläche, an der Komponenten miteinander (re)agieren. Es ist auch zu bedenken, was in einer Komponente an "Zustand" angehäuft wird. Der sollte sich bei Austausch nicht als hinderlich erweisen.

Ein Beispiel dafür sind Email-Dienstleister wie Google oder Hotmail. Wer sie benutzt, interagiert mit ihnen nicht nur über einen Email-Client (z.B. Web-Client oder Outlook), sondern akkumuliert auch "Zustand"; das sind all die Emails, die beim Dienstleister im Archiv liegen.
Wenn nun der Email-Dienstleister gewechselt werden soll, darf dieser "Zustand" dem nicht im Wege stehen. Er soll erhalten bleiben, wenn man z.B. von Hotmail nach Google oder Exchange umzieht. Darin liegen ja wertvolle Informationen aus Jahren der Arbeit. Wenn das nun nicht möglich ist, dann liegt ein lock-in vor. Der ist zu vermeiden. Darauf muss bei der Formulierung des Kontrakts geachtet werden.

Gerade wer auf externe Komponenten zurückgreifen will, tut gut daran, von vornherein den möglichen Fall des Wechsels anzusprechen. Im Kontrakt sollte stehen, was in dieser Hinsicht möglich ist, worauf zu achten ist. Bei externer Buchhaltung oder zugekauftem Marketing mag das relativ einfach sein, aber externe Softwareentwicklung macht da z.B. traditionell Probleme.
Das sollte aber nicht gegen out-sourcing sprechen, sondern nur für eine sorgfältige Kontraktgestaltung. Die Vorteile des out-sourcing gerade für Gründer sind so groß, dass sie nicht durch Unsicherheit in diesem Punkt verspielt werden dürfen. "Ach, dann machen wir das halt doch selbst, wenn es die Vereinbarungen so schwierig sind." wäre die falsche Reaktion.
Ein in-sourcing mag die Kontraktdefinition einfacher machen; sie fällt womöglich sogar ganz unter den Tisch, weil ja eben eine Leistung im Haus erbracht wird. Da muss man sich ja nicht um eine explizite Beschreibung kümmern, oder?

Der einfachere, weil fehlende Kontrakt zwischen Komponenten zieht dann aber später Probleme an. Ohne explizite Kontraktgrenzen wird das Unternehmen monolithisch, d.h. zu einem wenig flexiblen Block. Das verhindert zukünftige Entwicklung. Wissen ist dann eher implizit denn explizit; Leistungen werden irgendwie denn durch einen sauberen Kontrakt beschrieben erbracht. Wachstum fällt dann schwer.

Fazit

Unternehmensorganisation kann von der Softwareentwicklung lernen. Unternehmen wie Software sind komplexe Systeme, die davon profitieren, sauber strukturiert zu sein. Und beide profitieren davon, wenn sie nicht alles selbst leisten müssen, sondern sich auf ihren eigentlichen Zweck konzentrieren können.

Unternehmen sollten ihre Leistung daher als eine Kooperation von "Belangen" verstehen, die von Komponenten erbracht werden. Diese Komponenten sind Black Boxes definiert durch Kontrakte. Für jede Black Box kann dann entschieden werden, ob sie intern aufgebaut oder extern eingekauft werden soll.

Der externe Einkauf von Komponenten hat insbesondere dann vorteile, wenn das Unternehmen noch "in der Findungsphase ist", d.h. besonders flexibel agieren muss. Dann bedeutet Zukauf von externen Komponenten ein Plus an Flexibilität und Konzentration bei gleichzeitig geringeren Fixkosten.

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Fußnoten

[1] Ein solcher Vorteil könnte in mehr Qualität bei nicht-funktionalen Anforderungen wie Performance, Sicherheit, Usability oder Internationalisierung liegen. Dass er bei der Funktionalität liegt, ist eher nicht zu erwarten, da es sich ja sonst nicht um Standardfunktionalität handeln würde.

Dieser Artikel ist ein Beitrag zur Blog-Parade des

image

Donnerstag, 6. Oktober 2011

Skalierbare Softwarebausteine - Teil 2

In meinem Traum sehen fundamentale Softwarebausteine ganz einfach aus, wie ich im ersten Teil dieser kleinen Artikelserie beschrieben habe:

image

Es sind potenziell zustandsbehaftete und potenziell nebenläufige Funktionseinheiten auf beliebig vielen Abstraktionsebenen, die Input-Daten in Output-Daten transformieren. So einfach ist das :-)

Dabei könnte ich nun stehenbleiben und hübsche Diagramme mit solchen Funktionseinheiten malen. Das würde mir das Nachdenken über Softwarestrukturen schon erleichtern.

Doch ich will mich nicht in Wolkenkuckucksheim einrichten. Bubbles don´t crash. Der Traum muss also einen Realitätsbezug bekommen. Die konzeptionellen Funktionseinheiten müssen ausführbar sein; irgendwie müssen sie an Code angebunden werden.

Die Form einer normalisierten Funktionseinheit

Wenn solche traumhaften Flüsse ausführbar sein sollen, stellt sich als erstes Frage, welche Form eine Funktionseinheit in Code haben könnte. Bisher habe ich als Übersetzung Event-based Components favorisiert: Jede Funktionseinheit wird als Klasse implementiert, die für jeden Input-Stream eine Methode anbietet und für jeden Output-Stream einen Event.

Das finde ich inzwischen zu aufwändig. Es funktioniert ordentlich, doch es macht ziemliche Mühe und schränkt ein. Der Zwang zur Klasse ist in der Community auch immer wieder auf Stirnrunzeln gestoßen.

Deshalb jetzt ein Vorschlag mit kleinerem Fußabdruck:

delegate void FunctionalUnit<in TInput, out TOutput>(
    IMessage<TInput> input,
    Action<IMessage<TOutput>> output);

interface IMessage<out T> {
    string StreamQualifier { get; }
    T Data { get; }
}

Wie ist das? Deutlich einfacher, oder? Sie können eine Funktionseinheit bauen, wie Sie mögen, solange es einen Adapter gibt, der sie an diesen Delegaten anpasst. Funktionen, Methoden, auch EBC-Klassen sind möglich. Nur ein Beispiel:

class Program
{
    static void Main(string[] args)
    {
        FunctionalUnit<string, IEnumerable<string>> fuSplit;
        fuSplit = (inputMsg, outputCont) =>
            {
                var outputData = Split(inputMsg.Data);
                var outputMsg = new Message<IEnumerable<string>>(
                                           "", outputData);
                outputCont(outputMsg);
            };

        fuSplit(new Message<string>("", "a;b"),
                  (m) => Console.WriteLine(string.Join("/", m.Data)));
    }


    static IEnumerable<string> Split(string config)
    {
        return config.Split(';');
    }
}

Die Implementation der Funktionseinheit ist eine Funktion. Um die herum wickelt Main() einen Adapter, der den Input an FunctionalUnit übersetzt in das Funktionsargument und ihren Rückgabewert in den Aufruf des Continuation-Delegaten.

Dasselbe ist möglich mit einer Methode oder mit einer EBC-Klasse. Sollte es dabei um mehrere Input- und/oder Output-Streams gehen, kann der StreamQualifier von IMessage<> zur Unterscheidung herangezogen werden.

An dieser Stelle nehmen Sie bitte vor allem die Botschaft mit: Sie können Funktionseinheiten implementieren, wie Sie mögen.

Es gibt keinen Vorzug mehr für Klassen. Einzig, Ihre Implementation muss sich an den Delegaten adaptieren lassen. Das geht aber zumindest für statische und Instanz-Methoden/Funktionen und für EBC-Klassen.

Die Beschreibung von Flüssen

Dass Funktionseinheiten jede Form haben können, wird Sie etwas aufatmen gelassen haben. So ganz entfinstert wird Ihre Miene allerdings noch nicht sein, weil der Preis für die Freiheit der Form ein höherer Koordinationsaufwand zu sein scheint. Es müssen ja nun Adapter für Funktionseinheiten zusammengesteht werden, damit es zu einem Fluss kommt.

Das ist richtig. Deshalb wird nun die Frage nach einer Beschreibung von Flüssen drängender. Bisher konnte sie recht simpel in C# als Event-Event-Handler Zuweisungen von EBC-Funktionseinheiten übersetzt werden. Das geht nun nicht mehr so einfach. Eine DSL wäre zur Unterstützung schön; aus der könnte womöglich der ganze Adapter-Code und das Koordinieren von Funktionseinheiten generiert werden.

Ja, das könnte man. Aber ich möchte etwas anderes vorschlagen.

Aber erst einmal: Wie könnten Flüsse beschrieben werden? Sie könnten grafisch notiert werden. Dafür ist aber ein recht aufwändiger Designer nötig. Oder sie könnten textuell notiert werden. Ich halte letzteres für ohnehin nötig, selbst wenn es einen Designer gäbe.

ebc.xml (ebclang.codeplex.com) ist ein Versuch gewesen, Flüsse textuell zu beschreiben. Dazu gibt es auch einen Visualizer und einen Compiler. Aber XML scheint mir zu umständlich. Da steckt viel Rauschen drin.

Dann habe ich mit einer DSL experimentiert, die ich ebc.txt genannt habe. Die Transformation einer Konfigurationszeichenkette der Form “a=1;b=2” in ein Dictionary ließe sich damit so beschreiben:

ToDictionary {
  this.in –(string)-> (Split)
          –(string*)-> (Map)
          –(KeyValuePair<string,string>*)–> (Build)
          –(Dictionary<string,string>)-> this.out
}

Das ist doch lesbar, oder? Ich benutze diese Notation jedenfalls recht gern, um mal kurz in einer Email oder einem Diskussionsgruppenbeitrag einen Flow zu beschreiben.

Allerdings ist auch diese Notation noch recht länglich, finde ich. Für die Menschen-Mensch-Kommunikation ist sie ok. Doch auch sie enthält noch Rauschen.

Deshalb komme ich auf ebc.xml zurück, nur ohne XML. Die einfachste, maschinenlesbare Form der Beschreibung eines Flow ist eine Tabelle:

ToDictionary
this.in, Split
Split, Map
Map, Build
Build, this.out

Die erste Zeile bezeichnet die koordinierende Funktionseinheit, alle weiteren Zeilen verbinden Output mit Input.

Dass hier keine Typen mehr für Input/Output angegeben sind, ist nicht weiter schlimm. Entweder man rüstet das nach


this.in, Split: string
Split, Map: IEnumerable<string>

oder man überlässt die Prüfung auf Passgenauigkeit einer Übersetzungs- oder Laufzeitinstanz. Letzteres finde ich völlig ausreichend.

Mit solchen “Verbindungstabellen” lassen sich nun trefflich auch tiefe Fluss-Hierarchien beschreiben:

image

Jedes Komposit wird durch eine Tabelle beschrieben:

R
R, T
T.a, W
T.b, V
W, S.c
V, S.d
S, R

W
W, X
X, Y
Y, W

V
V, Z
Z, Y
Y, V

Wo Input- bzw. Output-Streams implizit bestimmbar sind, da müssen sie nicht angegeben werden, z.B. bei

R
R, T

Hier ist klar, dass R sich auf das Komposit bezieht und deshalb auf der linken Seite des Tupels ein Input-Stream gemeint ist. Rechts steht nicht das Komposit selbst, sondern eine enthaltene Funktionseinheit. Deren Input-Stream ist gefragt; doch da sie nur einen hat, muss der nicht näher benannt werden.

Wenn mehrere Input-/Output-Streams vorhanden sind, ist allerdings eine Qualifizierung nötig wie bei:


W, S.c

Solche Tabellen sind recht bequem hinzuschreiben. Das hat schon ebc.xml gezeigt. Als einfachste Notationsform mögen sie deshalb genügen. Minimal sind sie jedoch nicht.

Die Arbeitspferde einer hierarchischen Struktur sind die Blätter. Hier: S, T, X, Y, Z. Die Komposite bilden nur Kontexte, in denen sie zu “Kooperativen” zusammengefügt sind. Da die keine weitere eigene Aufgabe haben, können sie spätestens zur Laufzeit wegfallen als eigenständige Funktionseinheiten.

Das obige System kann auch in nur einer Tabelle so beschrieben werden:

/R, /R/T
/R/T.a, /R/W/X
/R/W/X, /R/W/Y
/R/W/Y, /R/S.c
/R/S, /R
/R/T.b, /R/V/Z
/R/V/Z, /R/V/Y
/R/V/Y, /R/S.d

Hier sind alle Blatt-Funktionseinheiten qualifiziert durch einen Pfad, der beschreibt, in welcher Schachtelung von Kompositen sie stecken. Damit lassen sich auch Y in W und Y in V unterscheiden.

Die Daten von T.a gehen nun direkt nach W/X, statt zuerst an W und dann an X.

An dieser Stelle halten Sie bitte wieder einen Moment inne und lassen Sie die Botschaft einsinken: Die Struktur beliebig komplexer Software lässt sich durch eine einzige Tabelle beschreiben.

Egal wieviele Ebenen, wieviele Komposite und Blätter: Ein solches System können Sie immer abbilden auf eine simple Tabelle der Output-Input-Beziehungen.

Damit meine ich nicht, dass Kontrollanweisungen wie if oder while überflüssig werden bzw. auch irgendwie über eine Tabelle verknüpft werden sollen. Die stecken ja in den Blättern der Holarchie und sind unsichtbar. Implementationsdetails. Mir geht es nur um die Funktionseinheiten der beschriebenen Form. Wie groß Sie die auf unterster Ebene machen, ist Ihre Sache. Je mehr Sie in Blättern verstecken, desto weniger profitieren Sie vom hiesigen Ansatz.

Höhepunkt: Flüsse zur Laufzeit

Und nun zum eigentlichen Punkt, den ich mit dem vorherigen und diesem Artikel machen will. Der Höhepunkt meines Traums ist eine Runtime Engine für Flüsse. Denn die wird sehr einfach möglich durch diese einfachste Beschreibung von Flüssen.

Ich bin der Meinung, dass wir Flüsse nicht mehr übersetzen sollten. Wir sollten sie auch nicht mehr von Hand “verstöpseln”. Stattdessen sollten wir sie ganz, ganz simpel über eine Tabelle beschreiben, die Blätter (Operationen) benennen – und eine Runtime Engine “interpretiert” dann die “Flusstabelle”.

image

Wie aus Ihren Funktionen, Methoden, EBC-Klassen Operationen werden, ist ein anderer Belang. Der finde ich gerade nicht so spannend. Dafür kann ein Container geschrieben werden, bei dem Sie Ihre Implementationen registrieren können und der sie hin FunctionalUnit-Adapter wickelt. Er könnte Operationen aus Assemblies sogar eigenständig sammeln.

Dito ist die Herkunft der einen, das ganze System beschreibenden Tabelle für mich zweitrangig. Die kann aus mehreren Tabellen generiert worden sein. Oder sie kann aus einer ebc.txt DSL übersetzt worden sein. Auch das ist eine Funktionalität, die sich kapseln lässt.

Nein, zentral ist für mich die Runtime Engine zur Interpretation von Flows. Da spielt die Musik. Und jetzt festhalten:

  • Die Runtime ist der zentrale Ort, an dem der Fluss (oder die Hierarchie der Flüsse) belauscht werden kann. Die Runtime leitet Output an Input weiter. Sie hat also jede Nachricht ausdrücklich in der Hand. Ihr können wir unsere Wünsche äußern, welche Nachrichten wir abgreifen wollen. Vielleicht wollen wir sie nur protokollieren, vielleicht wollen wir sie aber auch manipulieren. Alles ist möglich durch den zentralen Eingriffspunkt Runtime. Stellen Sie sich eine Logging vor, das sie zur Laufzeit mit Pfaden einschalten können: “log /*/T.*”, d.h. logge alle Nachrichten, deren StreamQualifier dem Pattern entspricht.
  • Die Runtime ist der zentrale Ort, an dem alle Flüsse in ihrer Geschwindigkeit kontrolliert werden können. Wenn wir wollen, können wir sie komplett anhalten – und dann wieder starten. Oder wir verlangsamen sie nur, um den Fluss vielleicht grafisch visualisiert mitverfolgen zu können.
  • Die Runtime ist der zentrale Ort, an dem wir Performancedaten messen lassen können. Was sind die Durchflusszeiten, wie groß sind die Nachrichten in Strom /R/S.c durchschnittlich usw.
  • Wenn die Runtime Output an Input “von Hand” zustellt, dann kann sie diese Übertragung selektiv aussetzen, während wir eine Operation zur Laufzeit ersetzen.
  • Oder wir können der Runtime zur Laufzeit eine Änderung des Flusses mitteilen. Der ist ja nur eine Tabelle, die wir aktualisieren können.
  • Und schließlich bekommt die Runtime jede Exception mit. Wenn wir wollen, stellen wir in der Runtime ein, was mit Exceptions passieren soll, die bei der Verarbeitung ganz bestimmter Streams auftreten.
  • Ach, bevor ich es vergesse: Natürlich ist die Runtime auch die Instanz, von der wir uns wünschen können, ob Funktionseinheiten synchron, async+sequenziell oder parallel ausgeführt werden sollen.

Sie sehen, ich träume von großen Vorteilen, wenn wir beginnen, Software als Hierarchie von Datenfluss-Funktionseinheiten zu betrachten.

Technisch ist das alles grundsätzlich möglich. Ich habe es in einem Spike schon ausprobiert. Hier und da lauern sicherlich noch Teufelchen im Detail. Aber wenn wir das Ergebnis für wünschenswert halten, dann werden wir Wege finden, es vom Traum zur Realität werden zu lassen.

Mit diesen beiden Postings wollte ich dazu meine Gedanken ein wenig ordnen. Ein Codeplex-Projekt für Implementation einer Runtime ist schon aufgesetzt. Jetzt muss ich nur noch die Zeit finden, sie schrittweise auch zu implementieren. Vielleicht machte ich daraus eine Application Kata mit vielen Iterationen. Dann können andere parallel und auf anderen Plattformen es auch angehen.

Einstweilen lassen Sie mich wissen, ob Sie mitträumen oder es sich für Sie wie ein Albtraum anhört…

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Mittwoch, 5. Oktober 2011

Skalierbare Softwarebausteine - Teil 1

Wie können wir Software mit evolvierbarer Struktur herstellen? Wie kann Software selbst so agil werden wie unsere Prozesse es schon geworden sind? Diese Frage treibt mich immer noch um. Die bisher angebotenen Lösungsansätze aus Richtung Clean Code finde ich noch nicht ausreichend. TDD und SOLID sind ganz gute Ansätze… nur fehlt ihnen aus meiner Sicht ein umfassender Blick auf Software. Das SRP in SOLID ist zwar grundlegend – doch DI ist demgegenüber merkwürdig sprachspezifisch, finde ich. Und TDD fokussiert auf Code, so dass von TDD wenig Hilfe zu erwarten ist, wenn noch gar kein Code oder eine Code-Idee vorhanden ist.

Ich glaube deshalb weiterhin, dass wir noch einen Schritt zurücktreten müssen. Wir müssen eine allgemeinere Vorstellung von Software entwickeln, in die dann TDD und SOLID usw. hineinpassen. TDD und SOLID sollen also nicht einfach ersetzt werden, sondern einen soliden Platz bekommen.

Warum die real existierende Objektorientierung uns behindert

Die Objektorientierung, so könnten Sie sagen, ist doch aber schon so eine allgemeine Vorstellung von Software. Ja, das stimmt. Irgendwie ist sie das. Und dann doch wieder nicht. Zumindest nicht die real existierende Objektorientierung, die sich von einer idealen ursprünglichen stark unterscheidet. Die real existierende Objektorientierung (REOO) hat aus meiner Sicht einfach einige große Mängel:

  • Sie REOO skaliert konzeptionell nicht. Wir können Software mit ihr nicht vom kleinsten bis zum größten Baustein durchgängig beschreiben. Dass Software aus Bausteinen unterschiedlicher Größe/Granularität zusammengesetzt ist, sollte außer Zweifel stehen. Damit meine ich nicht, dass Bausteine mal mehr und mal weniger LOC haben. Ich meine ihre konzeptionelle Größe, ihren Funktionsumfang. Eine ganze EXE umfasst mehr Funktionalität als eine Klasse in der EXE.
    Mir fällt es jedenfalls sehr schwer, Software aus Objekten unterschiedlicher Granularität vorzustellen. Der UML offensichtlich auch, sonst würde sie Klassendiagrammen nicht auch noch Komponenten- und Paketdiagramme beistellen. Weder Komponenten noch Pakete gehören jedoch zur Objektorientierung. Wie ihre Namen schon sagen, geht es nicht um Objekte (oder Klassen als deren Schemata), sondern um Komponenten und Pakete.
  • Die REOO ist fixiert auf ein essenziell statisches Bild der Realität. Nomen est omen: Die Realität besteht aus Objekten, aus “Dingern”, die erstmal sind, deren Existenz eine Dauer hat. Diese “Dinger” haben dann vor allem einen Zustand. Die Praxis der REOO versucht sie vor allem anhand von Daten zu identifizieren, für die sie verantwortlich sind. Objekte sind mithin Zuständigkeitsaggregate. Sie bündeln den Umgang mit Daten. Das wird positiv formuliert als Kapselung.
    Ich verstehe, dass diese Sichtweise Appeal hat. Menschen mögen Greifbares. Mit Lego kann jedes Kind umgehen, mit Mathematik jedoch nicht; die ist viel abstrakter. Also mögen Softwareentwickler auch lieber umgehen mit Greifbarem, eben mit Objekten.
    Wenn ich die Wurzel der Softwareentwicklung im Maschinenbau und der Elektrotechnik verorte (und nicht in der Mathematik), dann finde ich das auch nur konsequent. Die vormaligen Hardwarebausteine sind in die Software übertragen worden: aus konkreten Zahnrädern und Hebeln und Transistoren und Widerständen sind Objekte geworden.
    Oder ich könnte philosophisch werden und die Softwareentwicklung an die Griechen binden. Demokrit mit seinen Atomen und Platon mit seinen Ideen haben die Welt bestehend aus klar umrissenen Entitäten aufgebaut gesehen. Die einen waren anfassbar, wenn auch seeeeehr klein. Die anderen abstrakt, dafür beliebig groß. In jedem Fall haben beide ihre “Weltenbausteine” als unwandelbar angesehen, als statische Bausteine.
  • Die REOO verbindet Objekte über Abhängigkeiten. Ein Objekt, das viel kann, ist von Objekten abhängig, die weniger können. Dadurch entsteht eine Abhängigkeitshierarchie, über die die Gesamtleistung vertikal verteilt ist. Auf jeder Ebene passiert ein bisschen.
    Das hat die REOO als Problem erkannt – im Zusammenhang mit der Testbarkeit. Als Gegenmaßnahme empfiehlt sie IoC und DI und Attrappen. Damit werden Objekte auf jeder Ebene einzeln testbar.
    Ich halte das für eine Symptombehandlung. Die Abhängigkeiten bleiben bestehen. Funktionalität ist weiterhin vertikal in der Hierarchie verstreut.

Meine Kritik an der real existierenden Objektorientierung ist also sehr fundamental. Sie macht uns die Beschreibung von Lösungen schwer, weil wir die mit ihren Mitteln nicht leicht auf beliebigen Abstraktionsebenen denken können. Sie fixiert sich auf ein Weltbild, das es uns schwer macht, das zu beschreiben, was Software essenziell repräsentiert: Prozesse. Und die Ergebnisse der Objektorientierung, der Code, hat eine Form, die ganz fundamental anti-agil ist, also im Widerspruch zur zentralen Erkenntnis der Softwareentwicklung steht, dass Software sich ständig anpassen muss.

Aber nun genug des OO-Bashings :-) Es soll nur als Motivationshintergrund dienen, warum ich immer noch und wieder an einem anderen Software-Weltbild bastle. Nicht, dass mit REOO nix ginge. Doch, klar, es geht was. Mit Pferdefuhrwerken ging auch etwas. Jahrhunderte lang. Und dann kam der Trecker. Nun geht noch mehr.

Vorschlag für ein alternatives Weltbild

Damit ich nicht den Anschein erwecke, eine rundum-sorglos Lösung zu bieten – Silverkugeln gibts ja nicht und sind in unserer Branche bei vielen auch nur im Anschein verhasst –, spreche ich lieber mal von einem Traum.

Ja, ich habe einen Traum, in dem wir Software viel einfacher entwickeln. Das ist natürlich auch irgendwie dem verhaftet, was ich in 30 Jahren Softwareentwicklung erfahren habe. Wie Software in 100 Jahren entwickelt wird, kann ich mir wohl nicht vorstellen. Doch für Sie mag mein Traum dennoch esoterisch anmuten. Macht ja aber nichts, ist eben nur ein Traum :-) Träumen Sie doch mal mit. Lassen Sie sich darauf ein. Schicken Sie Ihre Skepsis für eine kleine Weile auf Urlaub. Die kann sich ja mal den Versuchen zur Rettung von Euro und Europa zuwenden.

In meinem Traum entwickeln wir Software anders, weil Software darin ein anderes Weltbild unterliegt. Software oder die Welt besteht darin nicht aus “Dingern”, sondern aus Prozessen. Im Kern steht nicht etwas Statisches, sondern Dynamik. Es geht ums Tun, um Bewegung, um Wandlung. Softwareentwickler fragen nicht, nach “Objekten”, sondern nach “Aktivitäten”.

Nach diesem Weltbild besteht Software aus diesem:

image

Das ist eine Aktivität. Da passiert etwas. Das ist neutral gesprochen eine Funktionseinheit. Kent Beck würde es vielleicht Element nennen.

“Baustein” möchte ich eigentlich ungern dazu sagen, da “Baustein” schon wieder Statik suggeriert wie “Objekt”. Aktion, Aktivität oder eben Funktionseinheit scheinen mir passender.

So eine Funktionseinheit “macht ihr Ding”, indem sie eingehende Daten verarbeitet. Sie steht also für das alte EVA-Prinzip: Eingabe-Verarbeitung-Ausgabe.

Daten können aus verschiedenen Richtungen in die Funktionseinheit einfließen (Input). Das, was sie daraus macht, kann in verschiedene Richtungen aus ihr herausfließen (Output).

Und wenn eine Funktionseinheit nicht reicht, dann arbeiten mehrere zusammen in einem Fluss. Der Output der einen wird zum Input der anderen:

image

Das war´s. So sieht Software aus. Auf jeder Abstraktionsebene. Damit lässt sich eine ganze Anwendung beschreiben wie auch der kleinste Teil einer Anwendung.

Zustand

Input ist ein Trigger für Funktionseinheiten. Kommt Input an, tun sie etwas. Allerdings muss sich eine Aktivität nicht ausschließlich auf einfließenden Input beziehen. Sie kann auch ein Gedächtnis haben, d.h. Zustand. Aus dem kann sie lesen und den kann sie verändern.

Zustand macht es zwar schwerer, über das Ergebnis einer Aktion nachzudenken, aber ich halte Zustand für einen so natürlichen Bestandteil der Welt, dass wir ihn nicht mit Macht ausschließen sollten. Das rückt Funktionseinheiten natürlich in die Nähe von REOO Objekten – doch der Unterschied besteht für mich im Fokus. Bei REOO steht Zustand eher am Anfang, bei meiner Träumerei eher am Ende.

Im einfachsten Fall ist solcher Zustand lokal. Eine Aktion hat individuellen Zustand:

image

Wenn Aktionen kooperieren sollen, dann kann es allerdings nötig sein, dass sie Zustand gemeinsam nutzen. Das ist dann shared state und sollte explizit gemacht werden. Das wird spätestens dann wichtig, wenn der Zugriff darauf gleichzeitig erfolgen soll. Dann muss er nämlich synchronisiert werden.

image

Nebenläufigkeit

Ohne weitere Angaben arbeiten Aktionen sequenziell und synchron. Während eine Funktionseinheit Input verarbeitet, tut das keine andere. Wenn sie Output produziert, stößt sie damit eine empfangende Funktionseinheit an, die ihn als Input verarbeitet, bevor die produzierende weitermacht.

image

Sequenzielle und synchrone Verarbeitung lässt sich gut denken und nachverfolgen. Aber in der realen Welt kommt sie eher nicht vor. Und sie nutzt die verfügbaren Prozessorressourcen womöglich nicht optimal.

Es ist deshalb konsequent, Aktionen auch nebenläufig betreiben zu können. Sie laufen dann auf (mindestens) einem eigenen Thread (und womöglich auf einem eigenen Prozessorkern).

image

Sobald der Fluss von Input-Output durch eine Aktion mit eigenem Thread läuft, findet die weitere Verarbeitung auf diesem Thread statt, bis wiederum eine Aktion mit eigenem Thread angestoßen wird usw.

image

Dass Aktionen nebenläufig betrieben werden können, bedeutet jedoch nicht, dass Nebenläufigkeit auch innerhalb von Aktionen stattfindet. Die scheint mir problematisch, weil sie dazu führt, dass Input in anderer Reihenfolge als der, in der er eintrifft, verarbeitet wird. In meinem Traum ist daher die Verarbeitung innerhalb einer Funktionseinheit immer noch sequenziell. Input wird in der Reihenfolge seines Eintreffens verarbeitet.

Zumindest sollte das wohl der Default sein. Wenn in ganz bestimmten Situationen eine Aktion davon profitiert, in sich ebenfalls nebenläufig zu sein, d.h. Input auf mehreren Threads parallel zu verarbeiten, dann sei das so. Es könnte so ausgedrückt werden:

image

Aktionen sind zunächst also synchron und sequenziell, dann können sie auch asynchron sequenziell sein und schließlich vollständig parallel.

Unabhängigkeit

Es ist vielleicht keiner Erwähnung wert, weil der Fluss von Input-Output es so natürlich macht, doch ich sage es lieber einmal ausdrücklich: Aktionen sind von einander unabhängig.

Zwei verbundene Aktionen wissen nichts von einander. Der Producer (generiert Output) weiß nicht, wer seinen Output weiterverarbeitet. Der Consumer (verarbeitet Input) weiß nicht, von wem sein Input stammt.

Das (!) ist ein fundamentaler Unterschied zur REOO. Und deshalb ist es wohl gut, dass ich ihn hier nochmal betone ;-)

Eine Gesamtleistung als Ergebnis einer Kooperation mehrerer Funktionseinheiten erfordert keinerlei Abhängigkeiten zwischen den Kooperationspartnern. Sie wissen nicht einmal, dass sie kooperieren.

Jede Aktion innerhalb einer “Kooperative” bekommt Input “von irgendwoher” und produziert Output “für unbekannt”.

Ein Zusammenhang von Funktionseinheiten wie dieser:

image

sieht also eher so aus:

image

Schachtelung

Um größere und sehr große Systeme zu beschreiben, müssen Aktionen hierarchisch angeordnet werden können. Aus großer Flughöhe sieht ein System dann eigentlich immer so aus:

image

Wenn man niedriger fliegt, kommen Details in den Blick:

image

Und wenn man noch tiefer runter geht, d.h. in das System hineinzoomt, dann kommen noch mehr Details in den Blick:

image

So kann es beliebig tief gehen. Software kann also als Baum dargestellt werden:

image

Das sieht nun wieder wie eine REOO Objekthierarchie aus. Das macht auch nichts, solange klar ist, dass der “Verfeinerungsbaum” einem wesentlichen Prinzip folgt: alle Aktionen, die nicht Blatt sind, haben ausschließlich die Aufgabe, Aktionen zu “Kooperativen” zu kombinieren.

In Nicht-Blättern stecken keine Algorithmen. Sie entscheiden nichts, sie enthalten keine Schleifen. Sie sorgen nur dafür, dass Output der einen Aktion zu Input für eine andere wird. That´s it.

Ebenfalls betonenswert: Funktionseinheiten auf allen Ebenen sehen gleich aus. Sie verarbeiten Input zu Output mit eventuellen Seiteneffekten. Und sie sind gleichzeitig Teil von darüber gelagerten Funktionseinheiten, von denen sie in einen Kooperationszusammenhang gestellt werden, wie sie Ganzes sind in Bezug auf Funktionseinheiten, die sie selbst zu einem Kooperationszusammenhang zusammenstellen.

Software ist damit grundsätzlich selbstähnlich aufgebaut; man könnte vielleicht sogar sagen, fraktal. Software ist eine Holarchie und die Funktionseinheiten sind ihre Holons.

Darüber habe ich früher schon öfter geschrieben wie hier oder hier oder 2005 hier. Aber auch wenn ich riskiere, damit zu langweilen, ist mir diese Sichtweise so wichtig, dass ich sie wiederhole. Wir tun uns einen Gefallen, wenn wir dahin kommen, Software so zu sehen. Aller Softwareentwurf kann durch solche Regelmäßigkeit nur einfacher werden.

Normalisierung

Zum Abschluss für heute noch eine Verallgemeinerung. Aktionen habe ich als Funktionseinheiten mit beliebig vielen Input- und Output-Strömen beschrieben. So soll es auch sein.

Für etwas mehr Regelmäßigkeit können Sie jedoch reduziert werden. Das hat später Vorteile, wie Sie sehen werden, wenn es an die Implementierung geht.

Die “Einheitsdarstellung” oder normalisierte Form für Aktionen ist diese:

image

Normalisierte Funktionseinheiten haben genau 1 Input-Strom und genau 1 Output-Strom. Immer.

Statt Input/Output entlang verschiedener Ströme fließen zu lassen, gibt es je nur einen Strom, auf dem Input/Output-Daten mit einem Qualifier näher beschrieben sind; der ordnet sie logisch einem der vielen früheren physischen Ströme zu.

Wie und ob sie sich von Input triggern lassen, sei dahingestellt. Ob sie Output herstellen, sei dahingestellt. Mir geht es um ihre Form. Alle Funktionseinheiten aller Ebenen können ohne Verlust an Flexibilität und Individualität gleich aussehen.

Zwischenstand

Soweit mein Traum von einer universellen, skalierbaren Grundstruktur von Software. Sie besteht aus einer beliebig tiefen Hierarchie von potenziell zustandsbehafteten und potenziell nebenläufigen Aktionen. Software tut etwas. Sie verarbeitet Daten. Deshalb wird sie beschrieben durch Tätigkeiten einheitlicher Form.

Klar, das ist Flow-Design nochmal beschrieben. Mir scheint eine so knappe Darstellung allerdings sinnvoll als Grundlage für das, was ich im Folgenden beschreiben möchte. Sie fasst den Stand der Flow-Design Überlegungen zusammen – allemal für die, die nicht alles verfolgen, was bisher dazu geschrieben wurde.

Ganz ohne Neuigkeit ist dieser Artikel andererseits aber auch nicht. Die Nebenläufigkeit ist jetzt “offiziell” im Bild und die Normalisierung. Der nächste Artikel wird zeigen, dass sie nicht nur konzeptionell nett ist.

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...

Montag, 3. Oktober 2011

Digitale Erleichterung für den Schulalltag – oder: Kinder sind keine Scherpas

Wie wäre das, wenn Sie jeden Tag mit 25% Ihres Körpergewichts auf dem Rücken zur Arbeit gehen müssten? Für mich wären das ca. 17 kg, aber ich bin nicht groß. Für viele unter Ihnen wären das bestimmt 20 kg oder gar 25 kg. Ein Gewicht, dass Sie natürlich in einem Rucksack tragen können - nur wäre das jenseits dessen, was sie als bequem ansehen würden. Auf einen Sonntagswanderausflug würden Sie bestimmt höchstens 5 oder max. 7 kg mitnehmen, also deutlich weniger als 10% Ihres Körpergewichts.

Millionen Menschen in Deutschland können sich das allerdings nicht aussuchen. Sie sind gezwungen, um die 25% Ihres Körpergewichts jeden Tag zur Arbeit zu schleppen. Und es sind kleine Menschen, deren Haltungsapparat noch nicht voll ausgebildet ist. Es sind unsere Kinder.

Kinderarbeit in fernen Ländern beklagen wir zurecht. Dass Kinder in Deutschland jedoch täglich mit echten Lasten größere Strecken zurücklegen müssen, ist nicht auf unserem Radar.

Im Bild meine Tochter Verena. Sie ist derzeit 11 Jahre alt und besucht die 6. Klasse eines Gymnasiums in Hamburg. Ihr Ranzen wiegt um die 7,5 kg; ihr Gewicht ist 28 kg. Der Effekt ist deutlich zu sehen: kompensierende Haltung, ja, Fehlhaltung.

image

Klar, das Gewicht am langen Arm zu tragen, wäre noch schlimmer. Die Empfehlung "Ranzen statt Aktentasche" ist korrekt. Nur sollte das nicht bedeuten, dass jedes Ranzengewicht dann auch gleich akzeptabel ist. Ich finde 7,5 kg oder 25% des Eigengewichts jedenfalls deutlich zuviel.

imageIch weiß nicht mehr, wie schwer mein Schulranzen früher war. In der Grundschule war er jedenfalls deutlich kleiner, er sah eher so aus wie nebenstehen. Dass er mit 7,5 kg bestückt war, ist unwahrscheinlich.

Später im Gymnasium bin ich mit einem recht dünnen Samsonite Koffer ausgekommen. Den habe ich – gegen alle Empfehlung – am langen Arm getragen. Da waren bestimmt auch keine 7,5 kg drin.

Heute ist das anscheinend anders. Brotdose, Trinkflasche und Hefte gibt es immer noch. Die Zahl der zu schleppenden Bücher scheint sich aber erhöht zu haben. Hier das, was ich an einem Tag in Verenas Ranzen gefunden habe: Mathe, Englisch, Französisch, Geschichte, Erdkunde, Musik.

image

Manche Bücher können in der Schule gelassen werden, wenn sie nicht für Hausaufgaben benötigt werden. Gute Idee – die natürlich mindestens gelegentlich dazu führt, dass ein Kind ein nötiges Buch vergisst. Und wenn es Bücher zuhause hat, dann vergisst es sicher auch mal eines einzupacken, das am nächsten Tag gebraucht wird. Aber das ist alles nicht so schlimm wie das täglich hohe Gewicht.

7,5 kg kommen vor allem zusammen durch Schulbücher und Notizbücher, die die Lehrer fordern. Nicht Schreibhefte, sondern echte kleine “Schreibbücher” (z.B. für spezielle Ansagen der Klassenlehrer).

Zum Gewicht kommt die Qualität der Bücher. Was trägt denn meine Tochter da. In Hamburg herrscht derzeit Lehrmittelfreiheit, d.h. Schulbücher müssen nicht gekauft werden (oder zumindest nicht alle). D.h. die Kinder “erben” ihre Bücher von Vorgängern. So sehen sie denn auch aus. Hier als Beispiel das Musikbuch:

image image image

So viel ist uns also die Bildung unserer Kinder wert?

Aber ich will nicht in ein Lamento über unser Bildungssystem verfallen. Mir geht es hier um etwas anderes. Ich finde die Gewichtsbelastung und auch die schlechte Lehrmittelqualität nämlich gänzlich unnötig. Sie ist anachronistisch, weil es besser geht. Es geht ganz einfach viel besser. Nämlich so:

image image

Das ist das iPad von Verena. Auf ihm sind alle Schulbücher, die sie sonst im Ranzen trägt, als PDFs. Das iPad 1 wiegt damit 0,875 kg!

Das iPad mit allen Büchern der 6. Klasse wiegt nur knapp 10% des Ranzens und also nur knapp 2,5% von Verena. Das, so finde ich, ist ein angemessenes Gewicht für den täglichen Schulweg.

Zugegeben, im iPad ist noch keine Brotdose und keine Trinkflasche enthalten :-) Aber alle Schulbücher und alle Schreibhefte können darin sein. Ohne Gewichtsveränderung kann darin sogar jedes Buch jeder Jahrgangsstufe inkl. aller je gemachten schriftlichen Ausfertigungen gespeichert sein. Rein technisch könnte Verena also mit einem iPad zur Einschulung ausgestattet werden und es bis zum Schulabschluss behalten. In 12 (oder 13) Jahren müsste sie für den Unterricht nicht ein Buch schleppen und nicht ein Heft kaufen. Sie könnte alle Arbeiten mit und im iPad erledigt.

Technisch ist das kein Problem.

Ob das immer didaktisch/pädagogisch wünschenswert ist, ist eine andere Frage. Mit einem Stift das Schreiben auf Papier zu lernen, ist sicher eine Fertigkeit, die Schule auch vermitteln sollte. Allerdings geschieht das heute schon schlecht genug, wie das Gekrakel vieler Schüler beweist. Der Zeigefinger sollte also nicht zu doll wackeln, wenn es ums Schreiben auf einem iPad geht. (Ob mit der virtuellen Tastatur oder einer zusätzlichen geschrieben würde, sei auch nochmal dahingestellt.)

Was in der Schule gelesen wird, was für die Schule geschrieben wird, was für die Schule “collagiert” wird (Hausarbeiten aus Text und Bildern), was notiert wird… das kann genauso gut oder gar noch besser (leichter, in höherer Qualität, mit mehr Lerneffekt fürs restliche Leben) mit einem iPad getan werden.

imageDamit kann es heute losgehen. Schulbücher müssten nicht mal aufwändig umgestaltet werden; ein Scan wie ich es mit den Büchern von Verena gemacht habe, reicht erstmal. Als Leseprogramm finde ich den GoodReader sehr nützlich.

Ein kleines Problem machen “Workbooks”, also Bücher, in die etwas hineingeschrieben werden soll. Die könnten technisch aber als PDF-Formulare ausgelegt werden. Das ist etwas mehr Aufwand als ein reiner Scan – doch der ist sehr überschaubar, wenn der ein Verlag das einmal tun würde zum Nutzen aller Schüler.

Schulhefte und Notizbücher werden ersetzt durch diverse Apps. An Textverarbeitungen und Notiz-Apps gibt es keinen Mangel. Da kann man organisieren und reinmalen und schreiben, dass es eine Freude ist. Und wenn die Lehrerin Termine ansagt, können die sogar in Kalender eingetragen werden. Ja, die Lehrerin könnte sogar einen Kalender für die Klasse zentral führen. Dann müssten nicht 30 Kinder eine Notiz machen. Den Kalender könnten Eltern dann abonnieren.

Es ist überhaupt nicht auszudenken, wie der Schulalltag sich vereinfachen ließe, wenn denn die Kinder statt mit gewichtigen Papierbergen mit einem iPad (bzw. einem vergleichbaren Device) ausgestattet würden. Ich wage ja gar nicht auszumalen, was da alles möglich wäre in puncto Apps für die Bildung. “Spielend lernen” könnte eine ganz neue Bedeutung bekommen. Physikalische Experimente auf dem iPad, Exploration mathematischer Probleme, ein anderer Zugang zur Chemie, Recherchieren am PC nicht als mühseliger Sonderfall, sondern als Bestandteil des Schulalltags…

Aber ich höre sie schon, die Kritikusse: Dem Plagiat sei damit ja Tür und Tor geöffnet. Und die wichtigen Fertigkeiten im Umgang mit dem Papier, die dann nicht mehr gelernt würden. Sowieso sei damit die Verblödung der Kinder und Jugendlichen durch “den Computer” auch noch staatlich gefördert.

imageKinder die zerfledderte Bücher schleppen und in Hefte krakeln: das ist der Inbegriff heutigen Schulideals, scheint mir. Naja, kein Wunder, wenn ich Lehrer sehe, die nicht einmal ein Handy haben und sich von der Elternschaft auch keines für den gelegentlichen Gebrauch zur Abstimmung bei Klassenreisen schenken lassen. Da wird kokettiert mit der eigenen Unkenntnis, wie Fotos elektronisch verbreitet werden könnten. (“Das geht doch irgendwie mit einer CD oder so, nicht wahr? Ich kenne mich da nicht aus. [hihi]”)

Nein, ich halte weiterhin dagegen: Eine Schulwelt, in der jedes Kind mit einem iPad (bzw. ähnlichem Device) ausgestattet ist, halte ich für wünschenswert.

Handfest dagegen sprechen nicht eventuelle pädagogische Einwände einer zaghaften Lehrerschaft, die sich früher schon gegen Schiefertafeln gewandt hat, als man noch auf Birkenrinde in der Schule geschrieben hat.

Handfeste Einwäde sind für mich derzeit:

  • Ein iPad kostet eine ganze Menge. Das kann sich nicht jede Familie für jedes Kind leisten. Es gibt auch günstigere Varianten, aber das kapazitive Display und die Geschwindigkeit sind für mich erstmal Minimalanforderung. Schlechter sollten Schul-Devices nicht ausgestattet sein.
    imageIch bin allerdings sicher, dass die Zeit hier für die Schüler arbeitet. In Russland ist man augenscheinlich schon weit(sichtig)er.
    Auf der anderen Seite: Wenn ein Device für mehrere Jahre gekauft wird, sagen wir mal mindestens für die Zeit nach der Grundschule (also max. 8-9 Jahre), dann wären selbst Kosten von 500 EUR kein Problem. Pro Jahr wären vielleicht 75 EUR fällig (inkl. einer Verlustversicherung). Dafür ließe sich doch eine Finanzierungsform finden, oder? Ein bisschen Kreativität wäre halt gefragt.
    Letztlich müsste sich am Preis also gar nicht soviel ändern. Mit einem potenten Hardware-Sponsor und einem potenten Telco-Provider und einer potenten Versicherung wäre da was zu machen. Warum probiert eine Privatschule das nicht einfach einmal aus? Fragen köst nix.
  • Wenn ein iPad verloren ginge, gestohlen oder beschädigt würde im Schulalltag, dann wären gleich alle Aufzeichnungen und Bücher weg. Dass das einem Ranzen passiert, ist möglich, aber unwahrscheinlich. Ein iPad hingegen ist empfindlicher und attraktiver für Diebe.
    Es müssten Vorkehrungen verschiedener Art getroffen werden, um einem Verlust entgegen zu wirken und ihn im Notfall zu kompensieren. Daten können über die Cloud gesichert werden. Der Zugang kann personalisiert werden. Klassenräume könnten abgeschlossen werden in der Pause. Und Devices könnten versichert werden. Man sollte eh darüber nachdenken, ob die Devices nicht “im Abo” gekauft werden, so dass sie nach vielleicht 3 Jahren ohnehin ausgetauscht würden.
  • imageSchulbücher müssten digitalisiert werden. Das kann jeder für sich machen, wie ich es getan habe. Aber eigentlich ist das natürlich Aufgabe der Verlage. Die tragen natürlich starke Bedenken, was ihre Umsätze angeht. Doch das wäre eigentlich kein Problem. Schulen kaufen heute Buchkontingente, das können sie auch morgen tun.
    Wie gesagt, im ersten Schritt müssten die Bücher nicht mal aufgepeppt werden. Ein Scan reicht in den meisten Fällen aus.
    Und danach kann jeder Verlag schrittweise seine Bücher in Apps oder anderes umwandeln (die die Kinder (bzw. ihre Eltern) kaufen). Da würde sich sogar ein Markt für weitere Titel auftun: das Schulbuch muss gekauft werden, doch zum Schulbuch gibt es dann vom Verlag noch Nachhilfe-Apps. Ha! Was für ein Geschäft!
  • Verena könnte heute womöglich schon ihr iPad in die Schule mitnehmen. Da ist ja alles, was sie braucht, drauf. Die Lehrer würden die Stirn runzeln, zucken, doch am Ende es akzeptieren, denke ich. Gefragt habe ich allerdings nicht, weil Verena nicht will. Es wäre ihr unangenehm, als einzige mit einem solchen Device im Unterricht zu sitzen. Da schleppt sie lieber 7,5 kg, als sich so als Außenseiter zu zeigen.
    Das bedeutet, Devices müssen in einer Weise eingeführt werden, die “peerkompatibel” ist. Kindern dürfen sich dadurch nicht ausgegrenzt fühlen. Es ginge also nur klassenweise.

Soweit die Hürden, die ich sehe. Es sind keine didaktischen, keine pädagogischen, sondern eher organisatorische.

Keine scheint mir aber unüberwindlich. Im Gegenteil. Wenn auch nur eine Schule das Ergebnis für wünschenswert hielte, könnte sie aus dem Stand ein Experiment aufsetzen. Warum nicht eine Klasse in einem höheren Jahrgang (z.B. 10. Klasse) mit Devices ausstatten und schauen, was passiert?

Sponsoren für Hardware, 3G und Versicherung ließen sich ganz bestimmt finden. Die Kosten wären dann kein Problem.Schulverlage würden es sicher auch mitmachen.

Die größte Herausforderung wäre wahrscheinlich, die Lehrerschaft zu überzeugen. Die kann selbst nicht verlässlich mit Devices umgehen. Die kann schon gar nicht verlässlich verstehen, was ihre Schüler mit Devices heute schon daheim alles tun. Die Barriere ist mithin in den Köpfen.

Deshalb scheint mir eine Privatschule der vielversprechendste Keim für eine solche Entwicklung. Dort ist man mehr als anderswo darauf angewiesen, sich zu entwickeln.

Also: Wo ist die innovative Schule, die unsere Kinder erleichtert? 0,875 kg sind machbar. Kein Kind muss 25% seines Körpergewichts an Material zur Schule schleppen.

Wenn uns unsere Kinder wirklich wichtig sind, dann bemühen wir uns, sie mit dem Besten auszustatten, was unsere Gesellschaft hervorgebracht hat. Und das sind nicht nur Goethe, Einstein, klimatisierte Busse für Klassenreisen und hübsche Tafelbilder. Machbar ist viel mehr. Machbar ist auch mehr als Smartboard-Schulen heute tun. Wir müssen es nur wollen.

Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat...