Skip to content

Programming Principles

Cedric Becker edited this page May 31, 2023 · 27 revisions

Programming Principles

SOLID

Single Responsibility Principle

Nach dem Single Responsibility Principle soll eine Klasse nur eine Verantwortlichkeit aufweisen.

In unserem Programmentwurf wurde das Single Responsibility Principle im Commit Configuration Commit in der Klasse Configuration umgesetzt.

Jede Klasse der Configuration Architektur hat nur eine Funktion, Eine Ausnahme stellt die Configuration Klasse selbst dar, da sie noch das IPlugin Interface implementiert. Dies soll noch in eine eigene Klasse ausgelagert werden.

Klasse Configuration

Das Klassenkonstrukt um Configuration ist aus mehreren Subklassen aufgebaut, die alle einzelne Aufgaben übernehmen.

Die Klasse Configuration hat die Aufgabe, die Konfiguration bereitzustellen. Sie implementiert die Schnittstelle zu anderen Programmteilen über die Plugin Schnittstelle des Kerns. Dies macht sie, indem sie das Interface IConfiguration implementiert.

Die Aufgabenteilung durch das Single Responsibility Principle wird dadurch erreicht, dass Funktionalitäten, wie das Speichern der Configuration auf dem Dateisystem und das Laden der jeweiligen Konfiguration, in einzelne Klassen ausgelagert wurden. Ebenso wurde die Datenhaltung der Konfiguration in einem Baum in eine extra Klassenstruktur ausgelagert.

%%{init: {'theme':'forest'}}%%
classDiagram

Configuration --> "1" ConfigMap
Configuration --> "1" ConfigurationSerializer
Configuration --> "1" ConfigurationDeserializer
Configuration ..> IConfiguration : implements
Configuration ..> IPlugin : implements
ConfigMap --> "0..n" ConfigKey
ConfigMap --|> ConfigTree
ConfigKey --|> ConfigTree

class ConfigTree{
    +member
}

class Configuration{  
    +configPath:File
}

class IConfiguration{
    <<interface>>
    +getConfiguration()
    +setConfigKey(key:String, value:String)
    +getConfigKey()
}

class ConfigMap{
    +member
}

class ConfigTree{
    +member
}
Loading

Open-Closed Principle

PluginLoader Commit

Durch das IPlugin Interface im Loader Programmteil unserer Anwendung wird das Open-Closed Principle umgesetzt.

Das IPlugin Interface stellt die load Methode bereit, die von jedem Kernplugin implementiert werden muss. (Beispiel für Plugin: ServletPlugin) Durch die Implementierung der load Methode in jedem Kernplugin wird sichergestellt, dass jedes Plugin seine eigene Initialisierung durchführen kann, ohne dass bestehender Code der Loader oder Core Klasse verändert wird. So können verschiedene Plugins dynamisch vom Kern oder einem externen Loader geladen und an den Kern angedockt werden.

package pluginmanager;


public interface IPlugin {

    void load(ICore core);

    void unload();

    void receiveNotification(String message);

    String getName();

}

Wird die Load Methode aufgerufen, initialisiert sich jedes Kernplugin selbst mit einer angepassten Loadmethode.

Der Loader ruft von jedem Plugin, das sich im passenden Java Package befindet, nach der Objekterstellung die Load Methode auf, um das Plugin zu initialisieren.

Liskov Substitution Principle

In der Configuration Klassenhierarchie, die bereits in der Klasse Configuration beschrieben wurde, wird das Liskov Substituion Principle umgesetzt. Der Commit, in der das Liskov Substitution Principle umgesetzt wurde, ist der gleiche wie der Commit, in dem das Single Responsibility Principle umgesetzt wurde: Configuration Commit

Die Klassen ConfigMap und ConfigKey erben von der Klasse ConfigTree. Die einzelnen Klassen haben dabei folgende Funktion.

  1. Basisklasse ConfigTree: Die abstrakte Klasse ConfigTree stellt die Basisklasse dar. Sie definiert eine gemeinsame Schnittstelle für verschiedene Arten von Konfigurationsknoten.

  2. Abgeleitete Klasse ConfigMap: Die Klasse ConfigMap erweitert die Basisklasse ConfigTree. Sie fügt eine spezifische Implementierung hinzu, um eine Konfigurationsstruktur als Map von Schlüssel-Wert-Paaren darzustellen. Sie stellt die Methode getConfigSubmap bereit, um eine Teilmenge der Konfigurationsmap als neue ConfigMap abzurufen, und die Methode getConfigKey, um einen spezifischen Konfigurationsschlüssel als ConfigKey abzurufen.

  3. Abgeleitete Klasse ConfigKey: Die Klasse ConfigKey erweitert ebenfalls die Basisklasse ConfigTree. Sie repräsentiert einen einzelnen Konfigurationsschlüssel mit einem Wert.

