Software-Architektur

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Grundlagen

Einleitung

Die vermutlich häufigste Problematik, die sich Programmierern bei der Entwicklung von Software stellt, ist die nach einer systematischen Erzeugung von 'gutem' Code und qualitativ hochwertiger Software - sprich nach der Entwicklung von Software als Qualitätsprodukt. Software Engineering ist also nach IEEE-Definition die Anwendung ingenieursmäßiger Entwurfsmethodiken auf den Bereich des Softwareentwicklung. Mit genau dieser Fragestellung beschäftigt sich der Teilbereich der Softwaretechnik in der Informatik.

Zweck dieses Artikels ist es, grundlegende Techniken vorzustellen, mit denen man als Entwickler seine Software etwas besser strukturieren und flexibler gegenüber Änderungen gestalten kann. Zum Verständnis dessen, wie das erreicht werden kann, müssen daher zunächst einige grundlegende Konzepte erklärt werden.

Daten

Essentiell für datenverarbeitende System ist der grundlegende Begriff des 'Datums' bzw. der Daten. Darunter versteht man die zeit- und wertdiskrete Repräsentation analoger Größen (Informationen). Beispielsweise resultiert aus der Messung einer analogen Größe (z.B. Temperatur) eine digitale Zahl (AD-Wandlung). Im Umkehrschluss können aus digitalen Daten auch wieder analoge Größen erzeugt werden (DA-Wandlung).

Daten besitzen immer ein Format, da ein diskretes Rechensystem selbst bestimmte Repräsentationen für die Berechnung an sich nutzen muss. Für Fließkommazahlen wird z.B. die IEEE-754 Repräsentation genutzt, aber selbst die Darstellung als Integer impliziert schon ein minimales und ein maximales Element und eine Zahlenauflösung dazwischen. Im Codebespiel dieses Artikels wäre ein internes 8.1 Bit-Format vom Sensor DS1621 in eine Komma-Darstellung zu konvertieren.

Das heißt in der Quintessenz: Konvertierungen zwischen Datenformaten sind essentieller Bestandteil jeder Software. Entsprechend wichtig ist Konsistenz dieser Daten im Gesamtsystem.

E/R Modell

Zur Modellierung von Daten eines Softwaresystems wird häufig das Entity-Relationship-Modell verwendet. Mit dieser Diagramm-Notation kann eindeutig ein gemeinsames und konsistentes Format von Daten spezifiziert werden. Das Codebeispiel dieses Artikels beschreibt nur die Verarbeitung eines Temperaturwerts, daher sei für kompliziertere Systeme auf die entsprechende Literatur verwiesen (Chen/IBM).

Erweiterte Backus-Naur-Form

Zur Darstellung von Datenformaten eignet sich sehr gut eine Notation in der EBNF. Diese kontextfreie Sprache dient zur eindeutigen Spezifikation von Daten und kann leicht durch Syntaxdiagramme visualisiert werden. Sie dient hauptsächlich zur Beschreibung von Transformationen von Daten durch Prozesse.

Datenfluss

Software fällt nur in den seltesten Fällen innerhalb von ein paar Stunden vom Himmel und ist dann für immer einfach da. Stattdessen unterliegt die Entwicklung derselbigen eines Prozesses, also eines zeitlich dauernden Vorgangs, durch den definierte Eingaben durch Arbeit zu Ausgaben transformiert werden.

Datenflussprinzip

Die Strukturierte Analyse und Design von Ed Yourdon u.a setzt darauf auf, zunächst zu identifizieren und zu spezifizieren, was die Software überhaupt leisten soll (Requirements bzw. Anforderungsanalyse) und mündet in einem Anforderungsdokument.

Aus diesem Anforderungsdokument werden nun die Funktionen abgeleitet, welche die Software tatsächlich während ihres Betriebs leisten muss.

Diese funktionalen Anforderungen werden innerhalb der Software auch als ein 'Prozess' betrachtet - einen Prozess, den die Software zur Durchführung der jeweiligen Funktion durchlaufen muss. Für das Code-Beispiel zum Artikel wäre die dort maßgebliche 'Funktion' der Software, die Werte eines I2C-Thermometers auf UART und LCD darzustellen. Der I2C-Slave ist also die Datenquelle, UART und LCD sind Datensenken.

Durch die Kombination der Definition eines 'Prozess' mit der hierarchischen Dekomposition eines komplizierten Vorgangs in kleinere Vorgänge ('Teile-und-Herrsche') kommen also also zwei Grundprinzipien der Informatik zum Tragen:

  • Prozess = Input-Processing-Output (Eingabe-Verarbeitung-Ausgabe)
  • Teile-und-Herrsche zur Unterteilung von Prozessen in Subprozesse

