Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 13 Min.

Moderne Softwarearchitektur

Das Konzept der funktionalen Architektur mit F# ist in der Praxis nicht so kompliziert, wie es auf den ersten Blick erscheint.
© dotnetpro
Die heutige moderne Softwarearchitektur kann auf ganz unterschiedliche Art und Weise bestimmt werden. Man spricht von Quality Driven Software Architecture, Microservices mit CQRS und Event-Sourcing; Method Aggregator und auch Domain Driven Design gibt es schon 20 Jahre.Eine einfache Definition besagt, dass Softwarearchitektur die Struktur, das Verhalten und die Fähigkeiten eines Softwaresystems beschreibt. Die Softwarearchitektur soll damit den Rahmen darstellen und eine Reihe von Standards, Richtlinien, Softwaredesigns und Programmiersprachen definieren, die zur Erstellung eines Systems nötig sind.Hierbei sollte man die Begriffe und die Architektur durchaus flexibel begreifen. Software verändert sich, sie wächst im Lauf der Zeit. Dementsprechend muss sich auch die Software­architektur mit der Zeit wandeln und neuen Anforderungen und Gegebenheiten anpassen. Da in der funktionalen Programmierung der Code die Architektur darstellt, möchte dieser Artikel einen Blick auf den Entwurf einer funktionalen Softwarearchitektur mit F# werfen.

Funktionale Architektur

Grundsätzlich wird bei einer funktionalen Architektur darauf geachtet, dass Funktionen als Haupt­elemente der Architektur verwendet werden und die Entwicklung des physikalischen Systems ganz klar der funktionalen Struktur untergeordnet wird. Daher kann sich der Architekt voll und ganz auf die Funktionalität des Systems konzentrieren. Des Weiteren eignen sich die Prinzipien der funktionalen Programmierung besonders gut für den Entwurf einer funktionalen Softwarearchitektur.Zwar ist die Entwicklung einer funktionalen Architektur auch in Programmiersprachen wie C#, Java, C++ oder Kotlin möglich, aber noch einfacher ist die Herangehensweise bei der Verwendung einer funktionalen Sprache wie F#, Haskell, Clojure oder Erlang.

Ansatz und Denkrichtung

Es gibt Grundregeln der funktionalen Programmierung, die besonders relevant für die Entwicklung einer funktionalen Softwarearchitektur sind.Die erste Grundregel ist, dass an die Stelle eines Objekts mit gekapseltem Zustand (objektorientiert) die Funktion tritt, die auf unveränderlichen Daten arbeitet, Das heißt, dass Funktionen Stand-alone-Werte sind und als Variablen zugeordnet, in Listen gespeichert, als Parameter übergeben oder als Ergebnisse zurückgegeben werden können. Daher erlauben funktionale Sprachen ein von Typen getriebenes, systematisches Design von Datenmodellen und Funktionen.In einer funktionalen Architektur ist die Basis somit immer eine Funktion, die den Grundbaustein der Architektur bildet. Die funktionale Architektur beschreibt somit das Ergebnis der Funktionen (Bild 1), die das Produkt erfüllen muss, und legt fest, wie diese Funktionen aufgegliedert sind.
Aufbau funktionaler Architektur (Bild 1) © Autor
Die Hauptfunktionen werden aus den Systemanforderungen abgeleitet und auf einer relativ hohen Ebene beschrieben. Anschließend werden diese in kleinere, weniger komplexe Teilfunktionen unterteilt. Die funktionale Architektur beschreibt die Hierarchie der Funktionen und wie die verschiedenen Teilfunktionen miteinander verbunden sind, sie gibt aber nicht an, wie die einzelne Funktion technologisch realisiert wird.Eine weitere Grundregel bezieht sich auf die Zusammensetzung der Funktionen, um ein System aufzubauen. Hierbei geht es um die Komposition als wichtigsten Weg. Zwei einfache Funktionen können komponiert werden, indem der Ausgang der einen mit dem Eingang der anderen verbunden wird. Das Ergebnis ist eine darauffolgende Funk­tion, die als Ausgabepunkt für weitere Kompositionen verwendet werden kann (Bild 2).
Sichtweise auf die Pipeline (Bild 2) © Autor
Komposition ist ein so wichtiges Konzept, dass funktionale Sprachen eine Reihe von Standardwerkzeugen besitzen, wie zum Beispiel Monaden, die ­eine Komposition ermöglichen, auch wenn die Ein- und Ausgänge nicht ganz übereinstimmen. Das ist dann der Fall, wenn es in der Entwicklung zu Seiteneffekten kommt.Eine Monade [1] stellt eine generische Struktur in der funktionalen Programmierung dar, die Seiteneffekte behandelt, etwa wenn eine Funktion eine Variable ändert, die nicht lokal oder außerhalb ihres Anwendungsbereichs ist. Sie bietet einen skalierbaren Ansatz für die Komposition einer Funk­tion durch die Verwendung von Bind- und Unit-Konzepten.Bild 2 zeigt die architektonische Sicht, die sich aus dem Zusammenfügen von größeren Funktionen aus kleineren Funktionen ergibt. Das System funktioniert dabei eher wie Pipelines beziehungsweise wie funktionale Workflows mit Ein- und Ausgängen. Hierbei hat jede der Funktionen in der Pipeline beziehungsweise dem Workflow im Allgemeinen die gleiche Struktur. Es werden Daten gelesen, logische Entscheidungen getroffen und Daten bei Bedarf umgewandelt, und schließlich werden alle neuen Daten oder Ereignisse am anderen Ende ausgegeben. Jeder dieser Schritte kann wiederum als eine kleinere Funktion behandelt werden. Verzweigungen und andere Arten von Komplexität können hinzukommen; aber selbst wenn die Pipelines und Workflows größer und komplexer werden, fließen die Daten immer nur in eine Richtung.Dieser kompositorische Ansatz bedeutet, dass Sie nur diejenigen spezifischen Komponenten kombinieren, die Sie für eine bestimmte Logik benötigen. Wenn Sie im System neue Funktionen hinzufügen, wird die für jeden Arbeitsablauf erforderliche Funktionalität unabhängig definiert. Damit legt die funktionale Softwarearchitektur bei der Strukturierung eine andere Herangehendweise an den Tag als der beliebte objektorientierte Ansatz und vermeidet so viele Arten von Komplexität und Wechselwirkung im System.Wenn Sie in einer funktionalen Softwarearchitektur ein und dieselbe Funktion in verschiedenen Arbeitsabläufen benötigen, so können Sie diese Funktionalität einmal als Unterfunktion definieren und diese dann als gemeinsamen Schritt in den Arbeitsabläufen (Workflow-Prinzip), die diese Funk­tion benötigen, wiederverwenden. Das verleiht dem kompositorischen Ansatz seinen Charme. Pipelines beziehungsweise Workflows werden als unabhängige Einheiten entworfen und erstellt, die alle Vorteile der Wiederverwendung und Modularisierung nutzen.

Interaktion

