Skip to main content

posts

TwinCAT OOP: When to Use Inheritance and Interfaces

A practical guide to inheritance and interfaces in TwinCAT Structured Text, with examples for PLC frameworks.

TwinCAT and CODESYS support object-oriented extensions for IEC 61131-3 Structured Text. That does not mean every PLC program should be written like a desktop application.

The useful question is smaller:

When should a PLC programmer use inheritance, and when should a PLC programmer use interfaces?

The short answer is this:

Use inheritance for small, stable hierarchies. Use interfaces for behavior that must work across different devices.

Function Blocks as Classes

In TwinCAT, a function block can act like a class. It has internal state. It can expose methods. It can expose properties. The cyclic body or an explicit update() method can process its behavior.

A simple light could expose one property:

FUNCTION_BLOCK Light
VAR
    _on : BOOL;
END_VAR

The property On controls the internal state or a mapped output.

PROPERTY On : BOOL

Getter:

On := _on;

Setter:

_on := On;

Usage stays simple:

VAR
    light : Light;
END_VAR

light.On := TRUE;

So far, this is not revolutionary. It is only a function block with a cleaner public API. The interesting part starts when the system contains different kinds of lights.

Some lights are only on or off. Some are dimmable. Some have RGB channels. Some may be virtual devices used for simulation or testing.

Putting every variant into one large Light function block is possible. It is also how small examples become large maintenance problems. The block grows flags, unused variables, special cases, and customer-specific branches. Very elegant, if the goal is job security through confusion.

Inheritance

Inheritance lets one function block extend another function block.

A dimmable light can extend a basic light:

FUNCTION_BLOCK LightDimmable EXTENDS Light
VAR
    dimmingValue : LREAL;
END_VAR

The derived block inherits the public members of Light and adds its own property.

PROPERTY DimmingValue : LREAL

Usage:

VAR
    light : LightDimmable;
    dimmingValue : LREAL;
END_VAR

light.On := TRUE;
light.DimmingValue := 0.3;
dimmingValue := light.DimmingValue;

This works well when the relationship is stable:

A dimmable light is a light.

It also allows a reference to the base type to refer to an instance of the derived type.

VAR
    lightSimple : Light;
    lightDimmable : LightDimmable;
    lightReference : REFERENCE TO Light;
END_VAR

lightReference REF= lightDimmable;

IF __ISVALIDREF(lightReference) THEN
    lightReference.On := TRUE;
END_IF

The reverse does not work. A reference to LightDimmable cannot point to a plain Light, because a plain Light has no DimmingValue property.

That is correct. It is also the first important limitation.

The base class starts to define the shape of the hierarchy. If more variants appear, the hierarchy can become awkward:

  • dimmable light
  • RGB light
  • dimmable RGB light
  • simulated light
  • diagnostic light
  • vendor-specific light

At that point, inheritance is no longer solving the problem. It is organizing the problem into a tree and hoping the real world behaves like a tree. It often does not.

Where Inheritance Fits

Inheritance is useful when the hierarchy is shallow and stable.

Good candidates are small families with clear relationships:

  • basic indicator and specialized indicator
  • base simulation block and one test implementation
  • common diagnostic base with a few narrow extensions

The practical rule is:

If the hierarchy needs a diagram to understand, it is probably already too deep.

For PLC frameworks, inheritance should usually stay one level deep. It should not become the main tool for sharing behavior across unrelated devices.

Interfaces

An interface defines what a function block must provide. It does not define how the function block works internally.

For a switchable device, the interface could be:

INTERFACE ISwitchable

With one property:

PROPERTY On : BOOL

A light can implement it:

FUNCTION_BLOCK Light IMPLEMENTS ISwitchable
VAR
    on : BOOL;
END_VAR

A ventilator can implement the same interface:

FUNCTION_BLOCK Ventilator IMPLEMENTS ISwitchable
VAR
    on : BOOL;
END_VAR

A ventilator is not a light. But both can be switched on and off. That is the point.

The interface describes behavior the caller may use. It avoids forcing unrelated devices into the same inheritance tree.

Interface References

An interface variable can refer to any instance that implements the interface.

VAR
    device : ISwitchable;
    light : Light;
    ventilator : Ventilator;
END_VAR

device := light;

IF device <> 0 THEN
    device.On := TRUE;
END_IF

device := ventilator;

IF device <> 0 THEN
    device.On := FALSE;
END_IF

The caller does not need to know whether the device is a light, fan, pump, or simulated object. It only needs the contract:

This object has an On property with the agreed meaning.

That last part matters. The compiler checks that the property exists. It does not check the semantic convention. A troll implementation could switch the output off when On is set to TRUE. The compiler will not care. Your machine might.

Interfaces define syntax. The engineering team must still define the protocol.

Keep Interfaces Small

Interfaces should be focused. One interface should describe one capability.

Examples:

INTERFACE ISwitchable
INTERFACE IDimmable EXTENDS ISwitchable
INTERFACE IRequestOperation
INTERFACE IStartStopOperation

Do not create one large interface for every possible device feature. That only recreates the oversized function block problem with different syntax.

A useful interface answers one question:

What does the caller need from this object?

If a caller only needs to switch devices on and off, use ISwitchable. Do not force it to know about dimming, diagnostics, simulation mode, motor currents, RGB colors, and whatever the next project meeting invents.

Optional Capabilities

Interfaces also help when some devices support additional behavior.

Assume a room contains several switchable devices. Some are dimmable. Others are not.

The basic operation can use ISwitchable:

VAR CONSTANT
    LIGHT_COUNT : DINT := 8;
END_VAR
VAR
    lights : ARRAY[0..LIGHT_COUNT - 1] OF ISwitchable;
    N : DINT;
    value : BOOL;
END_VAR

FOR N := 0 TO LIGHT_COUNT - 1 DO
    IF lights[N] <> 0 THEN
        lights[N].On := value;
    END_IF
END_FOR

To apply dimming only where supported, query the additional interface:

VAR
    dimmable : IDimmable;
    dimmingValue : LREAL;
END_VAR

FOR N := 0 TO LIGHT_COUNT - 1 DO
    IF lights[N] <> 0 THEN
        IF __QUERYINTERFACE(lights[N], dimmable) THEN
            dimmable.DimmingValue := dimmingValue;
        END_IF
    END_IF
END_FOR

The caller does not need a separate array for every light type. It can operate on common behavior and query optional behavior only when needed.

This is useful in automation frameworks because real systems rarely contain perfectly uniform devices. A project may contain standard hardware, optional hardware, simulated hardware, and customer-specific devices in the same control concept.

Interfaces as Communication Protocols

An interface is more than a list of methods and properties. In a PLC framework, it becomes part of the communication protocol between function blocks.

For example, there are at least two different ways to put a device into operation.

One interface may describe commands:

INTERFACE IStartStopOperation

METHOD start
METHOD stop

Another may describe cyclic intent:

INTERFACE IRequestOperation

METHOD requestOperation

Both can be valid. They do not mean the same thing.

start() is an event. It says something should happen now. requestOperation() can mean that operation is requested as long as the method is called cyclically. That difference is important for watchdogs, communication loss, and state-machine behavior.

The interface alone does not explain all of this. The convention must be documented and used consistently.

Practical Guidelines

Use inheritance when:

  • the relationship is a clear is-a relationship
  • the hierarchy is shallow
  • the base behavior is stable
  • derived blocks should really inherit the base API

Use interfaces when:

  • different devices share a behavior but not an implementation
  • a caller should depend on capability, not concrete type
  • optional features must be queried at runtime
  • a framework needs replaceable components
  • simulation and real hardware should use the same protocol

Avoid both when a plain function block or composition is simpler. OOP is a tool, not a certificate of adulthood.

Limitations

There are several practical limits.

First, interface references must be checked before use.

IF device <> 0 THEN
    device.On := TRUE;
END_IF

Second, a REFERENCE TO must be checked with __ISVALIDREF() before access.

IF __ISVALIDREF(lightReference) THEN
    lightReference.On := TRUE;
END_IF

Third, OOP does not remove PLC constraints. Code still runs in scan cycles. It must stay deterministic. It must be easy to debug online. It must not hide timing behavior behind nice-looking abstractions.

Fourth, interfaces do not replace documentation. They define the callable surface. They do not fully define the expected behavior, timing, error handling, or reset semantics.

Conclusion

Inheritance and interfaces are both useful in TwinCAT Structured Text, but they solve different problems.

Inheritance shares structure and behavior inside a small hierarchy. It is useful when the relationship is stable and obvious.

Interfaces define contracts. They are better when different objects must provide the same capability without sharing the same base class.

For PLC frameworks, interfaces usually scale better. They let the application depend on behavior instead of concrete implementation. They also make simulation, testing, and customer-specific variation easier to manage.

The best design usually starts before coding:

Define the protocol first. Then implement the devices behind it.