IEC 61131-3: Objektkomposition mit Hilfe von Interfaces

Während der Begriff der Vererbung gerne und häufig verwendet wird, so wird der Einsatz von Interfaces eher selten behandelt. Dabei bieten Interfaces etliche Vorteile, die die Flexibilität eines SPS-Programms erhöhen und auch die Wartbarkeit verbessern. Der folgende Post soll die Möglichkeiten von Interfaces in Zusammenhang mit der IEC 61131-3 vorstellen.

Das bekannteste Konzept der objektorientierten Programmierung dürfte die Vererbung sein. Diese liegt sicherlich daran, dass die meisten Lehrbücher dieses als erstes behandeln und dadurch in den Vordergrund stellen. Gerne werden Gegenstände aus der realen Welt als Klassen dargestellt, die somit eine ‘ist-ein-Beziehung’ abbilden. Ein Hund ist ein Säugetier; ein Säugetier (engl. Mammal) ist ein Lebewesen (engl.: Living organism). Auf diese Weise könnte recht einfach ein Mensch (engl.: Human) oder eine Spinne (engl.: Spider) abgebildet werden.

FUNCTION_BLOCK PUBLIC FB_LivingOrganism
FUNCTION_BLOCK PUBLIC FB_Mammal EXTENDS FB_LivingOrganism
FUNCTION_BLOCK PUBLIC FB_Dog EXTENDS FB_Mammal
FUNCTION_BLOCK PUBLIC FB_Human EXTENDS FB_Mammal
FUNCTION_BLOCK PUBLIC FB_Insect EXTENDS FB_LivingOrganism
FUNCTION_BLOCK PUBLIC FB_Spider EXTENDS FB_Insect

Darstellung als UML-Klassendiagramm:

Picture01

Da der Begriff Klasse im Kontext der objektorientierten Programmierung geläufiger ist, wird dieser im Folgenden dem Begriff Funktionsblock vorgezogen.

Vererbung

Beschäftigt man sich mit dem Konzept der objektorientierten Programmierung, so dürfte das bekannteste Merkmal die Vererbung sein. Diese erlaubt die einfache Definition neuer Klassen (Subklassen), die von einer bestehenden Klasse (Basisklasse) abgeleitet werden. Die Subklasse erbt alle Properties, Methoden, Aktionen, Eingänge und Ausgänge der Basisklasse automatisch. Sie müssen in der Subklasse nur dann neu programmiert werden, wenn ihre geerbten Funktionalitäten erweitert oder geändert werden müssen.

Während Aktionen, Eingänge und Ausgänge immer an die Subklasse vererbt werden, kann die Vererbung von Properties und Methoden durch die Schlüsselwörter PUBLIC, PROTECTED, INTERNAL und PRIVATE beeinflusst werden. Ebenfalls kann bei der Definition eines Funktionsblocks das Anlegen von Subklassen, also die Möglichkeiten der Vererbung, eingeschränkt werden (PUBLIC, INTERNAL und FINAL).

Doch die Vererbung bringt auch einige Nachteile mit sich:

  • Änderungen an Basisklassen können sich auf Subklassen auswirken, die von dieser erben. Subklassen und Basisklassen sind eng miteinander gekoppelt.
  • Befinden sich Basisklassen und Subklassen in einer SPS-Bibliothek, so kann die Vererbungshierarchie dieser Bausteine nicht vom Anwender der SPS-Bibliothek geändert werden.
  • IEC 61131-3 erlaubt, wie auch Java und C#, keine Mehrfachvererbung. Eine Subklasse hat immer eine Basisklasse. So wäre es nicht ohne weiteres möglich, mit der obigen Klassenhierarchie einen Funktionsbaustein für Spiderman anzulegen. Ein Erben von FB_Human und(!) FB_Spider ist nicht möglich.
  • Subklassen sind immer an ihre Basisklassen gebunden. Dieses kann die Wiederverwendung einer Subklasse erschweren.
Anmerkung:

Basisklassen mutieren schnell zu einem ‘Sammelbecken’ von Methoden und Properties, die in irgend welchen Subklassen benötigt werden. Solche ‘Superbasisklassen’ sind irgendwann nur noch schwer zu pflegen. Ich versuche deshalb, Basisklassen möglichst klein zu halten. Je tiefer man in die Vererbungshierarchie eintaucht, desto kleiner sollten die Klassen werden. Ebenfalls versuche ich die Anzahl der Vererbungsebenen gering zu halten. Ein DIT (Depth of inheritance) von maximal 6 kann hierbei als Richtwert dienen.

Interfaces

Bei Interfaces handelt es sich um eine Definition von Methoden und Properties, die in eine Klasse implementiert wurden. Klassen, die das gleiche Interface implementieren, sehen von außen identisch aus und können gleichwertig behandelt werden. Die genaue Implementierung der vorgeschriebenen Methoden und Properties wird nicht durch das Interface beschrieben. Jede Klasse, die das Interface implementiert, kann die interne Gestaltung selber festlegen.

Beispiel:

Das Interface I_Sample enthält die Methode M_DoFoo() mit einem Rückgabewert vom Typ BYTE.

INTERFACE I_Sample
METHOD M_DoFoo : BYTE

Zwei Funktionsblöcke implementieren das Interface.

FUNCTION_BLOCK PUBLIC FB_A IMPLEMENTS I_Sample
METHOD M_DoFoo : BYTE
M_DoFoo := 100;
RETURN;

FUNCTION_BLOCK PUBLIC FB_B IMPLEMENTS I_Sample
METHOD M_DoFoo : BYTE
M_DoFoo := 200;
RETURN;

Außer dem gemeinsamen Interface haben die beiden Funktionsblöcke keine weitere Bindung. Trotzdem lassen sich beide gleichwertig behandeln.

PROGRAM MAIN
VAR
  aTest     : ARRAY[1..2] OF I_Sample;
  fbA       : FB_A;
  fbB       : FB_B;
  a         : INT;
  nIndex    : INT;
END_VAR

aTest[1] := fbA;
aTest[2] := fbB;
FOR nIndex := 1 TO 2 DO
  a := aTest[nIndex].M_DoFoo();
END_FOR

Im ersten Schleifendurchlauf wird auf die Methode M_DoFoo() des Funktionsblocks FB_A zugegriffen, im zweiten Durchlauf auf die Methode M_DoFoo() von FB_B. Die Implementierung der Methoden wird in jedem Funktionsblock (FB_A und FB_B) individuell festgelegt.

Während Vererbungen eine ‘ist-ein-Beziehung’ darstellen, könnte man Interfaces als eine ‘verhält-sich-wie-ein-Beziehung’ oder ‘hat-ein-Beziehung’ bezeichnen.

Diese Art der Bindung reduziert die Abhängigkeiten erheblich. So wird innerhalb der FOR-Schleife (Zeile 12 bis 14) immer mit dem Interface I_Sample gearbeitet. Die Schleife arbeitet gegen das Interface I_Sample und nicht gegen eine konkrete Implementierung. Die Funktionsblöcke FB_A und FB_B könnten jederzeit gegen andere Funktionsblöcke ausgetauscht werden, solange diese das Interface I_Sample implementiert haben. Eine Anpassung der FOR-Schleife wäre nicht notwendig.

Da ein Interface einen Datentyp darstellt, können diese auch über Eingänge oder Parameter von Methoden an Funktionsblöcke übergeben werden. Der Funktionsblock ruft die Methoden des Interfaces auf, ohne konkret wissen zu müssen, welche Funktionalität dadurch zur Ausführung kommt (Polymorphie).

Objektkomposition

Ein Funktionsblock kann mehrere Interfaces implementieren. Dadurch können Funktionalitäten, die durch Interfaces definiert werden, in einem Funktionsblock zusammengeführt werden. Die Klassenhierarchie bleibt sehr flach und die Abhängigkeiten der Funktionsblöcke untereinander sind minimal. Durch die Aufteilung der einzelnen Aufgaben auf mehrere, kleinere Schnittstellen besteht auch nicht die Gefahr, dass alle erdenklichen Leistungsmerkmale in einer ‘Superbasisklasse’ abgedeckt werden.

Beispiel:

Eine SPS-Bibliothek stellt die Interfaces I_Light, I_Delayable und I_Dimmable zur Verfügung. Zu jedem Interface gibt es eine entsprechende Implementierung (FB_Light, FB_DelayedLight und FB_DimmingLight), die sich ebenfalls in der SPS-Bibliothek befinden.

InterfacesFunktionsblöcke, mit den implementierten Interfaces
Picture02Picture03

FB_DelayedLight und FB_DimmingLight erben von FB_Light. FB_Light enthält die Methoden M_On() und M_Off(), als auch das Property bControlLevel. Da diese Basisfunktionalität bei allen Funktionsblöcken exakt gleich ist, dient dieser Funktionsblock als Basisklasse.

Darstellung als UML-Klassendiagramm:

Picture04
Aufgabenstellung:

Ein Programmierer soll auf Basis dieser SPS-Bibliothek einen Funktionsblock entwerfen, der sowohl die Funktionalität von FB_DelayedLight als auch von FB_DimmingLight enthält.

Hierbei bietet es sich an, auf die Interfaces I_Delayable und I_Dimmable zurückzugreifen. Auch kann der Funktionsblock FB_Light als Basisklasse genutzt werden. Das bringt den Vorteil mit sich, dass überall wo I_Delayable, I_Dimmable und FB_Light angewendet werden, auch der eigene Baustein ersetzt werden kann. Weiter unten gibt es hierzu ein kleines Beispiel.

FUNCTION_BLOCK FB_MyLight EXTENDS FB_Light IMPLEMENTS I_Delayable, I_Dimmable

Somit besitzt der Funktionsblock die folgenden Methoden und Properties:

Picture05
Tipp:

Damit die Methoden nicht komplett neu programmiert werden müssen, kann auf die schon fertigen Funktionsblöcke zurückgegriffen werden. Hierzu habe ich jeweils eine Instanz von FB_DimmingLight und FB_DelayedLight angelegt.

VAR
  fbDimmingLight     : FB_DimmingLight;
  fbDelayedLight     : FB_DelayedLight;
END_VAR

In den jeweiligen Methoden leite ich die Aufrufe an diese Instanzen weiter:

METHOD M_SetControlLevel
VAR_INPUT
  nControlLevel: BYTE;
END_VAR

fbDimmingLight.M_SetControlLevel(nControlLevel);
IF (nControlLevel = 0) THEN
  fbDelayedLight.M_Off();
ELSE
  fbDelayedLight.M_On();
END_IF

Somit muss die Logik für das Dimmen oder für das verzögerte Ein-/Ausschalten nicht ein zweites Mal entwickelt werden.

Darstellung in CFC:

Methoden und Properties lassen sich nicht direkt in den graphischen Darstellungsarten der IEC 61131-3 ansprechen. Soll dieses aber möglich sein, so kann für jede Methode und für jedes Property ein entsprechender Ein-/Ausgang angelegt werden. Änderungen an den Eingängen werden an die jeweiligen Methoden weitergereicht und Zustände aus den Properties den Ausgängen zugewiesen.

FUNCTION_BLOCK FB_MyLight EXTENDS FB_Light IMPLEMENTS I_Delayable, I_Dimmable
VAR_INPUT
  bRecallMinLevel       : BOOL;
  bSetControlLevel      : BOOL;
  nSetControlLevel      : BYTE;
END_VAR
VAR_OUTPUT
  nControl              : BYTE;
END_VAR
VAR
  rtrigRecallMinLevel   : R_TRIG;
  rtrigSetControlLevel  : R_TRIG;
  fbDimmingLight        : FB_DimmingLight;
  fbDelayedLight        : FB_DelayedLight;
  _nControlLevel        : BYTE;
END_VAR

rtrigOn(CLK := bOn);
IF (rtrigOn.Q) THEN
  M_On();
END_IF

rtrigOff(CLK := bOff);
IF (rtrigOff.Q) THEN
  M_Off();
END_IF

rtrigRecallMinLevel(CLK := bRecallMinLevel);
IF (rtrigRecallMinLevel.Q) THEN
  M_RecallMinLevel();
END_IF