Beim Lesen und Schreiben von Dateien oder auch bei Zugriffen auf Datenbanken und APIs wird in der funktionalen Programmierung darauf geachtet, diese Vorgänge so weit wie möglich an den Rändern der Pipeline zu halten. Es soll durchgängig gewährleistet sein, dass der Kernbereich (Core Domain), die Geschäftslogik, von der Infrastruktur (I/O-System) isoliert wird.Der Infrastrukturcode weiß somit über die Kerndomäne Bescheid, aber nicht umgekehrt. Die Abhängigkeiten sind nur einseitig, und die I/O-Operationen werden, wie in Bild 3 dargestellt, an den Rändern der Pipeline gehalten.
Zwiebelarchitektur mit überlagertem funktionalem Workflow (Bild 3) © Autor
Einer der Vorteile der funktionalen Programmierung ist, dass diese Isolierung der Kerndomäne von der Infrastruktur auf natürliche Weise erfolgt. Der funktionale Ansatz sorgt hier ganz automatisch für die Verwendung einer sogenannten Zwiebelarchitektur (Onion Architecture) beziehungsweise hexagonalen Architektur für die Infrastruktur [2]. Diese Architektur überlagert in der Praxis den funktionalen Workflow (Bild 3).Dadurch ergibt sich auch eine klare Unterscheidung zwischen Unit- und Integrationstests. Unit-Tests beziehen sich auf den Kernbereich und sind deterministisch und schnell, während Integrationstests für die gesamte Pipeline beziehungsweise den Workflow durchgeführt werden.Als Softwarearchitekt besteht somit die Herausforderung darin, zu entscheiden, wie Sie diese Pipelines oder Workflows in logische Einheiten gruppieren.Es gibt in der modernen Softwareentwicklung eine Vielzahl von Entwurfsmustern. die bei der Entwicklung helfen. Die klassischen Prinzipien der geringen Kopplung und der hohen Kohäsion gelten für funktionalen Code ebenso wie für objektorientierten Code.

Frontend-Entwicklung

Die funktionale Architektur hat auch mit dem Einzug von Single Page Applications (SPA) an Bedeutung gewonnen. Auch hier liegt der Schwerpunkt von funktionaler Programmierung auf Unveränderlichkeit, einseitigem Datenfluss und Input/Output an den Rändern, als Reduzierung der Komplexität.Das am häufigsten verwendete funktionale Frontend-Entwurfsmuster ist Model-View-Update (MVU) aus der Elm-Architektur [3].In diesem Entwurf enthält die Anwendung ein unveränderliches Modell mit zwei Schlüsselfunktionen. Die erste ist die sogenannte Aktualisierungsfunktion, die das Modell aktualisiert, wenn eine Nachricht oder ein Ereignis vom Browser eintrifft. Die zweite Funktion ist die Ansichtsfunktion, die eine Ansicht aus diesem Modell rendert.Die gerenderte Ansicht kann Browser-Ereignisse mit Nachrichten verknüpfen, die im Domänenmodell definiert sind und ihrerseits eine entsprechende Domänennachricht auslösen können.Funktionale Programmierprinzipien lassen sich also im gesamten Bereich von Microservices und Serverless im Back­end bis hin zu Model-View-Update im Frontend verwenden. Die funktionale Architektur ist aufgrund der funktionalen Programmierung auch sehr gut nachvollziehbar, da der Code, also die Funktion beziehungsweise der Anwendungsfall, die Architektur darstellt.

Unveränderliche Daten

Die größte Besonderheit beim Arbeiten mit funktionalen Sprachen ist das Programmieren mit unveränderlichen Daten. Im Gegensatz zu anderen Sprachen gibt es hier keine ­Zuweisungen, die Attribute von Objekten verändern können. Das heißt: Wenn in einem funktionalen Programm Veränderungen durchgeführt werden sollen, generiert man immer neue Objekte. Daraus ergeben sich folgende Vorteile und Bausteine:
  • Konsequente Verwendung unveränderlicher Daten.
  • Keine versteckte Abhängigkeit.
  • Kontrolle von Seiteneffekten, da keine verdeckten Zustandsänderungen.
  • Keine inkonsistenten Daten.
  • Modellierung mit Kombinatoren und höher stehenden Abstraktionen (Verwendbarkeit von Domänenmodellierung).
  • Architekturstruktur über Funktionen.
Nun aber genug der Theorie, jetzt geht es an die Umsetzung. Eine leicht zu erfassende und zu testende Architektur ist für den Anfang ein guter Ausgangspunkt zum Verständnis funktionaler Softwarearchitektur.

Beispiele

