IEC 61131-3: SOLID – Das Liskov Substitution Principle

„Das Liskov Substitution Principle (LSP) fordert, dass abgeleitete FBs immer zu ihren Basis-FB kompatibel sind. Abgeleitete FBs müssen sich so verhalten wie ihr jeweiliger Basis-FB. Ein abgeleiteter FB darf den Basis-FB erweitern, aber nicht einschränken.“ Dieses ist die Kernaussage des Liskov Substitution Principle (LSP), welches Barbara Liskov schon Ende der 1980iger Jahre formulierte. Obwohl das Liskov Substitution Principle (LSP) eines der einfacheren SOLID-Prinzipien ist, tritt deren Verletzung doch sehr häufig auf. Warum das Liskov Substitution Principle (LSP) wichtig ist, zeigt das folgende Beispiel.

Ausgangssituation

Erneut wird das Beispiel verwendet, welches zuvor in den beiden vorherigen Posts entwickelt und optimiert wurde. Kern des Beispiels sind drei Lampentypen, welche durch die Funktionsblöcke FB_LampOnOff, FB_LampSetDirect und FB_LampUpDown abgebildet werden. Die Schnittstelle I_Lamp und der abstrakte Funktionsblock FB_Lamp gewährleisten eine saubere Entkopplung zwischen den jeweiligen Lampentypen und dem übergeordneten Controller FB_Controller.

FB_Controller greift nicht mehr auf konkrete Instanzen, sondern nur noch auf eine Referenz des abstrakten Funktionsblock FB_Lamp zu. Für das Auflösen der festen Koppelung wird das IEC 61131-3: SOLID – Dependency Inversion Principle (DIP) angewendet.

Zur Realisierung der geforderten Funktionsweise, stellt jeder Lampentyp seine eigenen Methoden bereit. Aus diesem Grund besitzt jeder Lampentyp einen entsprechenden Adapter-Funktionsblock (FB_LampOnOffAdapter, FB_LampSetDirectAdapter und FB_LampUpDownAdapter), der für das Mapping zwischen der abstrakten Lampe (FB_Lamp) und den konkreten Lampentypen (FB_LampOnOff, FB_LampSetDirect und FB_LampUpDown) zuständig ist. Unterstützt wird diese Optimierung durch das IEC 61131-3: SOLID – Single Responsibility Principle (SRP).

Erweiterung der Implementierung

Die drei geforderten Lampentypen lassen sich durch das bisherige Software-Design gut abbilden. Trotzdem kann es passieren, dass Erweiterungen, die auf dem ersten Blick einfach wirken, später zu Schwierigkeiten führen. Als Beispiel soll hier der neue Lampentyp FB_LampSetDirectDALI dienen.

DALI steht für Digital Addressable Lighting Interface und ist ein Protokoll zur Ansteuerung von lichttechnischen Geräten. Grundsätzlich verhält sich der neue Baustein wie FB_LampSetDirect, allerdings wird der Ausgangswert bei DALI nicht in 0-100 %, sondern in 0-254 angegeben.

Optimierung und Analyse der Erweiterungen

Welche Ansätze stehen zur Verfügung, um diese Erweiterung umzusetzen? Dabei sollen auch die unterschiedlichen Ansätze genauer analysiert werden.

Ansatz 1: Quick & Dirty

Hoher Zeitdruck kann dazu verleiten die Umsetzung Quick & Dirty zu realisieren. Da FB_LampSetDirect sich ähnlich verhält wie der neue DALI-Lampentyp, erbt FB_LampSetDirectDALI von FB_LampSetDirect. Um den Wertebereich von 0-254 zu ermöglichen, wird die Methode SetLightLevel() von FB_LampSetDirectDALI überschrieben.

METHOD PUBLIC SetLightLevel
VAR_INPUT
  nNewLightLevel    : BYTE(0..254);
END_VAR
nLightLevel := nNewLightLevel;

Auch der neue Adapter-Funktionsblock (FB_LampSetDirectDALIAdapter) wird so angepasst, dass die Methoden den Wertebereich von 0-254 berücksichtigen.

Als Beispiel sollen hier die Methoden DimUp() und On() gezeigt werden:

METHOD PUBLIC DimUp
IF (fbLampSetDirectDALI.nLightLevel <= 249) THEN
  fbLampSetDirectDALI.SetLightLevel(fbLampSetDirectDALI.nLightLevel + 5);
END_IF
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(fbLampSetDirectDALI.nLightLevel);
END_IF
METHOD PUBLIC On
fbLampSetDirectDALI.SetLightLevel(254);
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(fbLampSetDirectDALI.nLightLevel);
END_IF

Das vereinfachte UML-Diagramm zeigt die Integration der Funktionsblöcke für die DALI-Lampe in das bestehende Software-Design:

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 1 (TwinCAT 3.1.4024) auf GitHub

Dieser Ansatz setzt die Forderungen durch eine pragmatische Herangehensweise schnell und einfach um. Doch dadurch wurden auch einige Besonderheiten hinzugefügt, welche den Einsatz der Bausteine in einer Applikation erschweren.

Wie soll sich z.B. eine Bedieneroberfläche verhalten, wenn sich diese auf eine Instanz von FB_Controller verbindet und FB_AnalogValue einen Wert von 100 ausgibt? Bedeutet 100, dass die aktuelle Lampe auf 100 % steht, oder gibt die neue DALI-Lampe einen Wert von 100 aus, was deutlich unter 100 % liegen würde?

Der Anwender von FB_Controller muss immer den aktiven Lampentyp kennen, um den aktuellen Ausgangswert korrekt interpretieren zu können. FB_LampSetDirectDALI erbt zwar von FB_LampSetDirect, verändert aber dessen Verhalten. In diesem Beispiel durch das Überschreiben der Methode SetLightLevel(). Der abgeleitete FB (FB_LampSetDirectDALI) verhält sich anders als der Basis-FB (FB_LampSetDirect). FB_LampSetDirect kann nicht mehr durch FB_LampSetDirectDALI ersetzt (substituiert) werden. Das Liskov Substitution Principle (LSP) wird verletzt.

Ansatz 2: Optionalität

Bei diesem Ansatz enthält jeder Lampentyp eine Eigenschaft, die Auskunft über die genaue Funktionsweise des Funktionsblock zurückgibt.

In .NET wird z.B. dieser Ansatz in der abstrakten Klasse System.IO.Stream verwendet. Die Klasse Stream dient als Basisklasse für spezialisierte Streams (z.B. FileStream und NetworkStream) und legt die wichtigsten Methoden und Eigenschaften fest. Hierzu gehören auch die Methoden Write(), Read() und Seek(). Da nicht jeder Stream alle Funktionen zur Verfügung stellen kann, geben die Eigenschaften CanRead, CanWrite und CanSeek Auskunft darüber, ob die entsprechende Methode vom jeweiligen Stream unterstützt wird. So kann bei NetworkStream zur Laufzeit geprüft werden, ob ein Schreiben in den Stream möglich ist, oder ob es sich um einen read-only Stream handelt.

Bei unserem Beispiel wird I_Lamp durch die Eigenschaft bIsDALIDevice erweitert.

Dadurch erhält auch FB_Lamp und somit jeder Adapter-Funktionsblock diese Eigenschaft. Da die Funktionalität von bIsDALIDevice in allen Adapter-Funktionsblöcken gleich ist, wird bIsDALIDevice in FB_Lamp nicht als abstract deklariert. Dadurch ist es nicht notwendig, dass alle Adapter-Funktionsblöcke diese Eigenschaft selbst implementieren müssen. Die Funktionalität von bIsDALIDevice wird von FB_Lamp an alle Adapter-Funktionsblöcke vererbt.

