Softwarekomponenten in der Entwicklung separat versionieren mit Hilfe von Git-Submodulen

Einleitung

In meinem Beitrag Datenmodelle und Sichten habe ich schon einmal beschrieben, dass eine komponentenbasierte Softwareapplikation aus mehreren Teilkomponenten besteht. Wenn man eine Schichtenarchitektur zugrunde legt, dann nutzen die Komponenten der oberen Schichten Komponenten der darunterliegenden Schicht, die unteren Komponenten wissen aber nichts von den Oberen.

Wiederverwendung

Wenn man komponentenbasiert entwickelt, dann tut man dies auch deshalb, um Komponenten mit bestimmten Fähigkeiten so zu entwickeln, damit diese (später) auch in anderen Softwareprojekten wiederverwendet werden können und nicht noch einmal entwickelt werden müssen. Hat eine Komponente einen gewissen Reifegrad erreicht, bekommt sie eine Versionsnummer und wird als Komponentenpaket freigegeben und ggfs. für andere Benutzer zur Verfügung gestellt. Im Bereich von .NET geschieht dies zumeist über so genannte Nuget-Pakete. Sie enthalten die erstellte Komponente und einige Zusatzinformationen zu dieser und können über Server anderen Entwicklern zur Verfügung gestellt werden. Für Open-Source-Komponenten geschieht dies zumeist über den von Microsoft betriebenen Dienst nuget.org.

Codeversionierung in einem einzelnen Repository ist möglich, aber keine gute Lösung

Wenn man nun ein Entwicklungsprojekt aufsetzt, so erstellt man eine Projektmappe in die man dann die einzelnen Projekte – ein Projekt pro Komponente – einfügt.

Könnte man die Komponenten alle einzeln entwickeln und dann einzeln freigeben, würde in der Projektmappe nur ein einzelnes Projekt mit Verweisen auf freigegebene Komponenten (z.B. Nuget-Pakete) enthalten sein. Dies ist aber in der Praxis unrealistisch, da man immer wieder Fälle hat, wo man an einer genutzten Komponente noch Änderungen vornehmen muss, um die Gesamtsoftware zum Laufen zu bringen. Daher entwickelt man eigentlich immer mehrere Komponenten einer Softwareapplikation parallel. Wenn man alle Komponenten als Quellcode in seiner Projektmappe hat ist dies auch kein Problem. Man kann dann an verschiedenen Stellen Änderungen durchführen und dann die Gesamtapplikation direkt testen.

Versionierung

Der Quellcode einer Software wird normalerweise von einem Versionskontrollsystem verwaltet. Heutzutage ist Git meist das System der Wahl. Mit Hilfe eines Versionskontrollsystems lassen sich alle Änderungen am Code nachverfolgen und bei Bedarf kann man auch mal auf ältere Stände zurück gehen.   

Bei größeren, komponentenbasierten Projekten bearbeitet man wie schon gesagt den Code von mehreren Projekten parallel. Projekte in der Softwareentwicklung sind ja nichts anderes wie Ordner und Dateien auf der Festplatte. Man kann nun natürlich den Code aller Softwarekomponenten in Ordnern und Unterordnern organisieren und gemeinsam versionieren. Bei Git spricht man auch von einem so genannten Git-Repository, welches einem Bereich entspricht, in dem unter einem festzulegenden Namen dann Dateien und Ordner unter Versionskontrolle stehen. Wenn man also den gesamten Quellcode mit allen Projekten in ein Repository ablegt, dann kann man alles, was man will, versionieren.

Abbildung 1: Eine typische Struktur eine Projektmappe für ein .NET-Entwicklungsprojekt

In Abbildung 1 ist die typische Struktur der Ordner und Dateien eines .NET-Entwicklungsprojektes, wie man es mit Visual Studio entwickelt, dargestellt.

Als unterster Ordner wurde ein Ordner src/ angelegt in dem dann die Projektmappendatei (Solution, .sln-Datei) und in je einem Unterordner pro Projekt dann die Quelldateien und die zugehörigen Projektdefinitionsdateien (.csproj-Dateien) liegen.

