Patrick Schriner (mit dem ich übrigends das Vergnügen hatte, zusammen an der Universität Bonn zu studieren) fragte uns, ab welcher Projektgröße sich kontinuierliche Integration in PHP-Projekten lohnt. Diese Frage greife ich gerne auf, möchte sie aber in einem etwas weiteren Rahmen betrachten.
Kontinuierliche Integration (Englisch: Continuous Integration) bedeutet, dass jeder Entwickler seinen Stand der Entwicklung mindestens einmal am Tag mit seinen Teammitgliedern teilt, also seine Änderungen am Code in die Versionskontrolle überführt. Die Einführung von kontinuierlicher Integration wird oft missverstanden als „Wir installieren einen Continuous Integration Server“. Ein solcher kann den aktuellen Stand der Software automatisch aus der Versionskontrolle holen und mit automatisierten Tests sicherstellen, dass dieser Stand der Software korrekt funktioniert. Dies ist aber nicht die Voraussetzung dafür, dass die Entwickler ihren Stand des Codes regelmäßig synchronisieren. Im Gegenteil: der Einsatz eines Continuous Integration Servers lohnt sich erst, wenn die Entwickler dies bereits tun – und es natürlich auch automatisierte Tests gibt, die ausgeführt werden können.
Integrieren die Entwickler ihren Stand der Entwicklung mehrmals täglich, so reduzieren sie das Risiko, dass beim Zusammenführen ihrer Änderungen (Englisch: Merge) Konflikte auftreten. Je seltener man Änderungen zusammenführt, desto wahrscheinlicher und problematischer sind diese Konflikte. Das geflügelte Wort der „Merge-Hölle“ beschreibt die Situation, in die man als Team gerät, wenn nur alle paar Tage, Wochen, Monate integriert wird und dann nichts mehr wirklich zusammen passt, da die Entwicklungen zu weit auseinander gelaufen sind. Und selbst wenn eine solche Integration ohne Merge-Konflikt durchgeführt werden kann, so bedeutet dies nicht, dass die Software auch tatsächlich so funktioniert, wie sich die Entwickler das vorstellen.
Ein Team, mit dem ich vor ein paar Jahren einmal gearbeitet habe, hat beispielsweise nur einmal im Monat integriert. Und dann eine Woche damit verbracht, die resultierenden Merge-Konflikte aufzulösen und die Software wieder in einen Zustand zu überführen, in dem man sie verwenden konnte. Das eigentliche Problem dieses Teams war aber ein kommunikatives: Die Entwickler sprachen zu wenig miteinander. Dies spiegelte sich im Code mit seinen Merge-Konflikten wider.
Moderne, verteilte Versionskontrollsysteme wie Git haben kurzlebige Feature Branches populär gemacht. Hierbei wird zu Beginn der Arbeit an einem neuen Feature ein eigener Branch für dieses Feature angelegt. Wenn das Feature fertig implementiert ist, wird der entsprechende Branch in den Hauptentwicklungszweig überführt (Merge). Das Konzept der Feature Branches funktioniert allerdings nur dann, wenn diese Entwicklungszweige kurzlebig sind. Langlebige Branches führen in die Merge-Hölle, wenn nicht mindestens einmal täglich die Änderungen aus dem Hauptentwicklungszweig in den Branch integriert werden.
Arbeiten alle Entwickler auf demselben Hauptentwicklungszweig (Trunk-Based Development) und committen sie mehrmals täglich in diesen, so können größere Umbauarbeiten (Refactorings) am Code schwierig werden. In der Regel schafft man es nicht, in einem Rutsch beziehungsweise in einem Commit alle relevanten Stellen im Code zu ändern. Und selbst wenn einem Entwickler dies gelingt, so besteht immer noch die Gefahr, dass die Software jetzt wahlweise nur bei ihm oder nur bei seinen Kollegen funktioniert.
Die Best Practice „Branch by Abstraction“ kann bei Trunk-Based Development und größeren Umbauarbeiten helfen. Hierbei führt man eine Abstraktionsschicht für die Komponente der Software ein, die man ändern möchte. Nachdem man den Rest des Systems so refaktoriert hat, dass die Abstraktionsschicht (und nicht mehr die zu ändernde Komponente) verwendet wird, implementiert man die neue Version der Komponente in neuen Klassen. Danach passt man die Abstraktionsschicht so an, dass nun die neuen Klassen anstelle der alten verwendet werden. Jetzt können sowohl die alte Implementierung als auch die Abstraktionsschicht entfernt werden.
Eine Alternative zu „Branch by Abstraction“, um die Arbeit bei Trunk-Based Development zu erleichtern, ist der Einsatz von Feature Flags. Hierbei wird ein neues Feature so implementiert, dass es – beispielsweise über eine Konfigurationseinstellung – aktiviert und deaktiviert werden kann. Ist der Einsatz von Feature Flags möglich, so können neue Features experiment-getrieben entwickelt werden. Dieses spannende Thema haben wir bereits in unserem Artikel zu Integrationstests behandelt.
Führt man in dem von einem Continuous Integration Server durchgeführten Build-Prozess nicht nur Tests aus, sondern setzt man hier auch statische Codeanalyse ein, so spricht man von kontinuierlicher Inspektion. Hierbei erfasst man für jeden Entwicklungsstand der Software Metriken, die verschiedene Aspekte der internen Qualität der Software erfassen. Dies sind diejenigen Aspekte der Softwarequalität, die für die Entwickler relevant sind. So ist es beispielsweise wichtig, dass der Code einfach zu lesen, zu verstehen, anzupassen und zu erweitern ist. Ist dies nicht der Fall, so wird es mit der Zeit immer schwieriger und damit teurer, die kontinuierlich gestellten und meist unvorhersehbaren Änderungswünsche des Kunden umzusetzen. Irgendwann führen selbst minimale Änderungen zu unerwarteten Seiteneffekten.
Die auf Basis der Daten aus der kontinuierlichen Inspektion erstellten Berichte geben den Entwicklern wichtige Argumente an die Hand, wenn sie das Konzept der technischen Schulden und die Notwendigkeit eines Refactorings ihrem Management beziehungsweise dem Kunden erklären müssen:
Im letzten Sprint haben wir keine neuen Features (oder weniger als sonst) implementiert. Aber wir haben den Code aufgeräumt und Codeduplikate entfernt, die Komplexität reduziert, fehlende Tests geschrieben etc., wie man an den Entwicklungen dieser Diagramme erkennen kann. Dank dieser Aufräumarbeiten haben wir unsere technischen Schulden gesenkt und werden in den nächsten Sprints in der Lage sein, neue Features schneller und zuverlässiger umzusetzen.
Die ursprüngliche Frage war, ab welcher Projektgröße sich kontinuierliche Integration lohnt. Wir glauben, dass es sich in Projekten jeder Größe lohnt, dass alle Entwickler kleinschrittig entwickeln und ihren aktuellen Entwicklungsstand in Form von kleinen Commits mit dem Team teilen. Ob sich der Einsatz eines Continuous Integration Servers lohnt, hängt weniger von der Größe des Projektes, sondern vielmehr von der Lebensdauer der entwickelten Software ab. Für die Entwicklung einer Webseite, die im Rahmen einer Werbekampagne nur vier Wochen in Produktion sein wird, lohnt sich ein Continuous Integration Server weniger als für die Entwicklung einer unternehmenskritischen Anwendung, die über Jahre in Produktion sein wird. Wir sehen einen großen Wert darin, dass die kontinuierliche Integration durch einen Continuous Integration Server unterstützt wird. Dieser „erzwingt“, dass die automatisierten Tests ausgeführt und Softwaremetriken über die Zeit gesammelt werden.