Bild in lightbox öffnen

Wie Composite Patterns hierarchische Maschinenstrukturen einfacher machen


In diesem Blogbeitrag geht es um ein spezielles „Composite Pattern“ (zusammengesetztes Muster). Es ermöglicht uns, mit einer Hierarchie von Objekten (wie einer n-Baumstruktur) auf einheitliche Weise zu arbeiten. Durch die Entwicklung eines Tree-Nodes, der Betriebsmodi ausführen und eine beliebige Anzahl von Child-Nodes verwalten kann, können wir alle Baumelemente in einem Aufruf durchlaufen.

Die Maschinen, die wir programmieren, haben alle eine hierarchische Struktur (siehe Abbildung 1).

Ein leidiges Thema bei den Maschinen sind die Betriebsmodi, daher sollen diese nun als Beispiel dienen. Das Beispiel ist sehr vereinfacht und unvollständig, da es nur dazu dient, ein Prinzip zu erklären Ein vollständiges Beispiel würde den Rahmen des Blogbeitrags sprengen.

Unsere Maschine

Die Maschine hat drei Stationen: eine Zuführ-, eine Ausgabe- und eine Pressstation.

Da ein Bild mehr sagt als tausend Worte, schaue dir Abbildung 2 an. Ich habe nur die Struktur des Inlets gezeichnet, um die Baumstruktur zu verdeutlichen. Die anderen Stationen wurden weggelassen.

Die Idee

Wir erstellen einen Tree-Node, der einen Betriebsmodus ausführen kann und beliebig viele Kinder haben kann. Außerdem sollte der Node den entsprechenden Betriebsmodus für alle Child-Nodes aufrufen, sodass wir einen Traversierungsaufruf über alle Baumelemente erhalten. Die Aufrufe des entsprechenden Betriebsmodus sollten über eine Schnittstelle erfolgen, die ein Client aufrufen kann (Operation Mode Handling und Parent-Node).

Beim Aufruf des Constructor (FB_init) sollte der Parent-Node übergeben werden, der Child-Node ruft die Methode addChildNode mit sich selbst auf. Auf diese Weise sparen wir uns die Methoden zum Anhängen und die Deklarationsstruktur spiegelt auch die Baumstruktur wider (einfache Handhabung und gute Lesbarkeit). Die Klassenstruktur ist in Abbildung 3 dargestellt und die Grundidee der Kommunikation ist in Abbildung 4 dargestellt.

In der Einleitung habe ich davon gesprochen, dass es sich hierbei um eine spezialisierte Form des Composite Pattern handelt. Das klassische Composite Pattern unterscheidet zwischen Node und Leaf, d. h. wie bei einer Ordnerstruktur gibt es den Ordner (Root Node), der Dateien (Leaves) und Unterordner (Nodes) enthält. Ein Ordner kann nicht gleichzeitig eine Datei sein. In unserem Beispiel kann jedoch jeder Node ein Leaf sein und auch Nodes und Leaves enthalten.

Node Implementation

Für die Umsetzung des Beispiels werden unsere Cinnamon-Framework-Bibliotheken benötigt. Sie finden sie in unserem Style Guide

Im Beispiel werden CNM_AbstractObject, CNM_RetrunTypes, CNM_Collections, CNM_State-Machine-Lib, CNM_CollectionInterfaces und CNM_CycleManagerInterface verwendet.

Schnittstellen

Wie immer beginnen wir mit den Schnittstellen. Wir benötigen die drei Schnittstellen INodeApi, INode und INodeBehavior. Die Struktur der Schnittstellen ist in Abbildung 5 dargestellt.

INodeApi

INTERFACE INodeApi EXTENDS CNM_AbstractObject.IObject

   METHOD executeAutomaticMode :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
END_VAR
END_METHOD

METHOD executeHoming : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
END_VAR
END_METHOD

METHOD executeManualMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
END_VAR
END_METHOD

PROPERTY stopRequest : BOOL
   SET
END_PROPERTY

INode

INTERFACE INode EXTENDS INodeApi

   METHOD addChildNode
VAR_INPUT
   child :INode;
END_VAR
END_METHOD