Für FB_LampSetDirectDALIAdapter wird in der Methode FB_init() die Backing-Variable der Eigenschaft bIsDALIDevice auf TRUE gesetzt.

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains  : BOOL;
  bInCopyCode   : BOOL;
END_VAR
SUPER^._bIsDALIDevice := TRUE;

Bei allen anderen Adapter-Funktionsblöcken behält _bIsDALIDevice seinen Initialisierungswert (FALSE). Der Einsatz der Methode FB_init() ist bei diesen Adapter-Funktionsblöcken nicht notwendig.

Der Anwender von FB_Controller (Baustein MAIN) kann jetzt zur Laufzeit des Programms abfragen, ob die aktuelle Lampe eine DALI-Lampe ist oder nicht. Ist dieses der Fall, wird der Ausgangswert entsprechend auf 0-100 % skaliert.

IF (__ISVALIDREF(fbController.refActiveLamp) AND_THEN fbController.refActiveLamp.bIsDALIDevice) THEN
  nLightLevel := TO_BYTE(fbController.fbActualValue.nValue * 100.0 / 254.0);
ELSE
  nLightLevel := fbController.fbActualValue.nValue;
END_IF

Anmerkung: Wichtig ist hierbei die Verwendung des Operators AND_THEN statt THEN. Hierdurch wird der Ausdruck rechts von AND_THEN nur dann ausgeführt, wenn der erste Operand (links von AND_THEN) TRUE ist. Das ist hierbei wichtig, da sonst bei einer ungültigen Referenz auf die aktive Lampe (refActiveLamp) der Ausdruck fbController.refActiveLamp.bIsDALIDevice die Ausführung des Programms beenden würde.

Im UML-Diagramm ist zu erkennen wie FB_Lamp über die Schnittstelle I_Lamp die Eigenschaft bIsDALIDevice erhält und somit von allen Adapter-Funktionsblöcken geerbt wird:

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 2 (TwinCAT 3.1.4024) auf GitHub

Auch bei diesem Ansatz wird das Liskov Substitution Principle (LSP) weiterhin verletzt. FB_LampSetDirectDALI verhält sich nach wie vor unterschiedlich zu FB_LampSetDirect. Diese Unterschiedlichkeit muss vom Anwender berücksichtigt (Abfragen von bIsDALIDevice) und korrigiert (Skalierung auf 0-100 %) werden. Dieses wird schnell übersehen oder fehlerhaft umgesetzt.

Ansatz 3: Harmonisierung

Um das Liskov Substitution Principle (LSP) nicht weiter zu verletzen, wird die Vererbung zwischen FB_LampSetDirect und FB_LampSetDirectDALI aufgelöst. Auch wenn beide Funktionsblöcke auf dem ersten Blick sehr ähnlich wirken, so sollte an dieser Stelle auf die Vererbung verzichtet werden.

Die Adapter-Funktionsblöcke stellen sicher, dass alle Lampentypen mit den gleichen Methoden steuerbar sind. Unterschiede gibt es allerdings weiterhin bei der Darstellung des Ausgangswertes.

In FB_Controller wird der Ausgangswert der aktiven Lampe durch eine Instanz von FB_AnalogValue dargestellt. Übermittelt wird ein neuer Ausgangswert durch die Methode Update(). Damit auch der Ausgangswert einheitlich dargestellt wird, wird vor dem Aufruf der Methode Update() dieser auf 0-100 % skaliert. Die notwendigen Anpassungen erfolgen ausschließlich in den Methoden DimDown(), DimUp(), Off() und On() von FB_LampSetDirectDALIAdapter.

Als Beispiel soll hier die Methode On() gezeigt werden:

METHOD PUBLIC On
fbLampSetDirectDALI.SetLightLevel(254);
IF (_ipObserver <> 0) THEN
  _ipObserver.Update(TO_BYTE(fbLampSetDirectDALI.nLightLevel * 100.0 / 254.0));
