Moderne C++ Softwarearchitektur

Ein Whitepaper zum Thema wiederverwendbare Module von Florian Seibold

Wir beschreiten seit einigen Jahren den Weg von „Wir machen Embedded C++“ zu „Wir verstehen Embedded C++“. Dabei begegnen uns regelmäßig Stolpersteine auf unserem Weg, die sich bisher nach und nach in Wohlgefallen auflösten. In diesem Artikel möchte ich unsere Erfahrungen teilen und Ihnen Lust auf modulares Architekturdesign und Denken in Modulen machen.

Als wir 2017 unsere Vorgehensweise in der Embedded-Software-Entwicklung umgekrempelt haben, gab es noch keine Library für das tägliche Brot im Embedded-Bereich. Daher mussten wir fast alles selbst machen.

Seitdem stellen wir uns vor der Implementierung einer Aufgabe folgende Fragen:

  • Gibt es in der Standard-Library Hilfen, mit denen ich meine Aufgabe weitestgehend lösen kann? Wenn ja, hat sich das Problem meistens schon von selbst erledigt.
  • Ist mein Lösungsansatz generisch genug, damit auch andere meine Implementierung sinnvoll nutzen können und sich der Aufwand rentiert?
  • Ist mein Lösungsansatz für alle Anwendungsfälle, für die das Modul beschrieben ist, effizient in Footprint und CPU-Last?

Warum stellen wir uns diese Fragen?

In den letzten Jahren haben sich intern immer wieder unterschiedliche Trends abgezeichnet, die es zu kontrollieren galt. Zum Beispiel gab es eine Zeit, in der das Implementer-Pattern Teil jedes zweiten Lösungsansatzes war. Mit diesen vier einfach zu stellenden Fragen versuchen wir Module für uns zu schaffen, die sinnhaft sind. Sinnhaft, weil sie nicht die Standard Library nachimplementieren, an anderen Stellen wiederverwendet werden können und möglichst effizient sowie Embedded-tauglich umgesetzt sind. Das alles hat unser Vorgehen erst einmal auf den Kopf gestellt und dann neu geordnet. Die Wertigkeit, Robustheit und Verständlichkeit von Entwicklungsergebnissen ist spürbar gestiegen. Auch unsere Kunden danken es uns.

Standardisierte und leicht Verständliche Architektur

Über Architektur kann man streiten – und das ist auch gut so. Ich glaube daran, dass jede konstruktive Diskussion, auch wenn sie teilweise hitzig ist, früher oder später einen Sprung an Source-Code-Professionalität nach sich zieht.

Ob eine Architektur auch verständlich und nutzerorientiert ist, zeigt sich erst, wenn Menschen damit arbeiten, die an der Entwicklung nicht beteiligt waren. In unserem Fall holen wir regelmäßig Feedback von unseren Kunden ein und fragen nach, inwieweit sie verstehen, was in dem Code passiert und wo sie welche Funktionalität vermuten. Noch eine Stufe tiefer gehen wir mit neuen Mitarbeitern, die noch eine externe Sichtweise haben und uns helfen können Strukturprobleme aufzulösen, die wir selbst nicht mehr sehen. Iterativ sind wir zu folgendem Zwischenergebnis gekommen: Architektur, die immer wieder für unsere Kunden und uns funktioniert, ist in den Basis-Layern immer gleich aufgebaut und damit generisch.

Moduel, die man gerne wiederverwendet

Unser Ziel ist es, kleine und verständliche Software-Module zu produzieren, die in unterschiedlichen Projekten und Anwendungsfällen eindeutig Mehrwert bieten und die man als Entwickler gerne wiederverwendet.

Soweit die Theorie. In der Praxis braucht es eine Philosophie und klare Regeln, nach denen entwickelt wird. Für mich hat sich herauskristallisiert, dass alle Module folgende vier Kriterien klar erfüllen müssen:

  • Funktional:
    Software ist funktional, wenn sie effektiv und exakt diejenige Aufgabe erledigt, für die sie beschrieben ist. Zusätzlicher „Schnick-Schnack“, der nicht eindeutig Mehrwehrt für das Modul und dessen Nutzer erzeugt, hat auch nichts darin zu suchen.
  • Wiederverwendbar:
    Software ist wiederverwendbar, wenn sie so generisch aufgebaut ist, dass sie nicht nur für exakt einen Anwendungsfall sinnvoll genutzt werden kann, sondern für eine ganze passende Kategorie an Anwendungsfällen.
  • Testbar:
    Unit-, Integrations- und Systemtests sind auch im Embedded-Software Bereich nicht mehr wegzudenken. Egal ob es sich um Haftungsthemen für Sicherheitsfunktionen oder die Reduktion von Reklamationen geht, gut gemachte Unit-Tests können viel bewirken. Dabei sind Unit-Tests nur so gut, wie auch die Struktur der Klasse selbst, die sie abtesten sollen. Nur Funktionen mit geringer Komplexität können stabil und nachvollziehbar abgetestet werden.
  • Benutzerfreundlich:
    Mein Lieblingskriterium, wenn auch vermutlich das schwierigste der Vieren: Es muss Spaß machen, das Modul zu benutzen. Die API, die Dokumentation und die Beispiele müssen so gut verständlich sein, dass Externe das Modul lieber benutzen, als sich selbst Gedanken über eine mögliche Implementierung zu machen.

Beispiel: Average-Klasse

Als kleines Beispiel soll hier die Average-Klasse dienen. Die beiden statischen Funktionen der Klasse bilden einen buchhalterisch korrekten Mittelwert aus dem übergebenen Datenarray oder aus dem Inhalt eines Puffer-Objekts, der zum Beispiel ein Ringpuffer sein kann, und gibt diesen zurück. Buffer ist in diesem Beispiel ein Interface für alle Puffer-Implementierungen. Die Rechenoperationen in den beiden value()-Funktionen sind für Embedded optimiert.

Ich denke es ist klar, was man von der Klasse an Funktionalität erwarten kann und wie man sie verwendet. Was Sie jetzt nicht sehen, aber sicherlich vermuten, kann ich Ihnen versichern: Die Klasse ist leicht testbar und leicht wiederverwendbar.

Den ausführlichen Bericht mit Code-Beispielen, Erläuterungen und Lessons-Learned finden Sie hier als PDF:

Viele gute quergedachte Ideen wünscht Ihnen die

querdenker engineering