The Observer Pattern is suitable for applications that require one or more function blocks to be notified when the state of a particular function block changes. The assignment of the communication participants can be changed at runtime of the program.
In almost every IEC 61131-3 program, function blocks exchange states with each other. In the simplest case, one input of one FB is assigned the output of another FB.
This makes it very easy to exchange states between function blocks. But this simplicity has its price:
Inflexibility. The assignment between fbSensor and the three instances of FB_Actuator is hard-coded in the program. Dynamic assignment between the FBs during runtime is not possible.
Fixed dependencies. The data type of the output variable of FB_Sensor must be compatible to the input variable of FB_Actuator. If there is a new sensor component whose output variable is incompatible with the previous data type, this necessarily results in an adjustment of the data type of the actuators.
The following example shows how, with the help of the observer pattern, the fixed assignment between the communication participants can be dispensed with. The sensor reads a measured value (e.g. a temperature) from a data source, while the actuator performs actions depending on a measured value (e.g. temperature control). The communication between the participants should be changeable. If these disadvantages are to be eliminated, two basic OO design patterns are helpful:
- Identify those areas that remain constant and separate them from those that change.
- Never program directly to implementations, but always to interfaces. The assignment between input and output variables must therefore no longer be permanently implemented.
This can be realized elegantly with the help of interfaces that define the communication between the FBs. There is no longer a fixed assignment of input and output variables. This results in a loose coupling between the participants. Software design based on loose coupling makes it possible to build flexible software systems that cope better with changes, since dependencies between the participants are minimized.
Definition of Observer Pattern
The observer pattern provides an efficient communication mechanism between several participants, whereby one or more participants depend on the state of one participant. The participant providing a state is called Subject (FB_Sensor). The participants, which depend on the state, are called Observer (FB_Actuator).
The Observer pattern is often compared to a newspaper subscription service. The publisher is the subject, while the subscribers are the observers. The subscriber must register with the publisher. When registering, you may also specify which information you would like to receive. The publisher maintains a list in which all subscribers are stored. As soon as a new publication is available, the publisher sends the desired information to all subscribers in the list.
This becomes more formal in the book (Amazon Advertising Link *) Design pattern. Elements of reusable object-oriented software expressed by Erich Gamma, Richard Helm, Ralph E. Johnson and John Vlissides:
The Observer pattern defines a 1-to-n dependency between objects, so that changing the state of an object causes all dependent objects to be notified and automatically updated.
In which way the subject receives the data and how the observer processes the data is not discussed here in more detail.
The method Update() notifies the observer of the subject, if the value changes. Since this behaviour is the same for all observers, the interface I_Observer is defined, which is implemented by all observers.
The function block FB_Observer also defines a property that returns the current actual value.
Since the data is exchanged by method, no further inputs or outputs are required.
FUNCTION_BLOCK PUBLIC FB_Observer IMPLEMENTS I_Observer VAR fValue : LREAL; END_VAR
Here is the implementation of the method Update():
METHOD PUBLIC Update VAR_INPUT fValue : LREAL; END_VAR THIS^.fValue := fValue;
und das Property fActualValue:
PROPERTY PUBLIC fActualValue : LREAL fActualValue := THIS^.fValue;
The subject manages a list of observers. Using the methods Attach() and Detach(), the individual Observers can log on and off.
Since all Observers implement the interface I_Observer, the list is of type ARRAY [1..Param.cMaxObservers] OF I_Observer. The exact implementation of the observer does not have to be known at this point. Further variants of observers can be created, as long as they implement the interface I_Observer, the subject can communicate with them.
The method Attach() contains the interface pointer to the observer as a parameter. Before it is stored in the list (line 23), the system checks whether it is valid and not already contained in the list.
METHOD PUBLIC Attach : BOOL VAR_INPUT ipObserver : I_Observer; END_VAR VAR nIndex : INT := 0; END_VAR Attach := FALSE; IF (ipObserver = 0) THEN RETURN; END_IF // is the observer already registered? FOR nIndex := 1 TO Param.cMaxObservers DO IF (THIS^.aObservers[nIndex] = ipObserver) THEN RETURN; END_IF END_FOR // save the observer object into the array of observers and send the actual value FOR nIndex := 1 TO Param.cMaxObservers DO IF (THIS^.aObservers[nIndex] = 0) THEN THIS^.aObservers[nIndex] := ipObserver; THIS^.aObservers[nIndex].Update(THIS^.fValue); Attach := TRUE; EXIT; END_IF END_FOR
The method Detach() also contains the interface pointer to the Observer as a parameter. If the interface pointer is valid, the Observer is searched in the list and the corresponding position is deleted (line 15).
METHOD PUBLIC Detach : BOOL VAR_INPUT ipObserver : I_Observer; END_VAR VAR nIndex : INT := 0; END_VAR Detach := FALSE; IF (ipObserver = 0) THEN RETURN; END_IF FOR nIndex := 1 TO Param.cMaxObservers DO IF (THIS^.aObservers[nIndex] = ipObserver) THEN THIS^.aObservers[nIndex] := 0; Detach := TRUE; END_IF END_FOR
If there is a status change in the subject, the method Update() is called by all valid interface pointers in the list (line 8). This functionality is found in the private method Notify().
METHOD PRIVATE Notify VAR nIndex : INT := 0; END_VAR FOR nIndex := 1 TO Param.cMaxObservers DO IF (THIS^.aObservers[nIndex] <> 0) THEN THIS^.aObservers[nIndex].Update(THIS^.fActualValue); END_IF END_FOR
In this example, the subject generates a random value every second and then notifies the observer using the Notify() method.
FUNCTION_BLOCK PUBLIC FB_Subject IMPLEMENTS I_Subject VAR fbDelay : TON; fbDrand : DRAND; fValue : LREAL; aObservers : ARRAY [1..Param.cMaxObservers] OF I_Observer; END_VAR // creates every sec a random value and invoke the update method fbDelay(IN := TRUE, PT := T#1S); IF (fbDelay.Q) THEN fbDelay(IN := FALSE); fbDrand(SEED := 0); fValue := fbDrand.Num * 1234.5; Notify(); END_IF
There is no statement in the subject to access FB_Observer directly. Access always takes place indirectly via the interface I_Observer. An application can be extended with any observer. As long as it implements the interface I_Observer, no adjustments to the subject are necessary.
The following module should help to test the example program. A subject and two observers are created in it. By setting appropriate auxiliary variables, the two observers can be both connected to the subject and disconnected again at runtime.
PROGRAM MAIN VAR fbSubject : FB_Subject; fbObserver1 : FB_Observer; fbObserver2 : FB_Observer; bAttachObserver1 : BOOL; bAttachObserver2 : BOOL; bDetachObserver1 : BOOL; bDetachObserver2 : BOOL; END_VAR fbSubject(); IF (bAttachObserver1) THEN fbSubject.Attach(fbObserver1); bAttachObserver1 := FALSE; END_IF IF (bAttachObserver2) THEN fbSubject.Attach(fbObserver2); bAttachObserver2 := FALSE; END_IF IF (bDetachObserver1) THEN fbSubject.Detach(fbObserver1); bDetachObserver1 := FALSE; END_IF IF (bDetachObserver2) THEN fbSubject.Detach(fbObserver2); bDetachObserver2 := FALSE; END_IF
Subject: Interface or base class?
The necessity of the interface I_Observer is obvious in this implementation. Access to an observer is decoupled from implementation by the interface.
However, the interface I_Subject does not appear necessary here. And in fact, the interface I_Subject could be omitted. However, I have planned it anyway, because it keeps the option open to create special variants of FB_Subject. For example, there might be a function block that does not organize the observer list in an array. The methods for logging on and off the different Observers could then be accessed generically using the interface I_Subject.
The disadvantage of the interface, however, is that the code for logging in and out must be implemented each time, even if the application does not require it. Instead, a base class (FB_SubjectBase) seems to be more useful for the subject. The management code for the methods Attach() and Detach() could be moved to this base class. If it is necessary to create a special subject (FB_SubjectNew), it can be inherited from this base class (FB_SubjectBase).
But what if this special function block (FB_SubjectNew) already inherits from another base class (FB_Base)? Multiple inheritance is not possible (however, several interfaces can be implemented).
Here, it makes sense to embed the base class in the new function block, i.e. to create a local instance of FB_SubjectBase.
FUNCTION_BLOCK PUBLIC FB_SubjectNew EXTENDS FB_Base IMPLEMENTS I_Subject VAR fValue : LREAL; fbSubjectBase : FB_SubjectBase; END_VAR
The methods Attach() and Detach() can then access this local instance.
METHOD PUBLIC Attach : BOOL VAR_INPUT ipObserver : I_Observer; END_VAR Attach := FALSE; IF (THIS^.fbSubjectBase.Attach(ipObserver)) THEN ipObserver.Update(THIS^.fValue); Attach := TRUE; END_IF
METHOD PUBLIC Detach : BOOL VAR_INPUT ipObserver : I_Observer; END_VAR Detach := THIS^.fbSubjectBase.Detach(ipObserver);
METHOD PRIVATE Notify VAR nIndex : INT := 0; END_VAR FOR nIndex := 1 TO Param.cMaxObservers DO IF (THIS^.fbSubjectBase.aObservers[nIndex] <> 0) THEN THIS^.fbSubjectBase.aObservers[nIndex].Update(THIS^.fActualValue); END_IF END_FOR
Thus, the new subject implements the interface I_Subject, inherits from the function block FB_Base and can access the functionalities of FB_SubjectBase via the embedded instance.
Update: Push or pull method?
There are two ways in which the observer receives the desired information from the subject:
With the push method, all information is passed to the observer via the update method. Only one method call is required for the entire information exchange. In the example, only one variable of the data type LREAL has ever passed the subject. But depending on the application, it can be considerably more data. However, not every observer always needs all the information that is passed to it. Furthermore, extensions are made more difficult: What if the method Update() is extended by further data? All observers must be customized. This can be remedied by using a special function block as a parameter. This function block encapsulates all necessary information in properties. If additional properties are added, it is not necessary to adjust the update method.
If the pull method is implemented, the observer receives only a minimal notification. He then gets all the information he needs from the subject. However, two conditions must be met. First, the subject should make all data available as properties. On the other hand, the observer must be given a reference to the subject so that it can access the properties. One solution may be that the update method contains a reference to the subject (i.e. to itself) as a parameter.
Both variants can certainly be combined with each other. The subject provides all relevant data as properties. At the same time, the update method can provide a reference to the subject and pass the most important information as a function block. This method is the classic approach of numerous GUI libraries.
Tip: If the subject knows little about its observers, the pull method is preferable. If the subject knows its observers (since there are only a few different types of observers), the push method should be used.