Yourdons Datenflussdiagramme (DFD) dienen dazu, diese Zerlegungen von Software-Funktionen zu beschreiben. Sie beschreiben, woher Daten kommen (Datenquelle), welche Transformationen sie in ihrer Verarbeitung durchlaufen und wohin die Daten am Ende ihrer Transformation fließen (Datensenke). Das wird letztlich als Datenflussprinzip formuliert. Diese DFDs werden immer im Zusammenhang mit Entity-Relationship-Diagrammen (ER-Diagramme) erstellt, beschreibt, was die eigentlichen Daten überhaupt sind und was relevant für die Datenhaltung ist.

Als Datenquellen/senken in DFDs dienen Datenbanken (Persistenz, Symbol: Ξ) und Terminals (Benutzerinteraktion, Symbol: Rechtecke). Verarbeitungen werden als Prozesse in Kreisen symbolisiert. Das Code-Beispiel verwendet keine Persistenz für die Sensordaten - dort müssten sowohl der I2C-Sensor als Datenquelle, sowie die Datensenken (UART, LCD) mit einem Terminal-Symbol verwendet werden. Zum Einen, weil die formatierten Daten direkt dem Anwender gezeigt werden (LCD) oder weil sie von anderen externen Teilnehmern stammen (I2C-Sensor) bzw. zu extern geliefert werden (z.B. UART).

Die Kombination aus DF- und ER-Diagrammen dient als erstes Design-Dokument, welches spezifiziert, was die Software überhaupt für Funktionen hat! Für den Praktiker ist an dieser Stelle wichtig, dass es gut ist, Daten fließen zu lassen und nicht zur Unkenntlichkeit zu zerstückeln.

Um es mit Mitteln der Elektrotechnik etwas griffiger zu formulieren: Datenflussdiagramme stellen im Prinzip die Leiterbahnen und die 'Verbraucher' dar. Analog zum 'Prozess' ist der Stromfluss zwischen den Potentialen (von Quellen zu den Senken) - die Dauer des Prozesses kann man als Äquivalent zur elektrischen Arbeit interpretieren (⇒ was sie auf etlichen Abstraktionsebenen unterhalb der Softwareebene letztlich auch ist).

Kontrollfluss

Zur Erklärung des Kontrollflusses soll das Beispiel des Stromflusses erweitert werden. Oft reicht bereits einfach ein geschlossener Stromkreis (zwischen Quelle und Senke) aus, sodass ein Strom fließt und an einem Verbraucher Arbeit verrichtet wird. Manchmal muss dieser Prozess aber abhängig von bestimmten Bedingungen gemacht werden - z.B. sollte ein Dämmerungsschalter bei Tag (in nicht-arktischen Gegenden) keinen Strom fließen lassen. Bedingungen werden mit Kontrolle gleichgesetzt - man benötigt eine Instanz zum Kontrollieren, ob die Bedingung eingehalten ist. Z.B. ob ein bestimmtes Flag gesetzt ist oder ein bestimmter Schwellwert überschritten wurde. Kontrollfluß ist also etwas, was kostet (im Sinne von 'den Datenfluss ausbremst') und was immer an Daten gekoppelt ist. Unter Kontrollmechanismen versteht man in etwa die klassischen if/for/while/switch-Konstrukte. Kontrolle ist immer schlecht - je mehr man hat, desto mehr Overhead kann man damit erzeugen (i.S.v. die Kontrolleure kontrollieren) und dementsprechend mehr Fehlerpotential in seine Software generieren. Ziel eines sauberen Designs ist es also, Kontrolle zu minimieren.

Datenflussdiagramme können diese Aufgabe der Kontrolllogik nicht übernehmen - sie stellen nur dar, wie Daten fließen sollen und geben somit noch kein direktes Design vor.

Structure Charts

Um aus den entstandenen ER- und DF-Diagrammen ein Design abzuleiten, wird eine Darstellung als Structure Chart verwendet. Diese entsprechen einem Baum aus Funktionsaufrufen und visualieren, zwischen welchen Komponenten Datenfluss und wo Kontrollfluss stattfindet.

Dieser Vorgang nutzt die in den DFDs identifizierten Prozesse und stellt diese in einer Baumstruktur dar. Datenfluss von und zu Subprozessen wird durch kleine weiße Pfeile symbolisiert. Übernimmt ein Subprozess eine Kontrollaufgabe symbolisiert ein kleiner schwarzer Pfeil das Ergebnis dieser Prüfung (Kontrolltoken). Der Aufbau des Baums erfolgt weitestgehend wieder nach IPO-Muster von links nach rechts (linkester Teilbaum prüft Eingabe des Gesamtprozess, rechtester übernimmt Ausgabeformatierung).

