IEC 61131-3: Das Abstract Factory Pattern

Sollen Instanzen eines Funktionsblocks anlegt werden, so muss vor dem Compilieren der genaue Typ des Funktionsblocks bekannt sein. Durch diese feste Zuordnung kann das Verhalten einer Anwendung nur schwer erweitert werden. Dieses ist z.B. der Fall, wenn sich der Funktionsblock in einer Bibliothek befindet und somit der Zugriff auf den Quellcode nicht möglich ist. Die Instanz-Variable ist fest an einen bestimmten Typ gebunden. Eine Klassenfabrik kann helfen diese starren Strukturen aufzubrechen.

Der Begriff Klassenfabrik bezeichnet in der objektorientierten Programmierung ein Objekt, welches andere Objekte erzeugt. Es gibt verschiedene Arten eine Fabrik umzusetzen. Eine davon ist die abstrakte Fabrik, die in dem folgenden Beispiel zum Einsatz kommt. Als Beispiel wird eine kleine SPS-Bibliothek zum Loggen von Meldungen erstellt. Eine abstrakte Klassenfabrik ermöglicht es dem Anwender, den Funktionsumfang der Bibliothek anzupassen, ohne Änderungen an den Sourcen durchführen zu müssen.

Variante 1: einfacher Funktionsblock

Der erste (naheliegende) Schritt besteht darin, einen Funktionsblock für das Loggen der Meldungen zu entwickeln. Hierbei nimmt die Methode Write() den Text entgegen und der Baustein schreibt die Meldung in eine Textdatei.

Sample01

Außerdem erweitert die Methode Write() den Text um das Wort Logger. Zur Kontrolle wird der gesamte Text zurückgegeben.

METHOD PUBLIC Write : STRING
VAR_INPUT
  sMsg    : STRING;
END_VAR

sMsg := CONCAT('logger: ', sMsg);
// open file
// write the message into the file
// close file
Write := sMsg;

Die Anwendung des Bausteins sieht recht einfach aus.

PROGRAM MAIN
VAR
  fbLogger     : FB_Logger();
  sRetMsg      : STRING;
END_VAR

sRetMsg := fbLogger.Write('Hello');

Möchte der Anwender jedoch statt einer einfachen Textdatei wahlweise eine csv- oder xml-Datei nutzen, muss der Entwickler den Baustein erweitern.

Variante 2: Funktionsblock mit Funktionsauswahl

Ein möglicher Ansatz besteht darin, über die Methode FB_Init() jeder Instanz von FB_Logger einen Dateipfad mitzugeben, der festlegt, in welcher Datei die Meldungen gespeichert werden. Die Dateiendung dient hierbei als Kennung des Dateiformat.

Die Methode FB_init() erhält neben den beiden impliziten Parameter bInitRetains und bInCopyCode den zusätzlichen Parameter sFilename.

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains  : BOOL;
  bInCopyCode   : BOOL;
  sFilename     : STRING;
END_VAR

sFilenameExtension := F_ToLCase(RIGHT(sFilename, 3));

Die Dateiendung wird in der Variablen sFilenameExtension abgelegt.

FUNCTION_BLOCK PUBLIC FB_Logger
VAR
  sFilenameExtension : STRING;
END_VAR

In der Methode Write() differenziert eine IF-Anweisung die verschiedenen Varianten und ruft jeweils die passende private Methode auf. Dadurch ist der Funktionsblock in der Lage die Meldungen in unterschiedliche Dateiformate abzulegen.

METHOD PUBLIC Write : STRING
VAR_INPUT
  sMsg    : STRING;
END_VAR

sMsg := CONCAT('logger: ', sMsg);
IF (sFilenameExtension = 'txt') THEN
  Write := WriteTxt(sMsg);
ELSIF (sFilenameExtension = 'csv') THEN
  Write := WriteCsv(sMsg);
ELSIF (sFilenameExtension = 'xml') THEN
  Write := WriteXml(sMsg);
END_IF

Somit erhält FB_Logger fünf Methoden.

Sample02

FB_init() wird automatisch aufgerufen, wenn eine Instanz von FB_Logger angelegt wird. In diesem Beispiel somit beim Starten des Programms. Die Methode kann durch eigene Parameter erweitert werden, diese werden beim Deklarieren einer Instanz mit übergegeben.

WriteTxt(), WriteCsv() und WriteXml() wurden als private Methoden deklariert und sind somit nur innerhalb von FB_Logger aufrufbar.

