Komponentenbasierte Architektur in Symfony2

Vielen Symfony2 Entwicklern ist das Prinzip der Separation of Concerns bekannt und findet Anwendung bei der Umsetzung ihrer Projekte, indem sie ihre Projekte in logische Einheiten teilen. Diese Teilung hat zum Ziel die Software verständlicher, wartbarer, erweiterbarer und testbarer zu gestalten. Schon das zu Grunde liegende Framework lebt dieses Prinzip vor und präsentiert sich als Sammlung von unabhängig voneinander nutzbaren Bausteinen, die in Form von Bundles eingebunden werden. Ein Blick in die Dokumentation des Bundle Konzepts zeigt, dass die Schöpfer des Frameworks sogar erwähnen, dass in Symfony alles ein Bundle ist. Diese Philosophie führte dazu, dass zahlreiche Entwickler sich das Bundle Konzept zu Nutze gemacht haben, um ihr Projekt logisch zu unterteilen um damit eine Separation of Concerns zu erreichen.

Betrachten wir als Beispielprojekt eine Software as a Service Anwendung, die Kunden für einen monatlichen Beitrag erlaubt den Urlaub von Mitarbeitern zu verwalten. Ein mögliche Teilung der Businesslogik wäre: Kundenverwaltung (customer management), Mitarbeiterverwaltung (employee management), Urlaubsverwaltung (absence management) und Zahlungsabwicklung (payment). Die Struktur könnte wie folgt aussehen:

Struktur Beispielprojekt Bundles

Jedes der Bundles in diesem Beispiel würde die Controller, Formulare, Entities, Services und Resourcen beinhalten, die nötig sind um die Funktionalität aus seiner Domäne anzubieten. Zusätzlich fällt noch Code an, der von der gesamten Anwendung genutzt wird. Dieser Code liegt in der Regel im app/ Verzeichnis oder in einem allgemeinen Bundle, in unserem Beispiel nennen wir es CommonBundle.

Weg von Application Bundles

Der ursprüngliche Gedanke hinter dem Bundle Konzept von Symfony2 war Funktionalität von Drittanbietern einbinden bzw. zwischen verschiedenen Symfony Applikationen teilen zu können. Für die Separierung der Anwendungslogik innerhalb eines Projekts sind Bundles in der Regel aber gar nicht nötig, da diese logischen Einheiten das Projekt nie verlassen werden.

Die Entwickler des Frameworks haben diese Tendenz erkannt und geben mit der Einführung der offiziellen Symfony Best Practices den Rat, die Businesslogik in einem einzelnen Bundle, dem AppBundle, abzulegen. Da dieser Code das Projekt nie verlassen wird und es keinen technischen Grund dafür gibt ihn überhaupt in ein Bundle zu legen, gehen sie noch einen Schritt weiter und schlagen vor, die Bundle Struktur zu verlassen und das src Verzeichnis frei zu nutzen. Die logische Strukturierung erfolgt also über Namespaces im allgemeinen Quellenverzeichnis. Es wird sogar diskutiert, in Symfony 3 das AppBundle vollständig aus der Standardinstallation zu entfernen und dessen Inhalt in das Verzeichnis app zu verschieben.

Für unser Beispielprojekt würde das bedeuten, die Symfony spezifischen Teile wie Controller, Service-Definitionen, Ressourcen und Formulare der einzelnen Bundles  wieder zusammen in eines zu legen. Dies stellt auch kein Problem dar, da wir bereits festgestellt haben, dass sie selten so generisch sind, dass sie jemals unverändert mit einem anderen Projekt über den Bundle-Mechanismus geteilt werden würden. Die frontendspezifischen Teile können wir über Unterverzeichnisse logisch voneinander trennen, um in unserem Bundle eine saubere Struktur zu erhalten. Die Businesslogik wollen wir nun neben das AppBundle im src Verzeichnis unterbringen. Als Businesslogik verstehen wir die Klassen und Services, die z.B. von unseren Controllern genutzt werden um dem User die Funktionalität anzubieten.

Wir wollen nun also das Quellverzeichnis für unsere Anwendungslogik frei nutzen und die Komplexität unserer Software durch logische Einheiten bändigen. Schon in den 90er Jahren hat man erkannt, dass Objektorientierung alleine nicht reicht, um dieses Ziel zu erreichen und das Konzept der komponentenbasierte Softwareentwicklung vorgeschlagen, welches wir jetzt auf unsere Symfony Projekte anwenden wollen.

Was ist eine Softwarekomponente?

In der komponentenbasierten Softwareentwicklung betrachten wir unser Projekt als ein Zusammenschluss aus einzelnen Softwarekomponenten. Jede dieser Softwarekomponenten stellt klar definierte Funktionalität zur Verfügung und kann dafür wiederum andere Komponenten verwenden. Damit dies möglich ist, brauchen wir klar definierte Schnittstellen zwischen den einzelnen Komponenten und einen Standard, der festlegt wie die Komponenten definiert, instanziert und miteinander verdrahtet werden. Dieser Standard wird in der komponentenbasierten Softwareentwicklung Komponentenmodell genannt. Da die Funktionalität einer Komponente klar spezifiziert ist und nur über die Schnittstelle in Anspruch genommen wird, muss ein Entwickler für ihre Verwendung also lediglich die Schnittstelle und keine Implementierungsdetails verstanden haben.

Zusammengefasst brauchen wir also für eine Softwarekomponente:

  • Eine Spezifikation über die Funktionalität, die unsere Komponente leisten soll
  • Eine klar definierte Schnittstelle zur Komponente
  • Eine Implementierung der Schnittstelle (die andere Komponenten nutzen kann)
  • Ein Komponentenmodell, das die Komponenten instanziert und innerhalb der Anwendung zur Verfügung stellt

Diese Definition wollen wir uns jetzt zu Nutze machen, um unsere Symfony2 Applikationen sauber zu strukturieren.

Softwarekomponenten in Symfony2

Betrachten wir unser Beispielprojekt vom Anfang und teilen die Businesslogik mit dem Konzept der Softwarekomponenten in logische Einheiten. Diese legen wir dann nach dem Symfony Best Practices in separate Namespaces neben das AppBundle. Stecken wir also zuerst unsere einzelnen Komponenten der Businesslogik und deren Abhängigkeiten zueinander ab. Eine mögliche Struktur zeigt das folgende UML Komponentendiagram:

CBSE Beispielprojekt Komponenten

In dem Diagramm fällt auf, dass die grafische Oberfläche, die in Form unserer Controller und Views im AppBundle implementiert ist, auch als Komponente betrachtet wird. Die GUI Komponente macht Gebrauch von anderen Komponenten, um ihre Funktionalität zu erfüllen.

Die klar definierte Schnittstelle unserer Komponenten erreichen wir in PHP über Interfaces. Zu beachten ist, dass neben dem Interface selbst auch die Klassen zur Schnittstelle gehören, die als Parameter in die Methoden des Interfaces gereicht werden oder Rückgabewerte sind.

Als Implementierung der Schnittstelle legen wir eine PHP Klasse, die das Interface der Komponente implementiert, zusammen mit dem Interface in einen Namespace. Die Implementierungsklasse ist nur der Einstiegspunkt, was bedeutet, dass die Implementierung natürlich aus beliebig vielen Klassen bestehen kann. Diese sollten aber nicht ausserhalb des Komponentennamespace verwendet werden, da wir sonst keine klare Schnittstelle mehr hätten.

Das Komponentemodell liefert uns Symfony mit seinem Service Container. Der Service Container erlaubt es uns, unsere Komponentenimplementierung als Service in der Applikation zu registrieren, und somit für andere zu Verfügung zu stellen. Er kümmert sich um die Instanzierung und jede Komponente ist über die Dependency Injection in der Lage sich anderen Komponenten, zur Implementierung ihrer Funktionalität, injecten zu lassen. Genauso können unsere Controller sich Instanzen von Komponenten holen und auf ihre Funktionalität zugreifen, um diese dem Benutzer zu Verfügung zu stellen.

Die Struktur unseres Beispielprojekts würde dann wie folgt aussehen:

Struktur Beispielprojekt Komponenten

Jede Komponente liegt in einem separaten Namespace im Quellverzeichnis und besteht aus einer Schnittstelle und einer Implementierung. Die Schnittstellendefinitionen bestehen in unserem Beispiel aus dem Interface und den Objekten im jeweiligen Model Namespace. Die Model Klassen sind die Datenhaltungsobjekte und dienen zur Kommunikation mit den Komponenten. Jede Komponente registriert sich selbst und seine Abhängigkeiten durch einen Eintrag in der Service-Konfigurationsdatei bei der Applikation. Da die Implementierung einer Komponenten mit sehr hoher Wahrscheinlichkeit mehrere Services zur eigenen Nutzung definieren wird, wäre hier auch denkbar jeweils eine separate Konfigurationsdatei pro Komponente anzulegen (z.B. src/AppBundle/Resources/config/<component name>-services.xml) und damit auch in der Konfiguration die Komponentenstruktur erkennbar zu machen.

Vorteile durch Softwarekomponenten

Nachdem wir nun das Konzept der Softwarekomponenten auf unserer Symfony Applikation angewendet haben, betrachten wir nun was wir mit dem Ansatz erreicht haben. Nicht zu übersehen ist die klare Struktur. Es ist für uns ohne große Mühe ersichtlich, aus welchen logischen Elementen unsere Applikation besteht. Es ist somit denkbar einfach, neuen Entwicklern das Projekt und die Projektstruktur näher zu bringen. Wenn wir präzise Informationen über die Architektur unseres Projekts erhalten wollen, können wir über die Service Definitionen sogar genau ermitteln, welche Komponenten existieren und wie diese voneinander abhängen. Wir könnten diese Daten sogar automatisiert ermitteln oder analysieren, da unserer Komponentenarchitektur in maschinenlesbarer Form, in diesem Fall XML, vorliegt. Diese Information kann für uns als Entwickler sehr hilfreich sein, wenn wir abschätzen wollen, welche Auswirkungen die Änderung einer Komponente auf das Projekt haben oder wenn komplexere Refactorings geplant werden sollen.

Durch das Einführen der klaren Schnittstellen zwischen den Komponenten, ist es für uns auch sehr einfach, die Implementierung einer Komponenten zu ersetzen. Halten wir uns an ihre Spezifikation und bestehende Schnittstelle hat der Austausch keinerlei Auswirkung auf unsere Applikation, unabhängig davon welchen Umfang diese hat. Zusätzlich erlaubt uns die klare Schnittstelle, jede Komponente in Isolation gegen ihre Spezifikation zu testen, indem wir Unittests für die Schnittstelle schreiben. Sollte zu einem späteren Zeitpunkt die Implementierung ausgetauscht werden, können wir die Funktion der Komponente durch die bestehenden Tests immer sicherstellen.

Die einzelnen Komponenten unabhängig von ihrer späteren Nutzung, sondern ausschliesslich nach ihrer Spezifikation zu entwickeln, resultiert auch in einer einfach erweiterbaren Architektur. In unserem Beispielprojekt wäre ohne tiefes Verständnis der Implementierung möglich, auf Basis der bestehenden Komponenten Erweiterungen zu implementieren. Ein Beispiel für eine solche Erweiterung könnte wie folgt aussehen:

CBSE Beispielprojekt Erweiterung Komponenten

Die Grafik zeigt, wie die bestehenden Komponenten genutzt werden, um eine öffentliche API für die Kunden zu Verfügung zu stellen, damit unser Dienst z.B. über eine mobile App angeboten werden kann. Genauso wäre denkbar, eine Schnittstelle für die Kommandozeile umzusetzen, mit der wir als Betreiber des Dienstes die Kunden und Zahlungsmittel ohne Browser verwalten können. Diese Wiederverwendbarkeit kann auch dazu genutzt werden, um auf Basis existierender Komponenten eine neue Applikation mit komplett anderem Nutzen zusammenzustellen ohne tiefes wissen über Implementierungsdetails zu haben. Sollten wir eine Komponente mit der Community oder zwischen Projekten teilen wollen, können wir wieder auf das Bundle Konzept von Symfony zurückgreifen und die Komponente zusammen mit ihrer Servicedefinition in eine Bundle verschieben und über composer bereitstellen.

Wir bei LeapHub arbeiten mit dieser Art von Architektur bei der Umsetzung unserer Marketing Automation Plattform Werbelift. Ich hoffe, dass ich mit unserem Ansatz einige Symfony Entwickler dabei unterstützen kann, auch ohne den Bundle-Mechanismus ihre Projekte in eine klare Form zu bringen. Solche sauberen Komponenten in euren Projekten einzusetzen wird zwangsweise dazu führen, dass ihr auch nach langer Zeit noch Freude dabei haben werdet, die Projekte zu warten und zu erweitern.

  • Philipp Marien

    Ich finde das Konzept sehr gut. Mich würde allerdings interessieren, wie ihr in der Komponenten-Architektur Symfony Commands und Doctrine Entitäten nutzt. Liegen diese mit im AppBundle?

    • Die Commands liegen bei uns jeweils in einem Command Namespace in der Komponente zu der sie gehören und sind als Service registriert. Allgemeine Commands liegen im AppBundle, wobei es selten welche gibt, die einen Task erfüllen, der sich nicht auf eine Komponente bezieht.

      Die Doctrine Entities liegen bei uns in der Regel im Model Namespace ihrer Komponente und sind gleichzeitig die Daten-Transfer-Objekte der Komponentenschnittstelle. Das führt natürlich dazu, dass einige Daten und Implementierungsdetails über die Schnittstelle gehen, die nicht nötig wären. Auf der anderen Seite sind die Implementierungen der Komponenten dadurch etwas einfacher, da kein Mapping von Entity zu DTO und zurück stattfinden muss.

  • Konstantin Gaus

    Hi,
    ein sehr interessanter Artikel. Vielen Dank. Gibt es auf GitHub ein Beispiel-Projekt? Ich bin in Symfony ziemlich neu und mir würde ein Beispiel weiterhelfen. Danke.

    • Freut mich, dass er dir gefallen hat. Leider habe ich aktuell kein öffentliches Beispiel. Ich werde das im Hinterkopf behalten und vielleicht im Rahmen des Symfony 3.3 Releases etwas vorbereiten und ein paar Worte dazu schrieben.