IEC 61131-3: Das Observer Pattern

Das Observer Pattern ist für Anwendungen geeignet, in denen gefordert wird, dass ein oder mehrere Funktionsblöcke benachrichtigt werden, sobald sich der Zustand eines bestimmten Funktionsblocks verändert. Hierbei ist die Zuordnung der Kommunikationsteilnehmer zur Laufzeit des Programms veränderbar.

In nahezu jedem IEC 61131-3 Programm tauschen Funktionsblöcke Zustände miteinander aus. Im einfachsten Fall, wird einem Eingang eines FBs der Ausgang eines anderen FBs zugeordnet.

Pic01

Somit lassen sich recht einfach Zustände zwischen Funktionsblöcken austauschen. Doch diese Einfachheit hat seinen Preis:

Unflexibel. Die Zuordnung zwischen fbSensor und den drei Instanzen von FB_Actuator ist fest im Programmcode implementiert. Eine dynamische Zuordnung zwischen den FBs während der Laufzeit ist nicht möglich.

Feste Abhängigkeiten. Der Datentyp der Ausgangsvariable von FB_Sensor muss kompatibel zu der Eingangsvariable von FB_Actuator sein. Gibt es einen neuen Sensorbaustein, dessen Ausgangsvariable inkompatibel zu dem vorherigen Datentyp ist, hat dies zwingend eine Anpassung des Datentyps der Aktoren zur Folge.

Aufgabenstellung

Das folgende Beispiel soll zeigen, wie mit Hilfe des Observern Pattern, auf die feste Zuordnung zwischen den Kommunikationseilnehmern verzichtet werden kann. Der Sensor liest aus einer Datenquelle einen Messwert (z.B. eine Temperatur) aus, während der Aktor in Abhängigkeit eines Messwertes Aktionen ausführt (z.B. eine Temperaturregelung). Die Kommunikation zwischen den Teilnehmern soll veränderbar sein. Sollen die genannten Nachteile eliminiert werden, so helfen zwei grundlegende OO-Entwurfsprinzipien:

  • Identifiziere jene Bereiche, die konstant bleiben und trenne sie von denen, die sich verändern.
  • Programmiere nie direkt auf eine Implementierung, sondern immer auf Schnittstellen. Die Zuordnung zwischen Ein- und Ausgangsvariablen darf somit nicht mehr fest implementiert werden.

Elegant realisierbar ist dieses mit Hilfe von Interfaces, welche die Kommunikation zwischen den FBs definieren. Es erfolgt nicht mehr eine feste Zuordnung von Ein- und Ausgangsvariablen. Hierdurch entsteht zwischen den Teilnehmern eine lose Koppelung. Softwaredesign auf Basis von loser Kopplung ermöglicht es, flexible Softwaresysteme aufzubauen, die mit Veränderungen besser klarkommen, da die Abhängigkeiten zwischen den Teilnehmern minimiert werden.

Definition des Observer Pattern

Das Observer Pattern bietet einen effizienten Kommunikationsmechanismus zwischen mehreren Teilnehmern, wobei ein bzw. mehrere Teilnehmer von dem Zustand eines Teilnehmers abhängig sind. Der Teilnehmer, der einen Zustand zur Verfügung stellt, wird hierbei Subject (FB_Sensor) genannt. Die Teilnehmer, die von dem Zustand abhängig sind, heißen Observer (FB_Actuator).

Das Observer Pattern wird häufig mit einem Zeitungsabonnementdienst verglichen. Der Herausgeber ist das Subject, während die Abonnenten die Observer darstellen. Der Abonnent muss sich beim Herausgeber registrieren. Bei der Registrierung wird evtl. noch angegeben, welche Informationen gewünscht werden. Vom Herausgeber wird eine Liste gepflegt, in dem alle Abonnenten gespeichert sind. Sobald eine neue Veröffentlichung vorliegt, sendet der Herausgeber an alle Abonnenten aus der Liste die gewünschten Informationen.