rtrigSetControlLevel(CLK := bSetControlLevel);
IF (rtrigSetControlLevel.Q) THEN
  M_SetControlLevel(nSetControlLevel);
END_IF

fbDimmingLight();
fbDelayedLight();
_bControlLevel := fbDelayedLight.bControlLevel;
IF (_bControlLevel) THEN
  _nControlLevel := fbDimmingLight.nControlLevel;
ELSE
  _nControlLevel := 0;
END_IF

bControl := bControlLevel;
nControl := nControlLevel;

Der Baustein kann somit problemlos in CFC o.ä. eingesetzt werden:

Picture06

Interface zur Laufzeit abfragen

Instanzen von Funktionsblöcken können als Parameter an andere Funktionsblöcke übergeben werden. Somit hat der Funktionsblock uneingeschränkten Zugriff auf alle Methoden, Properties und Ein-/Ausgänge des übergebenen Bausteins.

Beispiel:

Die SPS-Bibliothek soll um einen Funktionsblock erweitert werden, der drei Beleuchtungsbausteine gruppiert. Über Eingänge können alle drei Bausteine gesteuert werden. So sollen über bAllOn und bAllOff alle Beleuchtungen ein- bzw. ausgeschaltet werden. Der Eingang bAllCallMinLevel soll sich nur auf die Bausteine auswirken, die das Interface I_Dimmable implementiert haben. Über Ausgänge werden die Stellgrößen ausgegeben.

FUNCTION_BLOCK PUBLIC FB_RoomController
VAR_INPUT
  bAllOn            : BOOL;
  bAllOff           : BOOL;
  bAllCallMinLevel  : BOOL;
  refLight01        : REFERENCE TO FB_Light;
  refLight02        : REFERENCE TO FB_Light;
  refLight03        : REFERENCE TO FB_Light;
END_VAR
VAR_OUTPUT
  aControlLevel     : ARRAY[1..3] OF BOOL;
  aDimmLevel        : ARRAY[1..3] OF BYTE;
END_VAR

An den Baustein sollen alle Varianten der Beleuchtungsbausteine übergeben werden können. Aus diesem Grund, wird FB_Light als Datentyp der drei Beleuchtungsbausteine verwendet. Somit kann an den Eingängen jeder Baustein übergeben werden, der von FB_Light abgeleitet wurde.

VAR
  fbRoomController  : FB_RoomController;
  fbLight           : FB_Light;
  fbDelayedLight    : FB_DelayedLight;
  fbDimmingLight    : FB_DimmingLight;
END_VAR
Picture07

Die Eingänge bAllOn und bAllOff lassen sich somit recht einfach umsetzen. Wird eine positive Flanke an dem Eingang erkannt, so wird über die Referenz die entsprechende Methode aufgerufen. Vor dem Aufruf, sollte geprüft werden, ob die Referenz gültig ist.

IF (__ISVALIDREF(refLight01)) THEN
  refLight01.M_On();
END_IF
IF (__ISVALIDREF(refLight02)) THEN
  refLight02.M_On();
END_IF
IF (__ISVALIDREF(refLight03)) THEN
  refLight03.M_On();
END_IF

Aber wie kann die Methode M_RecallMinLevel() aufgerufen werden, wenn ein Funktionsblock vorliegt, der das Interface I_Dimmable implementiert hat? Schließlich ist der Datentyp des Eingangs FB_Light und dieser kennt die Methode M_RecallMinLevel() nicht.

Die Funktion __QUERYINTERFACE() ermöglicht eine Typkonvertierung von einer Interface-Referenz zu einer anderen. War die Konvertierung erfolgreich, so liefert die Funktion TRUE zurück und der zweite Parameter enthält eine Referenz auf das gewünschte Interface.

iLight          : I_Light;
iDimmableLight  : I_Dimmable;

IF (__ISVALIDREF(refLight01)) THEN
  iLight := refLight01;
  IF (__QUERYINTERFACE(iLight, iDimmableLight)) THEN
    iDimmableLight.M_RecallMinLevel();
  END_IF
END_IF

Somit kann zur Laufzeit die Implementierung eines Interfaces abgefragt und anschließend auch genutzt werden.