In dem Beispiel gibt es in der Projektmappe aktuell zwei Projekte, eines für die Umsetzung der Sicht und eines für die Umsetzung der Datenmodelle. Alle Dateien und Ordner werden nun in einem gemeinsamen Git-Reoisitory mit Namen „MDD4All.Notes.git“ versioniert.

Das Problem ist dann allerdings, dass man Änderungen an einzelnen Komponenten, sprich Softwareprojekten, nicht mehr einzeln nachvollziehen kann. Stellen Sie sich vor, man ändert an einer Datenmodellkomponente und einer Komponente, die eine Sicht realisiert. Dann kann man die Änderungen auch gemeinsam ins Versionskontrollsystem hochladen. Nun hat man eine neue Version für eine Änderung an zwei unterschiedlichen Komponenten erzeugt. Eigentlich sind die Änderungen jedoch inhaltlich vollkommen unterschiedlich. Das sieht man dann im Versionsverlauf so einfach aber nicht mehr.

Eine gemeinsame Versionierung aller Komponenten widerspricht auch dem Prinzip „Trennung von Funktionen“ (separation of concerns), welches im Bereich der Softwareentwicklung heute ein weitgehend akzeptiertes Entwurfsprinzip darstellt. Deshalb werden ja gerade verschiedene Projekte und Komponenten separiert. Wenn man es dann wieder gemeinsam versioniert, führt man das ein wenig ad absurdum.

Projekte einzeln versionieren und gemeinsam bearbeiten mit Hilfe von Git-Submodulen

