Von der aktiven Abfrage zu einem reaktiven System

Nein, das ist kein Bild von Gipf-Oberfrick sondern...
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
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
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.

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 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:
Abbildung 3: Der Adapter als R-Komponente
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:
  • 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.
Und damit schlagen wir die Brücke zum Architektur-Stil Ports & Adapter: das Protokoll zur Verwendung der Polling-Komponente definiert einen sog. Secondary-Port, die konkrete Adapter-Implementierung stellt in unserem Fall die R-Komponente Driver bereit.

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.