Formeller wird dieses im Buch (Amazon-Werbelink *) Design Patterns: Entwurfsmuster als Elemente wiederverwendbarer objektorientierter Software von Gamma, Helm, Johnson und Vlissides ausgedrückt:

Das Observer Pattern definiert eine 1-zu-n-Abhängigkeit zwischen Objekten, so dass die Änderung des Zustands eines Objekts dazu führt, das alle abhängigen Objekte benachrichtigt und automatisch aktualisiert werden.

Implementierung

Wie das Subject die Daten erhält und wie der Observer die Daten weiterverarbeitet, soll an dieser Stelle nicht weiter vertieft werden.

Observer

Über die Methode Update() wird der Observer vom Subject bei Wertänderung benachrichtigt. Da dieses Verhalten bei allen Observern gleich ist, wird das Interface I_Observer definiert, welches alle Observer implementieren.

Der Funktionsblock FB_Observer definiert außerdem eine Eigenschaft, die den aktuellen Istwert zurückliefert.

Pic02Pic03

Da die Daten per Methode ausgetauscht werden, sind weitere Ein- oder Ausgänge nicht notwendig.

FUNCTION_BLOCK PUBLIC FB_Observer IMPLEMENTS I_Observer
VAR
  fValue      : LREAL;
END_VAR

Hier die Implementierung der Methode Update():

METHOD PUBLIC Update
VAR_INPUT
  fValue      : LREAL;
END_VAR
THIS^.fValue := fValue;

und das Property fActualValue:

PROPERTY PUBLIC fActualValue : LREAL
fActualValue := THIS^.fValue;

Subject

Das Subject verwaltet eine Liste von Observern. Über die Methoden Attach() und Detach() können sich die einzelnen Observer an- und abmelden.

Pic04Pic05

Da alle Observer das Interface I_Observer implementieren, ist die Liste vom Typ ARRAY [1..Param.cMaxObservers] OF I_Observer. Die genaue Implementierung der Observer muss an dieser Stelle nicht bekannt sein. Es können weitere Varianten von Observern erstellt werden, solange diese das Interface I_Observer implementieren, kann das Subject mit diesen kommunizieren.

Die Methode Attach() enthält als Parameter den Interface Pointer auf den Observer. Bevor dieser in die Liste abgelegt wird (Zeile 23), wird geprüft ob er gültig und nicht schon in der Liste enthalten ist.

METHOD PUBLIC Attach : BOOL
VAR_INPUT
  ipObserver          : I_Observer;
END_VAR
VAR
  nIndex              : INT := 0;
END_VAR

Attach := FALSE;
IF (ipObserver = 0) THEN
  RETURN;
END_IF
// is the observer already registered?
FOR nIndex := 1 TO Param.cMaxObservers DO
  IF (THIS^.aObservers[nIndex] = ipObserver) THEN
    RETURN;
  END_IF
END_FOR

// save the observer object into the array of observers and send the actual value
FOR nIndex := 1 TO Param.cMaxObservers DO
  IF (THIS^.aObservers[nIndex] = 0) THEN
    THIS^.aObservers[nIndex] := ipObserver;
    THIS^.aObservers[nIndex].Update(THIS^.fValue);
    Attach := TRUE;
    EXIT;
  END_IF
END_FOR

Auch die Methode Detach() enthält als Parameter den Interface Pointer auf den Observer. Ist der Interface Pointer gültig, wird in der Liste nach dem Observer gesucht und die entsprechende Stelle gelöscht (Zeile 15).

METHOD PUBLIC Detach : BOOL
VAR_INPUT
  ipObserver     : I_Observer;
END_VAR
VAR
  nIndex         : INT := 0;
END_VAR

Detach := FALSE;
IF (ipObserver = 0) THEN
  RETURN;
END_IF
FOR nIndex := 1 TO Param.cMaxObservers DO
  IF (THIS^.aObservers[nIndex] = ipObserver) THEN
    THIS^.aObservers[nIndex] := 0;
    Detach := TRUE;
  END_IF
END_FOR

Liegt eine Statusänderung im Subject vor, so wird von allen gültigen Interface Pointern, die sich in der Liste befinden, die Methode Update() aufgerufen (Zeile 8). Diese Funktionalität liegt in der privaten Methode Notify().

METHOD PRIVATE Notify
VAR
  nIndex          : INT := 0;
END_VAR

FOR nIndex := 1 TO Param.cMaxObservers DO
  IF (THIS^.aObservers[nIndex] <> 0) THEN
    THIS^.aObservers[nIndex].Update(THIS^.fActualValue);
  END_IF
END_FOR

In diesem Beispiel generiert das Subject jede Sekunde einen Zufallswert und benachrichtigt anschließend die Observer über die Methode Notify().

FUNCTION_BLOCK PUBLIC FB_Subject IMPLEMENTS I_Subject
VAR
  fbDelay                : TON;
  fbDrand                : DRAND;
  fValue                 : LREAL;
  aObservers             : ARRAY [1..Param.cMaxObservers] OF I_Observer;
END_VAR

// creates every sec a random value and invoke the update method
fbDelay(IN := TRUE, PT := T#1S);
IF (fbDelay.Q) THEN
  fbDelay(IN := FALSE);
  fbDrand(SEED := 0);
  fValue := fbDrand.Num * 1234.5;
  Notify();
END_IF

Im Subject gibt es keine Anweisung, bei der auf FB_Observer direkt zugegriffen wird. Der Zugriff findet immer indirekt über das Interface I_Observer statt. Eine Anwendung kann mit beliebigen Observer erweitert werden, solange diese das Interface I_Observer implementieren, sind keine Anpassungen am Subject notwendig.

Pic06

Anwendung

Der folgende Baustein soll helfen das Beispielprogramm zu testen. In diesem wird ein Subject und zwei Observer angelegt. Durch Setzen entsprechender Hilfsvariablen können die beiden Observer zur Laufzeit sowohl mit dem Subject verbunden, als auch wieder getrennt werden.

PROGRAM MAIN
VAR
  fbSubject               : FB_Subject;
  fbObserver1             : FB_Observer;
  fbObserver2             : FB_Observer;
  bAttachObserver1        : BOOL;
  bAttachObserver2        : BOOL;
  bDetachObserver1        : BOOL;
  bDetachObserver2        : BOOL;
END_VAR

fbSubject();
IF (bAttachObserver1) THEN
  fbSubject.Attach(fbObserver1);
  bAttachObserver1 := FALSE;
END_IF
IF (bAttachObserver2) THEN
  fbSubject.Attach(fbObserver2);
  bAttachObserver2 := FALSE;
END_IF
IF (bDetachObserver1) THEN
  fbSubject.Detach(fbObserver1);
  bDetachObserver1 := FALSE;
END_IF
IF (bDetachObserver2) THEN
  fbSubject.Detach(fbObserver2);
  bDetachObserver2 := FALSE;
END_IF

Beispiel 1 (TwinCAT 3.1.4022) auf GitHub

Optimierungen

Subject: Interface oder Basisklasse?

Die Notwendigkeit des Interfaces I_Observer ist bei dieser Implementierung offensichtlich. Die Zugriffe auf Observer werden durch das Interface von der Implementierung entkoppelt.

Dagegen erscheint das Interface I_Subject hier nicht erforderlich. Und tatsächlich könnte auf das Interface I_Subject verzichtet werden. Ich habe es aber trotzdem vorgesehen, da es die Option offen hält, spezielle Varianten von FB_Subject anzulegen. So könnte es einen Funktionsblock geben, der die Observer-Liste nicht in einem Array organisiert. Der Zugriff auf die Methoden zum An- und Abmelden der unterschiedlichen Observer könnte dann generisch über das Interface I_Subject erfolgen.

Der Nachteil bei dem Interface liegt allerdings darin, dass der Code für das An- und Abmelden jedesmal neu implementiert werden muss, auch dann, wenn es die Applikation nicht erfordert. Stattdessen erscheint eine Basisklasse (FB_SubjectBase) für das Subject sinnvoller. Der Verwaltungscode für die Methoden Attach() und Detach() könnten in diese Basisklasse verlagert werden. Besteht die Notwendigkeit ein spezielles Subject (FB_SubjectNew) zu erstellen, so kann von dieser Basisklasse (FB_SubjectBase) geerbt werden.

Was ist aber, wenn dieser spezielle Funktionsblock (FB_SubjectNew) schon von einer anderen Basisklasse (FB_Base) erbt? Mehrfachvererbung ist nicht möglich (dagegen können aber mehrere Interfaces implementiert werden).

Hier bietet es sich an, die Basisklasse in den neuen Funktionsblock einzubetten, also eine lokale Instanz von FB_SubjetBase anzulegen.

FUNCTION_BLOCK PUBLIC FB_SubjectNew EXTENDS FB_Base IMPLEMENTS I_Subject
VAR
  fValue              : LREAL;
  fbSubjectBase       : FB_SubjectBase;
END_VAR

In den Methoden Attach() und Detach() kann dann auf diese lokale Instanz zugegriffen werden.

Methode Attach():

METHOD PUBLIC Attach : BOOL
VAR_INPUT
  ipObserver          : I_Observer;
END_VAR

Attach := FALSE;
IF (THIS^.fbSubjectBase.Attach(ipObserver)) THEN
  ipObserver.Update(THIS^.fValue);
  Attach := TRUE;
END_IF

Methode Detach():

METHOD PUBLIC Detach : BOOL
VAR_INPUT
  ipObserver		: I_Observer;
END_VAR

Detach := THIS^.fbSubjectBase.Detach(ipObserver);

Methode Notify():

METHOD PRIVATE Notify
VAR
  nIndex              : INT := 0;
END_VAR

FOR nIndex := 1 TO Param.cMaxObservers DO
  IF (THIS^.fbSubjectBase.aObservers[nIndex] <> 0) THEN
    THIS^.fbSubjectBase.aObservers[nIndex].Update(THIS^.fActualValue);
  END_IF
END_FOR

Somit implementiert das neue Subject das Interface I_Subject, erbt von dem Funktionsblock FB_Base und kann über die eingebettete Instanz auf die Funktionalitäten von FB_SubjectBase zugreifen.

Pic07

Beispiel 2 (TwinCAT 3.1.4022) auf GitHub

Update: Push- oder Pull-Methode?

Es gibt zwei Varianten, wie der Observer die gewünschten Informationen vom Subject erhält:

Bei der Push-Methode werden über die Update-Methode alle Informationen an den Observer übergeben. Für den gesamten Informationsaustausch ist nur der Aufruf einer Methode notwendig. In dem Beispiel war es immer nur eine Variable vom Datentyp LREAL, die das Subject übergeben hat. Je nach Anwendung, können es aber deutlich mehr Daten sein. Doch nicht jeder Observer benötigt immer alle Informationen, die an ihn übergeben werden. Weiterhin werden Erweiterungen erschwert: Was ist, wenn die Methode Update() um weitere Daten erweitert wird? Es müssen alle Observer angepasst werden. Abhilfe schafft in diesem Fall die Nutzung eines speziellen Funktionsblocks als Parameter. Dieser Funktionsblock kapselt alle notwendigen Informationen in Eigenschaften. Kommen weitere Eigenschaften hinzu, so ist es nicht notwendig die Update-Methode anzupassen.

Wird die Pull-Methode implementiert, so erhält der Observer nur eine minimale Benachrichtigung. Dieser holt sich dann aus den Subject alle Informationen die benötigt werden. Hierzu müssen allerdings zwei Bedingungen erfüllt sein. Zum einen muss das Subject alle Daten als Eigenschaften zur Verfügung stellen. Zum anderen muss der Observer eine Referenz auf das Subject erhalten, damit dieser auf die Eigenschaften zugreifen kann. Ein Lösungsansatz kann darin bestehen, dass die Update-Methode als Parameter eine Referenz auf das Subject (also auf sich selbst) enthält.

Natürlich lassen sich beide Varianten miteinander kombinieren. Das Subject stellt alle relevanten Daten als Eigenschaften zur Verfügung. Gleichzeitig kann die Update-Methode eine Referenz auf das Subject mitliefern und die wichtigsten Informationen als Funktionsblock übergeben. Dieser Ansatz ist das klassische Vorgehen von zahlreichen GUI-Bibliotheken.

Tipp: Sofern das Subject wenig über seine Observer weiß, ist die Pull-Methode vorzuziehen. Kennt das Subject dagegen seine Observer (da es nur wenige verschiede Arten von Observern geben kann), sollte die Push-Methode angewendet werden.

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.

7 thoughts on “IEC 61131-3: Das Observer Pattern”

  1. Super Artikel!,

    ich hatte genau so etwas auch schon im Kopf aber nie die Zeit mir ernsthaft über die Implementierung Gedanken zu machen.
    Ich würde sagen, man könnte im Falle der Push Methode auch von einer Art ereignisgesteuerten Programmierung sprechen. (Mit dem Ziel so etwas nachzubilden, starteten meine Überlegungen).

    Ich freue mich auf weitere interessante Artikel.
    Gruß Wolfgang

  2. Sehr guter Artikel!

    In den Abfragen ob die Interfaces null sind fehlt “”
    z.B. :IF (THIS^.aObservers[nIndex] 0) THEN
    muss IF (THIS^.aObservers[nIndex] 0) THEN heißen

    1. Hallo Andreas,
      vielen Dank für den Hinweis. Jetzt sollte alles wieder passen.
      Leider hat der Editor für WordPress Probleme mit dem Kleiner-Zeichen und dem Größer-Zeichen (scheinbar wurden auch die Zeichen in den Kommentaren ausgefiltert). Ich muss den Quellcode der Artikel immer manuell durchsehen und die Zeichen ‘escapen’. Dabei muss ich wohl einige Stellen übersehen haben.
      Stefan

  3. Hallo, in der Beschreibung des Texts wird bei der Input-Variable “ipObserver” die Formulierung “Interface Pointer” verwendet. Müsste die Variable dann nicht als REFERENCE TO I_Observer bzw. POINTER TO I_Observer deklariert sein? Und müsste das Array, das die registrierten Obeserver abspeichert nicht auch ein Array of Reference To… sein? Oder sind Variablen, die als Interface typisiert sind automatisch Pointer?
    Wenn ich den Code so richtig verstehe, dann kommt es beim Speichern der Observer im I_Subject zu einem copy-by-value des Observers, nicht zu einem copy-by-reference. Somit bekommt man sofort Probleme, wenn der Observer an anderer Stelle ebenfalls registriert wird, da es sich nicht mehr um dieselbe Instanz handelt.

    1. Hi,
      die Bezeichnung ‘Interface-Pointer’ kann durchaus verwirrend wirken. Aber hinter einem Interface verbirgt sich immer ein Pointer. Gut zu sehen ist dieses im Online Monitor. Dort wird in der Spalte ‚Value‘ nur eine Adresse angezeigt. Eine Variable vom Typ eines Interfaces, welche noch keinem FB zugewiesen wurde, besitzt den Wert 0. Vor diesem Hintergrund ist es bei uns üblich von einem Interface-Pointer zu sprechen, sobald eine Variable vom Typ eines Interfaces deklariert wird. Ein POINTER TO I_Foo wäre somit vergleichbar mit einem Pointer auf einen Pointer.
      fbFoo : FB_Foo; // hat das Interface I_Foo implementiert
      ipFoo : I_Foo := fbFoo; // ipFoo zeigt auf die Methoden und Eigenschaften die fbFoo durch das Interface I_Foo zur Verfügung stellt.
      ipFoo2 : I_Foo; // ipFoo2 ist nicht initialisiert und hat den Wert 0.
      Stefan

  4. Sehr gelungener Artikel!

    Könntest sie bitte ein Beispiel der “PULL-Methode” hinzufügen?

    Vielen Dank und Gruß
    Markus

Leave a reply to Wolfgang Cancel reply