IEC 61131-3: The Principles KISS, DRY, LoD and YAGNI

The 5 SOLID principles were presented in the previous posts. In addition to the SOLID principles, however, there are other principles that are also briefly presented here. What all these principles have in common is the goal of making software more maintainable and more reusable.

Don’t Repeat Yourself (DRY)

The DRY principle states (as the name suggests) that program code should not be duplicated unnecessarily. Instead, a function should be implemented only once and called at desired points in the program.

The DRY principle can help improve the maintainability of code, as it becomes easier to make changes to a function if it is implemented in only one place in the program code. In addition, the DRY principle can help reduce errors in the program, since duplicated code often leads to unexpected behaviour when a change is made in only one of the duplicated locations. Thus the DRY principle is an important principle in the software development, which can contribute to the improvement of the code quality.

Although the DRY principle is easy to understand and implement, it is probably the most disregarded principle. Because nothing is easier than to repeat source code by copy & paste. Especially when the time pressure is particularly high. Therefore, you should always try to implement shared functions in separate modules.

The following short example shows the application of the DRY principle. A PLC program receives different temperature values from several sensors. All temperature values are to be displayed in an HMI and written to a log file. To make the temperature values more readable, the formatting should be done in the PLC:

FUNCTION F_DisplayTemperature : STRING
VAR_INPUT
  fSensorValue  : LREAL;
  bFahrenheit   : BOOL;
END_VAR
IF (fSensorValue > 0) THEN
  IF (bFahrenheit) THEN
    F_DisplayTemperature := CONCAT('Temperature: ', 
                    REAL_TO_FMTSTR(fSensorValue, 1, TRUE));
    F_DisplayTemperature := CONCAT(F_DisplayTemperature, ' °C');
  ELSE
    F_DisplayTemperature := CONCAT('Temperature: ',
                    REAL_TO_FMTSTR(fSensorValue * 1.8 + 32, 1, TRUE));
    F_DisplayTemperature := CONCAT(F_DisplayTemperature, ' °F');	
  END_IF
ELSE
    F_DisplayTemperature := 'No sensor data available';
END_IF

In this example the function F_DisplayTemperature() is implemented only once. For the formatting of the temperature values this function is called at the desired places in the program. By avoiding duplicated code, the program becomes clearer and easier to read. If, for example, it is necessary to change the number of decimal places, this only has to be done in one place, namely in the function F_DisplayTemperature().

In addition to the use of functions, inheritance can also help to comply with the DRY principle by relocating a functionality in a base FB and using it by all derived FBs.

However, there may be cases in which the DRY principle should be deliberately violated. This is always the case if the readability of the source code is worsened by the use of DRY. Thus for the circle computation the formula for the circumference (U=2rπ) or for the area (A=r2π) is sufficiently readable. An outsourcing into separate functions does not increase the code quality, but only the dependence to further modules, in which the functions for the circle computation are. Instead, a global constant should be created for π and used in the calculations.

In summary, the DRY principle helps make program code cleaner and shorter by avoiding code duplication.

Law Of Demeter (LoD)

The Law of Demeter is another principle whose observance can significantly minimize the couplings between function blocks. The Law of Demeter specifies that only elements in the immediate vicinity should be accessed from a function block (or method or function). In concrete terms, this means that only accesses to the following elements are permitted:

  • Variables of the own function block (everything between VAR/END_VAR)
  • Methods/properties of the own function block
  • Methods/properties of the function blocks that were created in the own function block
  • Parameters passed to methods or function blocks (VAR_INPUT)
  • Global constants or parameters contained in a parameter list

The Law of Demeter could therefore also be called: Don’t talk to strangers. Strangers are elements that are not directly present in the function block. In contrast, the own elements are called friends.

Also this principle originates from the 1980iger years, thus from the time, in which the object-oriented software development increased strongly in popularity. The name Demeter is to be led back on a software project of the same name, in which this principle was recognized for the first time (Demeter is in the Greek mythology the sister of Zeus and the Goddess of the agriculture). At the end of the 1980s, this principle was further elaborated by Ian Holland and Karl J. Lieberherr and published under the title Assuring Good Style for Object-Oriented Programs.

The following graphic is intended to illustrate the Law of Demeter in a little more detail:

FB_A contains an instance of FB_B (fbB). Therefore, FB_A can directly access the methods and properties of FB_B.

FB_B contains an instance of FB_C. Therefore, FB_B can access FB_C directly.

FB_B could offer a property or a method that returns the reference to FB_C (refC). Access from FB_A to the instance of FB_C via FB_B would thus theoretically be possible:

nValue := fbB.refC.nValue;

The instance on FB_C is created in FB_B. If FB_A accesses this instance directly, a fixed coupling between FB_A and FB_C is created. This fixed coupling can lead to problems in the care, maintenance and testing of the program. If FB_A is tested, not only FB_B must be present, but FB_C as well. A frequent violation of the Law of Demeter is therefore also helpful in the early detection of maintenance problems.

Even creating a corresponding local variable in which the reference to FB_C is stored does not solve the actual problem:

refC : REFERENCE TO FB_C;
refC REF= fbB.refC;
nValue := refC.nValue;

At first glance, these dependencies are not always apparent, as FB_C is accessed indirectly via FB_B.

Example

Here is a concrete example that illustrates the problem again and also offers a solution.

The function blocks FB_Building, FB_Floor, FB_Room and FB_Lamp represent the structure of a building and its lighting. The building consists of 5 floors, each containing 20 rooms and each room contains 10 lamps.

Each function block contains the corresponding instances of the underlying elements. The function blocks each provide a property that offers a reference to these elements. FB_Lamp contains the property nPowerConsumption, via which the current power consumption of the lamp is output.

A function is to be developed that determines the power consumption of all lamps in the building.

One solution could be to access each individual lamp via several nested loops and add up the power consumption:

FUNCTION F_CalcPowerConsumption : UDINT
VAR_INPUT
  refBuilding : REFERENCE TO FB_Building;
END_VAR
VAR
  nFloor, nRoom, nLamp : INT;
END_VAR
IF (NOT __ISVALIDREF(refBuilding)) THEN
  F_CalcPowerConsumption := 0;
  RETURN;
END_IF
FOR nFloor := 1 TO 5 DO
  FOR nRoom := 1 TO 20 DO
    FOR nLamp := 1 TO 10 DO
      F_CalcPowerConsumption := F_CalcPowerConsumption + refBuilding
                                  .refFloors[nFloor]
                                  .refRooms[nRoom]
                                  .refLamps[nLamp].nPowerConsumption;
    END_FOR
  END_FOR
END_FOR

The „diving‟ into the object structure down to each lamp seems somehow impressive. But this makes the function dependent on all function blocks, even those that are only indirectly addressed via a reference.

The access of refBuilding to refFloors does not violate the Law of Demeter, since refFloors is a direct property of FB_Building. However, all further accesses to the references have the consequence that our function also becomes dependent on the other function blocks.

If, for example, the structure of FB_Room or FB_Floor changes, the function for power consumption may also have to be adapted.

To comply with the Law of Demeter, each function block could offer a method (CalcPowerConsumption()) in which the power consumption is calculated. In each of these methods, the underlying method CalcPowerConsumption() is called:

The CalcPowerConsumption() method in FB_Building only accesses its own elements. In this case, it accesses the property refFloors to call the method CalcPowerConsumption() of FB_Floor:

METHOD CalcPowerConsumption : UDINT
VAR
  nFloor : INT;
END_VAR
FOR nFloor := 1 TO 5 DO
  CalcPowerConsumption := CalcPowerConsumption +                                
                             refFloors[nFloor].CalcPowerConsumption();
END_FOR

In CalcPowerConsumption() of FB_Floor, only FB_Room is accessed:

METHOD CalcPowerConsumption : UDINT
VAR
  nRoom : INT;
END_VAR
FOR nRoom := 1 TO 20 DO
  CalcPowerConsumption := CalcPowerConsumption +
                             refRooms[nRoom].CalcPowerConsumption();
END_FOR

Finally, the power consumption of all lamps in the room is calculated in FB_Room:

METHOD CalcPowerConsumption : UDINT
VAR
  nLamp : INT;
END_VAR
FOR nLamp := 1 TO 10 DO
  CalcPowerConsumption := CalcPowerConsumption +
                             refLamps[nLamp].nPowerConsumption;
END_FOR

The structure of the function F_CalcPowerConsumption() is thus much simpler:

FUNCTION F_CalcPowerConsumption : UDINT
VAR_INPUT
  refBuilding : REFERENCE TO FB_Building;
END_VAR
IF (NOT __ISVALIDREF(refBuilding)) THEN
  F_CalcPowerConsumption := 0;
  RETURN;
END_IF
F_CalcPowerConsumption := refBuilding.CalcPowerConsumption();

After this adjustment, F_CalcPowerConsumption() is only dependent on FB_Building and its method CalcPowerConsumption(). How FB_Building calculates the power consumption in CalcPowerConsumption() is irrelevant for F_CalcPowerConsumption(). The structure of FB_Room or FB_Floor could change completely, F_CalcPowerConsumption() would not have to be adapted.

The first variant, in which all function blocks were iterated through, is very susceptible to changes. No matter which function block the structure changes, an adjustment of F_CalcPowerConsumption() would be necessary every time.

Sample 1 (TwinCAT 3.1.4024) on GitHub

However, it must be taken into account that nested structures do make sense. The Law of Demeter does not have to be applied here. It can be helpful to distribute the configuration data hierarchically over several structures in order to increase readability.

Keep It Simple, Stupid (KISS)

The KISS principle states that code should be as „simple‟ as possible so that it is as easy to understand as possible and thus effective to maintain. Here, „simple‟ is also to be understood as „plain‟. This means a simplicity that tries to leave out the unnecessary but still fulfils the customer’s requirements. By following the KISS principle, a system is:

  • easy to understand
  • easy to extend
  • easy to maintain