METHOD runAutomatic :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute : BOOL;
END_VAR
END_METHOD

METHOD runHoming :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute : BOOL;
END_VAR
END_METHOD

METHOD runManual : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute : BOOL;
END_VAR
END_METHOD

INodeBehavior

INTERFACE INodeBehavior EXTENDS CNM_AbstractObject.IObject

   METHOD executeMode :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   node :INodeApi;
END_VAR
END_METHOD

METHOD runMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   node :INode;
END_VAR
END_METHOD

Klassen

Delegates

Zuerst implementieren wir die drei Delegates für Automatic, Homing und Manual, da diese für unseren AbstractNode benötigt werden. Die Struktur der Delegates ist in Abbildung 6 zu sehen.

AutomaticBehavior

FUNCTION_BLOCK AutomaticBehavior EXTENDS CNM_AbstractObject.Object IMPLEMENTS INodeBehavior

   METHOD executeMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   node :INodeApi;
END_VAR
   IF (THIS^.isObjectValid(node)) THEN
      executeMode := node.executeAutomaticMode(execute := execute);
   ELSE
      executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
   END_IF
END_METHOD

METHOD runMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   node :INode;
END_VAR
   IF (THIS^.isObjectValid(node)) THEN
      runMode := node.runAutomatic(execute := execute);
   ELSE
      executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
   END_IF
END_METHOD

PROPERTY className :CNM_AbstractObject.ClassName
   GET
      className := 'AutomaticBehavior';
   END_GET
END_PROPERTY

HomingBehavior

FUNCTION_BLOCK HomingBehavior EXTENDS CNM_AbstractObject.Object IMPLEMENTS INodeBehavior

   METHOD executeMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   node :INodeApi;
END_VAR
   IF (THIS^.isObjectValid(node)) THEN
      executeMode := node.executeHoming(execute := execute);
   ELSE
      executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
   END_IF
END_METHOD

METHOD runMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   node :INode;
END_VAR
   IF (THIS^.isObjectValid(node)) THEN
      runMode := node.runHoming(execute := execute);
   ELSE
      executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
   END_IF
END_METHOD

PROPERTY className :CNM_AbstractObject.ClassName
   GET
      className := 'HomingBehavior';
   END_GET
END_PROPERTY

ManualBehavior

FUNCTION_BLOCK ManualBehavior EXTENDS CNM_AbstractObject.Object IMPLEMENTS INodeBehavior

   METHOD executeMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   node :INodeApi;
END_VAR
   IF (THIS^.isObjectValid(node)) THEN
      executeMode := node.executeManualMode(execute := execute);
   ELSE
      executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
   END_IF
END_METHOD

METHOD runMode : CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   node :INode;
END_VAR
   IF (THIS^.isObjectValid(node)) THEN
      node := node.runManual(execute := execute);
   ELSE
      executeMode := CNM_ReturnTypes.SingleExecutionState.ERROR;
   END_IF
END_METHOD

PROPERTY className :CNM_AbstractObject.ClassName
   GET
      className := 'ManualBehavior';
   END_GET
END_PROPERTY

Abstract Node

Jetzt kann endlich der Abstract Node implementiert werden und damit der Kern unseres Konzepts. Es gibt dabei drei Methoden, die man verstehen sollte:

  • Der Constructor (FB_init), da hier unsere Baumstruktur aufgebaut wird.

  • Die geschützte Methode handleMode, da sie die Ausführung und Rückgabewerte des eigenen Node und der Child-Nodes verwaltet.

  • Die Property stopRequest, da sie eine Stoppanforderung mit allen Child-Nodes teilt.

FUNCTION_BLOCK ABSTRACT AbstractNode EXTENDS CNM_AbstractObject.Object IMPLEMENTS INode
   VAR
      childNodes :CNM_Collections.ArrayList(initialSize := 15);
   stopRequestIsPending :BOOL;
END_VAR

METHOD PROTECTED iterateChilds :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
END_VAR
VAR_OUTPUT
   child :INodeApi;
END_VAR
VAR
   object :CNM_AbstractObject.IObject;
