-
Notifications
You must be signed in to change notification settings - Fork 0
Clean Architecture
Um die Clean Architecture im Smarthome-Bridge umzusetzen, wurde eine Plugin Architektur entwickelt. Der Kern hat keine Abhängigkeiten auf die jeweiligen Plugins. Er stellt ein Interface namens IPlugin bereit, was von den jeweiligen Plugins implementiert werden muss, damit sie mit dem Kern kommunizieren können.
So können sehr einfach Programmteile entfernt oder hinzugefügt werden, ohne vorhandenen Source Code zu verletzen.
Da die Plugins trotzdem geladen und instantiiert werden, wurde ein Plugin Loader entwickelt, das mithilfe eines ClassLoaders die Plugin Klassen nachladen kann. Es wurden Vorbereitungen getroffen, um die Plugins später auch von JAR Dateien nachladen zu können. Damit soll gewährleistet werden, dass Benutzer der Smarthome Bridge selbst Plugins mithilfe des IPlugin Interfaces entwickeln können. Diese Plugins können einfach in einem Plugin Ordner abgelegt werden und werden dann beim Start automatisch geladen.
Der Core hat durch das IPlugin Interface keine Abhängigkeiten an andere Programmteile.
Die Plugins, die das IPlugin Interface verwenden, 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, Persistenzfunktionen, Dashboards, etc hinzufügt werden können.
Jedes Plugin erweitert um Features. Features sind einzelne Funktionen die Plugins anbieten. Die Plugins können einzelne Features am Kern registrieren. So kriegen auch andere Plugins mit, welche Features vorhanden sind.
Beispielsweise muss ein Frontend Plugin wissen, woher es seine Informationen bekommt. Dazu muss das SmartHomeAnbindungsPlugin ein CoreFeature registrieren. Für diesen Zweck können die Plugins das ICoreFeatureProvider Interface des Kerns verwenden. Damit werden die Features registriert und dem Kern bekannt gemacht. Andere Plugins können über die getCoreFeatures Methode die Features holen.
Die Plugins implementieren für jedes Feature, das registriert werden soll, das ICoreFeature Interface und registrieren das Feature mittels registerFeature des ICoreFeatureProvider Interfaces. Andere Plugins können diese Features dann über den ICoreFeatureProvider verwenden.
Diese Plugin Architektur soll in den nächsten Schritten der Entwicklung verwendet werden, um das Minecraft-Smarthome Plugin anzubinden. Leider sind wir noch nicht so weit gekommen. Die Grundsteine dafür wurden jedoch gelegt.
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 der Smarthome-Bridge 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 unsere Smarthome-Bridge ist vorgesehen. Da diese ebenfalls von uns entwickelt wurde, ist ein Austausch hier nicht geplant. 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 sind 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 in minecraft-adapter erweitert DeviceIdentifier. (Anmerkung: 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.) 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 (welcher ein "normaler" Java-Logger ist, aber nicht sein müsste. Vielleicht will man die Ausgaben später an den Spielchat umleiten?) weiter. 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 von 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.
