„The Liskov Substitution Principle (LSP) requires that derived function blocks (FBs) are always compatible to their base FB. Derived FBs must behave like their respective base FB. A derived FB may extend the base FB, but not restrict it.” This is the core statement of the Liskov Substitution Principle (LSP), which Barbara Liskov formulated already in the late 1980s. Although the Liskov Substitution Principle (LSP) is one of the simpler SOLID principles, its violation is very common. The following example shows why the Liskov Substitution Principle (LSP) is important.
I use once again the example, which was already developed and optimized in the two previous posts. The core of the example are three lamp types, which are mapped by the function blocks FB_LampOnOff, FB_LampSetDirect and FB_LampUpDown. The interface I_Lamp and the abstract function block FB_Lamp secure a clear decoupling between the respective lamp types and the higher-level controller FB_Controller.
FB_Controller no longer accesses specific instances, but only a reference of the abstract function block FB_Lamp. The IEC 61131-3: SOLID – The Dependency Inversion Principle is used to break the fixed coupling.
To realize the required functionality, each lamp type provides its own methods. For this reason, each lamp type also has a corresponding adapter function block (FB_LampOnOffAdapter, FB_LampSetDirectAdapter and FB_LampUpDownAdapter), which is responsible for mapping between the abstract lamp (FB_Lamp) and the concrete lamp types (FB_LampOnOff, FB_LampSetDirect and FB_LampUpDown). This optimization is supported by the IEC 61131-3: SOLID – The Single Responsibility Principle.
Extension of the implementation
The three required lamp types can be mapped well by the existing software design. Nevertheless, it can happen that extensions, which seem simple at first sight, lead to difficulties later. The new lamp type FB_LampSetDirectDALI will serve as an example here.
DALI stands for Digital Addressable Lighting Interface and is a protocol for controlling lighting devices. Basically, the new function block behaves like FB_LampSetDirect, but with DALI the output value is not given in 0-100 % but in 0-254.
Optimization and analysis of the extensions
Which approaches are available to implement this extension? The different approaches will also be analyzed in more detail.
Approach 1: Quick & Dirty
High time pressure can tempt to realize the Quick & Dirty implementation. Since FB_LampSetDirect behaves similarly to the new DALI lamp type, FB_LampSetDirectDALI inherits from FB_LampSetDirect. To enable the value range of 0-254, the SetLightLevel() method of FB_LampSetDirectDALI is overwritten.
METHOD PUBLIC SetLightLevel VAR_INPUT nNewLightLevel : BYTE(0..254); END_VAR nLightLevel := nNewLightLevel;
The new adapter function block (FB_LampSetDirectDALIAdapter) is also adapted so that the methods regard the value range 0-254.
As an example, the methods DimUp() and On() are shown here:
METHOD PUBLIC DimUp IF (fbLampSetDirectDALI.nLightLevel <= 249) THEN fbLampSetDirectDALI.SetLightLevel(fbLampSetDirectDALI.nLightLevel + 5); END_IF IF (_ipObserver <> 0) THEN _ipObserver.Update(fbLampSetDirectDALI.nLightLevel); END_IF
METHOD PUBLIC On fbLampSetDirectDALI.SetLightLevel(254); IF (_ipObserver <> 0) THEN _ipObserver.Update(fbLampSetDirectDALI.nLightLevel); END_IF
The simplified UML diagram shows the integration of the function blocks for the DALI lamp into the existing software design:
Sample 1 (TwinCAT 3.1.4024) on GitHub
This approach implements the requirements quickly and easily through a pragmatic strategy. But this also added some specifics that complicate the use of the blocks in an application.
For example, how should a user interface behave when it connects to an instance of FB_Controller and FB_AnalogValue outputs a value of 100? Does 100 mean that the current lamp is at 100 % or does the new DALI lamp output a value of 100, which would be well below 100 %?
The user of FB_Controller must always know the active lamp type in order to interpret the current output value correctly. FB_LampSetDirectDALI inherits from FB_LampSetDirect, but changes its behavior. In this example, the behavior is changed by overwriting the SetLightLevel() method. The derived FB (FB_LampSetDirectDALI) behaves differently to the base FB (FB_LampSetDirect). FB_LampSetDirect can no longer be replaced (substituted) by FB_LampSetDirectDALI. The Liskov Substitution Principle (LSP) is violated.
Approach 2: Optionality
In this approach, each lamp type contains a property that returns information about the exact function of the function block.
In .NET, for example, this approach is used in the abstract class System.IO.Stream. The Stream class serves as the base class for specialized streams (e.g., FileStream and NetworkStream) and specifies the most important methods and properties. This includes the methods Write(), Read() and Seek(). Since not every stream can provide all functions, the properties CanRead, CanWrite and CanSeek provide information about whether the corresponding method is supported by the respective stream. For example, NetworkStream can check at runtime whether writing to the stream is possible or whether it is a read-only stream.
In our example, I_Lamp is extended by the property bIsDALIDevice.
This means that FB_Lamp and therefore every adapter function block also receives this property. Since the functionality of bIsDALIDevice is the same in all adapter function blocks, bIsDALIDevice is not declared as abstract in FB_Lamp. This means that it is not necessary for all adapter function blocks to implement this property themselves. The functionality of bIsDALIDevice is inherited by FB_Lamp to all adapter function blocks.
For FB_LampSetDirectDALIAdapter, the backing variable of the property bIsDALIDevice is set to TRUE in the method FB_init().
METHOD FB_init : BOOL VAR_INPUT bInitRetains : BOOL; bInCopyCode : BOOL; END_VAR SUPER^._bIsDALIDevice := TRUE;
For all other adapter function blocks, _bIsDALIDevice retains its initialization value (FALSE). The use of the FB_init() method is not necessary for these adapter function blocks.
The user of FB_Controller (MAIN block) can now query at program runtime whether the current lamp is a DALI lamp or not. If this is the case, the output value is scaled accordingly to 0-100 %.
IF (__ISVALIDREF(fbController.refActiveLamp) AND_THEN fbController.refActiveLamp.bIsDALIDevice) THEN nLightLevel := TO_BYTE(fbController.fbActualValue.nValue * 100.0 / 254.0); ELSE nLightLevel := fbController.fbActualValue.nValue; END_IF
Note: It is important to use the AND_THEN operator instead of THEN. This means that the expression to the right of AND_THEN is only executed if the first operand (to the left of AND_THEN) is TRUE. This is important here because otherwise the expression fbController.refActiveLamp.bIsDALIDevice would terminate the execution of the program in case of an invalid reference to the active lamp (refActiveLamp).
The UML diagram shows how FB_Lamp receives the property bIsDALIDevice via the interface I_Lamp and is thus inherited by all adapter function blocks:
Sample 2 (TwinCAT 3.1.4024) on GitHub
This approach still violates the Liskov Substitution Principle (LSP). FB_LampSetDirectDALI behaves further on differently to FB_LampSetDirect. The user hast to take this difference into account (querying bIsDALIDevice) and correct it (scaling to 0-100 %). This is easy to overlook or to implement incorrectly.
Approach 3: Harmonization
In order not to violate the Liskov Substitution Principle (LSP) any further, the inheritance between FB_LampSetDirect and FB_LampSetDirectDALI is resolved. Even if both function blocks appear very similar at first glance, the inheritance should be avoided with at this point.
The adapter function blocks ensure that all lamp types can be controlled using the same methods. However, there are still differences in the representation of the output value.
In FB_Controller the initial value of the active lamp is represented by an instance of FB_AnalogValue. A new initial value is transmitted by the Update() method. To ensure that the initial value is also displayed uniformly, it is scaled to 0-100 % before the Update() method is called. The necessary adjustments are made exclusively in the methods DimDown(), DimUp(), Off() and On() of FB_LampSetDirectDALIAdapter.
The On() method is shown here as an example:
METHOD PUBLIC On fbLampSetDirectDALI.SetLightLevel(254); IF (_ipObserver <> 0) THEN _ipObserver.Update(TO_BYTE(fbLampSetDirectDALI.nLightLevel * 100.0 / 254.0)); END_IF
The adapter function block contains all the necessary instructions, which causes the DALI lamp to behave to the outside as expected. FB_LampSetDirectDALI remains unchanged with this solution approach.
Sample 3 (TwinCAT 3.1.4024) on GitHub
Through various techniques, it is possible for us to implement the desired extension without violating the Liskov Substitution Principle (LSP). Inheritance is a precondition to violate the LSP. If the LSP is violated, this may be an indication of a bad inheritance hierarchy within the software design.
Why is it important to follow the Liskov Substitution Principle (LSP)? Function blocks can also be passed as parameters. If a POU would expect a parameter of the type FB_LampSetDirect, then FB_LampSetDirectDALI could also be passed when using inheritance. However, the operation of the SetLightLevel() method is different for the two function blocks. Such differences can lead to undesirable behavior within a system.
The definition of the Liskov Substituation Principle
Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.
This is the more formal definition of the Liskov Substitution Principle (LSP) by Barbara Liskov. As mentioned above, this principle was already defined at the end of the 1980s. The complete elaboration was published under the title Data Abstraction and Hierarchy.
Barbara Liskov was one of the first women to earn a doctorate in computer science in 1968. In 2008, she was also one of the first women to receive the Turing Award. Early on, she became involved with object-oriented programming and thus also with the inheritance of classes (function blocks).
Inheritance places two function blocks in a specific relationship to each other. Inheritance here describes an is-a relationship. If FB_LampSetDirectDALI inherits from FB_LampSetDirect, the DALI lamp is a (normal) lamp extended by special (additional) functions. Wherever FB_LampSetDirect is used, FB_LampSetDirectDALI could also be used. FB_LampSetDirect can be substituted by FB_LampSetDirectDALI. If this is not ensured, the inheritance should be questioned at this point.
Robert C. Martin has included this principle in the SOLID principles. In the book (Amazon advertising link *) Clean Architecture: A Craftsman’s Guide to Software Structure and Design, this principle is explained further and extended to the field of software architecture.
By extending the above example, you have learned about the Liskov Substitution Principle (LSP). Complex inheritance hierarchies in particular are prone to violating this principle. Although the formal definition of the Liskov Substitution Principle (LSP) sounds complicated, the key message of this principle is simple to understand.
In the next post, our example will be extended again. The Interface Segregation Principle (ISP) will play a central role in it.