END_VAR
   iterateChilds := THIS^.childNodes.iterate(
      execute := execute,
      object => object
   );
   IF (NOT __QUERYINTERFACE(object, child)) THEN
      child := 0;
   END_IF
END_METHOD

METHOD PROTECTED handleMode :CNM_ReturnTypes.SingleExecutionState
VAR_INPUT
   execute :BOOL;
   behavaior :INodeBehavior;
END_VAR
VAR_INST
   cycleManger :CNM_State_Machine_Lib.SingleExecuteCycleManager();
   everthingIsOk :BOOL;
END_VAR
VAR
   childNode :INodeApi;
   allChildsFinished :BOOL;
   myState :CNM_ReturnTypes.SingleExecutionState;
   errorCount :__UXINT;
END_VAR
VAR CONSTANT
   EXECUTION_STEP :DINT := 2;
   EVALUATION_STEP :DINT := 3;
END_VAR
   cycleManger.execute := execute;

   CASE cycleManger.currentStep OF
      CNM_ReturnTypes.DefaultSteps.STEP.INIT:
      everthingIsOk := TRUE;
      behavaior.runMode(execute := FALSE, node := THIS^);
      THIS^.iterateChilds(execute:=FALSE);
      WHILE (THIS^.iterateChilds(execute:=TRUE,child => childNode) <> CNM_ReturnTypes.SingleExecutionState.BUSY) DO
         IF (childNode <> 0) THEN
            behavaior.executeMode(execute := FALSE, node := childNode);
         END_IF
      END_WHILE
      THIS^.stopRequest := FALSE;
      cycleManger.transitions.nextStep.noCondition.now();
      EXECUTION_STEP:
      THIS^.iterateChilds(execute:=FALSE);
      allChildsFinished := TRUE;
      errorCount := 0;
      WHILE (THIS^.iterateChilds(execute:=TRUE,child => childNode) <> CNM_ReturnTypes.SingleExecutionState.BUSY) DO
         IF (childNode <> 0) THEN
            CASE behavaior.executeMode(execute := TRUE, node := childNode) OF
               CNM_ReturnTypes.SingleExecutionState.BUSY: allChildsFinished := FALSE;
               CNM_ReturnTypes.SingleExecutionState.ERROR:
               errorCount := errorCount+1;
               ELSE;//nothing to do
            END_CASE
         END_IF
      END_WHILE
      myState := behavaior.runMode(execute := TRUE, node := THIS^);
      cycleManger.transitions.nextStep.ifTrue(
         (myState <> CNM_ReturnTypes.SingleExecutionState.BUSY)
         AND allChildsFinished
      ).now();
      everthingIsOk := ((myState <> CNM_ReturnTypes.SingleExecutionState.ERROR) AND (errorCount = 0));
      IF (NOT everthingIsOk) THEN
         THIS^.stopRequest := TRUE;
      END_IF
      EVALUATION_STEP:
      cycleManger.transitions.setSuccess.noCondition.now();
      cycleManger.assert.boolean.valueIsTrue(everthingIsOk, 'something went wrong');
      ELSE;//whatever
      END_CASE
      executeAutomaticMode := cycleManger.executionState;
   END_METHOD

   METHOD executeAutomaticMode : CNM_ReturnTypes.SingleExecutionState
   VAR_INPUT
      execute : BOOL;
   END_VAR
   VAR_INST
      modeBehavior :AutomaticBehavior();
   END_VAR
      executeAutomaticMode := THIS^.handleMode(execute, modeBehavior);
   END_METHOD

   METHOD executeHoming : CNM_ReturnTypes.SingleExecutionState
   VAR_INPUT
      execute : BOOL;
   END_VAR
   VAR_INST
      modeBehavior :HomingBehavior();
   END_VAR
      executeHoming := THIS^.handleMode(execute, modeBehavior);
   END_METHOD

   METHOD executeManualMode :CNM_ReturnTypes.SingleExecutionState
   VAR_INPUT
      execute : BOOL;
   END_VAR
   VAR_INST
      modeBehavior :ManualBehavior();
   END_VAR
      executeManualMode := THIS^.handleMode(execute, modeBehavior);
   END_METHOD

   PROPERTY stopRequest :BOOL
      SET
         VAR
            childNode :INodeApi;
         END_VAR
         IF (THIS^.stopRequestIsPending <> stopRequest) THEN
            THIS^.iterateChilds(execute:=FALSE);
            WHILE (THIS^.iterateChilds(execute:=TRUE,child => childNode) <> CNM_ReturnTypes.SingleExecutionState.BUSY) DO
               IF (childNode <> 0) THEN
                  childNode.stopRequest := stopRequest;
               END_IF
            END_WHILE
            THIS^.iterateChilds(execute:=FALSE);
         END_IF
         THIS^.stopRequestIsPending := stopRequest;
      END_SET
   END_PROPERTY

   METHOD addChildNode
   VAR_INPUT
      child : INode;
   END_VAR
      THIS^.childNodes.append(child);
   END_METHOD

   METHOD runAutomatic :CNM_ReturnTypes.SingleExecutionState
   VAR_INPUT
      execute : BOOL;
   END_VAR
      runAutomatic := CNM_ReturnTypes.SingleExecutionState.SUCCESS;
   END_METHOD

   METHOD runHoming :CNM_ReturnTypes.SingleExecutionState
   VAR_INPUT
      execute : BOOL;
   END_VAR
      runHoming := CNM_ReturnTypes.SingleExecutionState.SUCCESS;
   END_METHOD

   METHOD runManual :CNM_ReturnTypes.SingleExecutionState
   VAR_INPUT
      execute : BOOL;
   END_VAR
      runManual := CNM_ReturnTypes.SingleExecutionState.SUCCESS;
   END_METHOD

   METHOD FB_init
   VAR_INPUT
      (* if TRUE, the retain variables are initialized (warm start / cold start)*)
      bInitRetains :BOOL;
      (* if TRUE, the instance afterwards gets moved into the copy code (online change) *)
      bInCopyCode :BOOL;
      parentNode :INode;
   END_VAR
   VAR
      {attribute 'hide'}
      newHashstate3 :Hashcode;
   END_VAR
   VAR CONSTANT
      {attribute 'hide'}
      NUMBER_OF_LEFT_SHIFTS :UINT := 17;
      {attribute 'hide'}
      NUMBER_OF_LEFT_ROTATIONS :UINT := 45;
   END_VAR
      IF (THIS^.isObjectValid(parentNode)) THEN
         parentNode.addChildNode(THIS^);
      END_IF
   END_METHOD

   PROPERTY className :CNM_AbstractObject.ClassName
      GET
         className := 'AbstractNode';
      END_GET
   END_PROPERTY