Das Git-System bietet mit den so genannten Submodulen einen Mechanismus an, mit dem man genau das oben beschriebene Problem lösen kann (https://git-scm.com/book/en/v2/Git-Tools-Submodules). Submodule in Git sind nichts anderes als einzelne Git-Repositories, die in einem anderen Git-Repository – dem Hauptmodul – so eingebunden werden, dass sie wie ein Link vom Hauptmodul aus angebunden sind.

Verlinkt heißt in diesem Fall, dass im Hauptmodul lediglich ein Verweis auf ein oder mehrere Submodule gespeichert wird, die eigentlichen Dateien und Ordner aber im verlinkten, als Submodul eingebundenen Git-Repository, versioniert werden. Im Hauptmodul wird bei Verwendung von Submodulen eine Datei .gitmodules auf der Hauptebene angelegt. Daran erkennt man, dass das Repository mit Submodulen arbeitet.

Um das Prinzip der Submodule nun auf die Organisation der Entwicklungsdaten anzuwenden, muss man folgendermaßen vorgehen:

1. Jedes Softwareprojekt, aus dem eine Komponente entsteht, bekommt sein eigenes Git-Repository (Abbildung 2)

Abbildung 2: Alle Elemente des Datenmodell-Projektes liegen in einem eigenen Git-Repository

2. Man definiert ein Entwicklungs-Hauptprojekt, das ausschließlich nur noch die Projektmappendatei (.sln-Datei) enthält. Zur besseren Unterscheidung, dass es sich um ein Projekt mit Submodulen für Entwicklungszwecke handelt, benenne ich zusätzlich die Projektmappendatei mit dem Postfix „-dev“ (Abbildung 3).

Abbildung 3: Das neue Hauptprojekt enthält nur noch die Projektmappendatei

3. Nun werden die benötigten Projekte als Submodule mit Hilfe von Git dem Hauptprojekt hinzugefügt. Dabei legt Git einen entsprechenden Verweis auf das Submodul an, kopiert, bzw. klont den Code des Submoduls aber gleichzeitig an die entsprechend angegebene Stelle auf der Festplatte. Damit stehen dann die Quelldateien komplett zur Verfügung, werden als Submodul allerdings von Git separat verwaltet.

4. Nun, da die entsprechenden Quell- und Projektdateien per Submodul auf die Festplatte kopiert wurden, kann man die Projekte mit Hilfe von Visual Studio in die Projektmappe einbinden (Bestehendes Projekt hinzufügen).

    Die Struktur für unser Projektbeispiel aus Abbildung 1 sieht nach der Umstellung auf Submodule nun so aus (Abbildung 4), dass das Hauptmodul ausschließlich eine Projektmappendatei enthält und alle Komponentenprojekte per Submodul eingebunden und referenziert sind. Beim Einbinden des Submoduls muss man noch einen Pfad angeben, der angibt, wo das Submodul in das Hauptmodul „hingeklont“ werden soll. Hier zeigt sich, dass es sinnvoll ist anzugeben „src/<Projektname>/“. Damit landen die Submodule im Unterordner src/ und nicht direkt auf der Hauptebene.

    Abbildung 4: Die Gesamtstruktur realisiert mit Submodulen

    Im Dateisystem findet man dann eine Struktur wie in Abbildung 5 vor. Die Ordner der Komponentenprojekte mit den entsprechenden Projektnamen findet man nun doppelt vor, da sie sowohl als Submodul-Pfad, als auch im jeweiligen Git-Repository als Projektordner verwendet werden. Dazwischen liegt dann noch der Ordner src/ des Komponenten-Git-Repositories.

    Abbildung 5: Strukturierung mit Submodulen

    Nutzung von „-dev“-Projekten

    Was noch auffällt ist, dass in den Komponentenprojekten nun zwei Projektdateien liegen. Eine mit Namen des Projektes und eine mit Endung -dev.csproj. Was hat es damit auf sich? Man beabsichtigt vielleicht ja irgendwann mal eine Freigabe einer Komponente zu machen und daraus ein Nuget-Paket zu erstellen.

    Wenn man das tut, darf das entsprechende Projekt auch nur andere Komponenten als Nuget-Pakete referenzieren. Im Gegensatz dazu müssen die Projekte während der Entwicklung direkt auf andere Quellcode-Projekte verweisen, da man noch keine fertigen Nuget-Pakete vieler anderer Komponenten zur Verfügung hat, da diese selbst noch in der Entwicklung sind. In Visual Studio heißen diese beiden Verweise entweder PackageReference (Verweis auf ein Nuget-Paket) oder ProjectReference (Verweis auf ein Projekt). Damit man nun beide Möglichkeiten hat und nicht vor einer Freigabe immer wieder die Projektreferenzen durch Nuget-Referenzen ersetzen muss, wird die Projektdatei einfach kopiert. In der mit dem Zusatz -dev dürfen auch Projektreferenzen drin sein, in der ohne den -dev-Zusatz nicht.

    Bei der Entwicklung bindet man dann immer die -dev-Projekte in seine Projektmappe ein. Damit hat man eine klare Trennung zwischen Entwicklung und Freigabe einer Softwarekomponente erreicht.

    Zusammenfassung

    Mit Hilfe des Submodul-Mechanismus von Git lassen sich Softwarekomponenten separat versionieren und trotzdem auf transparente Weise im Entwicklungsprozess gemeinsam in eine Entwicklungsumgebung wie Visual Studio einbinden, ohne dass man dies in der täglichen Entwicklungsarbeit stark bemerken würde.

    Damit kann der Code einer Softwarekomponente sauber versioniert werden und es kann jederzeit eine saubere Komponentenfreigabe – unabhängig von der Nutzung der Komponente als Code – in verschiedenen Entwicklungsprojekten erfolgen.

    Das hier im Kontext von .NET beschriebene Verfahren lässt sich sicherlich auch auf viele andere Technologien und Programmiersprachen in der Softwareentwicklung übertragen. Auf jeden Fall kann man damit das Prinzip der Trennung von Funktionen auch auf die Versionskontrollebene übertragen. Ich nutze das bei meinen Entwicklungen seit Jahren erfolgreich und das einzige was man dabei beachten muss ist, dass man bei initialen Klonen des Hauptprojekts auch die Submodule mit klonen muss, damit man den gesamten Quellcode kopiert bekommt.

    Schreibe einen Kommentar