IEC 61131-3: SOLID – The Open/Closed Principle

Inheritance is a popular method for reusing existing function blocks. It enables new methods and properties to be added or existing methods overwritten without requiring access to the source code for the base function block. Designing software so that it can be extended without modifying the existing code is the key concept behind the Open/Closed Principle (OCP). But using inheritance also has disadvantages. These disadvantages can be minimised by employing interfaces – and this is not the only advantage of this method.

To put it another way, software behaviour should be open to extension without needing to modify the software. Based on our example from my previous posts, we’re going to develop a function block for managing lamp control sequences. We will then add additional functionality to extend this function block. We will use this example to illustrate the key concept underlying the Open/Closed Principle (OCP).

Starting situation

Our main starting point is the function block FB_SequenceManager. This provides access to the individual steps in a sequence via the aSequence property. The Sort() method provides a means to sort the list in accordance with various criteria.

The aSequence property is an array and contains elements of type ST_SequenceItem.

PROPERTY PUBLIC aSequence : ARRAY [1..5] OF ST_SequenceItem

To keep our example simple, we define our array as having fixed upper and lower bounds of 1 and 5. Array elements are of type ST_SequenceItem, which contains a unique ID (nId), the output value for the lamps (nValue) and the duration (nDuration) before switching to the next output value.

TYPE ST_SequenceItem :
STRUCT
  nId        : UINT;
  nValue     : USINT(0..100);
  nDuration  : UINT;
END_STRUCT
END_TYPE

In this example, we will not concern ourselves with methods for processing the sequence. Our example does, however, include a Sort() method for sorting the list by various criteria.

METHOD PUBLIC Sort
VAR_INPUT
  eSortedOrder : E_SortedOrder;
END_VAR

The list can be sorted in ascending order only by nId or nValue.

TYPE E_SortedOrder :
(
  Id,
  Value
);
END_TYPE

In the Sort() method, the eSortedOrder input parameter determines whether the list is sorted by nId or nValue.

CASE eSortedOrder OF
  E_SortedOrder.Id:
    // Sort the list by nId
    // …
  E_SortedOrder.Value:
    // Sort the list by nValue
    // …
END_CASE

Our example is a simple monolithic application which can be put together quickly to meet our requirements.

The UML diagram shows the monolithic structure of the application very clearly:

This does not, however, take account of the amount of work required to realise future extensions.

Sample 1 (TwinCAT 3.1.4024) on GitHub

Extension of the implementation

We are going to extend the application so that, in addition to nId and nValue, we can also sort the list by nDuration. Currently, the list is always sorted in ascending order. We would also like to be able to sort it in descending order.

How can we modify our example to meet these two client requirements?

Approach 1: Quick & dirty

One approach is to simply extend the existing Sort() method so it can also sort by nDuration. To do this, we add the field eDuration to E_SortedOrder.

TYPE E_SortedOrder :
(
  Id,
  Value,
  Duration
);
END_TYPE

We also need a parameter to indicate whether we want to sort in ascending or descending order:

TYPE E_SortedDirection :
(
  Ascending,
  Descending
);
END_TYPE

So the Sort() method now takes two parameters:

METHOD PUBLIC Sort
VAR_INPUT
  eSortedOrder      : E_SortedOrder;
  eSortedDirection  : E_SortedDirection;
END_VAR

The Sort() method now contains two nested CASE statements. The outermost of these deals with the sort direction, the innermost with the parameter by which to sort the list.

CASE eSortedDirection OF
  E_SortedDirection.Ascending:
    CASE eSortedOrder OF
      E_SortedOrder.Id:
        // Sort the list by nId in ascending order
        // …
      E_SortedOrder.Value:
        // Sort the list by nValue in ascending order
        // …
      E_SortedOrder.Duration:
        // Sort the list by nDuration in ascending order
        // …
    END_CASE
  E_SortedDirection.Descending:
    CASE eSortedOrder OF
      E_SortedOrder.Id:
        // Sort the list by nId in descending order
        // …
      E_SortedOrder.Value:
        // Sort the list by nValue in descending order
        // …
      E_SortedOrder.Duration:
        // Sort the list by nDuration in descending order
        // …
    END_CASE
  END_CASE
END_CASE

This approach is quick to implement. For a small application with a reasonably small amount of source code, this is absolutely a reasonable approach. But for this approach to be feasible, we have to have access to the source code. In addition, we need to ensure that FB_SequenceManager isn’t shared with other projects via, for example, a PLC library containing FB_SequenceManager. By adding a parameter to the Sort() method, we have also changed its signature. This means that program components that call this method with just a single parameter will no longer compile.

The UML diagram shows clearly that the structure is unchanged – it’s still a highly monolithic application:

Sample 2 (TwinCAT 3.1.4024) on GitHub

Approach 2: Inheritance

Another way to add features to the application is to use inheritance. This allows us to extend function blocks without having to modify the existing function block.

We start by creating a new function block which inherits from FB_SequenceManager:

FUNCTION_BLOCK PUBLIC FB_SequenceManagerEx EXTENDS FB_SequenceManager

The new function block contains a SortEx() method which takes two parameters specifying the required sort direction and order:

METHOD PUBLIC SortEx : BOOL
VAR_INPUT
  eSortedOrder      : E_SortedOrderEx;
  eSortedDirection  : E_SortedDirection;
END_VAR

Once again we add a data type E_SortedDirection which specifies whether the list should be sorted in ascending or descending order:

TYPE E_SortedDirection :
(
  Ascending,
  Descending
);
END_TYPE

Rather than extending E_SortedOrder, we create a new data type:

TYPE E_SortedOrderEx :
(
  Id,
  Value,
  Duration
);
END_TYPE

We can now implement the required sort functions in the SortEx() method.

To sort in ascending order, we can use the Sort() method from the base function block (FB_SequenceManager). We don’t need to reimplement the existing sorting algorithm. All we need to do is add the additional sort type:

CASE eSortedOrder OF
  E_SortedOrderEx.Id:
    SUPER^.Sort(E_SortedOrder.Id);
  E_SortedOrderEx.Value:
    SUPER^.Sort(E_SortedOrder.Value);
  E_SortedOrderEx.Duration:
    // Sort the list by nDuration in ascending order
    // …
END_CASE

Sorting in descending order needs to be programmed from scratch, however, as this cannot be achieved using existing methods.

If a new function block extends an existing function block, the new function block inherits the functionality of the base function block. The addition of further methods and properties enables it to be extended without needing to modify the base function block (open for extension). By using libraries, it’s also possible to protect the source code from modification (closed for modification).

Inheritance is therefore one way of implementing the Open/Closed Principle (OCP).

Sample 3 (TwinCAT 3.1.4024) on GitHub

This approach does, however, have two disadvantages.

Excessive use of inheritance can end up generating complex hierarchies. A child function block is absolutely dependent on its base function block. If new methods or properties are added to the base function block, every child function block will also inherit these new elements (if they are PUBLIC), even if the child function block has no intention of exposing these elements externally.

In some circumstances, extension by inheritance is only possible where the child function block has access to internal state information from the base function block. Access to these internal elements can be enabled by marking them as PROTECTED. This restricts access to child function blocks only.

In the example given above, the only reason we were able to add the sorting algorithms was because the setter for the aSequence property was declared as PROTECTED. If we did not have write access to the aSequence property, the child function block would not be able to modify the list, so would not be able to sort it.

This means, however, that the developer coding this function block always has to take into consideration two use cases. Firstly, a user making use of the function block’s public methods and properties. Secondly, users using the function block as a base function block and adding new functionality via PROTECTED elements. But which internal elements need to be marked as PROTECTED? And to enable their use, these elements also need to be documented.

Approach 3: Additional interface

Another approach is to use interfaces rather than inheritance. This, however, needs to be considered during the design phase.

If our aim is to design FB_SequenceManager so that users can add whatever sorting algorithms they want, then we should remove the code for sorting the list. The sorting algorithm should instead access the list via an interface.

In our example, we would add the interface I_SequenceSortable. This interface contains the SortList() method, which contains a reference to the list to be sorted.

METHOD SortList
VAR_INPUT
  refSequence  : REFERENCE TO ARRAY [1..5] OF ST_SequenceItem;
END_VAR

Next we create the function blocks containing the various sorting algorithms, each of which implements the I_SequenceSortable interface. As an example, we will take the function block for sorting by nId in ascending order.

FUNCTION_BLOCK PUBLIC FB_SequenceSortedByIdAscending IMPLEMENTS I_SequenceSortable

We can call the function block whatever we want; the crucial point is that it implements the I_SequenceSortable interface. This ensures that FB_SequenceSortedByIdAscending contains the SortList() method. The actual sorting algorithm is implemented in the SortList() method.

METHOD SortList
VAR_INPUT
  refSequence  : REFERENCE TO ARRAY [1..5] OF ST_SequenceItem;
END_VAR
// Sort the list by nId in ascending order
// …

The Sort() method of FB_SequenceManager takes a parameter of type I_SequenceSortable. When calling the Sort() method we pass to it a function block (e.g. FB_SequenceSortedByIdAscending) which implements the I_SequenceSortable interface and therefore also contains the SortList() method. FB_SequenceManager’s Sort() method calls SortList() and passes to it a reference to the aSequence list.

METHOD PUBLIC Sort
VAR_INPUT
  ipSequenceSortable  : I_SequenceSortable;
END_VAR
IF (ipSequenceSortable <> 0) THEN
  ipSequenceSortable.SortList(THIS^._aSequence);
END_IF

This means that a reference to the list to be sorted is passed to the function block containing the implemented sorting algorithm.

We create a separate function block for each sorting algorithm. This means we have access both to FB_SequenceManager containing the Sort() method, and to function blocks containing the sorting algorithms and implementing the I_SequenceSortable interface.

When it calls the Sort() method, FB_SequenceManager passes to it a function block (in our case FB_SequenceSortedByIdAscending). This function block contains the I_SequenceSortable interface subsequently used to call the SortList() method.

PROGRAM MAIN
VAR
  fbSequenceManager              : FB_SequenceManager;
  fbSequenceSortedByIdAscending  : FB_SequenceSortedByIdAscending;
  // …
END_VAR
fbSequenceManager.Sort(fbSequenceSortedByIdAscending);
// …

This approach avoids the use of inheritance. The sorting algorithm function blocks could employ their own inheritance hierarchy if required. These function blocks could also implement additional interfaces, since it is possible to implement multiple interfaces.

Using this interface realises a clear separation between data storage (the list) and data processing (sorting). The aSequence property does not need write access. We also avoid the need to access internal FB_SequenceManager variables.

In addition, we no longer need the E_SortedOrder and E_SortedDirection data types. The sort type is determined solely by which function block we pass to Sort().

We can also add new sorting algorithms without needing to modify or change existing elements.

Sample 4 (TwinCAT 3.1.4024) on GitHub

Optimization analysis

There are various methods for extending the functionally of an existing function block without having to modify it. As well as inheritance – a key feature of object-oriented programming (OOP) – interfaces may provide a better alternative.

Using interfaces brings greater decoupling. But the individual interfaces do have to be implemented in the software design. This means that we need to consider in advance which areas need to be abstracted via interfaces and which don’t.

With inheritance too, when we develop a function block we have to consider which internal elements should be made accessible (by using the PROTECTED keyword) to function blocks derived from it.

The definition of the Open/Closed Principle

The Open/Closed Principle (OCP) was originated in 1988 by Bertrand Meyer. It states:

Software entities should be open for extension, but closed for modification.

Software entity: This means a class, function block, module, method, service, etc.

Open: The behaviour of a software entity should be able to be extended.

Closed: This extensibility should not be achieved by modifying existing software.

When Bertrand Meyer defined the Open/Closed Principle (OCP) in the late 1980s, the focus was on C++ as a programming language. He used the concept of inheritance, a familiar concept in the object-oriented programming world. Object-orientated programming – at the time a fairly young discipline – was seen as promising big improvements in terms of reusability and maintainability as a result of the ability to reuse classes as base classes for new classes.

When Robert C. Martin took up Meyer’s principle in the 1990s, he took a different approach to its technical implementation. C++ allows the use of multiple inheritance, which is rare in more recent programming languages. Consequently, Robert C. Martin focused on the use of interfaces. More information can be found in his book (Amazon ad link *) Clean Architecture: A Craftsman’s Guide to Software Structure and Design.

Summary

Adhering to the Open/Closed Principle (OCP) does carry a risk of overengineering. We should only implement extensibility where it is actually needed. It is impossible to design software so that every conceivable extension can be implemented without needing to modify the source code.

This concludes my series of posts on SOLID principles. Other principles are of course available, including Keep It Simple, Stupid (KISS), Don’t Repeat Yourself (DRY), Law Of Demeter (LOD) and You Ain’t Gonna Need It (YAGNI). What all these principles have in common is the goal of making software more maintainable and more reusable.

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 comment