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 als Einführung in eine Softwarearchitektur mit Komponenten für eine Embedded Software gedacht. Er zeigt was die Ziele einer solchen Architektur sein können und kann als Grundlage für ein Komponentenkonzept benutzt werden. In einem weiteren Artikel, der online veröffentlicht wird, werde ich das Konzept anhand eines Beispiels anwenden.
Warum Komponenten?
Jeder kennt den Ausdruck „big ball of mud“. Vielleicht hat ihn der eine oder andere schon einmal vor sich auf dem Bildschirm gesehen. Gemeint ist Quellcode, der sich nur schwer erweitern, testen oder analysieren lässt. Die Implementierung einer scheinbar kleinen Funktionalität zieht überraschend viele Änderungen im Quellcode nach sich. Die Anpassung der Unittests ist zeitaufwändig und bei der Integration in die gesamte Software treten unerwartete Seiteneffekte auf.
Es gibt viele Möglichkeiten, wie diese Situation in der Software entstehen konnte. Der Faktor Zeit ist einer, der sehr wahrscheinlich eine Rolle spielt. Ich wage zu behaupten, dass auch fehlende oder unangemessene Architekturarbeit zu diesem Zustand geführt hat. Was dem „ball of mud“ in diesem Moment fehlt, ist eine passende fachliche Strukturierung, die aus einer durchdachten Entwurfsarbeit resultiert.
In einer gut strukturierten Software sind notwendige Erweiterungen lokal begrenzt, was nicht bedeuten muss, dass es nur eine Stelle gibt, die an neue Anforderungen angepasst werden muss. Die beteiligten Stellen haben die Eigenschaft, gut erweiterbar zu sein.
Aber nicht immer ist eine Glaskugel zur Hand, wenn man die Struktur einer Software entwirft. Neue Anforderungen sind nicht vorhersehbar und die Struktur muss angepasst werden. Man spricht hier von „evolutionärer Software“, die sich ständig an Veränderungen anpasst.
Der Vorteil eines Embedded Systems gegenüber einem Informationssystem ist, dass sich der Zweck und die Rahmenbedingungen selten so stark ändern, dass eine Umstrukturierung notwendig wird.
Divide and Conquer
Entwirft man eine Software mit Komponenten, zerlegt man die Gesamtfunktionalität in kleinere Einheiten. Jede dieser Komponenten hat eine genau definierte Schnittstelle, über die ihre Funktionalität genutzt werden kann. Das Konzept der Bildung funktionaler Einheiten, auch Module genannt, ist auf allen Abstraktionsebenen der Software anwendbar. Das Prinzip der Modularisierung ist inzwischen über 50 Jahre alt, aber unverzichtbar, um die komplexe Software beherrschbar zu machen.
Ist ein Leben ohne Komponenten denkbar?
Modularisierung gibt es nicht zum Nulltarif. Das Entwerfen, Erstellen und Implementieren von Schnittstellen erfordern einen gewissen Aufwand. Letztendlich hängt es von der Komplexität des eingebetteten Systems und den gewünschten Qualitätszielen ab, ob eine Embedded Software aus fachlichen Komponenten aufgebaut werden sollte. Hier gilt der Grundsatz: Die Architektur einer Software muss letztlich der Problemstellung angemessen sein.
Auf die Qualitätsziele kommt es an!
Ein Unternehmen, das die Qualitätsziele seiner Embedded Software kennt, ist in der Lage, die Frage zu beantworten, ob eine komponentenorientierte Softwarearchitektur sinnvoll ist. Die folgende Tabelle gibt meine Erfahrungen wieder, welche Qualitätsziele eine modularisierte Softwarearchitektur unterstützt. Die Qualitätsziele sind auch im Arc42 Quality Model unter der Systemeigenschaft #flexible zu finden.
Abbildung 1: Das arc42 Qualitätsmodell (https://quality.arc42.org/)
Qualitätsziel | Möglich Bedeutung im Projektkontext |
---|---|
Anpassbarkeit, Parametrierbarkeit | Hardwarevarianten und Produktlinien können mit einem Variantenkonzept gut als querschnittliche Schnittstelle in eine modularisierte Software integriert werden. |
Erweiterbarkeit | In einer klar strukturierten Software sind Erweiterungen besser planbar, da Änderungen einzelnen Komponenten zugeordnet werden können. Der Aufwand ist mit geringerem Risiko abschätzbar. Die Gliederung in Komponenten, Schnittstellen und deren interne Realisierung unterstützt die Klarheit der Verantwortlichkeiten von Entwickler- und Architektenrolle. Komponenten können besser extern entwickelt werden. |
Änderbarkeit | Abstrakte Schnittstellen und deren Realisierung sind „beweglicher“ und können leichter in andere Komponenten verschoben werden. |
Testbarkeit | Unittests können sehr spezifisch realisiert werden. Benötigte Schnittstellen können durch Mocks ersetzt werden. Bei Änderungen können diese Tests leicht angepasst werden. |
Verständlichkeit | Neue Mitarbeiter sind schneller produktiv. Geringere kognitive Belastung bei der Entwicklung. Technische Schulden sind weniger wahrscheinlich. |
Wiederverwendbarkeit | Komponenten können ohne direkte Systemabhängigkeit entworfen werden und sind somit leicht im Produktportfolio mehrfach einsetzbar. |
Sicherheit | Für funktional sichere Software wird in der Safety-Norm Modularisierung und lose Kopplung ab einem bestimmten Sicherheitsniveau „highly recommended“, d.h. diese Designprinzipien müssen nachweislich angewendet werden. |
Tabelle 1: Qualitätsziele die durch Modularisierung unterstützt werden
Ein Komponentenkonzept
Nachdem ich bisher die Motivation für eine Komponentenarchitektur beleuchtet habe, möchte ich mich nun den Entwurfsprinzipien und Eigenschaften von Komponenten beschreiben.
Eine wünschenswerte Eigenschaft einer Komponente ist eine möglichst hohe Kohäsion. Darunter versteht man eine enge Beziehung zwischen den Schnittstellenfunktionen und den internen Daten. Vermischt man mehrere Verantwortlichkeiten nimmt die Kohäsion ab. Ändern sich die Anforderungen, führt dies zu lokalen Änderungen.
Durch „Information Hiding“ wird nach außen nur das angeboten, was für die Nutzung notwendig ist. Werden Daten außerhalb der Komponente benötigt, ist es wichtig, diese nur „read only“ anzubieten.
Auf die Schnittstelle kommt es an
Wenn von der Schnittstelle einer Komponente die Rede ist, sind damit die angebotenen und die benötigten Schnittstellen gemeint. Benötigte Schnittstellen werden von anderen Komponenten zur Verfügung gestellt. Die Kopplung zwischen zwei Komponenten sollte so lose wie möglich sein. Dies wird durch die Schnittstellenparameter beeinflusst. Müssen Interna der beteiligten Komponenten über die Schnittstelle transportiert werden, sollte dies mit abstrakten Datentypen erfolgen. Werden Daten über eine Schnittstelle transportiert, sollte dies nur über Standarddatentypen der Programmiersprache atomar oder als POD erfolgen.
Das Prinzip des „Law of Demeter“ besagt, dass eine Komponente nur mit ihren unmittelbaren Nachbarn interagieren sollte und die Schnittstellen des Nachbarn keine Abhängigkeiten zu weiteren Komponenten haben sollten. Dies ist ein weiterer Beitrag zur losen Kopplung.
Schnittstellen sollten durch Verträge beschrieben werden. Darin werden alle Informationen zur Nutzung der Schnittstelle dokumentiert. Hilfreich für die Dokumentation von Schnittstellen ist die Verwendung von Formaten wie Doxygen, mit denen eine verlinkbare Dokumentation erzeugt werden kann. Auf diese Weise kann der Vertrag in die Architekturspezifikation eingebunden werden.
Die Benennung der Schnittstellen sollte mit Sorgfalt erfolgen. Der Name beschreibt die Fachlichkeit der enthaltenen Methoden aus der Sicht des Anbieters. Nach dem ersten Entwurf ist ein Feedback des Entwicklungsteams sehr wichtig.
Beim Entwurf der Schnittstellen ist das Prinzip der „Separation of Concerns“ zu beachten. Eine klare Trennung der Verantwortlichkeiten einzelner Schnittstellen führt zu einer höheren Stabilität derer und zahlt in die oben genannten Qualitätsziele ein.
Darstellung von Schnittstellen in UML
Die Darstellung eines angebotenen Interfaces mit „lollipop“ und eines benötigten Interfaces mit „socket“ an einem Komponentensymbol ist weit verbreitet. Sollen weitere Details einer abstrakten Schnittstelle dargestellt werden, wird der Stereotyp «interface» auf einem Klassensymbol verwendet und Namen und Methoden kursiv dargestellt.
Abbildung 2: Darstellungen von Schnittstellen in UML
Gemeinsamkeiten
Um Code von mehreren Komponenten aus nutzbar zu machen, muss ein gemeinsamer Bereich geschaffen werden. Dieser wird oft Common oder Utils genannt. Aber Vorsicht, es sollte darauf geachtet werden, dass hier nur technischer Code landet, der die Modularisierung nicht durchbricht. Wenn Klassen mit Fachlichkeit dort auftauchen, ist es wahrscheinlich der falsche Ort.
Komponenten in C++
Es ist sinnvoll, eine Komponente mit einer Komponentenklasse zu realisieren. Die Verantwortung dieser Klasse ist es, die Interaktion mit der Komponente zu ermöglichen und die Ownership über die internen Ressourcen zu übernehmen.
Querschnittliche Aufgaben werden auf abstrakte Komponentenschnittstellen abgebildet. Dies kann je nach Architektur und Abstraktionsebene unterschiedlich sein, aber Initialisierung, Konfiguration, Start und Beenden von Nebenläufigkeit sind gute Kandidaten für eine abstrakte Komponentenschnittstelle.
Das folgende UML-Klassendiagramm zeigt die Komponentenklasse einer Temperaturregelung, ohne den weiteren internen Aufbau darzustellen.
Abbildung 3: Komponente einer Temperaturregelung mit Schnittstellen
Dynamisch ladbare Komponenten
Im Rahmen eines Komponentenkonzeptes kann auch festgelegt werden, ob dynamisch ladbare Komponenten in einer Embedded Software mit einem Dateisystem unterstützt werden sollen. Wie immer sind auch hier die zu erreichenden Qualitätsziele die treibende Kraft. Technisch realisiert man die fachliche Schnittstelle (API) in einem binären Format (ABI), um die Austauschbarkeit von Komponenten ohne Neukompilierung der Embedded Software zu ermöglichen. Ob dies Teil des Komponentenkonzepts ist, sollte das Ergebnis einer wohlüberlegten Architekturentscheidung sein.
Zum Schluss
Meiner Meinung nach ist die fachliche Strukturierung einer Software mit Komponenten ab einer gewissen Komplexität extrem wichtig, um die oben genannten Qualitätsziele zu erreichen. Es bedarf außerdem kontinuierlicher Architekturarbeit um die fachliche Modularisierung mit den wachsenden Anforderungen im Einklang zu halten. Ich hoffe, dass dieser Artikel dazu beiträgt, evolutionäre Softwarearchitekturen voranzutreiben und dem „big ball of mud“ das Wasser abzugraben.
Die beispielhafte Anwendung des Komponentenkonzepts ist als Artikel online verfügbar unter: Embedded Komponentenarchitektur Teil 2
Alexander Eisenhuth ist seit 1996 in der Softwareentwicklung tätig. Seit 10 Jahren ist er hauptsächlich als Softwarearchitekt in agil organisierten Teams tätig. Mit seinem Mentoring-Programm unterstützt er angehende Software-Architekten in ihrer Rolle.
Mehr über das 1:1 Mentoring: www.eisenhuth-se.de