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.

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.

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:

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

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:

Hier die Darstellung der einzelnen POUs:

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:
- Es wird eine neue Klassenfabrik definiert, die von FB_AbstractLoggerFactory abgeleitet wird. Dadurch erhält die neue Klassenfabrik die Methode GetLogger().
- 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.

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.

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.

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.