Skip to content

Clean Architecture

Maximilian Burr edited this page May 28, 2023 · 15 revisions

Clean Architecture

Kernteil

Um die Clean Architecture im Kernteil zu ermöglichen wurde eine PluginArchitektur entwickelt. Der Kern hat keine Abhängigkeiten auf die jeweiligen Plugins. Er stellt ein IPlugin Interface bereit, was von den jeweiligen Plugins implementiert werden muss, damit Sie mit dem Kern kommunizieren können. So können sehr leicht Programmteile entfernt oder hinzugefügt werden ohne vorhandenen Source Code zu verletzen Da die Plugins trotzdem geladen und instanziiert werden, wurde ein Plugin Loader entwickelt, der mithilfe eines ClassLoaders die Plugin Klassen nachladen kann.

Der Core hat durch das IPlugin Interface keine Abhängigkeiten an andere Programmteile.

Die Plugins die mithilfe des IPlugin Interface erweitern die Funktionalität des Kerns um eine spezifische Funktion. Über diese Art von "Kernplugins" sollen dann Funktionalitäten wie die Schnittstellen zu verschiedenen Smarthome Systemen, Persistenzplugins, Dashboards, etc hinzufügt werden.

Durch die dynamisc

Minecraft-Smarthome

Das Minecraft-Smarthome wurde mittels der Clean Architecture in Module unterteilt. Seine Aufgabe ist es, einen Minecraft-Server zu einem Smarthome-System zu machen, und über das Netzwerk mit dem Smarthome-Connector zu kommunizieren. Dabei ergeben sich zwei offensichtliche Abängigkeiten, nämlich die Netzwerk-Kommunikation und die Interaktion mit dem Minecraft-Server. Natürlich gibt es hier inhärente Abhängigkeiten, die auch durch Nutzung der Clean Architecture nicht verhindert werden. Es wurde jedoch versucht, diese auf ein Minimum zu beschränken. Dazu wurde herausgearbeitet, welche Eigenschaften von Minecraft notwendig sind, um darin ein Smarthome-System umzusetzen. Jedes Software-System, das diese Eigenschaften besitzt, kann somit ohne Änderung des Applikations-Codes Minecraft ersetzen. Auch die Abhängigkeit auf unseren Smarthome-Connector ist vorgesehen. Da dieser ebenfalls von uns entwickelt wurde, ist ein Austausch hier nicht vorgesehen. Trotzdem wurde die Schnittstelle zwischen beiden zur Erleichterung von potentiellen späteren Änderungen minimal gehalten. Die Verwendung einer Rest-API wurde vollständig von der Applikationslogik entkoppelt. Die Rest-API könnte also nur durch Änderung ihres Plugins und Adapters durch beispielsweise grpc, Pipes, Shared Memory oder direkte Java-Aufrufe ersetzt werden.

Die "Module" sind hierbei sowohl Java-Module als auch Maven-Module. Die "Richtung" der Dependencies wird daher auch durch den Compiler durchgesetzt. Nur die Plugins, und nicht die Adapter oder der Applikations-Code, haben Zugriff auf die jeweiligen verwendeten Libraries, wie die Minecraft-Server-API "Paper", die wir verwenden.

Im Minecraft-Smarthome wurden dafür drei Module (Schichten) implementiert.

  • Eine Anwendungsschicht namens controller
  • Eine Adapterschicht namens minecraft-adapter
  • Ein Plugin namens minecraft

Plugin und Adapter zur Netzwerkkommunikation waren geplant, wurden aber nicht umgesetzt.

Im Folgenden werden die Begriffe "Minecraft-Plugin" und "Netzwerk-Plugin" verwendet, um das Plugin und den zugehörigen Adapter zu referenzieren. Zur einzelnen Adressierung von Adapter oder Plugin werden deren Namen verwendet.

Die Aufgabe von controller ist, ein Smarthome mit verschiedenen Geräten und deren aktuellen Zuständen zu repräsentieren und mit dem Minecraft- und Netzwerk-Plugin zu kommunizieren, welche die Zustände ändern können. Eine solche Änderung soll dann dem jeweils anderen Plugin mitgeteilt werden. Das Minecraft-Plugin kann außerdem Geräte hinzufügen und entfernen.

minecraft bindet einen Minecraft-Server als "UI" ein, um Zustände des Smarthomes darin anzeigen, ändern sowie Geräte hinzufügen und entfernen zu können.

minecraft-adapter passt den Minecraft Server an die Schnittstelle von controller an. Er abstrahiert gewissermaßen die Eigenheiten des Verhaltens von Minecraft weg, die auch nach der Abstraktion der eigentlichen Dependency des Minecraft Servers noch bleiben.

Im Folgenden soll beispielhaft gezeigt werden, wie Dependency Inversion eingesetzt wurde, um Kommunikation zwischen den Schichten nicht nur von Plugin zu Adapter zu Anwendungscode, sondern auch andersrum zu ermöglichen.

Das Interface DeviceIdentifier im Modul Controller wird vom Controller zur Identifikation und Unterscheidung von Geräten genutzt. Das Minecraft-Plugin Muss beim Hinzufügen eines Geräts eine Instanz von DeviceIdentifier übergeben. Es ist seine Aufgabe, es so zu implementieren, dass mittels der equals und hashCode Methode eine Unterscheidung verschiedener Geräte möglich ist und dass das Plugin mit dem korrekten DeviceIdentifier das entsprechende Gerät zuordnen kann, um beispielsweise eine Zustandsänderung anzuzeigen. Das Modul Controller trifft jedoch keine weiteren Annahmen, wie das geschehen soll.

Die Klasse SadBlockIdentifier (LINK) in minecraft-adapter erweitert DeviceIdentifier. minecraft-adapter legt fest, dass jedes Gerät durch einen Minecraft-Block repräsentiert werden soll. Zur eindeutigen Beschreibung eines Blocks auf einem Minecraft Server genügt dessen Position, die sich durch die UUID seiner Welt sowie dreidimensionale Integer-Koordinaten ergibt. Entsprechend gibt es die Attribute worldId, x, y und z und zugehörige Getter. Die Klasse ist immutable und akzeptiert keine Null-Werte beim Instantiieren. Die Methoden equals und hashCode werden implementiert und Nutzen zur Gleichheitsprüfung / Berechnung die vier beschriebenen Attribute.

controller kann somit verschiedene DeviceIdentifier des Minecraft-Plugins auf Gleichheit prüfen, indem die Positionen der zu den Geräten zugehörigen Blöcke verglichen werden und das, obwohl das Modul nichts von Blöcken weiß.

Als weiteres Beispiel bietet sich der verwendete Logger an. Es war von Interesse, Logging-Ausgaben auf der Minecraft-Server-Konsole auszugeben. Da controller nichts von Minecraft wissen darf und minecraft-adapter nicht mit Minecraft sprechen darf, wird wieder Dependency Inversion eingesetzt. Das Interface Logger im Modul Controller stellt Methoden zum Loggen auf verschiedenen Severity-Stufen bereit.

Die Klasse SadLogger in minecraft implementiert dieses Interface und seine Methoden und gibt die Aufrufe an den Minecraft-Server-Logger weiter. Der typischerweise in Java für Implementierungen von gleichnamigen Interfaces verwendete Suffix "Impl" wurde aus nicht sinnvollen Gründen in diesem Projekt durch den Präfix "Sad" ersetzt.

Die Dependency Injection wurde verwendet, um Instanzen der Interfaces an den Stellen, an denen Aufrufe geschehen sollen, zu erhalten. In einer Methode der Klasse SadBlockUpdateListener soll zum Beispiel eine Log-Ausgabe stattfinden. Die Logger-Instanz wird im Konstruktor einfach erwartet und zur Verwendung in der Methode als Attribut gespeichert.

Zum Instantiieren der Klassen bei Programmstart besitzt jedes Modul ein Subpackage namens start, welches eine oder mehrere Starter-Klassen beinhaltet, die von Starter-Klassen von in der Clean-Code-Zwiebel weiter außen liegenden Modulen verwendet werden können. Diese Start-Klassen instantiieren jeweils Klassen in ihrem Modul und bieten die Instanzen über Getter an. Ein weiter außen liegendes Modul holt sich die Instanzen und füttert damit und mit eigenen Instanzen die Dependency Injection seiner Klassen.

Anstatt einer main-Methode wird das Programm vom Minecraft-Server gestartet. Der Einstiegspunkt für unseren Code wandert damit in eine Unterklasse von org.bukkit.plugin.java.JavaPlugin (MinecraftPlugin). Da minecraft das einzige Modul mit Abhängigkeit auf die Minecraft-API ist, befindet sich diese dort. Darin wird beim Aufruf der onEnable Methode die Starter-Klasse des Moduls minecraft instantiiert.

Clone this wiki locally