Das Konzept der Softwareentwicklung mit Komponenten entstand in den 1990er Jahren. Dies geschah vor dem Hintergrund zunehmender Softwarekomplexität und der Forderung nach Wiederverwendbarkeit von Software. Die grundsätzliche Problemstellung hat sich bis heute nicht verändert und stellt sich auch für Software in Embedded Systems.
Der folgende Artikel ist die praktische Anwendung eines Komponentenkonzepts, das in einem separaten Artikel [1] eingeführt wurde. Es ist sehr empfehlenswert mit dem ersten Teil zu beginnen, da er die Motivation für eine Komponentenarchitektur behandelt. In diesem Artikel beschreibe ich den Entwurf einer komponentenbasierten Embedded Software anhand eines Fallbeispiels und zeige, wie die Software mit dem Builder Pattern initialisiert werden kann.
Das Fallbeispiel
Eingebettete Systeme sind in unserem Alltag allgegenwärtig. So habe ich als anschauliches Fallbeispiel einen Heizkörperthermostat (im Folgenden Thermostat genannt) gewählt, der uns täglich begegnet. Der Thermostat kann sowohl autark einzelne Heizkörper regeln, als auch in einem System mit weiteren Heizkörpern und einem Raumthermostat betrieben werden.
Abbildung 1: Temperatur-Regelsystemen mit 3 Heizkörpern, Fußbodenheizung und Raumthermostat
Thermostat Systemübersicht
Systemziel: Der Thermostat steuert einen Heizkörper, um die Raumtemperatur zu regeln.
Kernfunktionen sind:
- Regelung der Raumtemperatur
- Manuelle Einstellung der Solltemperatur
- Programmierbare Temperaturregelung (Tagesprogramm)
- Schutzfunktionen für Ventil und Heizkörper
- Regelung des Heizkörpers über einem optionalen Raumthermostat
- Einfache Einrichtung und Bedienung über Smartphone-App
- Kann für Entwicklungszwecke unabhängig von der Thermostat-Hardware betrieben werden (Hardware-Simulation)
Fachlicher Systemkontext
Abbildung 2: Fachliches Systemkontextdiagramm des Thermostats
Kernfunktion „Regelung der Raumtemperatur“
Bei der Raumtemperaturregelung wird der Heizkörper so gesteuert, dass die gewünschte Raumtemperatur erreicht wird. Es hängt davon ab, ob der Thermostat allein oder zusammen mit einem Raumthermostat betrieben wird. Ein Raumthermostat kann die Raumtemperatur genauer bestimmen, da er von jedem Thermostat die lokale Temperatur erhält.
Abbildung 3: Use Case zur Regelung der Raumtemperatur
Architekturentscheidung Komponentenarchitektur
Die Entscheidung für eine Komponentenarchitektur ist Teil der Architekturarbeit und sollte nicht aus dem Bauch heraus getroffen werden. Ich möchte hier keine Architekturentscheidung beschreiben, sondern lediglich Entscheidungsgrundlagen liefern, die eine Rolle spielen könnten. Es ist denkbar, dass die folgenden Funktionalitäten sowohl in der Thermostat-Software als auch auf dem Raumthermostat verwendet werden:
- Erstellung von Programmen zur Temperaturregelung
- Bedienbarkeit mit Smartphone
- HMI-Schnittstelle
Die Wiederverwendung von Komponenten ist aber nur ein Aspekt. Neben den Qualitätsmerkmalen, die ich in [1] angesprochen habe, möchte ich noch einmal auf die Modularisierung eingehen. Im Kern geht es immer noch darum, Wege gegen den „Big Ball of Mud“ zu finden. Carola Lilienthal hat in ihrem Buch [2] sehr ausführlich beschrieben, wie Modularisierung dabei helfen kann. Auch die Langlebigkeit, also über welchen Zeitraum die Software gewartet werden muss, ist hier ein wichtiger Aspekt.
Bausteinsicht
Das folgende Diagramm zeigt eine mögliche Bausteinansicht der ersten Ebene. Auf die Darstellung der Beziehungen zwischen den Komponenten habe ich verzichtet. Auf der obersten Ebene ergibt sich bereits ein klares Bild, wie die Software die Anforderungen umsetzt.
Abbildung 4: Fachliche Komponenten der Thermostat Software
An dieser Stelle noch ein Wort an den agilen Architekten. Es ist extrem wichtig, die Architekturarbeit der Dekomposition gemeinsam mit dem Entwicklungsteam zu machen. Denn die Architektur muss von allen verstanden und gemeinsam getragen werden. Um dies zu erreichen, ist es sehr hilfreich das Entwicklungsteam von Anfang an in den Designprozess mit einzubeziehen. Gelingt dies nicht, besteht die Gefahr, dass die Software-Architektur zum „Papiertiger“ wird und sich technische Schulden einschleichen. Der Weg zum „Big Ball of Mud“ ist dann vorgezeichnet.
Das notwendige Verständnis im Team gilt natürlich auch für das Komponentenkonzept, auf dessen Anwendung ich nun eingehen werde.
Komponentenschnittstellen unter der Lupe
Es folgt ein Ausschnitt aus der Komponentenarchitektur, die den Anwendungsfall „Regelung der Raumtemperatur“ realisiert. Die Komponente RoomTemp benötigt das Interface IRoomTemp, die im Standalone-Betrieb von der Komponente Temperature oder im Betrieb mit einem Raumthermostat von der Komponente RoomThermostat bereitgestellt werden kann. Technisch gesehen ist es eine Frage der Verknüpfung von angebotenem und benötigtem Interface. Dies kann je nach Systemanforderungen zu unterschiedlichen Zeitpunkten erfolgen. Im Folgenden wird vom Zeitpunkt der Initialisierung ausgegangen.
Abbildung 5: Ausschnitt aus dem Komponentenmodell des Thermostats
Software Design Pattern zur Initialisierung einer Komponentenarchitektur
Die Initialisierung eines eingebetteten Systems ist eine wichtige Aufgabe, die besondere Sorgfalt erfordert. Dabei wird die Hardware initialisiert und für die Anwendungssoftware vorbereitet. Die Anwendungssoftware initialisiert ihre Komponenten und ermöglicht so die Verarbeitung von Ereignissen an den Systemgrenzen, um den Zweck des Systems zu erfüllen.
Bei der Initialisierung einer komponentenbasierten Anwendung steht man nun vor der Herausforderung, eine Hierarchie von Objekten zu erstellen und die Abhängigkeiten untereinander aufzulösen. Dies geschieht auf zwei Ebenen. Zum einen auf der Ebene der Komponenten und zum anderen innerhalb der Komponenten.
In einer gut entworfenen Softwarearchitektur gehen die Abhängigkeiten von einer höheren Abstraktionsebene zu einer niedrigeren Abstraktionsebene, ohne dass zyklische Abhängigkeiten entstehen. In einem Embedded System ist die Hardware die unterste Schicht, die von der HAL-Software-Schicht angesprochen wird. Die Komponenten sind fachliche Komponenten, die Zugriffe auf die Hardware enthalten können. Fachliche Komponenten können eine Struktur haben, die technische Schichten (Applikation, Service, HAL) enthält. Die folgende Abbildung veranschaulicht dies:
Abbildung 6: Beispielhaftes Objektmodell einer komponentenbasierten Embedded-Applikation
Beim Auflösen der Abhängigkeit muss zuerst das unabhängige Element erzeugt und dann mit dem abhängigen Element verbunden werden. In der Darstellung der Abhängigkeit ist das unabhängige Element dasjenige, auf das die Pfeilspitze zeigt. Auf Komponentenebene sind dies die abstrakten Schnittstellen. Diese werden innerhalb der Komponenten realisiert (siehe Interface A). Die Verknüpfung der benötigten und angebotenen Schnittstellen wird durch Dependency Injection erreicht. Um alle Abhängigkeiten im Komponentenmodell aufzulösen, müssen also zunächst in allen Komponenten Objekte erzeugt werden, die die angebotenen Schnittstellen realisieren.
Neben dieser Anforderung gibt es in einer Embedded-Anwendung weitere technische Abhängigkeiten von Komponenten, die zur Initialisierungszeitpunkt aufgelöst werden müssen. Dies sind z.B. Geräteparameter oder die Initialisierung von Nebenläufigkeiten, die das Gesamtsystem benötigt.
Das Builder Pattern
Das Pattern wurde in dem Buch „Design Patterns“ der GoF veröffentlicht. An dieser Stelle möchte ich das Pattern nicht im Detail vorstellen. Eine gute Einführung findet sich in [3]. Letzen Endes geht es bei dem Pattern darum das Erzeugen komplexer Objekte an eine Builderklasse zu deligieren. Dieses komplexe Objekt ist in unserem Fall die komponentenbasierte Anwendung.
Neben dem Auflösen der Komponentenabhängigkeiten können auch Anforderungen umgesetzt werden, die sich auf die Initialisierung des Objektmodells in den Komponenten auswirken. Damit können Anforderungen realisiert werden, die sich zur Laufzeit nicht mehr ändern. In unserem Fall sind dies
- Betrieb mit/ohne einem Raumthermostat
- Betrieb mit simulierter Hardware
Das folgende Strukturdiagramm zeigt die Anwendung des Builder Pattern im Kontext des Thermostats. Es ist nur ein Ausschnitt dargestellt das ausreicht, um das Prinzip des Patterns zu verdeutlichen.
Abbildung 7: UML Klassendiagramm des Builder Patterns für die Applikation des Heizungsthermostats
Die folgende Tabelle erläutert die Zuständigkeiten der Klassen und Schnittstellen für die Initialisierung der Anwendung
Klasse | Rolle beimBuilder Pattern |
---|---|
ThermostatApplication | Client des konkreten Builders.
In unserem Anwendungsfall weiß die Applikation, ob der Thermostat mit einem Raumthermostat betrieben wird. Sie verwendet dann die entsprechenden Builder-Klassen, um die Komponenten zu initialisieren. |
IComponentBuilder | Deklariert die Schritte, die für die Initialisierung der Komponenten notwendig sind. Es ist zu beachten, dass diese Schritte unabhängig von den spezifischen Komponenten sind, sondern eher eine Abfolge oder ganz spezifische Aspekte darstellen. Über dieses Interface können übergreifende Aspekte von Komponenten realisiert werden. Diese sind: – Auflösen der Komponentenabhängigkeiten – Finalisieren der Komponente (ready to use) |
IComponent | Allgemeines Interface aller Komponenten. |
RoomThermostatBuilder | Realisiert das Interface IComponentBuilder. Hat das Wissen darüber welche Komponenten für einen Raumthermostat benötigt werden und wie diese verknüpft werden. |
HeaterTempControlBuilder | Realisiert das Interface IComponentBuilder. Hat das Wissen darüber welche Komponenten für einen „standalone“ Heizungsthermostat benötigt werden und wie diese verknüpft werden. |
TemperatureComponent, HeaterTempComponent, … | Fachliche Komponenten für die jeweilige Funktionalität. |
Tabelle 1: Verantwortung der Klassen beim Builder Pattern
Das folgende Sequenzdiagramm zeigt den Ablauf der Initialisierung der Komponenten für die Applikation des Heizungsthermostats mit einem Raumthermostat.
Im Schritt satisfyDespendencies werden die Abhängigkeiten aufgelöst. Im Schritt finalize wird die Initialisierung der Komponente abgeschlossen und alle notwendigen Aktivitäten durchgeführt, damit die Komponente ihre Aufgabe erfüllen kann.
Abbildung 8: UML Sequenzdiagramm zum Initialisieren der Komponenten
Vor der Implementierung des Patterns muss noch die Ownership zwischen Architekt und Team geklärt werden. Es ist wichtig, dass dies im Rahmen des Komponentenkonzepts einheitlich festgelegt wird. Um Komponenten möglichst unabhängig entwickeln zu können, sind „self-contained components“ hilfreich. Das bedeutet, dass die Komponentenklassen die ownership an allen internen Objekten haben und die angebotenen Interface per Pointer nach außen gegeben werden.
Information hiding in der Komponentenklasse mit Pimpl
Abschließend noch ein technisches Detail und ein Praxistipp, das bei der Deklaration von Komponentenklassen zu beachten ist. Diese Klasse ist die primären Schnittstellenklasse zur Integration von Komponenten in eine Gesamtsoftware. Es ist sehr sinnvoll die interne Realisierung der Interfaces komplett vor dem Verwender zu verbergen. Dies kann durch Anwendung des in [4] beschriebene Pimpl-Idiom leicht erreicht werden.
Fazit
Wie in [1] beschrieben, erfolgt die Modularisierung von Software auf mehreren Abstraktionsebenen der Software. Die Verwendung von fachlichen Komponenten auf der ersten Ebene führt zu einer leicht verständlichen Strukturierung der Software. Interfaces ermöglichen das Zusammenspiel von Komponenten unabhängig von deren Implementierung, was zu einer sehr flexiblen Verwendung von Komponenten führt. Das Wissen um die Verbindung zwischen angebotenem und benötigtem Interface kann mit Hilfe des Builder Patterns realisiert werden. Dies ermöglicht die Variation von Komponenten für den Einsatz auf unterschiedlichen Geräten oder in unterschiedlichen Systemkontexten.
Ich hoffe, dass ich mit dem Anwendungsbeispiel die Umsetzung des Komponentenkonzepts so anschaulich darstellen konnte, dass Entwickler / Architekten erfolgreich mit langlebigen Komponentenarchitekturen starten können.
Alexander Eisenhuth arbeitet seit 1996 in der Softwareentwicklung. Seit 10 Jahren arbeitet er hauptsächlich als Softwarearchitekt in agil organisierten Teams. Mit seinem Mentoring-Programm unterstützt er Softwarearchitekten bei den Herausforderungen ihrer Rolle.
Mehr über das 1:1 Mentoring: www.eisenhuth-se.de
[1] Konzept Komponentenarchitektur für Embedded-Software
[2] Langlebige Software-Architekturen, Carola Lilienthal, 2020 dpunkt.verlag
[3] Builder Pattern, Refactoring Guru, https://refactoring.guru/design-patterns/builder
[4] Vor- und Nachteile des d-Zeiger-Idioms, Marc Mutz, link