zurück zum Artikel

Patterns in der Softwarearchitektur: Das Pipes-and-Filters-Muster

Rainer Grimm

(Bild: Blackboard/Shutterstock.com)

Das Pipes-and-Filters-Architekturmuster beschreibt die Struktur für Systeme, die Datenströme verarbeiten.

Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung und Softwarearchitektur. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Das Pipes-and-Filters-Architekturmuster ist dem Schichtenmuster ähnlich und beschreibt die Struktur für Systeme, die Datenströme verarbeiten.

Die Idee hinter dem Schichtenmusters ist es, das System in Schichten zu strukturieren, sodass die höheren Schichten auf den Diensten der niedrigeren Schichten basieren. Das Pipes-and-Filters-Muster erweitert das Schichtenmuster auf natürliche Weise, indem es die Schichten als Filter und den Datenfluss als Pipes verwendet.

Zweck

Umsetzung

Struktur

Filter

Pipe

Data Source

Data Sink

Der interessanteste Teil des Pipes-and-Filters-Musters ist der Datenfluss.

Es gibt mehrere Möglichkeiten, den Datenfluss zu steuern.

Push-Prinzip

Pull-Prinzip

Gemischtes Push/Pull-Prinzip

Aktive Filter als unabhängige Prozesse

Das bekannteste Beispiel für das Pipes-and-Filters-Muster ist die UNIX Command Shell.

Unix Command Shell

Hier sind die Schritte der Pipeline:

Zum Schluss hier noch der Klassiker der Kommandozeilenverarbeitung mit Pipes von Douglas Mcllroy [1].

tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q

Wer wissen will, wie diese Pipeline funktioniert, findet die ganze Geschichte dahinter in dem Artikel "More shell, less egg [2]".

C++ unterstützt das Pipes-and-Filters-Muster dank der Ranges-Bibliothek in C++20.

Ranges

Das folgende Programm firstTenPrimes.cpp zeigt die ersten zehn Primzahlen beginnend mit 1000 an.

// firstTenPrimes.cpp

#include <iostream>
#include <ranges>
#include <vector>

bool isPrime(int i) {
    for (int j = 2; j * j <= i; ++j){
        if (i % j == 0) return false;
    }
    return true;
}

int main() {

    std::cout << '\n';
    
    auto odd = [](int i){ return i % 2 == 1; };

    auto vec = std::views::iota(1'000) 
      | std::views::filter(odd)           // (1)
      | std::views::filter(isPrime)       // (2)
      | std::views::take(10)              // (3)
      | std::ranges::to<std::vector>();   // (4)

    for (auto v: vec) std::cout << v << " ";

}

Die Datenquelle (std::views::iota(1'000)) erzeugt die natürliche Zahl, beginnend mit 1000. Zuerst werden die ungeraden Zahlen herausgefiltert (1) und dann die Primzahlen (2). Diese Pipeline hält nach zehn Werten an (3) und schiebt die Elemente in den std::vector (4). Die praktische Funktion std::ranges::to erstellt einen neuen Range (4). Diese Funktion ist neu in C++23. Deshalb kann ich den Code nur mit dem neuesten Windows-Compiler im Compiler-Explorer [3] ausführen.

Ich verwende in meinem folgenden Vergleich den Begriff universelle Schnittstelle. Das bedeutet, dass alle Filter die gleiche Sprache sprechen, wie beispielsweise xml oder json.

Vorteile

Nachteile

Der Broker strukturiert verteilte Softwaresysteme, die mit entfernten Dienstaufrufen interagieren. Er ist für die Koordination der Kommunikation, ihrer Ergebnisse und Ausnahmen zuständig. In meinem nächsten Artikel werde ich tiefer in das Architekturmuster Broker eintauchen.

Für Kurzentschlossene. Alle Details rund um C++20:

( [6])


URL dieses Artikels:
https://www.heise.de/-8312564

Links in diesem Artikel:
[1] https://de.wikipedia.org/wiki/Douglas_McIlroy
[2] http://www.leancrew.com/all-this/2011/12/more-shell-less-egg/
[3] https://godbolt.org/z/dWaM6EvK4
[4] https://www.modernescpp.de/index.php/c/2-c/42-c-2020220806075004
[5] https://aramis.de/
[6] mailto:rainer@grimm-jaud.de