Der Schwerpunkt eines Prozesses (also eines Top-Level-IPO-Vorgangs) wird auch als Zentrum der Transformation bzw. objektorientiert als Top-Level-Controller bezeichnet. Hat der Baum an dieser Stelle seine Wurzel, erhält man ein Design ohne unnötige Kontrollinstanzen.

Für ANSI-C und strukturierte Entwicklung ist man an dieser Stelle fertig. Bei Anwendung objektorientierter Analyse und Design-Methodiken werden nun die dort üblichen Handwerkszeuge vorgestellt.

Entwurfsmuster (Design Patterns)

In der Software-Architektur gibt es Konstrukte, die sich als gut funktionierend erwiesen haben und bei korrekter Anwendung ein Design drastisch verbessern können. An dieser Stelle verhält sich die Softwaretechnik als "praktisches Handwerk" analog zu anderen Tätigkeitsbereichen, wie etwa beim Design von Leiterplatten oder in der Mechanik - also eine Art Katalog von Do's und Don'ts, die ein Softwaredesign verbessern können.

Die hier vorgestellten Entwurfsmuster sind grundsätzlich objekt-orientierter Natur, sie setzen also ein grundlegendes Verständnis des Konzeptes eines 'Objekts' voraus, was aber die Verwendung der Muster für die rein prozedurale Programmierung (ANSI-C) nicht ausschließt.

Im Artikel wird unter einer 'Variable' also ein 4-Tupel verstanden, aus:

  • Adresse zur Laufzeit
  • Zustand (Wert)
  • Bezeichner
  • Datentyp

Ein Objekt ist also einfach eine spezielle Variable von einer Klasse/Struktur/Union (Datentyp) mit den speziellen Eigenschaften:

  • Adresse zur Laufzeit: 'this'-Pointer
  • Zustand: Zustände aller Member-Variablen
  • Datentyp: (Klasse/Struktur/Union)

und dem gedanklichen Trick, der die Objektorientierung ausmacht, dem Konzept der Datenkapsel: ein Objekt beinhaltet also nur logisch irgendwie zusammenhängende Daten und Funktionen. Der Grad, in dem ein Objekt abhängig von anderen Komponenten ist, wird in der Softwaretechnik als Kopplung bezeichnet. Der Grad, wie stark logisch zusammenhängend ein Objekt zu seinen Member-Variablen ist, ist dagegen als Kohäsion bekannt. Ziel beim Entwurf einer Software-Architektur ist es, Kohäsion zu maximieren und Kopplung zu minimieren, sodass austauschbare Software-'Bausteine' entstehen.

Entwurfsmuster sind dabei die Mittel, mit denen diese Minimierung von Kopplung und die Maximierung von Kohäsion erreicht werden kann.

Strategy-Pattern (Strategie)

Das einfachste Entwurfsmuster, welches die Grundlage für fast alle weiteren Muster bildet, ist die Strategie. Das Muster zeichnet sich einfach durch Polymorphie der Implementierungen gegenüber einer abstrakten Basisklasse aus. Jede Implementierung dieser Schnittstelle ist letztlich eine wählbare Strategie.

State-Pattern

Verbindet man das Strategiemuster mit mehreren gleichzeitig wählbaren Strategien, entsteht ein Zustandsmuster. Eine Ampel, die zeitabhängig mehrere Zustände von alleine durchläuft, ist ein Beispiel für das State-Pattern.

Observer-Pattern (Beobachter)

Das Beobachter-Muster beschreibt die Interaktion zwischen Objekten zur Laufzeit. Ein Beobachter möchte über Änderungen eines anderes Objektes benachrichtigt werden. Dieses Objekt stellt demzufolge einen Subscribe/Unsubscribe-Mechanismus bereit, über den sich ein abstrakter Beobachter registrieren darf. Ändert sich das Objekt, werden alle registrierten Beobachter mit dem aktuellen Objektzustand benachrichtigt.

Architekturmuster

Wie für die Strukturierte Analyse beschrieben, ist es auch bei objektorientierten Methodiken das Ziel, Kontrolle zu minimieren. Anhand der Structure Charts wurde ein Vorgehen zur hierarchischen Erstellung eines Designs beschrieben. Das objektorientierte Pendant zur Modellierung von kohäsiven und wenig gekoppelten Kontrollinstanzen stellen die Kompositmuster Model-View-Controller bzw. dessen vereinfachte Form Model-View-Presenter dar.

Model-View-Controller

Model-View-Controller

Das MVC-Muster ist ein etwas komplizierteres Muster, welches im Wesentlichen das Ziel hat, Verantwortlichkeiten zu trennen (also Objekte wirksam zu entkoppeln).