Zusammenfassend lässt sich sagen, dass das gezeigte Codebeispiel das Liskov Substitution Principle einhält, da die abgeleiteten Klassen die Verträge der Basisklasse einhalten und deren Funktionen erweitern, ohne das erwartete Verhalten zu ändern.

Im aktuellen Zustand müssen sich die Plugins, die Werte in die Configuration Datenstruktur schreiben, ihre Konfigurationhierachie selbst merken, um die richige Methode zum Lesen der Konfiguration zu verwenden. Fehlerbehandlung muss im aktuellen Zustand außerhalb der Configuration Klasse durchgeführt werden.

Interface Segregation Principle

Die im Abschnitt "[Dekorierer Pattern](#Dekorierer Pattern)" beschriebenen Klassen SadQueueingBlockUpdater und SadChunkedBlockUpdater implementieren beide das BlockUpdater Interface. Beide besitzen jedoch noch eine weitere Schnittstelle, da auf Minecraft-Ticks beziehungsweise Chunk Load Events reagiert werden muss. Wären die entsprechenden Methoden im BlockUpdater Interface deklariert, würde das Dekorierer Pattern so nicht mehr funktionieren, da jede Implementierungen auf Ticks und Chunk Loads reagieren müsste. Stattdessen sind diese Methoden daher in eigenen Interfaces namens RedstoneTickListener und ChunkLoadListener deklariert. So kann der aufrufende Code (SadMinecraftEventUnpacker) mit dem jeweils benötigten Interface arbeiten und hat keine unnötig starke Kopplung an andere involvierte Mechaniken wie BlockUpdates.

Dependency Inversion Principle

Das Dependency Inversion Principle wird im Kernteil des Progamms durch die Verwendung von Interfaces umgesetzt.

Desweiteren wird das Inversion of Control (IoC) Muster verwendet.

Zur Verwendung des Principles im Minecraft Smarthome finden sich im Kapitel Clean Archtitecture im entsprechenden Abschnitt ausführliche Beispiele.

Abhängikeit von abstrakten Schnittstellen

Anstatt Abhängigkeiten zu Implementierungen einer Klasse zu haben, hat die Klasse Core nur Abhängigkeiten zu abstrakten Schnittstellen, um das Dependency Inversion Principle umzusetzen.

Die Klasse Core hat beispielsweise eine Abhängigkeit von der abstrakten Schnittstelle IConfiguration. Statt direkt eine konkrete Implementierung von einer Klasse zu instantiieren, die die Funktionalität von IConfiguration aufweist, wird die konkrete Implementierung über den Konstruktor als Parameter vom Typ des Interfaces übergeben. Dadurch hängt Core nicht von einer spezifischen Implementierung ab, sondern nur von der abstrakten Schnittstelle. Die Klasse Core hat auch eine Abhängigkeit von der abstrakten Schnittstelle IPluginFactory. Auch hier wird die konkrete Implementierung über den Konstruktor übergeben, anstatt direkt eine spezifische Implementierung zu instantiieren. Die Klasse Core hat ebenfalls Abhängigkeiten von den abstrakten Schnittstellen IPublisher und IReceiver. Auch hier werden die konkreten Implementierungen über den Konstruktor übergeben. Diese 2 Klassen werden jedoch zukünftig in Plugins ausgelagert.

Die Abhängigkeit von abstrakten Schnittstellen wird auch in beide Richtungen verwendet. Durch die Implementierung des ICore Interfaces kann der Core auch von anderen Plugin Loadern verwendet werden. Der Core soll zukünfig eine Liste von Core Features anbieten, die durch die Plugins erweitert werden kann. Dadurch können Features, die von Plugins geschaffen wurden, anderen Plugins zur Verfügung gestellt werden.

%%{init: {'theme':'forest'}}%%
classDiagram
    class Core {
        + IConfiguration configuration
        + List<IPlugin> loadedPlugins
        + IPublisher publisher
        + IReceiver receiver
        + IPluginFactory pluginFactory
        + ApplicationState appState
        --
        + Core(plugins, pluginFactory, configuration, initialState, publisher, receiver)
        + getConfiguration()
        + run()
        + getCore()
        + setState(state)
        + getState()
        + getPublisher()
    }
    interface IConfiguration
    interface IPlugin
    interface IPluginFactory
    interface IPublisher
    interface IReceiver
    Core --|> IConfiguration
    Core --|> IPlugin
    Core --|> IPluginFactory
    Core --|> IPublisher
    Core --|> IReceiver
