Eric Normands Buch “Grokking Simplicity” habe ich über CASE Podcast gefunden. Der Pitch „Functional Programming in einer Standard-Sprache“ (im Buch Javascript) hat mich sofort gefangen. Da es mich genau dort abholt, wo ich stehe. Auf F# werden wir in der Arbeit „nicht mal eben so“ umsteigen, aber funktionaler Programmieren wollen wir schon. Die Vorteile liegen für uns auf der Hand: Bessere Testbarkeit, einfacher zu parallelisieren und der Code wird meiner Meinung nach auch verständlicher (insbesondere weil weniger State existiert, den man im Kopf behalten muss). Das Buch versprach also genau das zu erklären, was wir sowieso machen wollten. Ohne zu wissen, wie weit wir da kommen. Wenn das keine Spannung ist?!

Deshalb nun zum Buch: Das besteht aus zwei großen Teilen. Denen zwei Kapitel als Einstieg vorgeschaltet sind. Der erste große Teil befasst sich mit Actions, Calculations und Data. Das sind die drei Elemente, in die sich in FP alles einteilen lässt. Actions produzieren Seiteneffekte (es kommt also darauf an, wie oft und wann man sie ausführt). Dazu gehört etwa das Schreiben/Lesen aus einer Datenbank oder das Senden/Empfangen von Emails). Calculations liefern bei gleichem Input immer den gleichen Output und Data sind wirklich nur Datenklassen, die etwas beschreiben und bspw. der Input und Output von Calculations und Actions sind. Diese Aufteilung habe ich in meinen letzten Programmen schon so gemacht. Wenn man mit einer Onion-Architektur arbeitet und einen Rechenkern entwickelt, dann macht man das in der Regel nur mit Data und Calculations. Persistenz wird per Adapter hinzugefügt und kann auch erst sehr viel später entwickelt werden.

Die erste große Überraschung brachte das Buch in Kapitel 2 mit der Aussage, dass Code nach Änderungshäufigkeit strukturiert werden sollte, ohne das im Detail vollständig zu erklären. Genau darüber war ich bei „Righting Software“ Software gestolpert. Dort wurde genau dieser Satz gesagt, aber auch ohne nähere Erklärung, was mich damals beim Lesen ziemlich frustrierte. Mit dieser Aussage wurde Grokking Simplicity noch mehr zum Pageturner.

Um es vorwegzunehmen. Antwort auf häufige Änderungen ist das sogenannte Stratified Design: Innerhalbe einer Methode sollen Aufrufe anderer Methoden möglichst auf gleicher Ebene stattfinden. Also nicht der direkte Aufruf einer (low level) Array-Funktion neben dem Aufruf einer Business-Funktion wie der Steuerermittlung. Sauberer wäre es wenn auf einer Ebene nur Businessfunktionen aufgerufen werden und die Low-Level-Funktionen erst weiter unten im geschichteten Design verwendet werden. Auf diese Weise erreicht man es, dass die sich häufiger ändernden Businessfunktionen selbst wenige Abhängigkeiten haben und somit leichter geändert werden können, während die technischeren Funktionen viele Abhängigkeiten haben, aber in der Praxis weniger häufig geändert werden. Klingt für mich logisch und entspricht auch meiner Wahrnehmung. Dieses Design darf nicht mit Schichten_Architektur_ verwechselt werdne! Vorteil des Stratified Designs ist die deutlich bessere Code-Lesbarkeit, weil man sich auf jeder Schicht mit genau einem Thema befassen kann. Hilfreich ist es hierfür jede Funktion möglichst einer Ebene zuzuordnen. Die Anzahl der Ebenen ist nicht fix vorgegeben. Funktionen, die auf die Standard Library zugreifen (bspw. List oder Array-Funktionen), ändern sich in der Regel seltener als Business Funktionen. Andererseits werden die unteren Funktionen häufiger wiederverwendet. Das Unit Testing ist hier wichtiger und auch effizienter. Letzteres, weil sich die Funktionen weniger oft ändern und die Tests deshalb nicht laufend angepasst werden müssen.

Jetzt wieder der Reihe nach. Das Buch nimmt meiner Meinung nach alle Themen auf, die unter den Nägeln brennen, wenn man sich als “Objektorientierter” mit Funktonaler Programmierung (FP) beschäftigt. Zum Beispiel erzeugt FP deutlich mehr Objekte (als Zwischenergebnisse), die dann wieder Eingang in weitere Funktionen finden. Das ist heutzutage aber meistens kein Problem, weil der Garbage Collector zum einen aufräumt und zum anderen die wenigsten Anwendungen an diesen stellen ihre Performaceengpässe haben. Ein Vorwurf, den ich auch schon das ein oder andere Mal gehört habe und den ich ähnlich beantworte, garniert mit einem “Premature Optimization is the root of all evil” ;)

Ein wichtiger Aspekt ist die Arbeit mit Immutables, also unveränderlichen Objekten. Um das zu erreichen wird aus den meisten einfachen Operationen ein “Copy on Write”. D. h. bei jeder Manipulation eines Objekts wird eine Kopie erzeugt und dann auf dieser weitergearbeitet. Würde man das nicht machen, hätte man viele Actions und wenige Calcultions. In C# wurden diese Operationen mit den Record With-Expressioons in C# 9 deutlich vereinfacht. Diese machen in der Regel nur eine flache Kopie (shallow copy), d. h. referenzierte Objekte oder Listen werden nicht kopiert, sondern der Zeiger bleibt gleich. Wenn alle Objekte immutable sind, ist das aber auch kein Problem. Einzige Ausnahme hiervon ist die Arbeit mit Legacy-Systemen. Hier sagt Eric Norman, dass alles was aus Legacy-Code kommt bzw. an selbigen übergeben wird, als Deep Copy erzeugt werden muss, weil man sonst nicht sichergehen kann, dass der State manipuliert wird.

Hier streicht er einen wichtigen Unterschied zur OO-Programmierung heraus:

Es gibt keine Objekte, nur Daten (Fakten) über Ereignisse. Deshalb muss es auch keine Single Source of Truth in einem “Objekt” geben

Wenn das kein Foreshadowing für Event-Driven-Systeme ist…

Die nächsten Kapitel führen zuerst Higher Order Functions ein und danach werden Map(), Filter() und Reduce() vorgestellt. Letztere sind C# Programmieren als Select(), Where() und Aggregate() bekannt. Mir gefällt hierbei der Umstand, dass wirklich gut motiviert und erklärt ist, was da passiert, und warum das besser ist. Als mit For-Schleifen zu arbeiten (tl;dr: man schreibt weniger Boilerplate-Code und kann Highlevel-Code und Low-Level Code besser von einander trennen).

Das Bearbeiten von nested Objekts mittels rekursiver Funktion ist der nächste große Block. Auch hier ist die Herleitung gut gelungen. Wichtig dabei ist jedoch, dass man nicht zu tief in Objekte von außen hereingreift. Ein Weg das zu verhindern sind Funktionen, die innerhalb von Objekten bestimmte Manipulationen vornehmen. Natürlich per Copy on Write.

Der letzte größere inhaltliche Block befasst sich mit Problemen der Asynchronität, insbesondere mit shared Resources. Await wird leider nicht besprochen. Aber es zeigt sehr gut, wie man mit lokalen Variablen und Parametern die Komplexität deutlich reduziert. Der erste Trick ist natürlich möglichst wenige Ressourcen zu teilen und Parallelität auf das nötigste zu beschränken. Um Probleme besser zu verstehen, werden Timeline-Diagramme entworfen. Für jeden asynchronen Aufruf wird eine weitere Zeitlinie eröffnet. Gepunktete Linien deuten an, welche Aufgaben sicher VOR gewissen anderen Aufgaben ausgeführt wurden. Wann Aufgaben fertig werden kann bei asynchronen Aufrufen ohne await bzw. ähnliche Konstrukte jedoch nicht gesagt werden. Als Lösung mit Javascript Bordmitteln werdne einfache Funktionen entwickelt, die dafür sorgen, die die Reihenfolge bzw. das Beenden aller Aufgaben abwarten. Hier hilft Javascript etwas, weil grundsätzlich keine echte Parallelität existiert und die Funktionen damit etwas einfach sind als in C#. Vom Prinzip her klappt es so auch in C#, man muss allerdings noch Race-Conditions ausschließen oder nutzt direkt Funktionen wie Task.WhenAll(). Aber im Buch geht es an der Stelle auch mehr um das Problembewusstsein als um die konkrete technische Lösung. An der Stelle finde ich auch spannend, dass bisher gar nicht von Option oder Either die Rede war. Letzteres sind zwei Konstrukte, die gerade im C#-Umwelt für Funktionale Programmierung gerne genutzt werden. Dadurch macht man aus meiner Erfahrung die Hürde für einen Weg in Richtung Funktionaler Programmierung recht hoch, weil es verlangt, dass man seinen Programmierstil radikal ändert. Der Weg, den Grokking Simplicity vorstellt ist deutlich sanfter, aber der Effekt ist trotzdem riesig.

Im letzten inhaltlichen Kapitel werden zwei Architekturthemen angerisssen. Zuerst Reaktive Programmierung (im wesentlichen des Observer Pattern). Reactive Extensions (Rx) steht schon länger auf meiner „zu lesen“ Liste. Jetzt gewinnt es wieder etwas an Priorität. Die Onion Architektur wird hier auch kurz erklärt und motiviert, weil sie für FP gut klappt. Aber hier gibt es wirklich nur einen kurzen Abriss bevor das letzte Kapitel einen Ausblick liefert, wie man tiefer in das Thema FP eintauchen kann. Bei mir wird demnächst Scott Wlaschins Buch “Domain Modelling Made Functional” dran sein. Das steht auch schon länger auf meiner Liste und nur die Tatsache, dass es in F# ist, hat mich bisher abgehalten.

Alles in allen ist Grokking Simplicity ein wirklich tolles Buch, das ich gerne ein Jahr früher gelesen hätte.