Wenn ich nun eine Klasse für die Maschine, eine Station, eine Funktionsgruppe oder ein Gerät erstellen möchte, muss diese Klasse nur von AbstractNode erben. Dann kann ich die Klasse einfach zu unserer Hierarchie hinzufügen. Für die Funktionalität des Node werden die Methoden runAutomatic, runHoming, runManual bei Bedarf überschrieben. Abbildung 7 fasst die Struktur von AbstractNode noch einmal zusammen.

Operation Mode Handling

Jetzt brauchen wir einen Client, der dem Baum sagt, was er tun soll. Das ist unser Operation Mode Handler. In unserem Beispiel nehmen wir an, dass der Opmode eine vorzeichenlose kurze Integerzahl ist, die auf magische Weise von der HMI kommt. Wie bereits gesagt, ist das Beispiel unvollständig. Der Operation Mode Handler sollte einen Node erhalten und den Node dazu bringen, das zu tun, was getan werden muss.

Aus Gründen der Lesbarkeit erstellen wir eine Enumeration, die die verschiedenen Betriebsmodi darstellt.

OpMode enumeration

{attribute 'qualified_only'}
TYPE OpMode :
(
   IDLE := 0,
   HOMING := 1,
   AUTOMATIC := 2,
   MANUAL := 3
)USINT;
END_TYPE

Class OperationMode

FUNCTION_BLOCK OperationMode EXTENDS CNM_AbstractObject.Object
   VAR
      hmiButtonOperationMode :OpMode;
   rootNode :INodeApi;
END_VAR