Loading

Inversion of Control (IoC):

Die Klasse Core nimmt die Objekte

  • plugins
  • pluginFactory
  • configuration
  • publisher
  • receiver

im Konstruktor entgegen. Dadurch wird die Kontrolle über die Erstellung und Bereitstellung dieser Objekte an den aufrufenden Code (den sogenannten "Inversion of Control Container" oder "IoC Container") übergeben. Im Falle der Smarthome-Bridge übernimmt die PluginLoader Klasse in Verbindung mit der PluginFactory diese Funktion. Durch die Verwendung eines IoC Containers wird die Erstellung der konkreten Objekte zentralisiert und die Abhängigkeiten zwischen den Komponenten des Systems werden entkoppelt. Dadurch wird die Flexibilität, Erweiterbarkeit und Austauschbarkeit des Systems verbessert. Die Loader Klasse stellt die Objekte bereit, die in der Core Klasse verwendet werden. Im aktuellen Zustand des Projektes werden Basisfunktionalitäten wie Nachrichtenaustausch und Configuration direkt per Objekt übergeben und von der Loader Klasse instantiiert. In den nächsten Schritten sollen nur noch die Plugins übergeben werden, die von der Loader Klasse initialisiert werden. Es werden jedoch nur Interfaces übergeben, um eine lose Kopplung zu gewährleisten und nicht von konkreten Implementierung, wie beispielsweise der PluginFactory, abhängig zu sein.

GRASP

Lose Kopplung

Lose Kopplung wird unter anderem durch die Anwendung des Liskov Substitution Principle, Interface Segregation Principle und Dependency Inversion Principle sowie den Aufbau nach der Clean Architecture umgesetzt. All diese Methoden schwächen starke Abhängigkeiten und machen einzelne Codestücke austauschbarer.

Hohe Kohäsion

Hohe Kohäsion wird unter anderem durch die Anwendung des Single Responsibility Principle umgesetzt. Das Projekt ist in viele Klassen aufgeteilt, die meist nur eine Verantwortung haben, welche nicht (sinnvoll) weiter aufgetrennt werden kann.

DRY

Das DRY-Prinzip wurde wo immer möglich umgesetzt, indem mit verschiedenen Mechaniken wie der Clean Architecture, Design Patterns oder der Anwendung der anderen, konkreteren Programming Principles eine lose Kopplung erreicht und damit ein Aufrufen bereits bestehenden Codes aus einem anderen Kontext ermöglicht wurde.

Beispielsweise sorgen das Single Responsibility und Interface Segregation Principle dafür, dass Code ohne Kontext, der nicht zwingend notwendig ist, verwendet werden kann. Durch Dependency Inversion und Injection kann eine Instanz einer solchen, bereis frei nutzbaren Klasse, auch einfach an anderer Stelle verwendet werden. Im Minecraft Smarthome wird beispielsweise die Klasse SadDeviceProvider mehrfach instantiiert und genutzt.

Natürlich wurde auch im "Kleinen" das DRY-Prinzip umgesetzt, indem beispielsweise Code innerhalb von Klassen in Methoden ausgelagert und diese dann von mehreren Stellen aufgerufen werden. Ein Beispiel hierzu findet sich in der Klasse SadLogger oder im Kapitel Refactoring.

DRY

Das DRY-Prinzip wurde wo immer möglich umgesetzt, indem mit verschiedenen Mechaniken wie der Clean Architecture, Design Patterns oder der Anwendung der anderen, konkreteren Programming Principles eine lose Kopplung erreicht und damit ein Aufrufen bereits bestehenden Codes aus einem anderen Kontext ermöglicht wurde.

Beispielsweise sorgen das Single Responsibility und Interface Segregation Principle dafür, dass Code ohne Kontext, der nicht zwingend notwendig ist, verwendet werden kann. Durch Dependency Inversion und Injection kann eine Instanz einer solchen, bereis frei nutzbaren Klasse, auch einfach an anderer Stelle verwendet werden. Im Minecraft Smarthome wird beispielsweise die Klasse SadDeviceProvider mehrfach instantiiert und genutzt.

Natürlich wurde auch im "Kleinen" das DRY-Prinzip umgesetzt, indem beispielsweise Code innerhalb von Klassen in Methoden ausgelagert und diese dann von mehreren Stellen aufgerufen werden. Ein Beispiel hierzu findet sich in der Klasse SadLoggeroder im Kapitel Refactoring.