Als Software-Architektur bezeichnen wir ein abstraktes Modell, aus dem insbesondere eine strukturierte und hierarchische Anordnung der Bauteile eines Anwendungssystems und eine Beschreibung der Beziehungen dieser Teile untereinander hervorgeht [Bal11]. Mit dem Begriff Architektur (lat. architectura, Baukunst) verbinden wir zunächst allgemein die Gestaltung von Raum, was in der Praxis häufig dem Entwurf von Bauwerken entspricht. Durch den Architekturentwurf entsteht ein Modell, das Vorgaben für die spätere Konstruktion des Bauwerks macht. In der Softwaretechnik wird der Architekturbegriff mit gleicher Semantik genutzt, wobei die Bauteile entweder generisch als Komponenten (z.B. in der UML) oder als Module (z.B. in Java) bezeichnet werden. Es folgt eine entsprechende Definition der IEEE.

"Architecture is the fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution." (ISO/IEC/IEEE 42010:2011)

Prinzipien des Software Engineering

Der Zweck einer Software-Architektur ist an den allgemeinen Prinzipien des Software Engineering ausgerichtet. Zu diesen Prinzipien zählen nach Ghezzi et al. [GJM03]:

Komponentenbasierte Entwicklung

Komponentenbasierte Entwicklung drückt das Paradigma aus, dass Software Engineering zu einer echten Ingenieurwissenschaft macht. Die zentrale Idee besteht in dem oben genannten Prinzip der Modularisierung, d.h. der hierarchischen Aufteilung des Systems in seine Komponenten, und der damit verbundenen Wiederverwendbarkeit von Komponenten in anderen Projekten. Damit entsteht Analogie zu klassischen Ingenieurdisziplinen, in denen es üblich ist, zuvor geprüfte/zugelassene Bauteile und bewährte Verfahren wiederzuverwenden. Für die Wiederverwendbarkeit ist es wichtig, dass jede Komponente möglichst einen klar definierten Aspekt implementiert und kapselt. Es kann mehrere Komponenten geben, die demselben inhaltlichen Zweck dienen. Wenn für diese Komponenten eine gemeinsame Schnittstelle spezifiziert ist, sind sie als alternative Implementierungen leicht austauschbar. Die Verknüpfung von mehreren Basiskomponenten zu einer zusammengesetzten Komponente oder schließlich zu einem Gesamtsystem bleibt eine architektonische Aufgabe, in der sich das Entwurfsmuster Kompositum wiederfindet.

Es stellt sich nun die Frage, was genau eine Software-Komponente ausmacht. Dazu wird folgend eine verbreitete Definition von Clemens Szyperski et al. [SGM02] zitiert und diskutiert.

"A software component is a unit of composition with contractually specified interfaces and explicit context dependencies only. A software component can be deployed independently and is subject to composition by third parties." (Clemens Szyperski et al.)

Komponentenmodelle

Im Rahmen der komponentenbasierten Entwicklung werden wir uns in der Praxis für ein spezifisches Komponentenmodell entscheiden. Ein Komponentenmodell spezifiziert, wie Komponenten und deren Komposition textuell (z.B. in einer Programmiersprache) oder grafisch (z.B. in UML-Notation) abgebildet werden. Folgende Definition fasst zusammen, was wir von einem Komponentenmodell erwarten können [LW07].

" A software component model is a definition of (1) the semantics of components, that is, what components are meant to be, (2) the syntax of components, that is, how they are defined, constructed, and represented, and (3) the composition of components, that is, how they are composed or assembled." (Kung-Kiu Lau et al.)

Aktuelle Komponentenmodelle sind z.B. Microsoft .NET, Java Platform Module System (JPMS), OSGi und Spring. Zu frühen Komponentenmodellen, die heute veraltet sind, gehören Enterprise JavaBeans (EJBs), OMG CORBA Component Model (CCM) und Microsoft COM. Auch die UML bietet ein Komponentenmodell an, das nicht wie die vorherigen auf eine konkrete Implementierung sondern auf die Spezifikation, Dokumentation und Kommunikation eines Architekturentwurfs ausgerichtet ist. Die folgende Abbildung zeigt wie Komponenten in einem UML-Komponentendiagramm dargestellt werden.

