Seit geraumer Zeit hats bei mir Klick gemacht und habe endlich verstanden, warum vermehrt von dem Aspekt der Developer Experience die Rede ist. Genau wie der Durschnittskonsument eines digitalen Angebots dessen Qualität dadurch misst, wie gut die sogenannte User Experience ihn begleitet, so ist der Entwickler, welcher auf der anderen Seite des Lebenszyklus dieses Angebots steht, also an dessen Implementation und Bereitstellung steht, genauso stark von einer sogenannte Developer Experience geprägt. Oder anders gesagt, der Entwickler als solches ist mittlerweile auch eine rare Ressource geworden, welche von potentiellen Arbeitgebern unter anderem auch durch attraktive Developer Experiences umworben werden möchte. Das betrifft nicht nur den eigentlichen Technologie-Stack, sondern auch den Umgang damit.
Als Entwickler von digitalen Angeboten aller Art, bin ich selber sehr stark an dieser Thematik der Developer Experience interessiert. Aus diesem Grund möchte ich im heutigen TechUp einem Teilbereich genau dieses Themas nachgehen und dir, dem Leser, einen weiteren Aspekt veranschaulichen, mit dem man potentiell für eine bessere Developer Experience sorgen kann.
Development Integration
Die Developer Experience wird in erster Linie durch den eigenen Workflow definiert. Das heisst: Wie viele einzelne Schritte braucht es bis eine neue Iteration der Abarbeitung von Tasks entsteht. Dabei kann eine Iteration beispielsweise das Einfügen des ersten Implementationsversuchs eines gegeben Features sein, welcher im zweiten Schritt kompiliert und im dritten Schritt in einer Runtime zu Testzwecken hochgefahren wird. Die einzelnen Steps sind oft vielzählig und sind stark voneinander abhängig. Bevor ein neuer Code-Block auf der lokalen Maschine getestet werden kann, muss der Code kompiliert werden können. Das Kompilat muss den Weg in eine Runtime finden, und das kann nur passieren, wenn der Code kompiliert. Somit ist die Reihenfolge des Workflows streng vorgegeben. Jede Iteration kostet Zeit und Nerven. Man möchte also für effiziente Prozesse sorgen und die einzelnen Steps so einfach wie möglich halten. Und das führt mich zum Thema der Entwicklerintegration.
Was ich gerade erwähnt habe, war eine Komposition von Steps, welche den gesamten Entwickler-Workflow ausmachen. Die einzelen Bausteine sind dabei oft Kommandozeilenbefehle. Solche Befehle lassen sicht gut gemeinsam integrieren, beispielsweise als Bash-Scripts, welche repetitive Steps automatisieren sollen. Neben klassischen Shell-Scripts gibt es auch dedizierte Tools, die genau dieses Kompositionsvorhaben bereits abdecken können, und zu einem einfacheren Iterationsschritt im eigenen Workflow führen können.
Task-Runner wie GNU-Make
Diese dedizierte Tools nennt man einen Task-Runner. Eine Variante solch eines Task-Runners ist das bekannte GNU Make, welches schon einige Jahrzehnte auf dem Buckel hat. Oft ist der Kompilierungsprozess nicht offensichtlich und die einzelnen Steps in einem gegeben Worflow sind oft undurchsichtig. Dazu kommt, dass heutzutage mehrere Ökosysteme von Programmiersprachen in einer Business-Domäne (sprich in einem Team) etabliert sind und unterschiedliche Prozesse erfordern. Hier denke ich zum Beispiel ans das Node.JS-basierte npm
, mit dem man auch Frontend-Projekte kompiliert, aber zeitgleich ein Backend-Projekt im Form einer Quarkus-Codebase existiert, welches wiederrum eine Datenbank als drittes Ökosystem erfordert. Somit sind unterschiedliche, divergierende Prozesse erforderlich, um die einzlenen Steps einer gegeben Iteration zu meistern. GNU Make übernimmt genau diese Implemenationsdetails und erlaubt es, den gesamten Projekt-Stack zu abstrahieren. Es wird zu einer Blackbox, die sich managen lässt, auch wenn die einzelen Details des Stacks nicht bekannt sind. Make erlaubt es quasi eine Standardiserung des Workflows vorzunehmen: Es abstrahiert die einzelnen Einzelheiten eines jeden Tech-Stacks und liefert eine höhere Abstraktionsebene der Entwicklungsumgebung, sodass jeder neue Entwickler innert Minuten onboarded werden kann.
Ob Docker, Minikube, Vagrant oder einem einfachen Maven-Build, man kann damit Headspace einsparen und simple Primitive bieten, die einem helfen direkt einsteigen zu können. Zudem ist Make durch das Makefile als deklarative Konfigurationsdatei des gesamten Workflows selbsterklärend. Es ist natürlich hilfreich dabei noch ein README.md zu haben, welche das Makefile wiederrum in prosaischer Form in Klartext abstrahiert und auch für Non-Techies zugänglich macht. Schlussendlich geht es einfach darum, dass man sich auf das eigentliche Konzentrieren kann: Coden, Automatisieren, Abstrahierungen schaffen. Sich Gedanken zur Development-Integration mithilfe von Task-Runnern zu machen ist der Gegenstand des heutigen TechUps. Aus diesem Grund schauen wir uns Make mal genauer an und zeigen wie diese Implementationsdetails in der Real-World-Anwendung aussehen.
Make, wie war das nochmal
Zuerst einmal vorweg: Die Idee, Build-Management automatisiert zu betreiben ist alt. make
ist bereits seit 1976 zu haben und sogar als POSIX-Standard, dem sogenannten IEEE Std 1003.1, mal festgehalten worden. An dieser Stelle möchte ich gerne eine Anekdote zu make
referenzieren, da diese genau die Nützlichkeit einer solchen für damalige Verhältnisse neuwertigen Lösung exemplarisch darstellt:
“Make originated with a visit from Steve Johnson (author of yacc, etc.), storming into my office, cursing the Fates that had caused him to waste a morning debugging a correct program (bug had been fixed, file hadn’t been compiled,
cc *.o
was therefore unaffected). As I had spent a part of the previous evening coping with the same disaster on a project I was working on, the idea of a tool to solve it came up. It began with an elaborate idea of a dependency analyzer, boiled down to something much simpler, and turned into Make that weekend. Use of tools that were still wet was part of the culture. Makefiles were text files, not magically encoded binaries, because that was the Unix ethos: printable, debuggable, understandable stuff.”— Stuart Feldman, The Art of Unix Programming, Eric S. Raymond 2003
Also, dem Zitat Stuart Feldmans ist sinngemäss zu entnehmen, dass ein Exectuable debugged wurde, welches gar das eigentliche Kompilat von Interesse enthielt, und somit unnötig der ganze Morgen sinnfrei verschwendet wurde. Genau solche Fehler passieren, wenn man als Entwickler gerade nicht den Headspace hat und sich auf augenscheinlich unnötige Sidetasks fokussieren muss, bevor man mit der eigentlichen Aufgabe weiter machen kann. So kam make
zur Welt und wurde reichlich eingesetzt.
Falls jemand make
noch nicht kennen sollte, oder es nicht Teil des eigenen Workflows sein sollte, dem sei gesagt, dass es sich bei make
um den Kommandozeilenbefehl handelt, welcher ein sogenanntes Makefile
ausliest und dieses zur Ausführung bringt. Makefiles kommen heute noch sehr oft, gerade in der Open-Source-Welt häufig zum Einsatz. Gerade wenn es darum geht C
- oder C++
-basierte Quellcode-Basen auf dem Zielsystem zu kompilieren. Das geschieht beispielsweise wenn man Gentoo Linux oder das allseits bekannte AUR von Arch Linux nutzen sollte. Wenn auch das dir nichts sagt, so denke ich ist es an der Zeit, make
anhand eines Beispiels zu zeigen.
Makefile Einmaleins
Nehmen wir uns am besten mal ein solches Makefile zur Hand. Was wir im Folgenden sehen können ist ein konventionelles Makefile wie wir es beispielsweise aus unserem internen b-nova/solr-page-exposer
-Projekt vorfinden können. (Falls du mehr über unsere interne jamstack-Architektur wissen möchtest, kann ich dir dieses TechUp von Valentin sehr ans Herz legen: Unsere eigene Umsetzung einer jamstack-fähigen Headless-CMS-Architektur.)
|
|
Das Makefile liegt auf unterster Projektebene, was uns erlaubt direkt aus dem Projektverzeichnis make
einzutippen, was wiederum dazu führt, dass genau diese Datei gefunden wird und der erste Task ausgeführt wird. In diesem Fall wäre das der all
-Task. Der all
-Task selber ist wiederum eine Liste von weiteren Tasks, welche genau in dieser Reihenfolge ausgeführt werden.
Somit fürt ein simples $ make
auf Projekt-Ebene dazu, dass die tidy
-, die build
- und die run
-Stanzas genau in dieser Reihenfolge ausgeführt werden. Obwohl man dies definieren kann wie mal will, ist es gang und gebe, dass der erste Task immer dazu führen soll, dass das Projekt stets, idealerweise Platform-/Umgebungs-unabhängig kompiliert, sprich gebaut werden kann.
From Zero to Hero
Wenn wir ein etwas komplexeres Beispiel eines Makefile anschauen wollten, dann finden wir sowas in quasi jedem C/C++
-Projekt im Open-Source-Space. Mein Lieblings-Editor ist Neovim, lass uns deswegen doch einfach mal das Makefile von Neovim anschauen. Dies liegt im Github-Repository von Neovim:
|
|
Da geht definitv mehr, aber es abstrahiert auf eine elegante Weise die ganze Kompilierungskomplexität des C
-basierten Projektes. Natürlich muss die Ausgangssprache nicht zwingend so Low-Level wie C
oder auch Go
sein, sondern man kann damit einfach auch Build-Systeme wie JVM’s Maven oder Gradle vereinfachen und abstrahieren.
Aus Make wird Task
So, jetzt wissen wir was make
und Makefiles sind. Natürlich wirkt das ganze ein wenig altbacken, oder wird gerne zumindest mit antiken Developer-Technologien assoziiert und nicht etwa als Build-Tool der Zukunft aus dem Jahre 2030. Genau hier kommt Task ins Spiel.
Task ist, genau wie make, ein Build-Automation-Tool, oder spezifischer ausgedrückt ein Task-Runner. Auf den Punkt gebracht kann Task mit folgenden Eigenschaften charakterisiert werden:
- Es ist in Golang geschrieben
- Es hat somit nur eine Binary ohne Dependencies oder Shared Libraries
- Nutzt YAML als Markup-Sprache für seine Task-Definitionen
- Dessen Makefiles heissen neu einfach
Taskfile
- Weist ein Convenience-Skript für dessen reibungslose Installation auf
Taskfiles ad infinidum
Lass uns ein paar Taskfiles anschauen. Das offizielle Github-Repository weist einen testdata/
-Ordner auf, in dem eine Vielzahl verschiedener Testfälle drin sind, welche so ziemlich alle Features von Task aufweisen (und selbstverständlich als Grundlage für die Unit-Tests dienen).
Ich picke ein paar Beispiele raus und habe auch welche frei selber komponiert, um komplexere Real-World-Beispiele zu haben.
I. Summary-Task
|
|
II. Another-Task
|
|
$ task --help
Falls du dir nicht sicher bist, kannst du immer einfach task --help
in deiner Shell verwenden. Zudem sollte diese Page hier ein paar hilfreiche Anhaltspunkte zu Nutzung von Task liefern: https://taskfile.dev/usage/
Fazit zu Task
Fangen wir von vorne an. Wir haben gesehen, dass ein Task-Automatisierungstool wie Make oder Task essentiell für den eigenen Entwicklungsprozess ist, weil es die Wiederholbarkeit und Nachvollziehbarkeit von Prozessen erhöht. Es ermöglicht, eine Reihe von Schritten, die sonst manuell ausgeführt werden müssten, in einem einzigen Befehl auszuführen. Auch kann man dadurch Prozesse automatisch auslösen, wenn bestimmte Bedingungen erfüllt sind. Es erleichtert die Arbeit, indem es die notwendigen Schritte spezifiziert und diese automatisch ausführt. Es kann auch dazu verwendet werden, Abhängigkeiten zwischen verschiedenen Aufgaben zu verwalten und sicherzustellen, dass sie in der richtigen Reihenfolge ausgeführt werden.
Make vs. Task
Mit Make verglichen ist Task ein moderneres und flexibleres Task-Automatisierungstool, welches einige Vorteile gegenüber Make hat:
- Einfachheit: Task hat eine einfachere Syntax und erfordert weniger Konfiguration als Make.
- Plattformunabhängigkeit: Task ist in Golang geschrieben und läuft sowohl unter Windows als auch unter Linux und MacOS, während Make hauptsächlich für Unix-Systeme entwickelt wurde.
- Integrierbarkeit: Task kann problemlos in moderne JavaScript-Workflows integriert werden, wie z.B. Webpack oder Rollup, während Make hauptsächlich für die Kommandozeile entwickelt wurde.
- Erweiterbarkeit: Task hat eine gut dokumentierte API, die es ermöglicht, benutzerdefinierte Plugins zu erstellen, die die Funktionalität erweitern.
- Zusammenarbeit: Task unterstützt das Teilen und Zusammenarbeiten an Aufgaben durch den Einsatz von Plugins und unterstützt die Automatisierung von Workflows.
Allerdings gibt es auch Situationen, in denen Make besser geeignet sein kann, z.B. wenn ein Projekt bereits auf Make aufgebaut ist oder wenn es auf eine große Anzahl von Abhängigkeiten und komplexe Prozesse spezialisiert ist.
Ein Task-Automatisierungstool wie Make oder Task ist von großer Bedeutung für die Developer-Experience, da es die Wiederholbarkeit, Nachvollziehbarkeit und Effizienz von Prozessen erhöht. Es ermöglicht Entwicklern komplexe Prozesse in einfachen Schritten auszuführen und dadurch Zeit und Ressourcen zu sparen. Es erleichtert auch die Zusammenarbeit und Automatisierung von Workflows innerhalb eines Teams. Obwohl Make und Task unterschiedliche Vorteile haben, bieten beide eine Möglichkeit, Prozesse zu vereinfachen und zu automatisieren, was die Developer-Experience insgesamt verbessert. Es lohnt sich also, sich mit den Möglichkeiten von Task-Automatisierungstools auseinanderzusetzen und das passende Tool für die jeweiligen Anforderungen zu wählen.
Ausblick auf weitere Verbesserungen der Developer Experience
Wir haben bei b-nova schon mehrmals in unterschiedlichen TechUp-Beiträgen untersucht, wie man die Developer Experience steigern könnte. Wir haben uns beispielsweise angeschaut, wie man Version-Management automatisiert mit asdf
betreiben kann, oder wie man technische Cheatsheets wie tldr
oder cht.sh
für einen besseren Workflow anwendet. Falls dir das Thema rund um Developer Experience und Workflow-Gestaltung gefallen sollte, dann schau doch noch in die folgenden TechUps rein und erfahre, wie du deinen eigenen Entwickler-Workflow optimieren kannst, um somit auf das nächste Level zu kommen:
- Vereinfache deinen Workflow mit einem Versionsmanager wie asdf
- Merke dir weniger, weiss aber mehr mit State-of-the-art Cheatsheets
- Secrets Management mit Teller
Es gibt hier noch weitere Aspekte rund um Developer Experience, die ich dieses Jahr betrachten möchte. Dazu gehört definitiv, wie man mit der Komposition des eigenes Dev-Workflows umgeht, und welche Möglichkeiten in den letzten Jahren entstanden sind, um genau diesen Prozess zu entflechten und/oder gar neu zu denken. Ein anderes Thema, was bei mir noch in der Warteschleife steht ist funktionales Package Management mit Nix. Du siehst, es gibt noch vieles rund um das Thema zu erzählen. Somit, bis zum nächsten TechUp, bleib dran! 🚀
Weiterführende Links und Ressourcen
https://developerexperience.io/practices/good-developer-experience
https://www.gnu.org/software/make/
https://github.com/go-task/task