Write() ist die Methode, die der Anwender von FB_Logger zum Schreiben der Meldungen nutzen kann.

Das Ergebnis ist ein Funktionsblock, der intern alle notwendigen Fälle abdeckt. Der Anwender kann beim Anlegen der Instanz die gewünschte Arbeitsweise durch den Dateinamen vorgeben.

PROGRAM MAIN
VAR
  fbLoggerTxt    : FB_Logger('File.csv');
  sRetMsg        : STRING;
END_VAR

sRetMsg := fbLoggerTxt.Write('Hello');

Doch mit jeder weiteren Speicherart, wird der Baustein größer und belegt mehr Programmspeicher. Werden die Meldungen in eine csv-Datei geschrieben, so wird der Programmcode für die txt-Datei und xml-Datei mit in den Programmspeicher geladen, obwohl dieser nicht benötigt wird.

Variante 3: Funktionsblock mit dynamischer Instanziierung

Hierbei ist das Konzept der dynamischen Speicherverwaltung hilfreich. Dabei werden Instanzen von Funktionsblöcken zur Laufzeit angelegt. Um das bei unserem Beispiel nutzen zu können, werden die Methoden WriteTxt(), WriteCsv() und WriteXml() in separate Funktionsblöcke ausgelagert. Somit hat jede Variante seinen eigenen Funktionsblock, der die Methode Write() enthält.

METHOD PUBLIC Write : STRING
VAR_INPUT
  sMsg    : STRING;
END_VAR

Write := CONCAT('txt-', sMsg);
// open txt file
// write the message into the txt file
// close txt file

Der Operator __NEW() erhält als Parameter einen Standard-Datentypen, allokiert den notwendigen Speicher und liefert einen Zeiger auf das Objekt zurück.

pTxtLogger := __NEW(FB_TxtLogger);

pTxtLogger ist ein Zeiger auf FB_TxtLogger.

pTxtLogger : POINTER TO FB_TxtLogger;

Wurde __NEW() erfolgreich ausgeführt, so ist der Zeiger ungleich 0.

In der Methode FB_init() kann jetzt direkt eine Instanz des gewünschten Funktionsblocks dynamisch angelegt werden. Dadurch ist es nicht mehr notwendig, von allen möglichen Funktionsblöcken eine Instanz statisch anzulegen.

Um den Zugriff auf die Methode Write() des jeweiligen Logger-Bausteins zu vereinfachen, wird das Interface ILogger definiert. FB_TxtLogger, FB_CsvLogger und FB_XmlLogger implementieren diese Schnittstelle.

Zusätzlich zu den drei Zeigern auf die drei möglichen Funktionsblöcke, enthält FB_Logger eine Variable vom Type ILogger.

FUNCTION_BLOCK PUBLIC FB_Logger
VAR
  pTxtLogger   : POINTER TO FB_TxtLogger;
  pCsvLogger   : POINTER TO FB_CsvLogger;
  pXmlLogger   : POINTER TO FB_XmlLogger;
  ipLogger     : ILogger;
END_VAR

In FB_init() wird die jeweilige Instanz des Funktionsblocks angelegt und dem entsprechenden Zeiger zugewiesen.

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains : BOOL;
  bInCopyCode  : BOOL;
  sFilename    : STRING;
END_VAR
VAR
  sFilenameExtension : STRING;
END_VAR

sFilenameExtension := F_ToLCase(RIGHT(sFilename, 3));
IF (sFilenameExtension = 'txt') THEN
  pTxtLogger := __NEW(FB_TxtLogger);
  ipLogger := pTxtLogger^;
ELSIF (sFilenameExtension = 'csv') THEN
  pCsvLogger := __NEW(FB_CsvLogger);
  ipLogger := pCsvLogger^;
ELSIF (sFilenameExtension = 'xml') THEN
  pXmlLogger := __NEW(FB_XmlLogger);
  ipLogger := pXmlLogger^;
ELSE
  ipLogger := 0;
END_IF

In Zeile 14, 17 und 20 wird der Variablen ipLogger der dynamisch angelegte Funktionsblock zugewiesen. Das ist möglich, da alle Funktionsblöcke das Interface ILogger implementieren.

Die Methode Write() von FB_Logger greift über ipLogger auf die Methode Write() von FB_TxtLogger, FB_CsvLogger oder FB_XmlLogger zu:

METHOD PUBLIC Write : STRING
VAR_INPUT
  sMsg    : STRING;
END_VAR

IF (ipLogger <> 0) THEN
  Write := ipLogger.Write(CONCAT('logger: ', sMsg));
END_IF

So wie FB_init() jedes Mal beim Anlegen eines Funktionsblocks aufgerufen wird, wird FB_exit() beim Löschen dementsprechend einmalig durchlaufen. In dieser Methode sollte der Speicher, der zuvor mit __NEW() allokiert wurde, mit __DELETE() wieder freigegeben werden.

METHOD FB_exit : BOOL
VAR_INPUT
  bInCopyCode : BOOL;
END_VAR

IF (pTxtLogger <> 0) THEN
  __DELETE(pTxtLogger);
  pTxtLogger := 0;
END_IF

IF (pCsvLogger <> 0) THEN
  __DELETE(pCsvLogger);
  pCsvLogger := 0;
END_IF

IF (pXmlLogger <> 0) THEN
  __DELETE(pXmlLogger);
  pXmlLogger := 0;
END_IF

Das UML-Diagramm des Beispiels sieht wie folgt aus:

Picture03

Somit besteht das Beispiel aus vier Funktionsblöcken und einem Interface.

Sample03

Die Schnittstelle ILogger vereinfacht die Entwicklung von weiteren Varianten. Allerdings ist darauf zu achten, dass der neue Funktionsblock das Interface ILogger implementiert und die Methode FB_init() im Funktionsblock FB_Logger eine Instanz des neuen Funktionsblocks anlegt. Die Methode Write() von FB_Logger muss hierbei nicht angepasst werden.

Beispiel 1 (TwinCAT 3.1.4020) auf GitHub

Variante 4: abstract Factory

Beim Anlegen einer Instanz von FB_Logger wird durch die Dateiendung festgelegt, in welchem Format Meldungen geloggt werden. Bisher konnte zwischen txt-Datei, csv-Datei und xml-Datei gewählt werden. Sollen weitere Varianten hinzukommen, so muss weiterhin der Funktionsblock FB_Logger angepasst werden. Besteht kein Zugriff auf den Quelltext, so ist eine Erweiterung von FB_Logger nicht möglich.

Eine Klassenfabrik bietet eine interessante Möglichkeit den Funktionsblock FB_Logger deutlich flexibler zu gestalten. Hierbei wird ein Funktionsblock definiert (die eigentliche Klassenfabrik), der über eine Methode eine Referenz auf einen anderen Funktionsblock zurückliefert. Parameter, welche zuvor an die Klassenfabrik übergeben werden, entscheiden welche Art von Referenz erzeugt wird.

Die Funktionalität, die bisher in der Methode FB_init() von FB_Logger enthalten war, wird in einen separaten Funktionsblock, der Klassenfabrik FB_FileLoggerFactory, ausgelagert. Die Klassenfabrik FB_FileLoggerFactory erhält über die Methode FB_init() den Pfad auf die Log-Datei.

An die Methode FB_init() von FB_Logger wird eine Referenz auf die Klassenfabrik übergeben. Die Klassenfabrik liefert über die Methode GetLogger() die Referenz auf den entsprechenden Funktionsblock (FB_TxtLogger, FB_CsvLogger oder FB_XmlLogger) zurück. Das Anlegen der Instanzen erfolgt jetzt durch die Klassenfabrik, nicht durch den Funktionsblock FB_Logger.

Da die Klassenfabrik bei diesem Beispiel immer die Methode GetLogger() bereitstellt, wird diese von einem abstrakten Basis-Funktionsblock abgeleitet, welcher diese Methode vorgibt.

Abstrakte Funktionsblöcke enthalten keine Funktionalitäten. Die Rümpfe der Methoden bleiben leer. Somit kann ein abstrakter Funktionsblock mit einem Interface verglichen werden.

Da die Klassenfabrik von einer abstrakten Klasse (oder hier: abstrakter Funktionsblock) abgeleitet wird, spricht man von einer abstrakten Klassenfabrik.

Das UML-Diagramm sieht somit wie folgt aus:

Picture05

Hier die Darstellung der einzelnen POUs:

Picture06

Für die Nutzung von FB_Logger muss beim Anlegen einer Instanz eine Referenz auf die gewünschte Klassenfabrik übergeben werden.

PROGRAM MAIN
VAR
  fbFileLoggerFactory   : FB_FileLoggerFactory('File.csv');
  refFileLoggerFactory  : REFERENCE TO FB_FileLoggerFactory := fbFileLoggerFactory;
  fbLogger              : FB_Logger(refFileLoggerFactory);
  sRetMsg               : STRING;
END_VAR

sRetMsg := fbLogger.Write('Hello');

Die Klassenfabrik FB_FileLoggerFactory entscheidet in der Methode FB_init(), ob eine Instanz von FB_TxtLogger, FB_CsvLogger oder FB_XmlLogger angelegt werden soll.

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains  : BOOL;
  bInCopyCode   : BOOL;
  sFilename     : STRING;
END_VAR
VAR
  sFilenameExtension : STRING;
END_VAR

sFilenameExtension := F_ToLCase(RIGHT(sFilename, 3));
IF (sFilenameExtension = 'txt') THEN
  pTxtLogger := __NEW(FB_TxtLogger);
  ipLogger := pTxtLogger^;
ELSIF (sFilenameExtension = 'csv') THEN
  pCsvLogger := __NEW(FB_CsvLogger);
  ipLogger := pCsvLogger^;
ELSIF (sFilenameExtension = 'xml') THEN
  pXmlLogger := __NEW(FB_XmlLogger);
  ipLogger := pXmlLogger^;
ELSE
  ipLogger := 0;
END_IF

Die Methode GetLogger() von FB_FileLoggerFactory gibt das Interface ILogger zurück. Über dieses Interface kann auf die Methode Write() des Loggers zugegriffen werden.

METHOD PUBLIC GetLogger : ILogger
VAR_INPUT
END_VAR

GetLogger := ipLogger;

FB_Logger holt sich in FB_init() auf diese Weise Zugriff auf den gewünschten Logger.

METHOD FB_init : BOOL
VAR_INPUT
  bInitRetains       : BOOL;
  bInCopyCode        : BOOL;
  refLoggerFactory   : REFERENCE TO FB_AbstractLoggerFactory;
END_VAR

IF (__ISVALIDREF(refLoggerFactory)) THEN
  ipLogger := refLoggerFactory.GetLogger();
ELSE
  ipLogger := 0;
END_IF

In der Methode Write() von FB_Logger erfolgt der Aufruf indirekt über das Interface ILogger.

METHOD PUBLIC Write : STRING
VAR_INPUT
  sMsg    : STRING;
END_VAR

Write := 'Error';
IF (ipLogger <> 0) THEN
  Write := ipLogger.Write(sMsg);
END_IF

Beispiel 2 (TwinCAT 3.1.4020) auf GitHub

Vorteile einer abstract Factory

Dadurch, dass an FB_Logger eine Klassenfabrik übergeben wird, ist die Funktionalität erweiterbar, ohne das ein Funktionsblock geändert werden muss.

Als Beispiel, wird das obrige Programm so erweitert, dass über die Methode Write() von FB_Logger Meldungen in eine Datenbank geschrieben werden.

Hierzu sind zwei Schritte notwendig:

  1. Es wird eine neue Klassenfabrik definiert, die von FB_AbstractLoggerFactory abgeleitet wird. Dadurch erhält die neue Klassenfabrik die Methode GetLogger().
  2. Es wird der Funktionsblock fürs Loggen angelegt. Dieser implementiert das Interface ILogger und somit die Methode Write().

Die neue Klassenfabrik (FB_DatabaseLoggerFactroy) ist so ausgelegt, das verschiedene Arten von Datenbanken nutzbar wären. Die Methode FB_init() enthält drei Parameter vom Typ string. Zwei Parameter definieren den Benutzernamen und das Passwort, während der dritte Parameter die Verbindungsdaten für die Datenbank enthält.

FB_SQLServerLogger ist der Loggerbaustein für SQL-Server Datenbanken. Es könnten noch weitere Varianten folgen wie z.B. FB_OracleLogger für Oracle-Datenbanken.

Somit erweitert sich das Programm um die Funktionsblöcke FB_DatabaseLoggerFactroy und FB_SQLServerLogger.

Der linke Bereich stellt die Bausteine dar, die sich in einer SPS-Bibliothek befinden könnten. Auf der rechten Seite befinden sich die beiden Funktionsblöcke, die notwendig sind, um das Verhalten von FB_Logger zu verändern.

Picture07

Die Anwendung der neuen Funktionsblöcke ist denkbar einfach:

PROGRAM MAIN
VAR
  fbDatabaseLoggerFactory  : FB_DatabaseLoggerFactory('MyDatabase', 'User', 'Password');
  refDatabaseLoggerFactory : REFERENCE TO FB_DatabaseLoggerFactory := fbDatabaseLoggerFactory;
  fbLogger                 : FB_Logger(refDatabaseLoggerFactory);
  sRetMsg                  : STRING;
END_VAR

sRetMsg := fbLogger.Write('Hello');

Beispiel 3 (TwinCAT 3.1.4020) auf GitHub

Weder FB_Logger noch ein anderer Baustein aus der SPS-Bibliothek musste angepasst werden, um den Baustein FB_Logger in seiner Funktion zu erweitern. Dieses wurde möglich, in dem die Anhängigkeiten der Funktionsblöcke untereinander geändert wurden.

Bei der 3. Variante werden alle Logger-Funktionsblöcke (FB_TxtLogger, FB_CsvLogger, …) direkt von FB_Logger angelegt. Zwischen diesen Funktionsblöcken besteht somit eine feste Abhängigkeit.

Picture08

Bei der 4. Variante befindet sich zwischen den Logger-Funktionsblöcken und FB_Logger eine weitere Ebene. Diese Ebene ist jedoch abstrakt. Die Referenz, die an FB_Logger über die Methode FB_init() übergeben wird, ist eine Referenz auf einen abstrakten Funktionsblock.

Picture09

Erst bei der Benutzung der Bausteine, sprich wenn die Applikation entwickelt wird, legt der Anwender fest, welche konkrete Klassenfabrik anzuwenden ist. Der Funktionsblock FB_Logger sieht nur einen Funktionsblock, der von FB_AbstractLoggerFactory abgeleitet wurde und somit die Methode GetLogger() enthält. Diese Methode liefert das Interface ILogger zurück, hinter der sich die Methode Write() des eigentlichen Loggers befindet.

Wo die konkrete Klassenfabrik definiert wurde, innerhalb der gleichen SPS-Bibliothek oder an anderer Stelle, ist für den Baustein FB_Logger nicht relevant. Auch wie die Klassenfabrik die Logger-Bausteine anlegt, ist für FB_Logger ohne Bedeutung.

Genutzte Bausteine (FB_TxtLogger, …) werden nicht direkt an den nutzenden Baustein (FB_Logger) übergeben. Vielmehr wird die Kontrolle über das Erzeugen der genutzten Bausteine an ein weiteres Modul (FB_FileLoggerFactory, …) übertragen.

Dependency Injection

Der Funktionblock FB_Logger erhält über die Methode FB_init() eine Referenz auf die Klassenfabrik. Somit wird über diese Referenz Funktionalität in den Funktionsblock hineingegeben. Dieses Konzept wird Dependency Injection bezeichnet.

Open Closed Principle

Die objektorientierte Softwareentwicklung definiert einige sogenannte Prinzipen. Das Einhalten dieser Prinzipen soll helfen, die Softwarestruktur sauber zu halten. Eines dieser Prinzipen ist das Open Closed Principle oder auch Offen für Erweiterungen, geschlossen für Änderungen.

Offen für Erweiterungen:

Das bedeutet, dass sich durch die Verwendung von Erweiterungsmodulen die ursprüngliche Funktionalität eines Moduls verändern lässt. Dabei enthalten die Erweiterungsmodule nur die Anpassungen von der ursprünglichen Funktionalität.

Geschlossen für Änderungen:

Das bedeutet, dass keine Änderungen des Moduls nötig sind, um es zu erweitern. Das Modul bietet definierte Erweiterungspunkte, über die sich die Erweiterungsmodule anknüpfen lassen.

Wie das Beispiel zeigt, hilft eine Klassefabrik beim Umsetzen dieses Open Closed Principle.

Fazit

Durch den Einsatz einer abstract Factory konnte der Baustein FB_Logger in seiner Funktionalität erweitert werden, ohne dass dieser geändert wurde. Die neuen Sprachfeatures der IEC 61131-3 haben dieses möglich gemacht. Interfaces, Vererbung und die dynamische Speicherverwaltung bieten völlig neue Ansätze im Design von SPS-Bibliotheken.

Die genaue Definition des Abstract Factory Pattern ist in dem Buch (Amazon-Werbelink *) Design Patterns: Entwurfsmuster als Elemente wiederverwendbarer objektorientierter Software von Erich Gamma, Richard Helm, Ralph E. Johnson und John Vlissides zu finden.

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 )

Facebook photo

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

Connecting to %s

%d bloggers like this: