Beitrag erstellt am: 02.09.21
Ereignisorientierte, reaktive Systementwürfe setzen sich mehr und mehr durch, bieten sie doch gegenüber herkömmlichen
Systemen erhebliche Vorteile, siehe hierzu u.a. das Raktiv Manifesto.
Das Problem
Gerade im Embedded-Bereich, doch nicht nur dort, benötigen wir immer wieder Zugriff auf Komponenten, die auf Polling
ausgelegt sind und daher keine observierbare Schnittstelle anbieten: Clients sind gezwungen mittels aktiven periodischen Abfragen mögliche Zustandsänderungen zu detektieren. Das
erhöht die Systemlast unnötig und mindert die intellektuelle Beherrschbarkeit bezüglich des geordneten Umgangs mit Zustandsänderungen im abhängigen Client-Code. Dies u.a.
weil das Wissen um den nötigen Polling-Mechanismus an diversen Stellen im Client-Code auftaucht.
Abbildung 1: Aktive Abfrage via Polling
Die Lösung: der Polling-Adapter
Ein Adapter hilft uns, den Polling-Mechanismus an einer zentralen Stelle einzufangen, sprich zu kapseln: fortan übernimmt
der Adapter die Aufgabe des periodischen Pollings und stellt detektierte Zustandsänderungen mittels observierbarer Schnittstelle bereit. Soweit so gut und wenig
überraschend:
Abbildung 2: Polling mittels Adapter
Damit der Adapter (in der Abbildung Driver genannt) eine reaktive Schnittstelle bereit stellen kann, ist er fortan für die regelmässige aktive
Abfrage der verbundenen A-Komponente Polling verantwortlich. Die
A-Komponente Reactive registriert nur noch die nötigen Observer und wird nun über
auftretende Zustandsänderungen informiert; die Polling-Logik ist im Adapter eingefangen.
Damit der Adapter Zustandsänderungen detektieren kann, merkt er sich den letzten abgefragten Wert und vergleicht diesen beim Polling jeweils mit dem aktuell gelesenen Wert; die registrierten Observer werden nur im Falle einer Wertänderungen angestossen.
Als gratis Zugabe erhalten wir zudem die Möglichkeit, für den Adapter ein Service-Level Agreement mit der geforderten Aktualität der Änderungsdetektion festzulegen. Wer eine reaktive API wie ReactiveX (Rx) nutzt, setzt den Adapter beispielsweise als timer-basiertes Observable mit entsprechendem Intervall auf.
Problem also gelöst? Nicht ganz! Problematisch ist die direkte Abhängigkeit der A-Komponente Reactive vom Adapter.
Damit der Adapter Zustandsänderungen detektieren kann, merkt er sich den letzten abgefragten Wert und vergleicht diesen beim Polling jeweils mit dem aktuell gelesenen Wert; die registrierten Observer werden nur im Falle einer Wertänderungen angestossen.
Als gratis Zugabe erhalten wir zudem die Möglichkeit, für den Adapter ein Service-Level Agreement mit der geforderten Aktualität der Änderungsdetektion festzulegen. Wer eine reaktive API wie ReactiveX (Rx) nutzt, setzt den Adapter beispielsweise als timer-basiertes Observable mit entsprechendem Intervall auf.
Problem also gelöst? Nicht ganz! Problematisch ist die direkte Abhängigkeit der A-Komponente Reactive vom Adapter.
Eine Frage der Abhängigkeiten
In Abbildung 1 sind beide Komponenten mit der Software-Kategorie "A" annotiert. Es ist davon auszugehen, dass es sich um
je eigene A-Kategorien handelt, sprich, sie kapseln je eigene fachliche Themen aus unterschiedlichen Wissensgebieten, im Domain-driven Design fachlich abgegrenzter Kontext
(Bounded Context) genannt. Dafür spricht bereits der Umstand, dass die A-Komponente Polling
bezüglich des Umgangs mit Werteänderungen konzeptionell anders verfährt als die A-Komponente Reactive. Sie setzt nämlich auf die aktive Abfrage von Zuständen und eben nicht auf eine ereignisbasierte
reaktive Lösung. Damit passen die beiden Komponenten bezüglich ihrer Kategorien nicht zusammen. Und genau das führte uns ja zur Lösung mit dem Adapter.
Der Adapter ist denn auch nicht das Problem, sondern die Art seiner direkten Nutzung durch die Komponente Reactive.
Der Adapter ist denn auch nicht das Problem, sondern die Art seiner direkten Nutzung durch die Komponente Reactive.
Der Weg zur Lösung über Software-Kategorien
Die Regeln für die Komponentebildung mit Hilfe von Software-Kategorien
legen fest, dass zwischen Komponenten nicht kompatibler Kategorien vermittelt werden muss. Dazu nutzen wir Komponenten der Kategorie R, einen Adapter. Hätten wir Polling als
T-Komponente identifiziert, z.B. eine Sensor-Schnittstelle in einer Phone-API, wäre die Notwendigkeit einer R-Komponente noch offensichtlicher, denn die Mischkategorie A-T ist
streng verboten.
Damit ist auch klar, dass die A-Komponente Reactive den Adapter Driver nicht direkt verwenden darf, sondern dass vielmehr die Driver-Komponente als Mittler zwischen den Welten die beiden A-Komponenten kennt:
Damit ist auch klar, dass die A-Komponente Reactive den Adapter Driver nicht direkt verwenden darf, sondern dass vielmehr die Driver-Komponente als Mittler zwischen den Welten die beiden A-Komponenten kennt:
Abbildung 3: Der Adapter als R-Komponente
Das Resultat: eine Inversion der Verantwortlichkeiten und somit genau das, was das Dependency Inversion Principle
fordert: Details hängen von Abstraktionen ab und nicht umgekehrt. Im vorliegenden Fall heisst das:
Anstelle von Secondary Port verwende ich übrigens lieber den Begriff Verwendungsschnittstelle, er scheint mir etwas eingänglicher.
- die Abstraktion ist das Protokoll aus Sicht der A-Komponente Reactive: die Möglichkeit, sich auf Zustandsänderungen zu abonnieren.
- die Details der Anbindung liegen im Adapter Driver.
Anstelle von Secondary Port verwende ich übrigens lieber den Begriff Verwendungsschnittstelle, er scheint mir etwas eingänglicher.
In einem späteren Beitrag möchte ich näher auf das Thema DIP und das Konzept der Verwendungsschnittstelle eingehen. Denn,
obwohl die Idee dahinter einfach ist, sehe ich in meiner Praxis viele Entwickler, die mit der Umsetzung offensichtlich Mühe haben.