Eine Komponente kann in der UML durch andere Teilkomponenten oder durch Klassen realisiert werden. Beides sind im Sinne der UML sogenannte Classifier. In der obigen Abbildung wird die Komponente Provider durch die Teilkomponenten PartA und PartB realisiert, wobei PartA wiederum durch die Klassen ClassA1 und ClassA2 realisiert wird. Weitere Realisierungen sind nicht angegeben. Die Komponente Provider wird manifestiert durch das ausführbare Artefakt provider.jar. Das Komponentendiagramm enthält keine Information darüber, in welcher Laufzeitumgebung und auf welchem Host dieses Artefakt bereitgestellt wird. Diese Information würde in der Sicht eines zum Gesamtmodell zugehörigen Verteilungsdiagramms dargestellt werden. Die Komponente Provider bietet nach außen die Schnittstelle ProvidedInterface an, die von der Komponente Client genutzt wird. Intern wird diese Schnittstelle in der Teilkomponente PartA realisiert. Eine anschauliche Notationsbeschreibung des UML-Komponentendiagramms findet sich in der Dokumentation zu Microsoft Visual Studio sowie in den UML-Lehrbüchern, die im Kapitel Objektorientierung und UML referenziert werden.

Java Platform Module System (JPMS)

Im Gegensatz zur UML erlaubt es das Modulsystem in Java (JPMS) nicht, dass Komponenten hierarchisch ineinander verschachtelt werden. In Java können lediglich Packages als Realisierung der Module wie gewohnt hierarchisch untergliedert werden. Auf Ebene der Module können Abhängigkeiten explizit definiert werden (requires <module>). Die Sichtbarkeit der modulinternen Realisierung kann ebenso explizit nach außen freigegeben werden (export <package>).

Das folgende Code-Beispiel zeigt die beiden Java-Module client und simpleRegression, wobei letzteres eine einfache lineare Regression implementiert. Als Input für das Regressionsmodell dient eine Datenreihe mit 2D-Koordinaten im JSON-Format, d.h. zusammengehörige x- und y-Werte (Zeilen 13-14). Das Modul client ist ausschließlich vom Modul simpleRegression abhängig, dessen Abhängigkeiten ihm aber verborgen bleiben (Zeile 3). Es ist damit weitgehend entkoppelt. Der Zugriff auf das Package regression ist nur möglich, weil es explizit exportiert wird (Zeile 7 im Modul simpleRegression). Das Modul simpleRegression ist von folgenden Modulen abhängig: commons.math3 zum Trainieren des Regressionsmodells, gson zum Konvertieren von JSON in Java Objekte und java.desktop aufgrund der Verwendung der Klasse Point für 2D-Koordinaten (Zeilen 3-5). Weiterhin ergibt sich durch das Modul gson eine transitive Abhängigkeit zum Modul java.sql (Zeile 6).

// module-info.java
module client {
    requires simpleRegression;
}

// Client.java
package client;
import regression.SimpleRegressionModel;

class Client {

    public static void main(String[] args) {
        String json = "[{'x': 2, 'y': 4}, {'x': 4, 'y': 3}, {'x': 3, 'y': 6}, {'x': 9, 'y': 7}, {'x': 7, 'y': 8}]";
        SimpleRegressionModel model = new SimpleRegressionModel(json);

        System.out.println("Regression function: y = f(x) = " + model.getSlope() + "x + " + model.getIntercept());
        System.out.println("R² = " + model.getDeterminationCoefficient());

        double x = 5;
        System.out.printf("Prediction: y = f(%.1f) = %.1f", x, model.predict(x));
    }
}
// module-info.java
module simpleRegression {
    requires gson;
    requires commons.math3;
    requires java.desktop;
    requires transitive java.sql; // gson depends on java.sql
    exports regression; // exports package
}

// SimpleRegressionModel.java
package regression;
import com.google.gson.Gson;
import org.apache.commons.math3.stat.regression.SimpleRegression;
import java.awt.Point;

public class SimpleRegressionModel {

    Gson gson = new Gson(); // dependency to Google GSON
    SimpleRegression model = new SimpleRegression(); // dependency to Apache Commons Math

    public SimpleRegressionModel(String json) {
        Point[] points = gson.fromJson(json, Point[].class); // dependency to Java Desktop (Point class)
        for (Point p : points) model.addData(p.x, p.y);
    }

    public double getIntercept() { return model.getIntercept(); }

    public double getSlope() { return model.getSlope(); }

    public double getDeterminationCoefficient() { return model.getRSquare(); }

    public double predict(double x) { return model.predict(x); }
}

Die gezeigten Code-Beispiele zu JPMS finden sich im Verzeichnis /architecture/jpms des Modul-Repository. Das folgende UML-Komponentendiagramm veranschaulicht die Abhängigkeiten der Module aus dem obigen Code-Beispiel. Es wird deutlich, wie das JPMS dazu beiträgt, Abhängigkeiten zwischen Komponenten explizit zu gestalten und ggf. einzuschränken.

Kapselung auf Ebene von Klassen

Bisher ist nur über Modularisierung und Kapselung auf Ebene von Komponenten gesprochen worden. Im Feinentwurf werden die Komponenten durch Klassen realisiert, die ebenfalls interne Daten- und Verhaltensstrukturen in Form von privaten Attributen und Methoden voreinander verstecken können. Dazu dienen in objektorientierten Sprachen insbesondere die Sicherbarkeitsmodifizierer private und protected. Wenn auf eine Klasse regelmäßig von außen zugegriffen wird, ist eine stabile Schnittstelle wichtig. Der Zugriff kann in diesem Fall sinnvoll über Getter- und Setter-Methoden gekapselt werden, um die zugrundeliegenden internen Strukturen verändern zu können, ohne den Zugriff von außen zu beeinflussen. Grundsätzlich stellt sich die Frage, vor welchen äußeren Zugriffen die internen Strukturen gekapselt werden sollen.

Im letzteren Fall kann der Aufwand, eine Veränderung der Schnittstelle durchzusetzen, sehr hoch sein, weil die externen Anwender etwaige Änderungen aus Trägheit ablehnen und ggf. als Anwender abspringen, oder weil sie aufgrund ihrer Vielzahl gar nicht vollständig bekannt sind, usw. Es werden aber auch viele Klassen entwickelt, die nur teamintern für eine kleine Anwendung oder ein kleines Modul genutzt und nicht nach außen über eine API weitergegeben werden. Wenn die Anzahl der Zugriffe auf die Strukturen einer Klasse übersichtlich ist und relativ einfach per Refactoring auch an den abhängigen Stellen geändert werden kann, ist zu überlegen, ob Getter und Setter tatsächlich erforderlich sind. Der strukturelle Aufbau der Klasse soll hier weiterhin einfach bleiben und nicht die Narben der Versionsgeschichte der Klasse abbilden. Keep it simple! Getter und Setter bleiben stets sinnvoll, wenn sie zusätzliche Funktionalitäten übernehmen und diese vom Aufrufer auch erwartet werden. Sichtbarkeit kann seit Java 9 aber z.T. sinnvoller auf Modulebene als auf Klassenebene eingeschränkt werden.

Das Projekt Lombok ermöglicht es in Java, Getter und Setter ohne weitere Funktionalität, die lediglich Boilerplate-Code darstellen, über die Verwendung von Annotationen wie @Getter und @Setter zur Kompilierungszeit zu generieren. In der verwendeten Entwicklungsumgebung muss ein entsprechendes Plugin (s. hier für IntelliJ IDEA) installiert werden, damit die Lombok-Annotationen auch während der Entwicklung aufgelöst werden können.