IEC 61131-3: SOLID – Das Interface Segregation Principle

Der Grundgedanke des Interface Segregation Principle (ISP) hat starke Ähnlichkeit mit dem Single Responsibility Principle (SRP): Module mit zu vielen Zuständigkeiten können die Pflege und Wartbarkeit eines Softwaresystem negativ beeinflussen. Das Interface Segregation Principle (ISP) legt den Schwerpunkt hierbei auf die Schnittstelle des Moduls. Ein Modul sollte nur die Schnittstellen implementieren, die für seine Aufgabe benötigt werden. Im Folgenden wird gezeigt, wie dieses Designprinzip umgesetzt werden kann.

Ausgangssituation

Im letzten Post (IEC 61131-3: SOLID – Das Liskov Substitution Principle) wurde das Beispiel um einen weiteren Lampentyp (FB_LampSetDirectDALI) erweitert. Das Besondere an diesem Lampentyp ist die Skalierung des Ausgangwertes. Während die anderen Lampentypen 0-100 % ausgeben, gibt der neue Lampentyp einen Wert von 0 bis 254 aus.

So wie alle anderen Lampentypen, besitzt auch der neue Lampentyp (DALI-Lampe) einen Adapter (FB_LampSetDirectDALIAdapter). Die Adapter sind bei der Umsetzung des Single Responsibility Principle (SRP) hinzugekommen und stellen sicher, dass die Funktionsblöcke der einzelnen Lampentypen nur für eine einzelne Fachlichkeit zuständig sind (siehe IEC 61131-3: SOLID – Das Single Responsibility Principle).

Das Beispielprogramm wurde zuletzt so angepasst, dass von dem neuen Lampentyp (FB_LampSetDirectDALI) der Ausgangswert innerhalb des Adapters von 0-254 auf 0-100 % skaliert wird. Dadurch verhält sich die DALI-Lampe genau wie die anderen Lampentypen, ohne das Liskov Substitution Principle (LSP) zu verletzen.

Dieses Beispielprogramm soll uns als Ausgangssituation für die Erklärung des Interface Segregation Principle (ISP) dienen.

Erweiterung der Implementierung

Auch dieses Mal, soll die Anwendung erweitert werden. Allerdings wird nicht ein neuer Lampentyp definiert, sondern ein vorhandener Lampentyp wird um eine Funktionalität erweitert. Die DALI-Lampe soll in der Lage sein, die Betriebsstunden zu zählen. Hierzu wird der Funktionsblock FB_LampSetDirectDALI um die Eigenschaft nOperatingTime erweitert.

PROPERTY PUBLIC nOperatingTime : DINT

Über den Setter kann der Betriebsstundenzähler auf einen beliebigen Wert gesetzt werden, während der Getter den aktuellen Zustand des Betriebsstundenzählers zurückgibt.

Da FB_Controller die einzelnen Lampentypen repräsentiert, wird dieser Funktionsblock ebenfalls um nOperatingTime erweitert.

Die Erfassung der Betriebsstunden erfolgt im Funktionsblock FB_LampSetDirectDALI. Ist der Ausgangswert > 0, so wird jede Sekunde der Betriebsstundenzähler um 1 erhöht:

IF (nLightLevel > 0) THEN
  tonDelay(IN := TRUE, PT := T#1S);
  IF (tonDelay.Q) THEN
    tonDelay(IN := FALSE);
    _nOperatingTime := _nOperatingTime + 1;
  END_IF
ELSE
  tonDelay(IN := FALSE);
END_IF

Die Variable _nOperatingTime ist die Backing Variable für die neue Eigenschaft nOperatingTime und ist im Funktionsblock deklariert.

Welche Möglichkeiten gibt es, um den Wert von nOperatingTime aus FB_LampSetDirectDALI in die Eigenschaft nOperatingTime von FB_Controller zu übertragen? Auch hier gibt es jetzt verschiedene Ansätze, um die geforderte Erweiterung in die gegebene Softwarestruktur zu integrieren.

Ansatz 1: Erweiterung von I_Lamp

Die Eigenschaft für das neue Leistungsmerkmal wird mit in die Schnittstelle I_Lamp integriert. Somit erhält auch der abstrakte Funktionsblock FB_Lamp die Eigenschaft nOperatingTime. Da alle Adapter von FB_Lamp erben, erhalten die Adapter aller Lampentypen diese Eigenschaft, unabhängig ob der Lampentyp einen Betriebsstundenzähler unterstützt oder nicht.

Der Getter und der Setter von nOperatingTime in FB_Controller können somit direkt auf nOperatingTime der einzelnen Adapter der Lampentypen zugreifen. Der Getter von FB_Lamp (abstrakter Funktionsblock, von dem alle Adapter erben) liefert den Wert -1 zurück. Somit kann das Fehlen des Betriebsstundenzähler erkannt werden.

IF (fbController.nOperatingTime >= 0) THEN
  nOperatingTime := fbController.nOperatingTime;
ELSE
  // service not supported
END_IF

Da FB_LampSetDirectDALI den Betriebsstundenzähler unterstützt, überschreibt der Adapter (FB_LampSetDirectDALIAdapter) die Eigenschaft nOperatingTime. Der Getter und der Setter vom Adapter greifen auf nOperatingTime von FB_LampSetDirectDALI zu. Der Wert des Betriebsstundenzählers wird somit bis zu FB_Controller weitergegeben.

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 1 (TwinCAT 3.1.4024) auf GitHub

Dieser Ansatz setzt das Leistungsmerkmal wie gewünscht um. Auch werden keine der bisher gezeigten SOLID-Prinzipen verletzt.

Allerdings wird die zentrale Schnittstelle I_Lamp erweitert, nur um bei einem Lampentyp ein weiteres Leistungsmerkmal hinzuzufügen. Alle anderen Adapter der Lampentypen, auch die, die das neue Leistungsmerkmal nicht unterstützen, erhalten über den abstrakten Basis-FB FB_Lamp ebenfalls die Eigenschaft nOperatingTime.

Mit jedem Leistungsmerkmal, welches auf diese Weise hinzugefügt wird, vergrößert sich die Schnittstelle I_Lamp und somit auch der abstrakte Basis-FB FB_Lamp.

Ansatz 2: zusätzliche Schnittstelle

Bei diesem Ansatz wird die Schnittstelle I_Lamp nicht erweitert, sondern es wird für die gewünschte Funktionalität eine neue Schnittstelle (I_OperatingTime) hinzugefügt. I_OperatingTime enthält nur die Eigenschaft, die für das Bereitstellen des Betriebsstundenzählers notwendig ist:

PROPERTY PUBLIC nOperatingTime : DINT

Implementiert wird diese Schnittstelle vom Adapter FB_LampSetDirectDALIAdapter.

FUNCTION_BLOCK PUBLIC FB_LampSetDirectDALIAdapter EXTENDS FB_Lamp IMPLEMENTS I_OperatingTime

Somit erhält FB_LampSetDirectDALIAdapter die Eigenschaft nOperationTime nicht über FB_Lamp bzw. I_Lamp, sondern über die neue Schnittstelle I_OperatingTime.

Greift FB_Controller im Getter von nOperationTime auf den aktiven Lampentyp zu, so wird vor dem Zugriff geprüft, ob der ausgewählte Lampentyp die Schnittstelle I_OperatingTime implementiert. Ist dieses der Fall, so wird über I_OperatingTime auf die Eigenschaft zugegriffen. Hat der Lampentyp die Schnittstelle nicht implementiert, wird -1 zurückgegeben.

VAR
  ipOperatingTime  : I_OperatingTime;
END_VAR
IF (__ISVALIDREF(_refActiveLamp)) THEN
  IF (__QUERYINTERFACE(_refActiveLamp, ipOperatingTime)) THEN
    nOperatingTime := ipOperatingTime.nOperatingTime;
  ELSE
    nOperatingTime := -1; // service not supported
  END_IF
END_IF

Ähnlich ist der Setter von nOperationTime aufgebaut. Nach der erfolgreichen Prüfung, ob I_OperatingTime von der aktiven Lampe implementiert wird, erfolgt über die Schnittstelle der Zugriff auf die Eigenschaft.

VAR
  ipOperatingTime  : I_OperatingTime;
END_VAR
IF (__ISVALIDREF(_refActiveLamp)) THEN
  IF (__QUERYINTERFACE(_refActiveLamp, ipOperatingTime)) THEN
    ipOperatingTime.nOperatingTime := nOperatingTime;
  END_IF
END_IF
(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 2 (TwinCAT 3.1.4024) auf GitHub

Analyse der Optimierung

Das Verwenden einer separaten Schnittstelle für das zusätzliche Leistungsmerkmal entspricht der ‚Optionalität‘ aus IEC 61131-3: SOLID – Das Liskov Substitution Principle. In dem obigen Beispiel kann zur Laufzeit des Programms geprüft werden (mit __QUERYINTERFACE()), ob eine bestimmte Schnittstelle implementiert und somit das jeweilige Leistungsmerkmal unterstützt wird. Weitere Eigenschaften, wie bIsDALIDevice aus dem ‚Optionalität‘-Beispiel, sind bei diesem Lösungsansatz nicht notwendig.

Wird pro Leitungsmerkmal bzw. Funktionalität eine separate Schnittstelle angeboten, können andere Lampentypen diese ebenfalls implementieren, um so das gewünschte Leistungsmerkmal umzusetzen. Soll FB_LampSetDirect ebenfalls einen Betriebsstundenzähler erhalten, so muss FB_LampSetDirect um die Eigenschaft nOperatingTime erweitert werden. Außerdem muss FB_LampSetDirectAdapter die Schnittstelle I_OperatingTime implementieren. Alle anderen Funktionsblöcke, auch FB_Controller, bleiben unverändert.

Ändert sich die Funktionsweise der Betriebsstundenzähler und I_OperatingTime erhält zusätzliche Methoden, so müssen nur die Funktionsblöcke angepasst werden, die auch das Leistungsmerkmal unterstützen.

Beispiele für das Interface Segregation Principle (ISP) sind auch im .NET zu finden. So gibt es in .NET die Schnittstelle IList. Diese Schnittstelle enthält Methoden und Eigenschaften für das Anlegen, Verändern und Lesen von Auflistungen. Je nach Anwendungsfall ist es aber ausreichend, dass der Anwender eine Auflistung nur lesen muss. Das Übergeben einer Auflistung durch IList würde in diesem Fall aber auch Methoden anbieten, um die Auflistung zu verändern. Für diese Anwendungsfälle gibt es die Schnittstelle IReadOnlyList. Mit dieser Schnittstelle kann eine Auflistung nur gelesen werden. Ein versehentliches Verändern der Daten ist somit nicht möglich.

Das Aufteilen von Fachlichkeiten in einzelne Schnittstellen erhöht somit nicht nur die Wartbarkeit, sondern auch die Sicherheit eines Softwaresystems.

Die Definition des Interface Segregation Principle

Damit kommen wir auch schon zur Definition des Interface Segregation Principle (ISP):

Ein Modul, das eine Schnittstelle benutzt, sollte nur diejenigen Methoden präsentiert bekommen, die sie auch wirklich benötigt.

Oder etwas anders formuliert:

Clients sollten nicht gezwungen werden, von Methoden abhängig zu sein, die sie nicht benötigen.

Ein häufiges Argument gegen das Interface Segregation Principle (ISP) ist die erhöhte Anzahl von Schnittstellen. Ein Softwareentwurf kann im Laufe seiner Entwicklungszyklen jederzeit noch angepasst werden. Wenn Sie also das Gefühl haben, das eine Schnittstelle zu viele Funktionalitäten beinhaltet, prüfen Sie, ob eine Aufteilung möglich ist. Natürlich sollte ein Overengineering immer vermieden werden. Ein gewisses Maß an Erfahrung kann hierbei hilfreich sein.

Abstrakte Funktionsblöcke stellen ebenfalls eine Schnittstelle (siehe FB_Lamp) dar. In einem abstrakten Funktionsblock können Grundfunktionen enthalten sein, die der Anwender nur um die notwendigen Details ergänzt. Es ist nicht notwendig, alle Methoden oder Eigenschaften selbst zu implementieren. Aber auch hierbei ist es wichtig, den Anwender nicht mit Fachlichkeiten zu belasten, die für seine Aufgaben nicht notwendig sind. Die Menge der abstrakten Methoden und Eigenschaften sollte möglichst klein sein.

Die Beachtung des Interface Segregation Principles (ISP) hält Schnittstellen zwischen Funktionsblöcken so klein wie möglich, wodurch die Kopplung zwischen den einzelnen Funktionsblöcken reduziert wird.

Zusammenfassung

Soll ein Softwaresystem weitere Leistungsmerkmale abdecken, so reflektieren Sie die neuen Anforderungen und erweitern Sie nicht voreilig bestehende Schnittstellen. Prüfen Sie, ob separate Schnittstellen nicht die bessere Entscheidung sind. Als Belohnung erhalten Sie ein Softwaresystem das leichter zu pflegen, besser zu testen und einfacher zu erweitern ist.

Im Letzten noch ausstehenden Teil, wird das Open Closed Principle (OCP) näher erklärt.

Author: Stefan Henneken

I’m Stefan Henneken, a software developer based in Germany. This blog is just a collection of various articles I want to share, mostly related to Software Development.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: