Ein Schichtenmodell, das mitwächst: Controller, Service, Repository, DTO
Wie eine klare Trennung zwischen Controller, Service und Repository aus schwer wartbarem Code eine wiederverwendbare Basis macht und wo sie an ihre Grenzen stösst.
Viele Softwareprojekte starten schlank. Ein Controller nimmt Anfragen entgegen, eine Klasse spricht direkt mit der Datenbank, fertig. Nach ein paar Jahren und etlichen neuen Features sieht das anders aus: Datenbankzugriffe, Geschäftslogik und Schnittstellencode liegen in denselben Klassen, jede Änderung zieht unvorhergesehene Nebenwirkungen nach sich, und Reviews dauern länger, weil niemand mit Sicherheit sagt, was eine Anpassung sonst noch betrifft. Bei Batix stand die Softwareentwicklung genau vor diesem Problem, als das Team die Architektur für BX:EDUCATION plante. Die Lösung war ein Schichtenmodell mit klar getrennten Verantwortlichkeiten – Controller, Service, Repository, dazwischen DTOs und Models. Eingeführt wurde das Muster zuerst dort, inzwischen ist es in mehreren weiteren Batix-Projekten im Einsatz und wird von Projekt zu Projekt weiter verfeinert.
Warum gewachsener Code irgendwann bremst
Ohne klare Trennung landen Datenbankzugriff, Validierung und Geschäftslogik oft in derselben Klasse. Das funktioniert an Tag eins. Mit jedem neuen Feature wächst aber die Zahl der Stellen, die bei einer Änderung mitbedacht werden müssen. Ein Feld in der Datenbank umzubenennen heisst dann, Controller, Validierung und Anzeige-Logik gleichzeitig anzupassen – und zu hoffen, keine der Stellen übersehen zu haben. Ein typisches Symptom: Ein Pull Request für eine kleine fachliche Änderung wächst schnell auf mehrere hundert Zeilen an, weil dieselbe Logik an drei oder vier Stellen im Code nachgezogen werden muss.
Bei Batix war genau das der Ausgangspunkt für BX:EDUCATION: Der bestehende Code liess sich kaum noch warten oder erweitern, ohne an anderer Stelle etwas zu brechen. Der Aufwand für ein Refactoring, das diese Kopplung auflöst, wirkt zu Beginn hoch. Er zahlt sich aber genau dann aus, wenn ein Projekt über Jahre wächst statt nach einem Release zu enden.
Die Aufteilung: Controller, DTO, Service, Model, Repository
Der Controller nimmt Anfragen entgegen und gibt sie als DTO an den Service weiter. Ein DTO transportiert nur das, was die Schnittstelle nach aussen braucht, unabhängig davon, wie die Daten intern gespeichert sind. Der Service enthält die Geschäftslogik und spricht mit dem Repository über ein Model. Models bilden die Datenbank-Entitäten eins zu eins ab. Ein Beispiel: Eine interne Spalte wie ein Bearbeitungs-Flag oder ein technischer Zeitstempel gehört ins Model, weil sie Teil der Datenbank-Entität ist. Über die API muss sie deshalb noch lange nicht sichtbar sein, das entscheidet allein das DTO.
Diese Trennung hat einen konkreten Vorteil: Ändert sich die Datenbankstruktur, betrifft das nur Model und Repository. Ändert sich die externe Schnittstelle, betrifft das nur das DTO. Beides lässt sich unabhängig voneinander anpassen und testen. Ein weiterer Effekt zeigt sich beim Testen selbst: Der Service lässt sich mit einem gemockten Repository prüfen, ohne dass ein Test gegen eine echte Datenbank laufen muss.
Wiederverwendbare Queries dank AbstractRepository
Weil jedes Model eins zu eins einer Datenbank-Entität entspricht, liess sich ein grosser Teil der Datenbankabfragen nicht mehr pro Repository einzeln schreiben, sondern einmal in einem gemeinsamen AbstractRepository. Suchen, Filtern und Paginieren stehen damit für jede neue Entität sofort zur Verfügung, ohne dass jemand dieselbe Query ein weiteres Mal implementiert. Je mehr Entitäten im Laufe der Zeit dazukamen, desto deutlicher zeigte sich der Effekt: Ein neues Repository musste oft nur noch die fachlichen Ausnahmen ergänzen, der grössere Teil kam bereits aus der gemeinsamen Basis.
Sascha, der die Architektur bei Batix mitentwickelt hat:
«Weil unsere Models die Datenbank-Entitäten eins zu eins abbilden, liessen sich sehr viele Queries in einem AbstractRepository einmal bauen und dann in allen Plugins wiederverwenden. Der alte Code liess sich davor kaum noch warten oder erweitern.»
Die Grenze: Plugin-Architektur und geteilte Datenzugriffs-Logik
Der Ansatz stösst an eine Grenze, sobald mehrere Plugins auf dieselbe Datenbank zugreifen müssen. Bei BX:EDUCATION lag der datenbanknahe Code, Models und Repositories, zuerst nur in einem einzelnen Plugin. Andere Plugins brauchten diesen Code aber ebenfalls und hatten keinen sauberen Zugriff darauf.
Sascha:
«Wir hatten die Datenbank-Seite mit Models und Repositories zuerst nur in einem Plugin. Andere Plugins brauchten diesen Code aber auch. Am Ende mussten wir das in eine eigene Library auslagern – und die nachträglich in jedes Plugin zu integrieren, war aufwändig und mühsam.»
Die Lösung war eine eigenständige, gemeinsam genutzte Library für den datenbanknahen Teil. Nachträglich aufgebaut, liess sie sich nur mit Mehraufwand in die bereits bestehenden Plugins integrieren, jedes Plugin musste einzeln umgestellt werden. Im Rückblick hätte sich dieser Umbau vermeiden lassen, wenn die Datenzugriffsschicht von Beginn an als eigene, plugin-unabhängige Library geplant worden wäre.
Fazit
Wer eine Plugin- oder Modul-Architektur plant, sollte die Datenzugriffsschicht deshalb von Anfang an als eigenständige, gemeinsam nutzbare Komponente denken, nicht als Teil des ersten Plugins, das sie zufällig gebraucht hat. Bei Batix ist das Schichtenmodell seit BX:EDUCATION fester Bestandteil neuer Projekte und wird von Fall zu Fall weiter angepasst.
Häufig gestellte Fragen
Was ist der Unterschied zwischen einem Model und einem DTO?
Ein Model bildet die Datenbank-Entität eins zu eins ab und wird zwischen Service und Repository verwendet. Ein DTO transportiert nur das, was die externe Schnittstelle braucht, und wird zwischen Controller und Service eingesetzt. So bleiben Datenbankstruktur und Programmierschnittstelle unabhängig voneinander veränderbar, auch wenn sich eine der beiden Seiten ändert.
Lohnt sich eine Schichtenarchitektur auch für kleine Projekte?
Bei sehr kleinem Umfang steht der Mehraufwand an zusätzlichen Files und Mapping-Code oft in keinem guten Verhältnis zum Nutzen. Sobald Wartung und Erweiterung über einen längeren Zeitraum absehbar sind, zahlt sich die Trennung aus. Ein guter Anhaltspunkt ist, ob das Projekt voraussichtlich mehrere Jahre und mehrere Personen im Team überdauert.
Was macht ein AbstractRepository?
Es bündelt wiederkehrende Datenbankabfragen wie Suchen, Filtern oder Paginieren an einer Stelle. Einzelne Repositories erben diese Funktionen, statt sie jeweils neu zu implementieren. Neuer, entitätsspezifischer Code kommt nur noch für die tatsächlichen Ausnahmen dazu.
Was ist bei einer Plugin-Architektur zusätzlich zu beachten?
Datenbanknahe Komponenten wie Models und Repositories sollten von Beginn an als eigenständige, gemeinsam nutzbare Library aufgebaut werden. Liegen sie zuerst nur in einem einzelnen Plugin, lässt sich das später nur mit Mehraufwand korrigieren, wie die Erfahrung bei BX:EDUCATION zeigt.
Wo setzt Batix diese Architekturmuster ein?
Eingeführt wurde es bei der Entwicklung von BX:EDUCATION. Seither wird es projektspezifisch verfeinert und kommt in weiteren Batix-Projekten zum Einsatz.
Wer eine bestehende Software modernisieren oder ein neues System von Grund auf wartbar aufbauen will, findet in der individuellen Softwareentwicklung der Batix den passenden Ansprechpartner.
Kontakt aufnehmen