Die Aufgabe
Wir haben eine große Firma vor uns mit mehreren hundert Entwicklern, die in vielen verschiedenen IT-Fachabteilungen arbeiten und dort jeweils für einen bestimmten Teilaspekt der Gesamtfachlichkeit verantwortlich sind. Jede IT-Fachabteilung soll Komponenten produzieren, die von anderen IT-Fachabteilungen verwendet werden können. Dabei sollen die Fachkomponenten in verschiedenen Kontexten und Umgebungen einsetzbar sein, beispielsweise im Online-Webumfeld oder im Batch-Umfeld. Die Nutzbarkeit soll so wenig wie möglich eingeschränkt sein, um auf zukünftige Anforderungen reagieren zu können. Ein Lock-In auf Technologien soll so weit wie möglich vermieden werden. Ein wichtiger weiterer Aspekt ist die gute Testbarkeit, die gegeben sein muss.
Wie könnte so eine technische Fachkomponentenarchitektur aussehen?
Die Lösung
Grundsätzlich besteht eine Fachkomponente aus einer öffentlichen Schnittstelle, die den Kontrakt beschreibt, den die Komponente anbietet, und einer verborgenen Implementierung.
Technisch gesehen ist der öffentliche Teil eine Sammlung von Interfaces, DTO-Klassen und Exceptions, während der verborgene Teil die Implementierungen der Interfaces enthält. Natürlich kann die Logik hier noch in beliebige weitere Sub-Komponenten aufgeteilt werden.
Um das Beispiel überschaubar zu halten, haben wir hier zwei Fachkomponenten, die jeweils nur einen Service anbieten. Das ist zum einen der PartnerService inkl. DTO:
1public interface PartnerService {
2
3 public Partner getPartner(long id);
4
5}
6
7public class Partner {
8
9 private long id;
10 private String name;
11
12 // getters and setters omitted for readability
13
14}
Und zum anderen der InkassoService inkl. DTO:
1public interface InkassoService {
2
3 public void doBooking(BookingInfo bookingInfo);
4
5}
6
7public class BookingInfo {
8
9 private long partnerId;
10 private BigDecimal amount;
11 private String subject;
12
13 // getters and setters omitted for readability
14
15}
Dies ist jeweils der öffentliche Teil der Fachkomponente. Der verborgene Teil, also die Implementierung der Services, besteht der Einfachheit halber jeweils nur aus einer Klasse:
1public class PartnerServiceImpl implements PartnerService {
2
3 @Override
4 public Partner getPartner(long id) {
5 Partner partner = null;
6 // TODO do something to get partner
7 return partner;
8 }
9
10}
Die Implementierung des InkassoServices hat eine Abhängigkeit zum PartnerService, die über den Konstruktor injiziert wird.
1public class InkassoServiceImpl implements InkassoService {
2
3 private PartnerService partnerService;
4
5 public InkassoServiceImpl(PartnerService partnerService) {
6 this.partnerService = partnerService;
7 }
8
9 @Override
10 public void doBooking(BookingInfo bookingInfo) {
11 // TODO validate bookingInfo
12 Partner partner = partnerService.getPartner(bookingInfo.getPartnerId());
13 // TODO use partner to do the booking
14 }
15
16}
Abhängigkeitsstruktur Schnittstelle und Implementierung
Für das Build- und Dependency-Management wird Maven verwendet.
Wir trennen Schnittstelle und Implementierung einer Fachkomponente in zwei eigene Projekte. Dabei hängt das Impl-Projekt immer vom eigenen Schnittstellen-Projekt ab. Ein Impl-Projekt kann aber noch von beliebig vielen weiteren Schnittstellen-Projekten abhängen. Im obigen Beispiel hängt das Impl-Projekt von Inkasso vom Schnittstellen-Projekt von Partner ab. Wichtig ist dabei, dass Impl-Projekte so nie von anderen Impl-Projekten abhängen, auch nicht transitiv, und es nicht passieren kann, dass ein Entwickler einer Fachkomponente aus Versehen Implementierungsdetails anderer Komponenten verwendet. Jede Fachkomponente definiert sich ausschließlich über die Schnittstelle, jegliche Implementierungsdetails können jederzeit ausgetauscht werden. Die Businesslogik kann einfach über Unit-Tests getestet werden.
Bisher haben wir also zwei Projekte mit POJOs, die die Businesslogik bzw. die Schnittstelle enthalten. Was jetzt noch fehlt, ist die Konfiguration, die die Komponenten per Dependency Injection verknüpft. Hierfür schlage ich die Java-basierte Konfiguration von Spring vor. Für die Partner-Fachkomponente sieht diese Konfiguration so aus:
1@Configuration
2public class PartnerConfig {
3
4 @Bean
5 public PartnerService partnerService() {
6 return new PartnerServiceImpl();
7 }
8
9}
Diese Konfiguration kommt in ein eigenes Projekt, das eine Abhängigkeit zum eigenen Impl-Projekt hat. Hier wird Konfiguration und Infrastruktur klar von der Businesslogik getrennt, so gibt es beispielsweise auch keine Abhängigkeit zu Spring im Schnittstellen- und Impl-Projekt. Die Konfiguration der Inkasso-Fachkomponente hat nun zusätzlich eine Abhängigkeit zum Konfigurationsprojekt der Partner-Fachkomponente:
1@Configuration
2@Import(PartnerConfig.class)
3public class InkassoConfig {
4
5 @Autowired
6 private PartnerConfig partnerConfig;
7
8 @Bean
9 public InkassoService inkassoService() {
10 return new InkassoServiceImpl(partnerConfig.partnerService());
11 }
12
13}
Vollständige Abhängigkeitsstruktur inkl. Konfiguration
Die PartnerConfig wird in die InkassoConfig importiert, und bei der Erzeugung des InkassoService wird die PartnerConfig genutzt, um den PartnerService zu injizieren.
Auch wenn im Javamagazin-Artikel schon einige Vorteile dieser Art der Konfiguration genannt werden, möchte ich hier noch einmal auf die wichtigsten Features insbesondere in einem verteilten Entwicklungsumfeld verweisen:
- Navigation in Spring-Konfigurationen (auch über JAR-Grenzen hinweg)
- Auffinden von Konfigurationsdateien in fremden JARs
- Herausfinden, in welchen Konfigurationen eine bestimmte Klasse oder ein bestimmtes Interface verwendet wird
Die Konfiguration ist sehr einfach nachvollziehbar, da ich mit den Standard-IDE-Mechanismen sehr einfach durch die Konfiguration navigieren kann. Im obigen Beispiel für Inkasso bin ich mit einem Klick in der Definition des PartnerService, auch wenn diese in einem eingebundenen jar liegt und nicht als Source im Workspace. Das geht mit XML nicht.
Ist die Konfigurationsdatei eine Java-Klasse, so kann sie per „Open Type“ gefunden werden, ist sie eine xml-Datei, so kann sie per „Open Resource“ nicht gefunden werden.
In Java wiederum kein Problem, auch in JARs im Classpath. Bei xml zumindest über JARs im Classpath nicht möglich.
Die explizite Konfiguration mit JavaConfig fördert die Verständlichkeit und Nachvollziehbarkeit, Schlüsselfeatures für Fehlervermeidung, Fehlerbehebung und Wartbarkeit.
Verwendung einer Fachkomponente
Wir haben jetzt also die Konfiguration für eine Fachkomponente in Form einer Spring-Java-Konfiguration vorliegen. Um die Komponente konkret zu verwenden, benötigen wir natürlich einen instanziierten ApplicationContext, in dem die Konfiguration eingebunden wird.
Was haben wir für Möglichkeiten? Einfach ist es, wenn die Anwendung, die die Fachkomponente verwenden möchte, selbst eine Spring-Anwendung ist, dann kann die Konfiguration einfach dort eingebunden werden. Um beispielsweise die Inkasso-Fachkomponente einzubinden, muss nur die InkassoConfig-Klasse in den bereits bestehenden ApplicationContext eingebunden werden. Alle abhängigen Konfigurationen werden automatisch durch die InkassoConfig importiert.
Ist das nicht der Fall, brauchen wir eine Infrastruktureinheit, die den ApplicationContext verwaltet und die Services nach außen anbietet. Das kann beispielsweise eine Web-Anwendung sein, die die Services als Rest-Webservices anbietet. Das kann eine EJB sein, an die der ApplicationContext gehangen wird. Es kann eine Anwendung sein, die auf eine Queue horcht und Anfragen von dort verarbeitet. Und zuletzt kann es natürlich auch ein statischer Service-Locator sein, der den ApplicationContext intern hält.
Fazit
Die beschriebene Fachkomponentenarchitektur teilt die für eine Fachkomponente notwendigen Bestandteile in drei Projekte auf:
– ein Schnittstellen-Projekt
– ein Implementierungs-Projekt
– ein Konfigurations-Projekt
Durch die erlaubten Abhängigkeiten zwischen den Projekten erreichen wir einerseits eine Trennung von öffentlicher Schnittstelle und interner Implementierung und andererseits eine Trennung von Businesslogik und Infrastrukturcode. Die Verwendung von expliziter, Java-basierter Konfiguration von Spring ermöglicht eine leichte Handhabung in jeder Entwicklungsumgebung sowie eine leichte Nachvollziehbarkeit und Verständlichkeit, wodurch eine einfache Wartbarkeit gegeben wird. Durch die konsequente Anwendung von Dependency Injection erreichen wir eine leichte Testbarkeit. Durch die Tatsache, dass Impl-Projekte keine anderen Impl-Projekte referenzieren dürfen, wird die Anwendung von Dependency Injection forciert. Last, but not least: die Fachkomponente benötigt keine bestimmte Laufzeitumgebung und kann so in verschiedensten technischen und fachlichen Kontexten verwendet werden.
Ausblick
Natürlich bleiben noch einige Fragen offen, beispielsweise der Umgang mit Properties, der Umgang mit Ressourcen und der Umgang mit umgebungsabhängigen Konfigurationen. Hier bietet Spring 3.1 mit der Environment Abstraction ganz neue Möglichkeiten, die ich in den folgenden Blog-Einträgen behandele:
Eine Fachkomponentenarchitektur mit Spring 3.0/3.1 – Part Two: Ressourcen
Eine Fachkomponentenarchitektur mit Spring 3.0/3.1 – Teil 3: Properties
Zum Abschluss noch ein Wort zum Thema explizite vs. implizite Konfiguration
Definition explizite Konfiguration: Dependency Injection zwischen Komponenten wird explizit durch XML-Snippets oder Java-Code konfiguriert.
Definition implizite Konfiguration: Dependency Injection zwischen Komponenten läuft entweder über Konventionen oder Classpath-Scanning und Autowiring mit Hilfe von Annotationen.
Was bedeutet explizite / implizite Konfiguration?
Convention over Configuration ist in aller Munde, und über all dem XML-Bashing der letzten Jahre ist die explizite Konfiguration von Anwendungen ziemlich uncool geworden. Trotzdem stelle ich in diesem Blog-Eintrag ein Konzept vor, in dem explizite Konfiguration eine zentrale Rolle spielt. Warum?
- Die Voraussetzungen
- Explizite Konfiguration ist nicht gleichbedeutend mit XML
- Das hier ist Enterprise, Coolness ist nicht wichtig
Wir haben hier Hunderte Stakeholder, von anderen IT-Fachabteilungen über zentrale Architekturabteilungen bis hin zum Betrieb. Die Konfiguration der Anwendung MUSS verständlich und nachvollziehbar sein. Und eine explizite Konfiguration ist nun einmal leichter nachvollziehbar als das automatische Scannen und Instanziieren von Komponenten, die im Classpath liegen. Und mal ehrlich, wieviel Zeit kostet es uns, für eine erstellte Komponente eine Konfiguration zu erstellen? Zwei Minuten?
Genau genommen wird in meinem Konzept gar kein XML verwendet, da die Spring-JavaConfig erhebliche Vorteile gegenüber XML hat. Ganz ehrlich: müsste ich explizite Konfiguration mit XML machen, dann würde ich es nicht machen.
Ich stelle das Konzept nicht vor, weil ich denke, dass es cool und hip ist, sondern weil ich denke, dass es funktioniert. Und das ist immer noch das Wichtigste bei der Softwareentwicklung.
Weitere Beiträge
von Tobias Flohre
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Tobias Flohre
Senior Software Developer
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.