END_IF

Der Adapter-Funktionsblock enthält alle notwendigen Anweisungen, wodurch sich die DALI-Lampe nach Außen so verhält wie erwartet. FB_LampSetDirectDALI bleibt bei diesem Lösungsansatz unverändert.

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Beispiel 3 (TwinCAT 3.1.4024) auf GitHub

Analyse der Optimierung

Durch verschiedene Techniken ist es uns möglich, die gewünschte Erweiterung zu implementieren, ohne dass das Liskov Substitution Principle (LSP) verletzt wird. Voraussetzung, um das LSP zu verletzen, ist Vererbung. Wird das LSP verletzt, so ist dieses evtl. ein Hinweis auf eine schlechte Vererbungshierarchie innerhalb des Software-Designs.

Warum ist es wichtig, dass Liskov Substitution Principle (LSP) einzuhalten? Funktionsblöcke können auch als Parameter übergeben werden. Würde ein POU einen Parameter vom Typ FB_LampSetDirect erwarten, so könnte, bei der Verwendung von Vererbung, auch FB_LampSetDirectDALI übergeben werden. Die Arbeitsweise der Methode SetLightLevel() ist aber bei beiden Funktionsblöcken unterschiedlich. Solche Unterschiede können zu unerwünschten Verhalten innerhalb einer Anlage führen.

Die Definition des Liskov Substitution Principle

Sei q(x) eine beweisbare Eigenschaft von Objekten x des Typs T. Dann soll q(y) für Objekte y des Typs S wahr sein, wobei S ein Untertyp von T ist.

So lautet, etwas formeller ausgedrückt, die Definition des Liskov Substitution Principle (LSP) von Barbara Liskov. Wie weiter oben schon erwähnt, wurde schon Ende der 1980iger Jahre dieses Prinzip definiert. Die vollständige Ausarbeitung hierzu wurde unter dem Titel Data Abstraction and Hierarchy veröffentlicht.

Barbara Liskov promovierte 1968 als eine der ersten Frauen in Informatik. 2008 erhielt sie, ebenfalls als eine der ersten Frauen, den Turing Award. Schon früh beschäftigte sie sich mit der objektorientierten Programmierung und somit auch mit der Vererbung von Klassen (Funktionsblöcken).

Die Vererbung stellt zwei Funktionsblöcke in eine bestimmte Beziehung zueinander. Vererbung beschreibt hierbei eine istein-Beziehung. Erbt FB_LampSetDirectDALI von FB_LampSetDirect, so ist die DALI-Lampe eine (normale) Lampe, erweitert um besondere (zusätzliche) Funktionen. Überall wo FB_LampSetDirect verwendet wird, könnte auch FB_LampSetDirectDALI zum Einsatz kommen. FB_LampSetDirect kann durch FB_LampSetDirectDALI substituiert werden. Ist dieses nicht sichergestellt, so sollte die Vererbung an dieser Stelle hinterfragt werden.

Robert C. Martin hat dieses Prinzip mit in die SOLID-Prinzipien aufgenommen. In dem Buch (Amazon-Werbelink *) Clean Architecture: Das Praxis-Handbuch für professionelles Softwaredesign wird dieses Prinzip weiter erläutert und auf den Bereich der Softwarearchitektur ausgedehnt.

Zusammenfassung

Durch die Erweiterung des obigen Beispiels, haben Sie das Liskov Substitution Principle (LSP) kennen gelernt. Gerade komplexe Vererbungshierarchien sind anfällig für die Verletzung dieses Prinzips. Obwohl sich die formelle Definition des Liskov Substitution Principle (LSP) kompliziert anhört, so ist die Kernaussage dieses Prinzips doch einfach zu verstehen.

Im nächsten Post soll unserer Beispiel erneut erweitert werden. Dabei wird das Interface Segregation Principle (ISP) eine zentrale Rolle haben.

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: