IEC 61131-3: Die Prinzipien KISS, DRY, LoD und YAGNI

In den vorherigen Posts wurden die 5 SOLID-Prinzipien vorgestellt. Neben den SOLID-Prinzipien gibt es noch weitere Prinzipien, von denen ich einige ebenfalls kurz vorgestellen möchte. All diese Prinzipen haben das gemeinsame Ziel, die Wartbarkeit und die Wiederverwendbarkeit von Software zu verbessern.

Don’t Repeat Yourself (DRY)

Das DRY-Prinzip besagt (wie der Name schon vermuten lässt), dass man Programmcode nicht unnötig duplizieren sollte. Stattdessen sollte eine Funktion nur einmal implementiert und an gewünschten Stellen im Programm aufgerufen werden.

Das DRY-Prinzip kann helfen, die Wartbarkeit von Code zu verbessern, da es einfacher wird, Änderungen an einer Funktion vorzunehmen, wenn sie nur an einer Stelle im Programmcode implementiert wurde. Außerdem kann das DRY-Prinzip dazu beitragen, Fehler im Programm zu reduzieren, da duplizierter Code oft zu unerwarteten Verhaltensweisen führt, wenn eine Änderung nur an einer der duplizierten Stellen vorgenommen wird. Somit ist das DRY-Prinzip ein wichtiger Grundsatz in der Softwareentwicklung, welcher zur Verbesserung der Codequalität beitragen kann.

Obwohl das DRY-Prinzip einfach zu verstehen und umzusetzen ist, ist es wahrscheinlich das am meisten missachtete Prinzip. Denn nichts ist einfacher, als Quellcode durch Copy & Paste zu wiederholen. Gerade dann, wenn der Zeitdruck besonders hoch ist. Deshalb sollte man sich immer bemühen, gemeinsam genutzte Funktionen in separate Module zu implementieren.

Das folgende kurze Beispiel zeigt die Anwendung des DRY-Prinzips. Ein SPS-Programm erhält von mehreren Sensoren unterschiedliche Temperaturwerte. Alle Temperaturwerte sollen in einem HMI angezeigt und in eine Log-Datei geschrieben werden. Damit die Temperaturwerte besser lesbar sind, soll die Formatierung in der SPS erfolgen:

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 diesem Beispiel wird die Funktion F_DisplayTemperature() nur einmal implementiert. Für die Formatierung der Temperaturwerte wird diese Funktion an den gewünschten Stellen im Programm aufgerufen. Durch das Vermeiden von dupliziertem Code wird das Programm übersichtlicher und einfacher zu lesen. Ist es z.B. notwendig die Anzahl der Nachkommerstellen zu verändern, so muss dieses nur an einer Stelle, nämlich in der Funktion F_DisplayTemperature(), erfolgen.

Neben den Einsatz von Funktionen kann auch die Vererbung helfen das DRY-Prinzip einzuhalten, indem eine Funktionalität in einen Basis-FB verlagert und von allen abgeleiteten FBs verwendet wird.

Es kann aber Fälle geben, in denen das DRY-Prinzip bewusst verletzt werden sollte. Dieses ist immer dann der Fall, wenn sich durch den Einsatz von DRY die Lesbarkeit des Quellcode verschlechtert. So ist für die Kreisberechnung die Formel für den Umfang (U=2rπ) oder für die Fläche (A=r2π) ausreichend lesbar. Eine Auslagerung in separate Funktionen erhöht nicht die Codequalität, sondern nur die Abhängigkeit zu weiteren Modulen, in denen sich die Funktionen für die Kreisberechnung befinden. Stattdessen sollte für π eine globale Konstante angelegt und in den Berechnungen verwendet werden.

Zusammenfassend lässt sich sagen, dass das DRY-Prinzip dazu beiträgt, das Programmcode sauberer und kürzer wird, indem es die Duplizierung von Code vermeidet.

Law Of Demeter (LoD)

Das Law of Demeter ist ein weiteres Prinzip, dessen Beachtung die Kopplungen zwischen Funktionsblöcken deutlich minimieren kann. Das Law of Demeter legt fest, dass aus einem Funktionsblock (bzw. Methode oder Funktion) nur auf Elemente in unmittelbarer Nähe zugegriffen werden sollte. Konkret bedeutet dieses, dass nur Zugriffe auf die folgenden Elemente erlaubt sind:

  • Variablen des eigenen Funktionsblocks (alles zwischen VAR/END_VAR)
  • Methoden/Eigenschaften des eigenen Funktionsblocks
  • Methoden/Eigenschaften der Funktionsblöcke die im eigenen Funktionsblock angelegt wurden
  • Parameter die an Methoden oder Funktionsblöcke übergeben wurden (VAR_INPUT)
  • Globale Konstanten oder Parameter die in einer Parameterliste enthalten sind

Das Law of Demeter könnte somit auch heißen: Don’t talk to strangers. Als Strangers (Fremde) werden hierbei die Elemente bezeichnet, die nicht unmittelbar in dem Funktionsblock vorhanden sind. Im Gegensatz dazu, werden die eigenen Elemente Friends (Freunde) genannt.

Auch dieses Prinzip stammt aus den 1980iger Jahren, also aus der Zeit, in der die objektorientierte Softwareentwicklung stark an Popularität zugenommen hat. Der Name Demeter ist auf ein gleichnamiges Softwareprojekt zurückzuführen, in dem dieses Prinzip erstmal erkannt wurde (Demeter ist in der griechischen Mythologie die Schwester von Zeus und die Göttin der Landwirtschaft). Ende der 1980iger Jahre wurde dieses Prinzip von Ian Holland und Karl J. Lieberherr weiter ausgearbeitet und unter dem Titel Assuring Good Style for Object-Oriented Programs veröffentlicht.

Die folgende Grafik soll das Law of Demeter etwas genauer verdeutlichen:

In FB_A ist eine Instanz von FB_B (fbB) enthalten. Deshalb kann FB_A direkt auf die Methoden und Eigenschaften von FB_B zugreifen.

FB_B enthält eine Instanz von FB_C. Deshalb kann FB_B direkt auf FB_C zugreifen.

FB_B könnte eine Eigenschaft oder eine Methode anbieten, welche die Referenz auf FB_C zurückgibt (refC). Ein Zugriff aus FB_A auf die Instanz von FB_C über FB_B wäre somit theoretisch möglich:

nValue := fbB.refC.nValue;

Die Instanz auf FB_C wird in FB_B angelegt. Wenn FB_A auf diese Instanz direkt zugreift, entsteht eine feste Kopplung zwischen FB_A und FB_C. Diese feste Kopplung kann zu Problemen bei der Pflege, Wartung und dem Testen des Programms führen. Wird FB_A getestet, so muss nicht nur FB_B vorhanden sein, sondern auch FB_C. Ein häufiges Verletzen des Law of Demeter ist somit auch hilfreich bei der Früherkennung von Wartungsproblemen.

Auch das Anlegen einer entsprechenden lokalen Variablen, in der die Referenz auf FB_C abgelegt wird, löst das eigentliche Problem nicht:

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

Auf dem ersten Blick sind diese Abhängigkeiten nicht immer zu erkennen, da der Zugriff auf FB_C indirekt über FB_B erfolgt.

Beispiel

Hierzu ein konkretes Beispiel, welches das Problem nochmal verdeutlicht und auch einen Lösungsansatz anbietet.

Mit den Funktionsblöcken FB_Building, FB_Floor, FB_Room und FB_Lamp wird die Struktur eines Gebäudes und dessen Beleuchtung abgebildet. Das Gebäude besteht aus 5 Etagen, in der sich jeweils 20 Räume befinden und jeder Raum enthält 10 Lampen.

In jedem Funktionsblock sind die entsprechenden Instanzen der darunterliegenden Elemente enthalten. Die Funktionsblöcke stellen jeweils eine Eigenschaft zur Verfügung, welche eine Referenz auf diese Elemente anbietet. FB_Lamp enthält die Eigenschaft nPowerConsumption, über der die aktuelle Leistungsaufnahme der Lampe ausgegeben wird.

Es soll eine Funktion entwickelt werden, welche die Leistungsaufnahme aller Lampen in dem Gebäude ermittelt.

Ein Lösungsansatz könnte darin bestehen, dass über mehrere verschachtelte Schleifen auf jede einzelne Lampe zugegriffen und die Leistungsaufnahme addiert wird:

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

Das „Eintauchen‟ in die Objektstruktur bis hinunter zu jeder Lampe wirkt schon irgendwie beeindruckend. Doch dadurch ist die Funktion abhängig von allen Funktionsblöcken, auch von denen, die nur indirekt über eine Referenz angesprochen werden.

Der Zugriff von refBuilding auf refFloors verstößt nicht gegen das Law of Demeter, da refFloors eine direkte Eigenschaft von FB_Building ist. Alle weiteren Zugriffe auf die Referenzen haben aber zur Folge, dass unsere Funktion auch von den anderen Funktionsblöcken abhängig wird.

Ändert sich z.B. die Struktur von FB_Room oder FB_Floor, so muss evtl. auch die Funktion zur Leistungsaufnahme angepasst werden.

Um das Law of Demeter einzuhalten, könnte jeder Funktionsblock eine Methode anbieten (CalcPowerConsumption()), in welcher die Leistungsaufnahme berechnet wird. In jeder dieser Methoden, wird wiederrum die darunter liegende Methode CalcPowerConsumption() aufgerufen:

Die Methode CalcPowerConsumption() in FB_Building greift nur auf die eigenen Elemente zu. In diesem Fall auf die Eigenschaft refFloors, um darüber die Methode CalcPowerConsumption() von FB_Floor aufzurufen:

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

In CalcPowerConsumption() von FB_Floor wird wiederrum nur auf FB_Room zugegriffen:

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

Zuletzt wird in FB_Room die Leistungsaufnahme aller Lampen in dem Raum berechnet:

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

Der Aufbau der Funktion F_CalcPowerConsumption() gestaltet sich dadurch deutlich einfacher:

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

F_CalcPowerConsumption() ist nach dieser Anpassung nur noch abhängig von FB_Building und dessen Methode CalcPowerConsumption(). Wie FB_Building in CalcPowerConsumption() die Leistungsaufnahme berechnet, ist für F_CalcPowerConsumption() ohne Bedeutung. Der Aufbau von FB_Room oder FB_Floor könnte sich komplett ändern, F_CalcPowerConsumption() müsste nicht angepasst werden.

Die erste Variante, in der durch alle Funktionsblöcke iteriert wurde, ist sehr anfällig gegenüber Änderungen. Egal bei welchem Funktionsblock sich der Aufbau ändert, eine Anpassung von F_CalcPowerConsumption() wäre jedes Mal notwendig.

Beispiel 1 (TwinCAT 3.1.4024) auf GitHub

Allerdings ist zu berücksichtigen, dass verschachtelte Strukturen durchaus Sinn ergeben. Hier muss das Law of Demeter nicht anwendet werden. So kann es hilfreich sein, die Konfigurationsdaten über mehrere Strukturen hierarchisch zu verteilen, um so die Lesbarkeit zu erhöhen.

Keep It Simple, Stupid (KISS)

Das KISS-Prinzip besagt, dass Code so „simple‟ wie möglich sein sollte, damit dieser möglichst einfach zu verstehen und somit effektiv zu warten ist. Hierbei sollte „simple‟ mit „schlicht‟ übersetzt werden. Damit ist eine Schlichtheit gemeint, die versucht Unnötiges wegzulassen aber weiterhin die Anforderungen des Kunden zu erfüllen. Durch die Beachtung des KISS-Prinzips wird ein System:

  • einfach zu verstehen
  • einfach zu erweitern
  • einfach zu pflegen

Besteht die Anforderung darin zehn Millionen Datensätze zu sortieren, so wäre die Verwendung des Bubblesort-Algorithmus zwar einfach in der Umsetzung, doch wird die geringe Geschwindigkeit des Algorithmus nicht den Anforderungen des Kunden entsprechen. Es muss also immer eine Lösung gefunden werden, die den geforderten Erwartungen des Kunden entspricht und deren Umsetzung aber möglichst einfach (schlicht) ist.

Grundsätzlich sind zwei Arten von Anforderungen zu unterscheiden:

Funktionale Anforderung: Der Kunde bzw. Stakeholder fordert ein bestimmtes Leistungsmerkmal. Gemeinsam mit dem Kunden werden dann die genauen Anforderungen für dieses Leistungsmerkmal festgelegt und erst danach wird dieses implementiert. Funktionale Anforderungen erweitern eine Anwendung um eindeutige, von dem Kunden gewünschte, Funktionen (Leistungsmerkmale).

Nicht funktionale Anforderungen: Eine nicht funktionale Anforderung ist z.B. das Aufteilen einer Anwendung auf verschiedene Module oder das Vorsehen von Schnittstellen, um z.B. Unit-Tests zu ermöglichen. Nicht funktionale Anforderungen sind Leistungsmerkmale, die für den Kunden nicht unbedingt sichtbar sind. Diese können aber notwendig sein, damit das Softwaresystem gepflegt und gewartet werden kann.

Bei dem KISS-Prinzip geht es immer um die nicht funktionalen Anforderungen. Das „Wie‟ steht im Mittelpunkt. Also die Frage, wie die geforderten Funktionen erreicht werden. Das YAGNI-Prinzip, welches im folgenden Kapitel beschrieben wird, bezieht sich auf die funktionalen Anforderungen. Hier steht das „Was‟ im Mittelpunkt.

Das KISS-Prinzip kann auf mehrere Ebene angewendet werden:

Formatierung Quellcode

Der folgende Quellcode ist zwar sehr kompakt, doch wird hier das KISS-Prinzip verletzt, da dieser nur schwer zu verstehen und somit sehr fehleranfällig ist:

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;

Der Quellcode sollte so formatiert werden, dass der Ablauf besser erkannt wird. Auch sollten die Bezeichner für Variablen und Funktionen so gewählt werden, dass deren Bedeutung leichter zu verstehen ist.

Unnötiger Quellcode

Quellcode, der nicht dazu beiträgt, die Lesbarkeit zu verbessern, verletzt ebenfalls gegen das KISS-Prinzip:

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

Der Quellcode ist zwar gut strukturiert, auch wurden die Bezeichner so gewählt damit die Bedeutung leichter zu erkennen ist, doch kann der Quellcode deutlich reduziert werden:

bResult := F_CalcFoo();

Diese eine Zeile ist deutlich einfacher zu verstehen, wie die 6 Zeilen zuvor. Der Quellcode ist „schlichter‟, bei gleichem Funktionsumfang.

Softwaredesign / Softwarearchitektur

Auch das Design oder die Struktur einer Software kann gegen das KISS-Prinzip verstoßen. Wird z.B. für das Abspeichern von Konfigurationsdaten eine komplette SQL-Datenbank eingesetzt, obwohl eine Textdatei ausreichen würde, so wird ebenfalls das KISS-Prinzip verletzt.

Das Aufteilen eines SPS-Programms auf mehrere CPU-Cores ist nur dann sinnvoll, wenn es auch einen praktischen Nutzen hervorbringt. In einem SPS-Programm müssen in diesem Fall entsprechende Mechanismen eingebaut werden, um den Zugriff auf gemeinsame Ressourcen zu synchronisieren. Diese erhöhen die Komplexität des Systems erheblich und sollten nur dann zum Einsatz kommen, wenn die Anwendung dieses auch erfordert.

Ganz bewusst habe ich die Kapitel zu dem KISS-Prinzip und zu dem YAGNI-Prinzip an das Ende gesetzt. Von hier aus möchte ich nochmal einen kurzen Rückblick auf den Anfang der Serie über die SOLID-Prinzipien werfen.

Bei der Vorstellung der SOLID-Prinzipien habe ich gelegentlich auf die Gefahr des Overengineering hingewiesen. Abstraktionen sollten nur dann vorgesehen werden, wenn diese für die Umsetzung von Features notwendig sind.

Um dieses zu verdeutlichen, will ich das Beispiel für die Erklärung der SOLID-Prinzipien noch einmal verwenden (siehe: IEC 61131-3: SOLID – Das Dependency Inversion Principle).

Zwischen den drei Lampentypen und dem Controller besteht eine feste Abhängigkeit. Soll die Anwendung um einen weiteren Lampentyp erweitert werden, so ist es notwendig das Programm an verschiedenen Stellen anzupassen. Durch das Anwenden des Dependency Inversion Principle (DIP) und des Single Responsibility Principle (SRP) wurde das Programm deutlich flexibler. Das Integrieren von zusätzlichen Lampentypen wurde dadurch signifikant vereinfacht. Aber auch die Komplexität des Programms wurde durch diese Anpassungen deutlich größer, wie das UML-Diagramm zeigt:

(abstrakte Elemente werden in kursiver Schriftart dargestellt)

Bevor zusätzliche Abstraktionsebenen durch die Anwendung der SOLID-Prinzipien realisiert werden, sollte man den Mehraufwand immer kritisch hinterfragen.

Die erste Variante ist vom Aufbau vollkommen ausreichend, wenn das Programm in diesem Umfang ausschließlich in einem Projekt eingesetzt wird. Das Programm ist klein genug, um den Aufbau der Software zu verstehen und um kleine Anpassungen vorzunehmen. Das KISS-Prinzip wurde befolgt. Es wurde nicht mehr Komplexität als notwendig eingebaut.

Ist die erste Variante allerdings nur ein Zwischenschritt, z.B. bei der Entwicklung eines umfangreichen Lichtmanagementsystem, so ist damit zu rechnen, dass die Anwendung an Komplexität noch zunehmen wird. Auch ist es möglich, dass zu einem späteren Zeitpunkt die Entwicklung auf mehrere Personen verteilt werden muss. Der Einsatz von Unit-Tests ist ein weiterer Punkt, der die Umsetzung der SOLID-Prinzipien rechtfertigt. Ohne die Entkopplung der einzelnen Lampentypen durch Schnittstellen, ist der Einsatz von Unit-Tests nur schwer bzw. gar nicht möglich. Auch hier wird das KISS-Prinzip nicht verletzt. Das KISS-Prinzip muss somit immer im Kontext betrachtet werden.

You Ain’t Gonna Need It (YAGNI)

YAGNI steht für You Ain’t Gonna Need It und bedeutet frei übersetzt Du wirst es nicht brauchen. Es besagt, dass man in der Softwareentwicklung nur die Leistungsmerkmale realisieren sollte, die benötigt werden. Es sollen keine Funktionen oder Features implementiert werden, die vielleicht irgendwann einmal gebraucht werden könnten.

Im Gegensatz zu dem KISS-Prinzip, bei dem es immer um die nicht funktionalen Anforderungen geht, liegt der Fokus bei dem YAGNI-Prinzip auf den funktionalen Anforderungen.

Bei der Entwicklung von Software kann die Versuchung groß sein, zusätzliche Leistungsmerkmale ohne konkrete Anforderung zu implementieren. Das kann z.B. dann der Fall sein, wenn während der Entwicklung Leistungsmerkmale ohne Absprache mit dem Kunden implementiert werden, in dem festen Glauben, dass der Kunde diese später noch fordern wird.

Bezogen auf unser obiges Beispiel, wird das YAGNI-Prinzip dann verletzt, wenn man die Betriebsstundenerfassung implementieren würde (siehe: IEC 61131-3: SOLID – Das Interface Segregation Principle), obwohl dieses vom Kunden nicht gefordert wurde.

Wird während der Entwicklung festgestellt, dass ein bestimmtes Leistungsmerkmal sinnvoll sein könnte, so sollte die Implementierung erst nach Absprache mit dem Kunden erfolgen. Ansonsten erhält ein System nach und nach immer mehr Quellcode für Leistungsmerkmale, die niemand benötigt.

Durch dieses Beispiel wird noch einmal deutlich, dass alle bisher beschriebenen Prinzipien keine festen Regeln oder gar Gesetze sind. Die Prinzipien sind aber ein mächtiges Werkzeug, um die Codequalität von Software zu verbessern.

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