Ports-und-Adapter-Architektur

Ports-und-Adapter-Architektur

· 1,116 words · 6 minutes reading time Deutsch Rust Software Engineering

Was ist eine Ports-und-Adapter-Architektur?

Eine Ports-und-Adapter-Architektur ist ein Architekturmuster. Es ist auch unter dem Namen Sechseck-Architektur oder Hexagonale Softwarearchitektur bekannt.

Ein Software-System mit einer Ports-und-Adapter-Architektur besteht

  • aus einer fachlichen Komponente, die keine Abhängigkeiten zu Adapter-Komponenten hat und
  • aus Adapter-Komponenten, die fachliche Komponente als Teil des Software-Systems nutzbar machen. Ports sind die Schnittstellen, die fachliche Komponente zur Verfügung stellt. Adapter benutzen ausschließlich diese Ports um mit der fachlichen Komponente zu kommunizieren.
  
    %%{ init: {'theme': 'forest'} }%%
  
  flowchart
  ui[Nutzerschnittstelle]
  aUi[Adapter zur Nutzerschnittstelle]
  gl{{Langzeit-stabile Geschäftslogik}}
  aA[Adapter A]
  aB[Adapter B]
  trA[Technische Resource A]
  trB[Technische Resource B]
  ui  ~~~ aUi

  aUi --> ui
  aUi  --> gl

  gl  ~~~ aA
  gl  ~~~ aB
  aA --> gl
  aB --> gl
  aA --> trA
  aB --> trB

Die Annahmen sind wie folgt:

  • Die Geschäftslogik-Komponente ist die wichtigste Komponente des Software-Systems. Der Hersteller ist selbst dafür verantwortlich, in Ihr ist alles Wissen über das Geschäftsfeld versammelt.
  • Die Geschäftslogik ist langzeit stabil und weniger Änderungen unterworfen als beliebige technische Erfordernisse, die entsprechende variierende technische Komponenten münden. Modelliert die Geschäftslogik beispielsweise physikalische Zusammenhänge, so sind diese zeitlich unveränderlich.
  • Technische Komponenten (und so auch die Nutzerschnittstelle) können und sollten benutzt werden. Die Änderungsraten der technischen Komponenten liegen nicht in der Hand des Herstellers und unterliegen einem eigenständigen Softwarelebenszyklus.
  • Adaptor-Komponenten haben Abhängigkeiten zu den jeweiligen technischen Komponenten und der Geschäftslogik-Komponente. (Meist nicht beeinflussbare) Änderungen in einer technischen Komponente führen zu Anpassungen in der jeweiligen Adapter-Komponente, aber nicht zu Anpassungen in der Geschäftslogik-Komponente.

Die Essenz ist also:

Die Geschäftslogik-Komponente wird frei von Abhängigkeiten zu Fremdkomponenten entworfen.

Es ergeben sich folgende Vorteile:

  • Wartung: Bei notwendiger Änderung einer beliebigen technischen Komponente muß die Geschäftslogik-Komponente NICHT angepasst werden.
  • Funktionale Eignung: Die Geschäftslogik-Komponente kann isoliert entlang Ihrer Schnittstelle (den Ports) vollständig und automatisiert getestet werden.

Historische Einordnung der Ports-und-Adapter-Architektur

Ursprüngliche wurde die Ports-und-Adapter-Architektur von Alistair Cockburn als hexagonale Architektur erfunden um Unzulänglichkeiten im objektorientiertem Softwaredesign zu lösen.

In seiner ursprünglichen Form steht die fachliche Geschäftslogik im Zentrum. Diese Komponente ist von nichts abhängig. Alle nicht-fachlichen Komponenten, sind von der fachlichen Komponente abhängig. Die Abhängigkeit bezieht sich auf die Schlüsselfelder:

  • Benachrichtigung
  • Persistenz
  • Geschäftsereignisse
  • Verwaltung

siehe: Wikipedia: Hexagonale Architektur

Robert C. Martin (Onkel Bob) erweiterte die Ports-und-Adapter-Architektur zur Clean Architecture. Weitere konzentrische Ringe (Zwiebel-Architektur) werden eingeführt. Mittels Dependency Inversion bleibt die fachliche Geschäftslogik im Zentrum und hat keine Abhängigkeit zu äußeren Ringen von Schnittstellen und Adaptern.

siehe: Robert C. Martin: Clean Architecture; Prentice Hall 2018; ISBN 0-13-449416-4

Die Wortuhr

Eine Wortuhr zeigt die Uhrzeit in umgangssprachlichen Worten an. Sie ist eher ein Einrichtungsobjekt als ein Softwareproblem.

Wortuhr-Beispiel

Einmal erblickt, reifte die Idee eine Architektur zu entwickeln, die Uhrzeit-Ausgaben in Worten

  • wie an obigen embedded Gerät,
  • als "Bildschirmschoner"
  • als auch als Web-Application

aus einer wiedernutzbaren Geschäftslogik heraus zu gestattet.

Die Verantwortung der Geschäftslogik-Komponente ist also die die Übersetzung der Uhrzeit (Stunde; Minute) in eine Hervorhebung von Wörtern, die sprachspezifisch in eine 13x13 Buchstabenmatrix fest eingebettet ist.

Die Wortuhr ist komplett in Rust implementiert und im wordclock git-Repository verfügbar.

Das Repository der Wortuhr besteht aus den Komponenten

  • Geschäftslogik (crate wordclock)
  • Alternative technische Komponenten
    • einfaches Terminalprogram (als on-terminal Beispiel )
    • Terminal-Application mit Pancurses (pancurses-wordclock crate)
    • GPU optimierte Grafikausgabe (iced-wordclock crate)
    • Wasm Web application mittels Yew (yew-wordclock crate)

Geschäftslogik

  • Modul wordclock
  • Stellt einen WordClock abstrakten Datentyp zur Verfügung. Eine Instanz dieses Types wird Sprachabhängig mittels new erzeugt. Bisher sind die Sprachen "ch-bern", "en-uk", "de-de" unterstützt
  • Die Operation show_time_iterator des Datentyps WordClock Erzeugt einen Iterator, der es erlaubt mittels beliebiger technischer Komponenten die Urzeit als umgangssprachliche Wörter auszugeben.
  • Das Modul hat keine Abhängigkeiten zu anderen Modulen oder Crates. Es erfüllt somit die Anforderungen an eine Ports-Adapter-Architektur.
  • Das Modul benötigt keine Standard library und kann daher problemlos in embedded Kontexten oder webassembly Kontexten benutzt werden.
# Get the sources
git clone https://github.com/almedso/wordclock.git
cd wordclock
# Build the library crate
cargo build
# Run the tests
cargo test

Ein sehr einfaches Programm, das die umgangssprachliche, schweizerdeutsche Uhrzeit einmalig auf der Konsole ausgibt und sich danach beendet, verdeutlicht die Nutzung der zur Verfügung gestellten Schnittstelle.

cargo run --example on-terminal

Der Sourcecode des Beispiels ist Bestandteil des wordclock library crates und sieht wie folgt aus:

01 use chrono::{Timelike, Utc};
02 use wordclock::WordClock;
03
04 fn main() {
05     let clock = WordClock::new("ch-bern".to_string());
06     let now = Utc::now();
07     for (letter, highlight, end_of_row) in
08         clock.show_time_iterator(now.hour() as usize, now.minute() as usize)
09     {
10         if highlight {
11             print!("{}", letter);
12         } else {
13             print!(".");
14         }
15         if end_of_row {
16            println!("");
17         }
18     }
19 }
  • Zeile 5 schafft eine WordClock Instanz in schweizerdeutscher Sprache.
  • Zeile 7 und Zeile 8 erzeugt einen Iterator über die Buchstabenmatrix. Jedes Iteratorelement besteht aus
    • Einem Buchstaben aus der Matrix (von Links nach rechts, von oben nach unten)
    • Einem Indikator ob der Buchstabe zu einem angezeigtem Wort gehört (highlight flag)
    • Einem Indikator ob nach diesem Buchstaben eine neue Matrix-zeile beginnt (end_of_row flag)

Technische Komponenten

Die verschiedenen technischen Komponenten zeigen, wie die selbe Geschäftslogik der Wortuhr auf unterschiedlichen Medien, unterschiedlich komfortabel abgebildet werden.

Eine jede technische Komponente benötigt einen Uhrzeitgeber in Stunde und Minute und eine Möglichkeit "Buchstabenmatrizen" auszugeben.

Komfortables Terminalprogramm

Benutzt den pancurses crate und erzeugt ansprechende sich aktualisierende Terminalausgaben.

cd pancurses
cargo run

Hardwarebeschleunigte Grafikausgabe

Benutzt den iced crate und öffnet ein eigenes Fenster und erzeugt ansprechende sich aktualisierende Ausgaben. (Hardwareübergreifend und Betriebssystemübergreifend auf MAC, Windows, Linux)

cd ../iced
cargo run

Web-Application

  • Benutzt den yew crate.
  • Benutzt wasm-unknown-unknown als Target (cross compile) (keine standard library verfügbar)
  • Erweitertes Tooling via trunk
cd ../yew
rustup add target wasm-unknown-unknown
trunk serve --open

Ist über github actions deployed und erreichbar unter:

Fazit

Die praktische Implementierung einer Ports-Adapter-Architektur am Beispiel einer Wortuhr ist gelungen und liefert die erwarteten Vorteile:

  • Eine Geschäftslogik kann für unterschiedliche technische Erfordernisse ohne Änderungen wiederverwendet werden.
  • Die unterschiedlichen Build-Varianten, Betriebssystemzielumgebungen von Deep-Embedded über COT's Betriebssysteme bis hin zu WebAssembly lassen sich ohne Einschränkungen realisieren
  • Die Geschäftslogik kann durch an den Ports automatisiert über das CI getestet werden. Es existieren keine Abhängigkeiten, so dass über das Testfixture auch keine Mocks/Stubs/Spies injiziert werden müssen.