Die oben genannten Vorteile und Bausteine der funktionalen Programmierung führen zu Softwarearchitekturen, die robuster und flexibler sind als traditionelle objektorientierte Architekturen. Sie bestechen durch wenige Abhängigkeiten, bessere Wiederverwendbarkeit und geringere Komplexität. Die erweiterten Abstraktionsmöglichkeiten führen außerdem zu besseren Modellen, die näher an der Domäne sind und sich dadurch besonders für die strategischen Patterns des Domain Driven Designs (DDD) [4] eignen, sowie zu weniger technischem Ballast und gutem Domänenwissen.So bedeuteten beispielsweise das Design und die Implementierung einer Architektur für eine Enterprise-Business-Applikation immer eine große Herausforderung. Aufgrund der erhöhten Abstraktionsmöglichkeiten in funktionalen Sprachen werden hier aber keine externen Notationen benötigt. In der funktionalen Programmierung ist Architektur Code; daher ist eine Abkopplung überhaupt nicht notwendig, was es erleichtert, eine einfach zu verstehende und zu testende Architektur zu erstellen.Im Folgenden soll anhand zweier einfacher Beispiele die Entwicklung der funktionalen Architektur veranschaulicht werden. Leider wird die Architektur erst richtig wichtig, wenn Größe und Komplexität zunehmen; dies würde allerdings den Rahmen des Artikels sprengen. Daher werden die Beispiele vor allem die grundlegenden Aspekte zum Nachvollziehen veranschaulichen.Vorkenntnisse in funktionaler Programmierung mit F# sind von Vorteil, aber nicht zwingend notwendig.

Projektvorlage nutzen

Sollten Sie noch nicht mit F# programmiert haben, so finden Sie einen einfachen Einstieg über die Projektvorlage F# Tutorial(.NET Framework) in Visual Studio 2022. Darin enthalten ist das nötige Rüstzeug für den Einstieg in F#. Sie finden das entsprechende Tutorial für F# unter Create a new project in der Auswahlliste F#, Windows und All project types am Ende der Auswahlliste (Bild 4).
Projektauswahl in Visual Studio (Bild 4) © Autor

Anwendungsfall changeEmail

Das nachfolgende F#-Beispiel soll möglichst einfach und überschaubar bleiben. Es zeigt die Implementierung eines Anwendungsfalls zum Ändern einer E-Mail-Adresse in einer fiktiven Enterprise-Business-App-Architektur in Form einer Funktion in der Pipeline.Das aufgezeigte Modul DataAccess dient der Verdeutlichung zum Lesen und Schreiben der Daten im Prozess und wird nicht als implementierter Code aufgezeigt. Listing 1 zeigt die exemplarische Implementierung des Anwendungsfalls als Funktion in F#. Das Wichtigste im Programmcode dieses Listings ist die Typsignatur des Rückgabetyps DomainEvent. Bei diesem Muster handelt es sich um eine Liste der zurückgegebenen Ereignisse.
Listing 1: Die F#-Funktion changeEmail
let changeEmail (readCustomer : <span class="hljs-type">ReadCustomer</span>) <br/>      (saveCustomer : <span class="hljs-type">SaveCustomer</span>) <br/>      (cmd : <span class="hljs-type">ChangeEmailCommand</span>) : <br/>      <span class="hljs-type">Result</span><((DomainEvent list) * Customer),string> =<br/>    readCustomer cmd.CustomerId<br/>    |> Result.map (Customer.updateEmail cmd.NewEmail)<br/>    |> Result.bind saveCustomer 
Über die Liste DomainEvent werden die Domänenereignisse während der Ausführung gesammelt, und diese unterstützen die Ausgabe der erforderlichen Daten zur gleichen Zeit, in der die in der Liste vorhandenen Aggregate persistiert werden.Diese Implementierung ist auch immer sehr leicht auf ihren Anwendungsfall zu testen, da die Input-/Output-Teile an ­eine Funktion übergeben werden. Sollte die Komplexität des jeweiligen Anwendungsfalls wachsen, so können die einzelnen Erweiterungen einfach an die Funktion angehängt werden.Listing 2 veranschaulicht die Verwendung der Funktion changeEmail. Im Beispiel zeigt main args die Root-Implementierung für den I/O in der Anwendung, während handlecommand für die Weiterleitung der geparsten Eingabe sorgt. Das Parsen der Eingabe übernimmt der Befehl cmd, und das Ergebnis wird durch die Weitergabe des Befehls result an den gewünschten Anwendungsfall übergeben.
Listing 2: Nutzung der Funktion
<span class="hljs-keyword">let</span> main <span class="hljs-attr">args</span> =<br/>  <br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">readCustomer</span> = DataAccess.readCustomer<br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">saveCustomer</span> = DataAccess.saveCustomer<br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">changeEmail</span> = Usecase.changeEmail <br/>      readCustomer saveCustomer<br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">newCustomer</span> = Usecase.newCustomer saveCustomer<br/>    <span class="hljs-keyword">let</span> handle <span class="hljs-attr">command</span> =<br/>        match command <span class="hljs-keyword">with</span><br/>        | Commands.NewCustomer cmd -> newCustomer cmd<br/>        | Commands.ChangeEmail cmd -> changeEmail cmd<br/>        <br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">cmd</span> = Mapper.inputToCommand args    <br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">result</span> = cmd |> Result.bind handle 
Der Fall zeigt auf, dass Funktionen von verschiedenen Anwendungsfällen gemeinsam genutzt werden können. Die Verwendung einer Ergebnisliste stellt sich hierbei als sehr nützliches Muster heraus. Verwenden Sie bei der Implementierung von Funktionen so oft wie möglich spezielle Typen und bauen Sie innerhalb des Anwendungsfalls eine Pipeline für den Prozess auf, der die erstellten Typen nutzt. Das Beispiel zeigt die Kernidee in der funktionalen Programmierung: Es werden Funktionen höherer Ordnung und reine Funktionen so kombiniert, dass sie zu einer funktionalen Architektur führen. Das folgende Beispiel vollzieht nach, wie aus F#-Modulen eine entsprechende Architektur wird.

Architektur ist Code

Die grundlegendsten Teile eines F#-Programms sind, wie bereits erwähnt, Funktionen; diese werden im Allgemeinen in Modulen angeordnet. Die Funktionen bearbeiten Anwendungsfälle (Pipelines beziehungsweise Workflows), um Ausgaben zu erzeugen. Über die Module besteht die primäre Möglichkeit, organisierte Gruppierungen durchzuführen. Hierdurch entsteht über den Code automatisch eine funktionale Architektur.Um mit der Entwicklung eines kleinen Maschinensteuerungssystems in F# in diesem Beispiel zu starten, verwenden Sie Visual Studio und die Projektvorlage F# Console App für die Erstellung der Beispielarchitektur.Öffnen Sie hierfür Visual Studio 2022. Die Projektvorlage finden Sie unter der Auswahlliste F# nach dem Aufruf von Create a new project. Wählen Sie hier die Vorlage F# Console App aus und vergeben Sie als Vorbereitung auf das Beispiel den Namen ArchitectureDemo. Schließen Sie das Dialogfenster nach der Auswahl des
Frameworks.NET 7.0 über die Schaltfläche Create. Visual Studio erstellt daraufhin automatisch ein lauffähiges F#-Konsolenprogramm.Im nächsten Schritt fügen Sie jetzt der Projektumgebung die benötigten Module für die Funktionen hinzu. Erstellen Sie in dem Projekt über Add | New Items ... folgende vier F#-Sourcefiles:
  • ControlMap.fs
  • Extension.fs
  • OperatorInterface.fs
  • Subsystem.fs
Die Sourcefiles dienen jetzt der Aufnahme von F#-Modulen, die eine Gruppierung von F#-Codekonstrukten wie Werte, Typen und Funktionswerte ermöglichen. Das Gruppieren von Code in Modulen hilft dabei, verwandten Code zusammenzuhalten und Namenskonflikte im Programm zu vermeiden.Das Modul ControlMap stellt den einfachsten Teil des Beispielcodes dar. Hier werden die Konstanten für die Maschinensteuerung abgebildet, sodass zwei verschiedene Subsysteme nicht versuchen können, die gleichen Daten zu verwenden, was zwangsläufig zu Konflikten führen würde. Listing 3 zeigt den einfachen Aufbau der Konstanten in den entsprechenden Modulen.
Listing 3: Die Module zur Maschinensteuerung
module ControlMap<br/>module <span class="hljs-attr">Driver</span> =<br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">leftPort</span>  = <span class="hljs-number">0</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">rightPort</span> = <span class="hljs-number">1</span>    <br/>module <span class="hljs-attr">Copilot</span> =<br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">copilotPort</span> = <span class="hljs-number">2</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">ControlStick</span> = <span class="hljs-number">2</span><br/>module <span class="hljs-attr">PowerMotor</span> =<br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">LeftMotor</span> = <span class="hljs-number">4</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">RightMotor</span> = <span class="hljs-number">3</span>    <br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">leftServoPort</span> = <span class="hljs-number">7</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">rightServoPort</span> = <span class="hljs-number">8</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">MainVictor</span>  = <span class="hljs-number">1</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">CrossVictor</span> = <span class="hljs-number">5</span><br/>    <br/>module <span class="hljs-attr">CANBoard</span> =<br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">leftTopDrive</span> = <span class="hljs-number">1</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">leftBottomDrive</span> = <span class="hljs-number">2</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">rightTopDrive</span> = <span class="hljs-number">3</span><br/>    <span class="hljs-keyword">let</span> <span class="hljs-attr">rightBottomDrive</span> = <span class="hljs-number">4</span> 
Die let-Bindung in Listing 3 ermöglicht zudem das Binden eines Wertes an einen Namen, ähnlich wie bei Variablen in anderen Sprachen. let-Bindungen sind standardmäßig unveränderlich. Das heißt: Sobald ein Wert oder eine Funktion an einen Namen gebunden ist, ist keine direkte Änderung mehr möglich. Benötigen Sie eine veränderliche Bindung, so können Sie die Syntax let mutable ... verwenden.Das Modul Extension zeigt die Definition ­eines Aufbaus einer eigenen Bibliothek in F#. Die Funktion von Timeout in Listing 4 nimmt einfach zwei Curried-Argumente (zu Currying siehe [5]) entgegen und gibt einen neuen TimedCommand zurück.
Listing 4: Erstellen einer Extension in F#
open externalLib<br/>let (+-) (grp : <span class="hljs-type">CommandGroup</span>) (cmd : <span class="hljs-type">Command</span>) =<br/>    grp.AddSequential cmd<br/>    grp<br/>let (+|) (grp : <span class="hljs-type">CommandGroup</span>) (cmd : <span class="hljs-type">Command</span>) =<br/>    grp.AddParallel cmd<br/>    grp<br/>let timeout (length : <span class="hljs-type">double</span>) (cmd : <span class="hljs-type">Command</span>) =<br/>    {<span class="hljs-keyword">new</span> TimedCommand(length) <span class="hljs-keyword">with</span><br/>        override this.Initialize () = cmd.Start ()<br/>        override this.<span class="hljs-keyword">End</span> () = <span class="hljs-keyword">if</span> cmd.IsRunning() <br/>          <span class="hljs-keyword">then</span> cmd.Cancel ()<br/>    } 
Der Rückgabetyp in der Funktion ist das Ergebnis des letzten in der Funktion ausgeführten Ausdrucks. Das Schlüsselwort open ähnelt dem using-Schlüsselwort in C#. Es wird einfach für das entsprechende Modul oder den Namespace verwendet. Das Beispiel zeigt damit exemplarisch, wie Sie in F# eine externe ­Bibliothek einbinden können.Listing 5 stellt das Operator-Interface für die Steuerung zur Verfügung. Die aufgeführten Member definieren in F# Funktionen, die Teil einer Typdefinition sind. Hierzu zählen zum Beispiel Datensätze, Klassen, unterscheidbare Unions, Schnittstellen und Strukturen. Die Member sind öffentlich zugänglich, da sie im nachfolgenden Code benötigt werden.
Listing 5: Das Operator-Interface
module OperatorInterface<br/>open externalLib<br/>type OI() =<br/>    static member val Instance : OI = new OI()<br/>    member this<span class="hljs-selector-class">.driverLeft</span> = ControlStick(ControlMap<span class="hljs-selector-class">.Driver</span><span class="hljs-selector-class">.leftPort</span>)<br/>    member this<span class="hljs-selector-class">.driverRight</span> = ControlStick(ControlMap<span class="hljs-selector-class">.Driver</span><span class="hljs-selector-class">.rightPort</span>)<br/>    member this<span class="hljs-selector-class">.copilotStick</span> = ControlStick(ControlMap<span class="hljs-selector-class">.Copilot</span><span class="hljs-selector-class">.copilotPort</span>)<br/>    member this<span class="hljs-selector-class">.LeftYAxis</span>              with get() = this<span class="hljs-selector-class">.driverLeft</span><span class="hljs-selector-class">.GetRawAxis</span>(<span class="hljs-number">1</span>)<br/>    member this<span class="hljs-selector-class">.RightYAxis</span>             with get() = this<span class="hljs-selector-class">.driverRight</span><span class="hljs-selector-class">.GetRawAxis</span>(<span class="hljs-number">1</span>)<br/>    member this<span class="hljs-selector-class">.CopilotRightTrigger</span>    with get() = this<span class="hljs-selector-class">.copilotStick</span><span class="hljs-selector-class">.GetRawAxis</span>(<span class="hljs-number">3</span>)<br/>    member this<span class="hljs-selector-class">.CopilotLeftTrigger</span>     with get() = this<span class="hljs-selector-class">.copilotStick</span><span class="hljs-selector-class">.GetRawAxis</span>(<span class="hljs-number">2</span>) 
Das Hauptaugenmerk liegt auf der Datei Subsystem.fs. Listing 6 zeigt das Kernkonzept von F#. Im Subsystem sind die Befehle zusammengeführt, die für die gesamte Subsystemlogik gemeinsam benötigt werden.
Listing 6: Implementierung des Subsystems
type Manipulators() =<br/> <br/>    inherit Subsystem()<br/> <br/>    let solenoid = new DoubleSolenoid(ControlMap.Solenoid.Forward,<br/>        ControlMap.Solenoid.Reverse)	<br/>    <br/>    member this.State		<br/>        with get() =<br/>            if solenoid.Get() = DoubleSolenoid.Value.Reverse<br/>            then Lowered else Raised<br/> <br/>        and set value =<br/>            solenoid.Set(match value with<br/>                | Lowered -> DoubleSolenoid.Value.Reverse<br/>                | Raised -> DoubleSolenoid.Value.Forward)<br/> <br/>    member private this.SetState state =<br/>        {new InstantSubsystemCommand(this) with<br/>            override cmd.Initialize() = this.State <- state}<br/> <br/>    member this.Lower () = this.SetState Lowered<br/>    member this.Raise () = this.SetState Raised	<br/>    override this.InitDefaultCommand() = ()<br/> <br/>    interface IDisposable with<br/>        override this.Dispose() = solenoid.Dispose() 
Als Erstes wird über das Schlüsselwort type eine neue Klasse erstellt, die über die inherit-Klausel vom Subsystem erbt. Somit können Sie über die abgeleitete Klasse auf die Methoden und Eigenschaften der Basisklasse zugreifen.Da in F# das this-Schlüsselwort nicht automatisch gesetzt wird, muss es manuell angegeben werden. Die nachfolgenden Funktionen bilden die erdachte Steuerung über den Status der Maschine. Beachten Sie, dass es sich hierbei um die internen Methoden der Klasse handelt, anstatt die Befehle separat als Klasse zu deklarieren. Durch den Member ermöglichen Sie den Zugriff als Mitglied einer Instanz des Subsystems. Das IDisposable stellt die ordnungsgemäße Verwaltung von Bereinigungscode im .NET-Umfeld dar.Der Hauptteil des Programms wird in der Datei Program.fs abgebildet. Sie enthält die Definitionen aus Listing 7 und zeigt das Deklarieren der Befehlsgruppen. Aus der CommandLib werden Befehlsgruppen deklariert, indem sie von CommandGroup erben und dann im Konstruktor Aufrufe an addSequential und addParallel hinzufügen.
Listing 7: Implementierung der Befehle
open OI<br/>open Extensions<br/>open Subsystems<br/>open externalLib<br/>open externalLib.Commands<br/>type Machine() =<br/>    inherit CommandMachine()<br/>    let mutable autonomousCommand : Command option = None<br/>    let oi = OI.Instance<br/>    let drive = new Drive.Drive()<br/>    let intake = new Subsystems.Intake();<br/>    let shooter = new Subsystems.Shooter();  <br/>    let aManipulators = new Subsystems.AManipulators();<br/> <br/>    override this.MachineInit() =<br/>    <br/>        let driveDistance = drive.DriveDistanceCmd<br/>        let wait time = new WaitCommand(time)<br/>        let moveActuatorsDown () =<br/>            new CommandGroup()      <br/>            +- intake.Lower ()      <br/>        let moveActuatorsUp () =<br/>            new CommandGroup()<br/>            +- aManipulators.Raise ()<br/>            +- intake.Raise ()      <br/>        let moveBallIntoStorage () =<br/>            new CommandGroup()<br/>            +- intake.SetMotor Forward   
Mit den Definitionen der Operatoren +- und +| aus der Extension.fs können einzelne Aufrufe von addSequential auf weniger ausführliche Weise miteinander verkettet werden.Der Pipe Operator |> wird in F# auch bei der Verar­beitung von Daten häufig verwendet. Mit diesem Operator können Sie Pipelines von Funktionen auf flexible Weise einrichten. Auch hier ­erfolgt der Zugriff auf jeden Befehl über das Subsystem. Daher liest sich der Code viel intuitiver als in einer objektorientierten Implementierung.Bild 5 zeigt die funktionale Struktur der Module im Maschinensteuerungssystem. In dieser Struktur lassen sich auch sehr einfach Unterfunktionen realisieren, so zum Beispiel ­eine Startkontrolle.
Aufbau der Module (Bild 5) © Autor