METHOD FB_init
VAR_INPUT
   (* if TRUE, the retain variables are initialized (warm start / cold start)*)
   bInitRetains :BOOL;
   (* if TRUE, the instance afterwards gets moved into the copy code (online change) *)
   bInCopyCode :BOOL;
   rootNode :INodeApi;
END_VAR
VAR
   {attribute 'hide'}
   newHashstate3 :Hashcode;
END_VAR
VAR CONSTANT
   {attribute 'hide'}
   NUMBER_OF_LEFT_SHIFTS :UINT := 17;
   {attribute 'hide'}
   NUMBER_OF_LEFT_ROTATIONS :UINT := 45;
END_VAR
   THIS^.rootNode := rootNode;
END_METHOD

METHOD run
VAR_INST
   opModeChanged :BOOL;
   activeOpMode :OpMode := OpMode.IDLE;
   lastOpMode :OpMode := OpMode.IDLE;
END_VAR

   IF (THIS^.isObjectNull(THIS^.rootNode)) THEN
      RETURN;
   END_IF

   IF (THIS^.hmiButtonOperationMode <> lastOpMode) THEN
      THIS^.rootNode.stopRequest := TRUE;
      lastOpMode := THIS^.hmiButtonOperationMode;
   END_IF

   CASE activeOpMode OF
      OpMode.IDLE:
      THIS^.rootNode.executeAutomaticMode(FALSE);
      THIS^.rootNode.executeHoming(FALSE);
      THIS^.rootNode.executeManualMode(FALSE);
      activeOpMode := THIS^.hmiButtonOperationMode;
      OpMode.HOMING:
      activeOpMode := SEL(
         THIS^.rootNode.executeHoming(TRUE) = CNM_ReturnTypes.SingleExecutionState.BUSY,
         OpMode.IDLE,
         OpMode.HOMING
      );
      OpMode.AUTOMATIC:
      activeOpMode := SEL(
         THIS^.rootNode.executeAutomaticMode(TRUE) = CNM_ReturnTypes.SingleExecutionState.BUSY,
         OpMode.IDLE,
         OpMode.AUTOMATIC
      );
      OpMode.MANUAL:
      activeOpMode := SEL(
         THIS^.rootNode.executeManualMode(TRUE) = CNM_ReturnTypes.SingleExecutionState.BUSY,
         OpMode.IDLE,
         OpMode.MANUAL
      );
   ELSE
      activeOpMode := OpMode.IDLE;
   END_CASE

END_METHOD

PROPERTY className :CNM_AbstractObject.ClassName
   GET
      className := 'OperationMode';
   END_GET
END_PROPERTY

Abbildung 8 gibt dir einen Überblick über unseren Client.

Die Maschinenimplementierung

Keine Sorge, ich werde hier keine Maschine implementieren, ich zeige dir nur, wie es aussehen könnte.

Infeed

FUNCTION_BLOCK InfeedConveyor EXTENDS AbstractNode
   VAR
      infeedConveyor :Conveyor(THIS^);
   nokConveyor :Conveyor(THIS^);
   jamStopperinfeed :Stopper(THIS^, infeedConveyor);
   preStopperStu :Stopper(THIS^, infeedConveyor);
   nokLift :LiftUnit(THIS^, nokConveyor);
   stu :StrokeTransverseUnit(THIS^, infeedConveyor, nokLift);
END_VAR

Machine

FUNCTION_BLOCK Machine EXTENDS AbstractNode
   VAR
      infeed :InfeedConveyor(THIS^);
   press :PressStation(THIS^);
   outfeed :OutfeedConveyor(THIS^);
END_VAR

Main

PROGRAM MAIN
VAR
   machine :Machine(parentNode := 0);
   modeHandler :OperationMode(rootNode := machine);
END_VAR
   modeHandler.run();
END_PROGRAM

Zusammenfassung

Das Beispiel zeigt sehr gut, wie einfach es ist, hierarchische Kompositionen zu erstellen und zu verwenden, wenn sie dieselben Methoden/Vorgänge bereitstellen. Die Aufrufe dauern nicht länger, als wenn du jeden Aufruf in den Hauptteil einfügst, aber es liest sich gut, weil es die Struktur unserer Maschine widerspiegelt.