IEC 61131-3: The Abstract Factory Pattern

If instances of a function block have to be created, the exact type of the function block should be known before compiling. Properties of an application can hardly be expanded by this fixed assignment. For example, this will be the case when the function block is located in a library and the access to the source code is thus not possible. The instance variable is constrained by a specific type. A class factory can help to break these stiff structures.

The term class factory denotes an object in object-oriented programming, which generates other objects. A factory can be implemented in different ways. One of them is abstract factory, which is employed in the following example. As an example, a small PLC library for message logging is created. An abstract class factory allows the user to adjust the library range of functions without changing the sources.

Variant 1: A simple function block

The first (most obvious) step implicates developing a function block for logging or messaging. In the course of this, the Write() method receives the text and the function block writes the message in a text file.

Picture01

Furthermore, the Write() method expands the text by the word Logger. As a check, the whole text is returned.

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;

The implementation of the function block is relatively easy.

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

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

However, if the user would like to use optionally a CSV or XML file instead of the simple text file, the developer should expand the block.

Variant 2: A function block with function selection

One possible approach would be to give a file path to each instance of FB_Logger through the FB_init() method. This path defines in which file the messages are saved. The file extension serves as an identifier of the file format in this case.

The FB_init() method gets not only both implicit parameters bInitRetains and bInCopyCode, but also an additional parameter sFilename.

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

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

The file extension is stored in the variable sFilenameExtension.

FUNCTION_BLOCK PUBLIC FB_Logger
VAR
  sFilenameExtension : STRING;
END_VAR

In the Write() method, an IF statement distinguishes different variants and calls the compatible private method respectively. Thus, the function block is capable to store the messages in different file formats.

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

In this way, FB_Logger gets five methods.

Picture02

FB_init() is called automatically when an instance of FB_Logger is created. In this example, on startup of the application. The method can be expanded by its own parameters, which are forwarded as an instance is declared.

WriteTxt(), WriteCsv() und WriteXml() have been declared as private methods and thus can be called only within FB_Logger.

Write() is a method that the user of FB_Logger can utilise to write the messages.

The result is a function block, which covers all necessary cases internally. The user can specify the desired behaviour by the file name, when creating an instance.

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

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

However, the block becomes bigger with each memory type and occupies space in the program memory. If the reports are written in a CSV file, the program code for the TXT file and XML file is also loaded in the program memory, although it is not required.

Variant 3: A function block with dynamic instantiation

In this case, the concept of the dynamic memory management is helpful. Instance of the function block are created at run-time as well. In order to be able to use it in our example, the methods WriteTxt(), WriteCsv() and WriteXml() are transformed into separate function blocks. In this way, each variant has its own function block which contains the Write() method.

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

The operator __NEW() gets a standard data type as a parameter, allocates the necessary memory and returns a pointer to the object.

pTxtLogger := __NEW(FB_TxtLogger);

pTxtLogger is a pointer to FB_TxtLogger.

pTxtLogger : POINTER TO FB_TxtLogger;

If __NEW() is successfully executed, the pointer is unequal to zero.

An instance of a desired function block can be now dynamically created in the FB_init() method. As a consequence, it is not anymore necessary to create an instance of all possible function blocks statically.

In order to simplify an access to the Write() method of the respective logger block, the interface ILogger is defined. FB_TxtLogger, FB_CsvLogger and FB_XmlLogger implement this interface.

FB_Logger contains a variable of the type ILogger in addition to the three pointers to the three possible function blocks.

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

The respective instance of the function block is created and is assigned to the corresponding pointer in FB_init().

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

The dynamically created function block is assigned to the variable ipLogger in lines 14, 17 and 20. This is possible, because all function blocks implement the interface ILogger.

The Write() method of FB_Logger accesses the Write() method of FB_TxtLogger, FB_CsvLogger or FB_XmlLogger through ipLogger:

METHOD PUBLIC Write : STRING
VAR_INPUT
  sMsg    : STRING;
END_VAR

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

In the same way as FB_init() is called each time as a function block is created, FB_exit() is executed respectively one time when deleting. The memory previously allocated with __NEW() should be released again with __DELETE().

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

The UML diagram of the example looks as follows:

Picture03

Thus, the example contains four function blocks and one interface.

Picture04

The interface ILogger simplifies developing further variants. However, attention should be paid that the new function block implements the interface ILogger, and the method FB_init() in the function block FB_Logger creates an instance of the new function block. The Write() method of FB_logger doesn’t have to be adopted.

Example 1 (TwinCAT 3.1.4020) on GitHub

Variant 4: An abstract factory

As an instance of FB_Logger is created, it is set through the file extension, in what format the messages are logged. You could choose so far between a TXT file, CSV file or XML file. If further variants have to be added, the function block FB_Logger should still be adjusted. If the source code can not be accessed, it is not possible to expand FB_Logger.

A class factory offers an interesting opportunity to construct the function block FB_Logger distinctly more flexible. In this case, a function block is defined (the actual class factory), which provides a reference to another function block through a method. Parameters, which were previously transferred to the class factory, determine which sort of reference is generated.

The functionality, which was contained in the FB_init() method of FB_Logger up to now, is transferred into a separate function block, the class factory FB_FileLoggerFactory. The class factory FB_FileLoggerFactory gets the file path to the log file through the FB_init() method.

A reference to the class factory is passed to the FB_init() method of FB_Logger. The class factory provides the reference to the respective function block (FB_TxtLogger, FB_CsvLogger or FB_XmlLogger) through the GetLogger() method. Creating of the instances is carried out by the class factory and not by the function block FB_Logger().

Since the class factory always supplies the GetLogger() method in this example, it is derived from an abstract base function block, which specifies this method.

Abstract function blocks do not obtain any functionalities. The methods’ bodies stay void. In this way, an abstract function block can be compared with an interface.

Since the class factory is derived from an abstract class (here: abstract function block), it is referred as abstract class factory.

The UML diagram looks as follows:

Picture05

Here is the representation of the single POUs:

Picture06

To use FB_Logger, a reference has to be passed to the wanted class factory in case of creating an instance.

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');

The class factory FB_FileLoggerFactory determines in the FB_init() method, whether an instance of FB_TxtLogger, FB_CsvLogger or FB_XmlLogger should be created.

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

The GetLogger() method of FB_FileLoggerFactory() returns the interface ILogger. The Write() method of the logger can be accessed through this interface.

METHOD PUBLIC GetLogger : ILogger
VAR_INPUT
END_VAR

GetLogger := ipLogger;

In such a way, an access to the wanted logger is obtained by FB_Logger in FB_init().

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

The call in the Write() method of FB_Logger() is carried out indirectly through the interface ILogger.

METHOD PUBLIC Write : STRING
VAR_INPUT
  sMsg    : STRING;
END_VAR

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

Example 2 (TwinCAT 3.1.4020) on GitHub

Advantages of an abstract factory

As a result of passing a class factory to FB_Logger(), the functionality is expandable without having to adjust any function block.

As an example, the foregoing program is extended in such a way, that the messages are written in the database through the Write() method of FB_Logger.

For this purpose, two steps are necessary:

  1. A new class factory is defined, which is derived from FB_AbstractLoggerFactory. In this way, the new class factory obtains the GetLogger() method.
  2. A function block for logging is created. This block implements the interface ILogger and consequently the Write() method.

A new class factory is defined, which is derived from FB_AbstractLoggerFactory. In this way, the new class factory obtains the GetLogger() method.

The new class factory (FB_DatabaseLoggerFactory) is created in such a way, that different kinds of databases are available. The FB_init() method gets three parameters of type string. Two parameters define the user name and the password, while the third one contains connection data for the database.

FB_SQLServerLogger is a logger block for SQL server databases. Further variants can follow, for example, FB_OracleLogger for Oracle databases.

Thus, the program expands by the function blocks FB_DatabaseLoggerFactory and FB_SQLServerLogger.

The left section represents the blocks, which can be located in a PLC library. Both function blocks on the right side are necessary to change the behaviour of FB_Logger.

Picture07

The implementation of the new function blocks is very simple:

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');

Example 3 (TwinCAT 3.1.4020) on GitHub

Neither FB_Logger nor any other block from a PLC library has to be adjusted in order to expand the block FB_Logger in it’s function. This is possible, because the dependencies of the function blocks between themselves were changed.

In the 3d variant, all logger function blocks (FB_TxtLogger, FB_CsvLogger, …) are created directly by FB_Logger. Thus, a strong dependency exists between these function blocks.

Picture08

In the 4th variant, there is a further layer between the logger function blocks and FB_Logger. This layer is however an abstract one. The reference, which is passed to FB_Logger through the FB_init() method, is a reference to an abstract function block.

Picture09

The user determines which specific class factory to apply only when using blocks, i.e. when the application is developed. The function block FB_Logger sees only one function block, which is derived from FB_AbstractLoggerFactory and contains the GetLogger() method. This method returns the interface ILogger, behind which the Write() method of the actual logger is located.

It is not relevant for the block FB_Logger, where the specific class factory is defined: within the same PLC library or elsewhere. It is also irrelevant for FB_Logger, how the class factory creates the logger blocks.

The used blocks (FB_TxtLogger, …) are not directly passed to the using block (FB_Logger). But controlling is rather transferred to a further module (FB_FileLoggerFactory, …) through generating of the used blocks.

Dependency Injection

The function block FB_Logger obtains a reference to the class factory through the FB_init() method. Thus, functionality is added to the function block through this reference. This concept is denoted as Dependency Injection.

Open Closed Principle

Object-oriented programming defines several so-called principles. Following these principles should help to keep software structure clean. One of these principles is Open Closed Priciple, i.e. open for extension, but closed for modification.

Open for extension:

It means that the original functionality of a module can be changed through the usage of the extension modules. At the same time, the extension modules contain only the adjustments of the original functionality.

Closed for modification:

It means that no modifications of a module are necessary to extend it. The module provides defined extension points, through which the extension modules can be connected.

As the example shows, a class factory helps by implementing this Open Closed Principle.

Conclusion

The functionality of the block FB_Logger could be extended without modifying the block itself through the application of an abstract factory. The new language features of IEC 61131-3 have made it possible. Interfaces, inheritance and dynamic memory management offer entirely new approaches in design of PLC libraries.

You will find the exact definition of the abstract factory pattern in the book (Amazon Advertising Link *) Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph E. Johnson and John Vlissides.

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.

2 thoughts on “IEC 61131-3: The Abstract Factory Pattern”

  1. Great articles on OOP and IEC61131-3 Stefan!

    Question about using __NEW:
    Dynamically creating FB instances obviously have great advantages, but I am reluctant to start using it because of fear of memory fragmentation. Rebooting our computer every now and then is ok, but rebooting our control system to defragment memory will not be accepted 🙂
    Do you have any additional insight in this? Can we safely use __NEW?

    Some more info here:
    https://help.codesys.com/webapp/fb_factory;product=LibDevSummary;version=3.5.14.0

    “That is why the MemoryPool and the operators __New and __Delete can only be recommended for applications in very special cases. Especially when dealing with libraries this method should not be used!”

  2. Great article Stefan!

    Is there any way to supply a variable that points to the type needed for the _NEW parameter?

    Something like the following Pseudo code:

    fbToCreate := ‘FB_Test’; //string describing the FB I want to create.
    ptr := __NEW(fbToCreate);

    If there are many (i.e. hundreds) of FB’s that can be potentially __NEW’ed then we need a lot of individual ‘factories’ to create them, or a single master factory with a large IF..ELSIF..ELSIF or a large CASE block.

Leave a comment