If the requirement is to sort ten million records, using the bubblesort algorithm would be simple to implement, but the low speed of the algorithm will not meet the client’s requirements. Therefore, a solution must always be found that meets the customer’s required expectations, but whose implementation is as simple (plain) as possible.

Basically, two types of requirements are to be distinguished:

Functional requirement: The customer or stakeholder demands a specific feature. The exact requirements for this feature are then defined together with the customer and only then is it implemented. Functional requirements extend an application with clear functions (features) desired by the customer.

Non-functional requirements: A non-functional requirement is, for example, the splitting of an application into different modules or the provision of interfaces, e.g. to enable unit tests. Non-functional requirements are performance features that are not necessarily visible to the customer. However, these may be necessary so that the software system can be maintained and serviced.

The KISS principle is always about the non-functional requirements. The focus is on the „how‟. In other words, the question of how the required functions are achieved. The YAGNI principle, which is described in the following chapter, refers to the functional requirements. Here the focus is on the „what‟.

The KISS principle can be applied at several levels:

Formatting source code

Although the following source code is very compact, the KISS principle is violated here because it is difficult to understand and thus very error-prone:

IF(x<=RT[k-1](o[n+2*j]))THEN WT[j+k](l AND NOT S.Q);END_IF;
IF(x>RI[k+1](o[n+2*k]))THEN WO[j-k](l OR NOT S.Q);END_IF;

The source code should be formatted in such a way that the sequence is better recognised. Also, the identifiers for variables and functions should be chosen in such a way that their meaning is easier to understand.

Unnecessary source code

Source code that does not help to improve readability also violates the KISS principle:

bCalc := F_CalcFoo();
IF (bCalc = TRUE) THEN
  bResult := TRUE;
ELSE
  bResult := FALSE;
END_IF

Although the source code is well structured and the identifiers have been chosen so that their meaning is easier to recognise, the source code can be significantly reduced:

bResult := F_CalcFoo();

This one line is much easier to understand than the 6 lines before. The source code is „simpler‟, with the same range of functions.

Software design / software architecture

The design or structure of software can also violate the KISS principle. If, for example, a complete SQL database is used to store configuration data, although a text file would suffice, the KISS principle is also violated.

The division of a PLC programme into several CPU cores only makes sense if it also produces a practical benefit. In this case, appropriate mechanisms must be built into a PLC program to synchronise access to shared resources. These increase the complexity of the system considerably and should only be used if the application requires them.

I have deliberately placed the chapters on the KISS principle and the YAGNI principle at the end. From here, I would like to take a brief look back at the beginning of the series on the SOLID principles.

When introducing the SOLID principles, I occasionally pointed out the danger of overengineering. Abstractions should only be provided if they are necessary for the implementation of features.

To clarify this, I will use the example for the explanation of the SOLID principles again (see: IEC 61131-3: SOLID – The Dependency Inversion Principle).

There is a fixed dependency between the three lamp types and the controller. If the application is to be extended by another lamp type, it is necessary to adapt the programme at various points. By applying the Dependency Inversion Principle (DIP) and the Single Responsibility Principle (SRP), the programme became much more flexible. The integration of additional lamp types has been significantly simplified. However, the complexity of the programme was also significantly increased by these adjustments, as the UML diagram shows:

(abstract elements are displayed in italics)

Before additional levels of abstraction are realised by applying the SOLID principles, one should always critically question the extra effort involved.

The structure of the first variant is completely sufficient if the program is used exclusively in a project to this extent. The program is small enough to understand the structure of the software and to make small adjustments. The KISS principle was followed. No more complexity than necessary has been built in.

However, if the first variant is only an intermediate step, e.g. in the development of a comprehensive light management system, it is to be expected that the application will increase in complexity. It is also possible that at a later stage the development will have to be distributed among several people. The use of unit tests is another point that justifies the implementation of SOLID principles. Without decoupling the individual lamp types through interfaces, the use of unit tests is difficult or even impossible. Here, too, the KISS principle is not violated. The KISS principle must therefore always be considered in context.

You Ain’t Gonna Need It (YAGNI)

YAGNI stands for You Ain’t Gonna Need It and also means You will not need it. It means that in software development you should only implement the features that are needed. No functions or features should be implemented, which might be needed someday.

In contrast to the KISS principle, which always focuses on the non-functional requirements, the YAGNI principle focuses on the functional requirements.

When developing software, it can be tempting to implement additional features without a concrete requirement. This can be the case, for example, if features are implemented during development without consulting the customer, in the firm belief that the customer will demand them later.

Referring to our example above, the YAGNI principle would be violated if the operating hours recording were implemented (see: IEC 61131-3: SOLID – The Interface Segregation Principle), although this was not requested by the customer.

If it is determined during development that a particular feature could be useful, it should only be implemented after consultation with the customer. Otherwise, a system will gradually receive more and more source code for features that no one needs.

This example makes it clear once again that all the principles described so far are not fixed rules or even laws. However, the principles are a powerful tool for improving the code quality of software.

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