Da der Baustein FB_MyLight von FB_Light abgeleitet wurde, kann dieser ohne Anpassungen mit FB_RoomController angewendet werden. Des Weiteren wird die Methode M_RecallMinLevel() von FB_MyLight aufgerufen, da dieser das Interface I_Dimmable implementiert hat.

Beispiel (TwinCAT 3.1.4020) auf GitHub

Vorteile

Mit dem Baustein FB_RoomController lassen sich nicht nur die Bausteine aus der SPS-Bibliothek (FB_Light, FB_DimmingLight und FB_DelayedLight) nutzen, auch eigene Bausteine, die von FB_Light abgeleitet wurden, können direkt übergeben werden. Implementieren solche Funktionsblöcke das Interface I_Dimmable, so kann FB_RoomController auf die Methode M_RecallMinLevel() zugreifen, ohne den genauen Typ des Lichtbausteins kennen zu müssen. Allein die Tatsache, dass I_Dimmable implementiert wurde, ist ausreichend. FB_RoomController und die Lichtbausteine kommunizieren hierbei über das Interface I_Dimmable.

Bisher hätte man diese Aufgabe über eine Kommunikationsstruktur lösen können. Alle Lichtbausteine und FB_RoomController hätten als Ein-/Ausgangs-Variable eine Struktur, über der die Bausteine untereinander kommunizieren. Doch diese Variante hat einen Nachteil: Was ist, wenn der Baustein FB_RoomController erweitert werden soll? Als Beispiel könnte das Verwalten von Szenen dienen. Jeder Lichtbaustein besitzt 10 Szenen, die über FB_RoomController aufrufbar sein sollen. Bei der bisherigen Variante müsste FB_RoomController weitere Kommunikationsstrukturen für das Szenen-Management erhalten oder die vorhandene Kommunikationsstruktur müsste erweitert werden. In beiden Fällen müsste FB_RoomController so angepasst werden, dass dieser inkompatibel zur vorherigen würde. Die Entwicklung eines komplett neuen Bausteins (z.B. FB_RoomControllerV2) könnte noch eine weitere Variante darstellen.

Flexibler ist das Definieren eines neuen Interfaces, z.B. I_SceneManager. Dieses Interface legt die Methoden und Properties fest, die zum Aufruf der jeweiligen Szenen notwendig sind. Hat ein Lichtbaustein dieses Interface implementiert, so kann FB_RoomController die gewünschte Szene aufrufen. Erweiterungen oder Anpassungen an den vorhandenen Ein- und Ausgängen von FB_RoomController sind nicht notwendig; der Baustein bleibt abwärtskompatibel.

Fazit

Da Vererbung einfach einzusetzen ist, ist die Versuchung groß, Basisklassen zu erstellen, in denen alle möglichen Funktionalitäten abgelegt werden – Funktionalitäten die nur von der einen oder anderen Subklasse tatsächlich benötigt werden. Nach einigen Erweiterungszyklen entstehen Abhängigkeiten, die nur schwer zu pflegen sind. Interfaces bieten die Möglichkeit, eine spezielle Aufgabe zu spezifizieren. Bei der Entwicklung eines neuen Funktionsblocks kann auf diese Interfaces zurückgegriffen werden, ohne komplexe Vererbungshierarchien übernehmen zu müssen.

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.

4 thoughts on “IEC 61131-3: Objektkomposition mit Hilfe von Interfaces”

  1. Lach… nachdem ich schon sehr lange über den Nutzen von Interfaces sinniert habe, habe ich das nun endlich mal verstanden.
    Schön das du es in unkomplizierte und einfache Worte packen kannst. – Danke

  2. Hallo Stefan, danke für diesen hilfreichen Post.

    Jetzt würde ich gerne noch ein Array mit den verschiedenen Lichtern füllen.
    Wobei die Interfaces und Methoden ansprechbar bleiben sollen.

    In deinen Beispielen werden Referenzen verwendet, aber das geht ja nicht zusammen mit Arrays, oder?
    Kennst du eine Möglichkeit, so ein Array trotzdem zu erstellen, oder muss ich am Ende alle Objekte mit CASE’es durchlaufen?

Leave a comment