Die Wahl der richtigen Dateistruktur in deinem Projekt

Wenn wir über Softwarestruktur sprechen, denken wir oft an Entwurfsmuster und allgemein an Software-Architektur. Über die tatsächliche Dateistruktur innerhalb der Codebase wird weniger gesprochen, sie ist aber genauso wichtig. Eine gute Auswahl trägt wesentlich dazu bei, dass eine Software langfristig gut lesbar und wartbar ist. In diesem Artikel möchten wir die beiden konzeptionellen Ansätze des vertikalen und horizontalen Slicings vergleichen und Euch helfen, sich für einen passenden Ansatz für Euren Anwendungsfall zu entscheiden.

Die Entwicklung von Software bringt eine Vielzahl von Herausforderungen mit sich. Wahrscheinlich kennen wir alle eine Version des Witzes über die zwei schwierigen Probleme in der Informatik (Naming, Cache-Invalidierung und Off-By-1-Fehler). Es liegt in der Tat eine gewisse Schwierigkeit darin, Dingen gute Namen zu geben, die die wesentliche Funktion umfassen, das Ding von ähnlichen Dingen unterscheiden (die Wahl von "Ding" zum Beispiel scheint eine schlechte Idee zu sein) und dennoch einprägsam genug sind, um Euren Code und Eure Dateistruktur nicht zu verschmutzen (ich schaue Dich an, Java, mit Deinen lächerlich langen, wenn auch maximal spezifischen Klassennamen: InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState). Wenn es um die Dateistruktur geht, ist die Wahl guter Namen nur eines der Probleme. Oftmals streiten sich die Entwickler, wenn sie ein neues Projekt beginnen, über den besten Weg, das Projekt in sinnvolle Stücke aufzuteilen. In der Regel läuft es darauf hinaus, dass man sich über eine vertikale oder horizontale Aufteilung (Slices) streitet. In diesem Artikel möchte ich über diese beiden Ansätze sprechen, Vor- und Nachteile abwägen und herausfinden, ob es wirklich nur eine Frage des persönlichen Geschmacks ist oder ob die Wahl, die Ihr in Eurem Projekt trefft, sinnvolle Konsequenzen hat.

Slicing the cake

Ich muss zugeben, dass ich die Analogie eines Kuchens sehr mag, wenn es um die Struktur von Software geht.

Um einen Schichtkuchen zu bauen, fügt man Schicht für Schicht hinzu und arbeitet sich von unten nach oben vor. Am Ende hat man mehrere horizontale Schichten zusammengesetzt, wobei jede Schichten vorher in einem eigenen Verfahren vorbereitet wird (Backen der Kuchenschichten, Mischen der Füllung und des Zuckergusses). Um die Vorlieben jedes Gastes zu befriedigen, kann man auf ein Stück eine Kirsche legen, auf ein anderes eine Waffel oder ein Stück Schokolade und auf ein weiteres vielleicht ein Gummibärchen. Nun, da wir unseren Kuchen vorbereitet haben, können wir uns endlich der Software zuwenden:

Bei der Entwicklung einer Software denken wir oft in Form von Softwareschichten. Ein Beispiel für eine einfache (Backend-)Architektur könnte sein:

  • Daten-/Persistenzschicht: Verwaltet den Zugriff auf Daten durch Datenbankkommunikation, Dateisystemzugriff oder Zugriff auf interne oder externe APIs
  • Business-Schicht: Dienste, die Business-Logik ausführen, um die Verarbeitung von Daten zu ermöglichen
  • API-Schicht: Web-APIs für einen Client zum Zugriff auf und zur Bearbeitung von Daten

Wie die Schichten im Kuchen können die Schichten innerhalb der Software unabhängig voneinander entwickelt werden, möglicherweise von verschiedenen Teilen des Teams. Technologien und technologische Aspekte trennen die horizontalen Schichten, und um eine funktionierende Software (oder einen "funktionierenden" Kuchen) zu erhalten, müssen alle Schichten zusammengesetzt und miteinander verbunden werden.

Wie beim Kuchen, bei dem verschiedene Prozesse erforderlich sind, um die verschiedenen Schichten vorzubereiten, werden die horizontalen Schichten bzw. Slices in der Softwareentwicklung durch verschiedene technologische Konzepte getrennt.

Wenn wir eine Dateistruktur mit diesen technologischen/horizontalen Schichten im Hinterkopf erstellen, könnte sie wie folgt aussehen:

model/
  |-- Classroom.cs
  |-- Student.cs
repository/
  |-- ClassroomRepository.cs
  |-- StudentRepository.cs
service/
  |-- ClassroomService.cs
  |-- StudentService.cs
api/
  |-- ClassroomApi.cs
  |-- StudentApi.cs

Technologisch bedingte Dateistruktur (horizontale Slices)

Wir könnten die Kirsche oder das Gummibärchen oben auf einem Stück Kuchen als das Unterscheidungsmerkmal zwischen den Stücken sehen. Unsere Gäste repräsentieren die geschäftliche Seite der Dinge, die entweder das Stück mit der Kirsche oder das Stück mit dem Gummibärchen verzehren. In unserer Software stellen die verschiedenen Entitäten innerhalb der Geschäftsdomäne (Klassenzimmer, Schüler) die einzelnen unterscheidbaren Stücke des Kuchens dar.

Obwohl aus der Sicht unserer Gäste jede einzelne Schicht von Bedeutung ist, ist es wichtiger, ein vollständiges Stück mit allen Schichten zu erhalten. Um ein Stück des Kuchens zu servieren, müssen wir vertikale Schnitte erstellen. In unserer Software benötigen wir Komponenten aller integrierten Schichten, damit eine bestimmte Einheit richtig funktioniert. Vertikale Slices liefern uns also jeweils den gesamten Stack, der für die Verarbeitung einer einzelnen Geschäftseinheit oder eines Arbeitsablaufs erforderlich ist.

Eine domain-driven Dateistruktur würde die geschäftsseitige Unterscheidung der Entitäten (die Stücke des Kuchens) darstellen und würde dann wie folgt aussehen:

Classroom/
  |-- Classroom.cs
  |-- ClassroomRepository.cs
  |-- ClassroomService.cs
  |-- ClassroomApi.cs
Student/
  |-- Student.cs
  |-- StudentRepository.cs
  |-- StudentService.cs
  |-- StudentApi.cs

Domain-driven Dateistruktur (vertikale Slices)

Was den Kuchen zusammenhält

Bei beiden Ansätzen versuchen wir, mehrere Dateien nach einer bestimmten Logik zu gruppieren. Bei der Wahl von vertikalen Slices ist unsere Gruppierung eher geschäftsspezifisch, während wir bei horizontalen Slices nach Technologien und technologischen Aspekten innerhalb unserer Software gruppieren. Es ist nicht das erste Mal, dass wir uns auf Kent C. Dodds berufen, der die Vorteile der Colocation: keeping things together that change together (Bündeln von Dingen, die sich gemeinsam ändern) hervorhebt, um die Lesbarkeit und Wartbarkeit drastisch zu verbessern. Es stellt sich nun die offensichtliche Frage: Auf welche Art und Weise ändert sich unsere Software?

Den Kuchen vergrößern?

Mit einer funktionierenden Architektur und einer ersten laufenden Version der Software ist es in der Regel noch nicht getan. Und das ist der Teil, an dem Ihr die Entscheidungen, die Ihr zuvor über die Dateistruktur getroffen haben, vielleicht bereut. Wir können uns zwei Fälle vorstellen, wie man den Kuchen vergrößert.

Fall 1: Erweiterung der Domäne um einen neuen Geschäftsaspekt

In Kuchenstücken gedacht, könnte man dies so darstellen, dass man den Kuchen seitlich erweitert (ich überlasse es Eurer Fantasie, wie genau Ihr dies tun könntet), um ein weiteres Stück oder einen vertikalen Slice aufzunehmen. Wichtig ist, dass dieses neue Stück im Wesentlichen aus denselben (horizontalen) Schichten besteht, mit Ausnahme des individuellen Belags.

Bei der Verwendung einer domain-driven Dateistruktur (vertikale Slices) könnten wir einfach eine ähnliche Entität als Vorlage für alle erforderlichen Dateien auswählen, um die neue Entität anzulegen. Die Aktualisierung einer bestehenden Domänenentität wäre ebenso einfach, da wir alle Dateien in der Nähe finden könnten. Dies wäre ein gutes Beispiel für kohäsive Entitäten (Kohäsion: Grad, in dem sich Dinge gemeinsam verändern), die gemeinsam untergebracht (colocated) sind.

Bei einer technologiegesteuerten Dateistruktur müssten wir sicherstellen, dass jede Darstellung des Modells (*Repository.cs, *Service.cs, *Api.cs) entsprechend aktualisiert oder hinzugefügt wird. Dies wäre nicht nur aus Sicht des Entwicklers unbequem, sondern birgt auch ein gewisses Risiko, einen der vielen Ordner zu übersehen, in denen wir Dateien anpassen müssen.

Fall 2: Hinzufügen einer neuen Funktion, die eine Anpassung der Infrastrukturschichten des Projekts oder das Hinzufügen neuer Schicht erfordert

Bei einer domain-driven Dateistruktur würde dies die Überprüfung der entsprechenden Datei (z.B. *Repository.cs) in jedem Entitäts-Ordner erfordern. Bei einer technologiegesteuerten Dateistruktur (horizontale Slices) hätten wir wiederum ein hohes Maß an Colocation kohäsiver Dateien. Die Kohäsion der Dateien ist abhängig vom Kontext, in dem wir unsere Dateistruktur betrachten.

Ein Faktor für die Wahl des Ansatzes zur Strukturierung unserer Codebase ist damit die Art der Änderungen, die wir während der Lebensdauer unserer Anwendung erwarten.

Shared Logic

An dieser Stelle möchte ich hinzufügen, dass es auf jeden Fall einen Platz für gemeinsam genutzte (shared) Logik geben sollte. Zum Beispiel könnten alle *Repository.cs-Dateien entweder von einer gemeinsam genutzten Basisklasse BaseRepository.cs erben oder ein gemeinsames Modul zur Kommunikation mit der Datenbank verwenden. Bei einer technologieorientierten Dateistruktur könnten die Infrastrukturdateien in denselben Ordnern wie die Implementierungen zu finden sein. Die angepassten Dateistrukturen könnten wie folgt aussehen:

model/
  |-- Klassenzimmer.cs
  |-- Student.cs
repository/
  |-- ClassroomRepository.cs
  |-- StudentRepository.cs
  |-- BaseRepository.cs
service/
  |-- ClassroomService.cs
  |-- StudentService.cs
api/
  |-- ClassroomApi.cs
  |-- StudentApi.cs
  |-- BaseApi.cs

Technologisch gesteuerte Dateistruktur (horizontale Slices) mit shared Infrastruktur

Bei einer domain-driven Dateistruktur könnten wir den Infrastrukturcode in einem separaten Ordner unterbringen.

domain/
  |-- Classroom/
    |-- Classroom.cs
    |-- ClassroomRepository.cs
    |-- ClassroomService.cs
    |-- ClassroomApi.cs
  |-- Student/
    |-- Student.cs
    |-- StudentRepository.cs
    |-- StudentService.cs
    |-- StudentApi.cs

infrastructure/
  |-- BaseRepository.cs
  |-- BaseApi.cs

Domain-driven Dateistruktur (vertikale Slices) mit shared Infrastruktur

Die Verwendung von gemeinsam genutzten Modulen erhöht zwar die Kopplung, aber in Anbetracht der Anpassung an neue Funktionen ermöglicht sie uns, in Kombination mit einer domain-driven Dateistruktur das Beste aus beiden Welten zu erreichen: Domänenspezifische Dateien bleiben an einem Ort, und wir können dennoch indirekt Anpassungen an einer vollständigen horizontalen Schicht vornehmen, indem wir shared Code bearbeiten.

Agil vs. Wasserfall oder Vertikal vs. Horizontal?