In Anlehnung an die Objekthierarchie bildet es eine Baumstruktur, in welcher die Kontrollinstanz (der Controller) an der Spitze des Dreiecks sitzt. Dieser ist in dem Dreigespann auch der Einzige, der die Daten direkt verändern darf. Streng genommen zählen auch die registrierten Beobachter als Teile vom Datenmodell, daher darf deren Registrierung/Deregistrierung auch nur über den Controller verwaltet werden.

Das Datenmodell (die Daten) selber kann im Prinzip beliebig sein. Wie im Beispiel für die ThermometerGeneric<>-Policy kann das Datenmodell selber auch wieder ein Subcontroller-MVP/MVC sein. Wichtig für das Datenmodell ist eigentlich nur, dass das Datenmodell selber feststellen können muss, ob es geändert wurde oder nicht. Diese Aufgabe darf nicht durch den Controller übernommen werden (siehe Structure Charts und Anmerkung zu Kontrollfluss). Idealerweise ruft also der Controller einfach einen Setter des gewählten Datenmodells auf und dieses prüft darin, ob sich der aktuelle Parameter vom gespeicherten Zustand unterscheidet (onChange).

Die Sichten (Views) stellen letztlich nur die Ausgabeformatierung der eigentlichen Nutzdaten dar. Eine Sicht darf sich beim Controller an- bzw. abmelden, Aktualisierungen über Datenänderungen zu erhalten. Wird über den Controller eine Änderung des Datenmodells impliziert, werden die registrierten Sichten über diese Änderung benachrichtigt.

Ziel beim Schreiben eines MVC-Compounds sind dabei: stupide Views, dünne Controller-Klassen und fette Datenmodelle.

Für eine direkte Implementierung benötigt es im Gegensatz zu MVP Polymorphie für die Views (im Beispiel durch IView dargestellt) sowie für die Datenmodelle (im Beispiel IThermometer). Das lässt sich nicht vermeiden, benötigt aber letztlich auch nur einen Funktionsaufruf mehr, der bei einer klaren Objekthierarchie und klarem Kontrollfluss nicht teuer ist.

Model-View-Presenter

Model-View-Presenter

MVC ist ein sehr flexibler Mechanismus, mit dem sehr gut eine Trennung von Daten und deren Darstellung erreicht werden kann. Manchmal ist dessen volle Flexibilität gar nicht nötig und man möchte z.B. auf die Polymorphie des Datenmodells verzichten. In diesem Fall kann man ähnliche Ergebnisse auch mit einem Passive-View bzw. einem Supervising Controller erzielen. Im Codebeispiel generiert die Template-Policy Thermometer<> (cpp_generic/thermometer.hpp) eine MVP-Klasse aus Port (cpp_generic/i2cclient.hpp) und DS1621-Treiber (cpp_generic/sensor_ds1621.hpp). Die beiden Views (LCD+UART) registrieren sich auf Updates und werden bei Änderungen des Datenmodells benachrichtigt. Wesentlicher Unterschied gegenüber MVC ist es, dass das Datenmodell nicht mehr dem Benutzer gegenüber exponiert wird, sondern jeglicher Datenzugriff über den Controller (-> Presenter) passiert.

Kombinationen

MVC-MVP-Verschachtelung

Wie bereits angemerkt, kann eine Verschachtelung von MVP/MVC erfolgen, wodurch Komplexität durch weitere Dekomposition gelöst werden kann. Im mvc-tutorial/cpp_generic_final wird dies demonstriert, was einer FatClient-Implementierung entsprechen sollte. Das wäre z.B. beim RaspberryPi der Fall, wo unterschiedliche Sensorentypen an SPI oder I2C hängen können und per UI zur Laufzeit des Programms selektiert werden. Die Thermometer-Klasse dient somit als ein Proxy für die reale Implementierung durch die MVP-Policy. Vorteil ist für den Anwendungsentwickler, dass die Thermometer-Komponente immer durch die gleiche API bedient wird, aber erweiterbar für neue Sensorentypen bleibt.

Anwendung

Im Anhang des Artikels ist ein GitHub-Repository verlinkt, welches anhand eines Thermometer-ICs die Anwendung der beschriebenen Architekturmuster zeigen soll. Das Beispiel abstrahiert dabei den Sensor vollständig von der konkreten I2C-Slave-Implementierung, weswegen letztere als hardwareabhängiges Backend auch bewusst weggelassen wurde. Das Beispiel auf PIC, AVR oder ARM bzw. zu adaptieren bzw. in ein vorhandenes Framework zu integrieren ist nicht die direkte Aufgabe des Artikels.

Generiert man sich mit Hilfe von SWIG einen C-Wrapper für die C++-Implementierung, erhält man letztlich eine Implementierung, die der Structure-Chart-Lösung sehr ähnlich sehen sollte.

Codechart-ähnliche Darstellung des hierarchischen Aufbaus der Thermometer-API

Links