Go und der Ansatz des Layered Designs
(jerf.org)- Die Sprache Go verbietet zyklische Referenzen zwischen Paketen strikt, was ganz natürlich zu einem hierarchischen Design (Layered Design) führt
- Dieser Artikel erklärt die zwangsläufig entstehende Schichtenstruktur in Go-Projekten und argumentiert, dass sie auch ohne zusätzlich aufgezwungene Architektur völlig tragfähig ist
- Für den Fall zyklischer Abhängigkeiten werden konkrete und praxistaugliche Refactoring-Strategien Schritt für Schritt vorgestellt
- Jedes Paket ist als eigenständige, sinnvolle Funktionseinheit konzipiert und eignet sich dadurch besonders gut für Tests, Wartung und die Aufteilung in Microservices
- Letztlich verhindert dieser Ansatz das in der Praxis häufige Problem, "man wollte eine Banane, bekam aber den ganzen Dschungel"
Der Layered-Design-Ansatz in Go
Grundprinzipien
- Go verbietet zyklische Referenzen zwischen Paketen
- Die Import-Beziehungen aller Go-Programme müssen einen gerichteten azyklischen Graphen (DAG) bilden
- Diese Struktur ist keine Option, sondern eine auf Sprachebene erzwungene Designregel
Automatische Bildung von Paket-Schichten
- Abgesehen von externen Paketen lassen sich die internen Pakete eines Projekts automatisch nach Referenztiefe schichten
- Wie in der Abbildung unten befinden sich ganz unten zentrale Utility-Pakete wie metrics, logging und gemeinsame Datenstrukturen
- Darüber entsteht schrittweise eine Struktur, in der höhere Pakete Funktionalität kombinieren und sich nach oben aufbauen
Eigenschaften dieses Designansatzes
- Schichten basieren nicht auf hierarchischer Abstraktion, sondern auf der Richtung von Referenzen
- Ein Paket kann mehrere Pakete auf niedrigeren Ebenen referenzieren
- Auch bestehende Entwurfsansätze wie MVC oder hexagonale Architektur lassen sich auf diese Struktur "anwenden"
→ Dabei müssen die strukturellen Einschränkungen von Go jedoch zwingend berücksichtigt werden
Strategien zur Auflösung zyklischer Referenzen
Wenn zyklische Referenzen auftreten, sollte Refactoring in der folgenden Reihenfolge versucht werden:
1. Funktionalität verschieben
- Die am meisten empfohlene Methode
- Die Funktionalität, die den Zyklus verursacht, wird präzise analysiert und logisch an die passende Stelle verschoben
- Wird nicht oft eingesetzt, verbessert aber die konzeptionelle Klarheit am stärksten
2. Gemeinsame Funktionalität in ein separates Paket auslagern
- Typen oder Funktionen, die beide Seiten gemeinsam verwenden (
Usernameusw.), werden in ein drittes Paket verschoben - Auch wenn das Paket zunächst klein wirkt, sollte man es konsequent auslagern
→ Mit der Zeit ist die Wahrscheinlichkeit hoch, dass dieses Paket wächst
3. Ein übergeordnetes Kompositionspaket erzeugen
- Es wird ein drittes Paket erstellt, das die beiden zyklisch verbundenen Pakete zusammensetzt
- Beispiel: Die beidseitige Abhängigkeit von
CategoryundBlogPostwird in ein übergeordnetes Paket ausgelagert
→ Die unteren Pakete bleiben einfache structs, die eigentliche Funktionalität wird im oberen Paket zusammengesetzt
4. Interfaces einführen
- Abhängigkeiten werden durch Interfaces ersetzt, die nur die tatsächlich benötigten Methoden enthalten
- Entfernt unnötige Abhängigkeiten und verbessert die Testbarkeit
- Bei übermäßigem Einsatz kann das Design allerdings auch unnötig komplex werden
5. Kopieren
- Wenn die Abhängigkeit sehr klein ist, kann man sie einfach kopieren und verwenden
- Das mag wie ein Verstoß gegen DRY wirken, hilft in der Praxis jedoch oft dabei, das Design klarer zu machen
6. In einem Paket zusammenführen
- Wenn alle anderen Methoden nicht möglich sind, werden die beiden Pakete zusammengelegt
- Solange das Paket nicht zu groß wird, ist das akzeptabel
→ Eine pauschale Zusammenlegung sollte jedoch vermieden und sorgfältig entschieden werden
Praktische Vorteile dieses Designansatzes
- Jedes Paket bildet für sich eine sinnvolle Funktionseinheit und kann unabhängig getestet werden
- Da Referenzen innerhalb des Pakets begrenzt sind, lässt sich ein einzelnes Paket verstehen, ohne die gesamte Codebasis zu kennen
- Unbeabsichtigte globale Abhängigkeitsverflechtungen (= das Dschungelproblem) werden vermieden, und es wird dazu angeregt, nur das tatsächlich Benötigte zu verwenden
- Auch bei der Aufteilung in Microservices lassen sich Teile leicht extrahieren
→ Die meisten Abhängigkeiten sind klar definiert
Fazit
- Die Einschränkungen beim Paketdesign in Go sind kein lästiges Hindernis, sondern ein Mechanismus, der gutes Design fördert
- Auch ohne besondere Architektur lässt sich allein über die Referenzstruktur zwischen Paketen ein robustes Design umsetzen
- Eine präzise Analyse und Refactoring-Strategie für zyklische Referenzen ist nicht nur für Go, sondern auch für andere Sprachen wertvoll
4 Kommentare
Am Anfang macht es Spaß, wenn man schnell etwas zusammenschreibt und es läuft.
Sobald man aber anfängt, Tests hinzuzufügen,
fragt man sich, warum man das damals so gemacht hat.
„Ich wollte eine Banane, aber bekam den ganzen Dschungel mitgeliefert“ – dieser Spruch ist wirklich zu lustig.
Eines der schwierigsten Dinge bei der Entwicklung mit Spring war, glaube ich, die zirkuläre Abhängigkeit..
Diese Frustration, wenn sich alles endlos gegenseitig initialisiert und dann wegen eines Speicherlecks abstürzt...
Hacker-News-Kommentare
Dass zyklische Abhängigkeiten nicht erlaubt sind, ist eine hervorragende Designentscheidung beim Aufbau großer Programme
Großartiger Blogbeitrag
Eine Bonus-Technik im Zusammenhang mit dem Rat, es in ein „drittes Paket“ zu verschieben
Klingt, als würde jemand ein Buch über die strukturierte Methode von Yourdon lesen
Pakete können sich nicht gegenseitig zyklisch referenzieren
go:linknamemöglichErinnert an das konkrete Konzept eines Randomizers
Eine interessante Eigenheit von Golang ist, dass es auf Paketebene keine zyklischen Abhängigkeiten geben darf, in
go.modaber schonEine schöne Erklärung dafür, wie Jerf über Pakete nachdenkt und wie mit zyklischen Abhängigkeiten umgegangen wird