Ich stimme Jerry Virgo – der einen wirklich aufschlussreichen Artikel zu diesem Thema geschrieben hat – vollkommen zu, dass es nicht verwunderlich ist, dass in größeren Teams mit geteilten Verantwortlichkeiten die Codebase dies mit überwiegend horizontal geschichteten Dateistrukturen widerspiegelt. Kleinere Teams erfordern häufig die Entwicklung im Full-Stack, weshalb man in Start-ups eher auf vertikales Slicing stößt.

Die Dateistruktur lässt auch Rückschlüsse auf den vorwiegenden Modus der Entwicklung zu. Ein moderner agiler Ansatz modelliert neue Funktionen oder Arbeitsabläufe in User Stories, wobei jede User Story als ein vertikaler Slice dargestellt werden kann. Mit einer hauptsächlich vertikalen Dateistruktur ist das Hinzufügen eines neuen vertikalen Slices einfach und bequem. In Projekten, die einem Wasserfall-Ansatz folgen, können ganze technologisch kohärente Teile (horizontale Slices) des Systems nach und nach in Phasen aufgebaut werden, wobei das Ende jeder Entwicklungsphase durch die Fertigstellung einer kompletten horizontalen Schicht markiert wird, vielleicht sogar durch eigene, getrennte Teams, die an den verschiedenen horizontalen Schichten arbeiten.

So it's a piece of cake?

Die kurze und wenig überraschende Antwort lautet: Nein. Ihr werdet fast nie Projekte finden, in denen Ihr völlig unabhängige vertikale Slices habt, und selbst wenn Ihr einen vertikalen Slicing-Ansatz in Eurer Dateistruktur gewählt habt, werdet Ihr möglicherweise Schwierigkeiten haben, geeignete Wege zu finden, um entitätsübergreifende Funktionalität sauber in die Codebase zu integrieren. User Stories können und werden Slices erstellen, die entweder nicht durchgängig sind (stellt Euch die 4 CRUD Stories für eine Entität vor, die nur getrennte Dienste und APIs haben, aber auf einem gemeinsamen Modell und Repository ausgeführt werden) oder die mehrere Eurer bestehenden Slices überspannen (User Story der Zuweisung einer Entität zu einer anderen: Zuweisung eines Schülers zu einem Klassenzimmer).

In diesem Artikel habe ich mich zwar hauptsächlich auf die Extreme des vertikalen oder horizontalen Slicings konzentriert, aber es sind auch gemischte Ansätze denkbar. Sie könnten Ihr Projekt zum Beispiel in mehrere Module aufteilen, die jeweils einen eigenen Ansatz verfolgen.

Ein Blick in die Zukunft

Vielleicht werden wir eines Tages einen anderen Ansatz für die Verwaltung unserer Codedateien sehen. Das erste, was mir in den Sinn kommt, ist die Kennzeichnung von Dateien sowohl mit technologischen (Repository, Service, API) als auch mit fachlichen Tags (Classroom, Student). Ein Trend, der bereits vor einiger Zeit bei der Verwaltung von Bilddateien zu beobachten war. Je nach Anwendungsfall könnten wir dann entweder nach technologischen Tags oder nach Tags für den Unternehmensbereich gruppieren, aber auch das würde sicherlich seine eigenen Herausforderungen mit sich bringen.

Die Verwendung der Konvention, Dateien nach einer Kombination aus Entität und technologischer Rolle zu benennen, ist bereits eine Art von Tagging. Und da moderne IDEs über leistungsfähige projektweite Schnellsuchfunktionen für Dateien verfügen, können wir die Grenzen, die uns eine feste Dateistruktur auferlegt, bereits teilweise überwinden. Im Moment müssen wir uns noch für einen Ansatz für unsere Projekte entscheiden. Die Art der Entwicklung, die Größe des Teams und die Anforderungen an die langfristige Flexibilität sollten dabei berücksichtigt werden, da die Wahl der richtigen Methode für Eure Anwendung langfristige Vorteile mit sich bringen kann.

Genießt Euren Kuchen! 🍰