Fazit

Gerade heute kann eine funktionale Architektur einen entscheidenden Wettbewerbsvorteil darstellen. Es entsteht eine evolutionäre und nachhaltige Architektur, die es ermöglicht, schnell auf technische Neuentwicklungen zu reagieren. Durch die geringere Kopplung und weniger Abhängigkeiten erhöhen Sie die Wartbarkeit des Codes. Auch ein Blick auf die zugehörigen Architekturmuster wie funktionale Datenstruktur, Monoid, Funktor, Monade und Model-View-Update lohnt sich auf jeden Fall.

Fussnoten

  1. Monaden bei Wikipedia, http://www.dotnetpro.de/SL2405FunkArchitektur1
  2. Onion Architecture beziehungsweise Hexagonale ­Architektur bei Wikipedia, http://www.dotnetpro.de/SL2405FunkArchitektur2
  3. Elm-Architektur, http://www.dotnetpro.de/SL2405FunkArchitektur3
  4. Domain-Driven Design bei Wikipedia, http://www.dotnetpro.de/SL2405FunkArchitektur4
  5. Currying bei Wikipedia, http://www.dotnetpro.de/SL2405FunkArchitektur5

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Evolutionäres Prototyping von Business-Apps - Low Code/No Code und KI mit Power Apps
Microsoft baut Power Apps zunehmend mit Features aus, um die Low-Code-/No-Code-Welt mit der KI und der professionellen Programmierung zu verbinden.
19 Minuten
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige