Join us today!
Observer Design Pattern
Observer pattern is one the behavioral design patterns and it defines a one-to-many relationship between objects so that if a state has changed in one object, its dependent objects are updated and notified automatically.
For instance, if you subscribe a forum on our community, you get a notification whenever there is a new post.
To give another real world example, let's assume that you want to move to a new city and you need to rent a place. You visit numerous apartment buildings and provide the leasing offices your email address so they can contact you if an apartment becomes available. Since there will be more people like you who's looking for a place, all of the subscribers will get notified by the leasing office. In this scenario, we can consider you and others to be the OBSERVER/SUBSCRIBER and all the leasing offices to be the SUBJECT/PUBLISHER.
Observer Pattern class diagram:
Now we are going to use this design pattern in TwinCAT 3 for our sample PLC application. Let's imagine we have a scanner station and we are scanning parts one by one. We would like to have for now two HMI screens to display the current station data and the station data statistics.
Current station data HMI page will display the last result of scan (PASS or FAIL) and the total number of scanned parts count.
Station data statistics HMI page will display the total pass and failed part count. You can implement other logics to display more data if you like.
We will have a function block called FB_StationData as a PUBLISHER and it will notify all the HMI Screen function blocks after every new scan so that the values get updated in the HMI screens.
We will have an interface for the publisher and subscribers. FB_StationData will implement I_Publisher interface, which would have add a subscriber, remove a subscriber and notify subscribers methods.
FB_CurrentStationDataHMIScreen as a subscriber will implement I_Subscriber and it will have an update method.
We will define an array of I_Subscriber in FB_StationData and use the M_Update method of subscribers to notify them all whenever new data becomes available.
The class diagram for our sample application can be seen below:
The subject/publisher (FB_StationData) only knows that an observer/subscriber implements a certain interface. (I_Subscriber). It doesn't need to know anything else about the subscriber.
By using this design, we can add new subscribers at any time as long as the array size(cMaxSubscribers) is enough. We can also replace the subscribers or remove them at runtime.
If we decide to have a new publisher, we could easily add another publisher function block by implementing the I_Publisher.
We can use the publisher and subscriber function blocks independently from each other because they are not tightly coupled. We are striving for loosely coupled designs so that we can handle the changes easily in the future.
Our sample project structure:
Let's add the I_Publisher interface :
INTERFACE I_Publisher
M_AddSubscriber :
METHOD M_AddSubscriber : BOOL VAR_INPUT iSubscriber : I_Subscriber; END_VAR
M_RemoveSubscriber:
METHOD M_RemoveSubscriber : BOOL VAR_INPUT iSubscriber : I_Subscriber; END_VAR
M_NotifySubscribers:
METHOD M_NotifySubscribers : BOOL VAR_INPUT END_VAR
I_Subscriber will have only M_Update method:
INTERFACE I_Subscriber
M_Update:
METHOD M_Update : BOOL VAR_INPUT stData : ST_ScannerStationData; END_VAR
User defined scanner station data structure: ST_ScannerStationData
TYPE ST_ScannerStationData : STRUCT nTotalScannedParts : INT; bLastScanResult : BOOL; // True- Pass, False - Fail END_STRUCT END_TYPE
StationParam parameter file which will have a constant for the array size of subscribers.
{attribute 'qualified_only'} VAR_GLOBAL CONSTANT cMaxSubscribers : INT := 2; END_VAR
FB_StationData will implement the I_Publisher interface since it is responsible for notifying all of the subscribers. It is up to you to implement how the publisher receives the data. For this application, we will have a simulation function block which will output some simulated station data. We will use this data as input to FB_StationData.
FUNCTION_BLOCK FB_StationData IMPLEMENTS I_Publisher VAR_INPUT stStationData : ST_ScannerStationData; END_VAR VAR_OUTPUT END_VAR VAR _stStationData : ST_ScannerStationData; _aSubscribers: ARRAY [1..StationParam.cMaxSubscribers] OF I_Subscriber; END_VAR
_stStationData := stStationData;
M_AddSubscriber :
In M_AddSubscriber method, we are checking if the subscriber is already added in the array. If it is a new subscriber, we are adding the object into the array of subscribers then sending the station data using the M_Update method of the subscriber.
METHOD M_AddSubscriber : BOOL VAR_INPUT iSubscriber : I_Subscriber; END_VAR VAR nIndex : INT := 0; END_VAR
M_AddSubscriber := FALSE; IF (iSubscriber = 0) THEN RETURN; END_IF // is the subscriber already registered? FOR nIndex := 1 TO StationParam.cMaxSubscribers DO IF (_aSubscribers[nIndex] = iSubscriber) THEN RETURN; END_IF END_FOR // save the subscriber object into the array of subscribers and send the station data FOR nIndex := 1 TO StationParam.cMaxSubscribers DO IF (_aSubscribers[nIndex] = 0) THEN _aSubscribers[nIndex] := iSubscriber; ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Publisher: Added a subscriber', ''); _aSubscribers[nIndex].M_Update(_stStationData); M_AddSubscriber := TRUE; EXIT; END_IF END_FOR
M_RemoveSubscriber:
METHOD M_RemoveSubscriber : BOOL VAR_INPUT iSubscriber : I_Subscriber; END_VAR VAR nIndex : INT := 0; END_VAR
M_RemoveSubscriber := FALSE; IF (iSubscriber = 0) THEN RETURN; END_IF FOR nIndex := 1 TO StationParam.cMaxSubscribers DO IF (_aSubscribers[nIndex] = iSubscriber) THEN _aSubscribers[nIndex] := 0; ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Publisher: Removed a subscriber', ''); M_RemoveSubscriber := TRUE; END_IF END_FOR
M_NotifySubscribers:
Notifying every subscriber using their M_Update method.
METHOD M_NotifySubscribers : BOOL VAR nIndex : INT := 0; END_VAR
FOR nIndex := 1 TO StationParam.cMaxSubscribers DO IF (_aSubscribers[nIndex] <> 0) THEN _aSubscribers[nIndex].M_Update(stData := _stStationData); END_IF END_FOR
Now we will add two subscriber function blocks :
FB_CurrentStationDataHMIScreen:
This function block will display the total scanned part count and the last scan result.
FUNCTION_BLOCK FB_CurrentStationDataHMIScreen IMPLEMENTS I_Subscriber VAR_INPUT END_VAR VAR_OUTPUT END_VAR VAR _iPublisher : I_Publisher; _stData : ST_ScannerStationData; sDisplayMessage : STRING; sTemp1 : STRING; sTemp2 : STRING; END_VAR
sTemp1 := CONCAT('Total Scanned Part Count :', INT_TO_STRING(_stData.nTotalScannedParts)); sTemp2 := CONCAT(' Last Scan Result :', SEL(_stData.bLastScanResult,'FAIL','PASS') ); sDisplayMessage := CONCAT(sTemp1, sTemp2); ADSLOGSTR(ADSLOG_MSGTYPE_HINT, sDisplayMessage, '');
M_Update:
METHOD M_Update : BOOL VAR_INPUT stData : ST_ScannerStationData; END_VAR
_stData := stData;
In the FB_Init method, we will pass the I_Publisher reference so that a subscriber can register itself whenever a subscriber function block is initialized. We are also storing the I_Publisher reference. We can use this to unregister ourselves if we want to.
METHOD FB_init : BOOL VAR_INPUT bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start) bInCopyCode : BOOL; // if TRUE, the instance afterwards gets moved into the copy code (online change) iPublisher : I_Publisher; END_VAR
IF iPublisher <> 0 THEN _iPublisher := iPublisher; _iPublisher.M_AddSubscriber(iSubscriber := THIS^); END_IF
We also added a getter property.
PROPERTY P_G_Data : ST_ScannerStationData
Get:
VAR END_VAR P_G_Data := _stData;
FB_StationDataStatisticsHMIScreen:
This function block will display the total pass and fail part counts.
FUNCTION_BLOCK FB_StationDataStatisticsHMIScreen IMPLEMENTS I_Subscriber VAR_INPUT END_VAR VAR_OUTPUT END_VAR VAR _iPublisher : I_Publisher; _stData : ST_ScannerStationData; nTotalPassPartCount : INT; sDisplayMessage : STRING; sTemp1 : STRING; sTemp2 : STRING; END_VAR
//implement station data statistic logic here// IF _stData.bLastScanResult THEN nTotalPassPartCount := nTotalPassPartCount + 1; _stData.bLastScanResult := FALSE; END_IF sTemp1 := CONCAT('Total Pass Part Count :', INT_TO_STRING( nTotalPassPartCount) ); sTemp2 := CONCAT(' Total Failed Part Count :', INT_TO_STRING( _stData.nTotalScannedParts - nTotalPassPartCount)); sDisplayMessage := CONCAT(sTemp1, sTemp2); ADSLOGSTR(ADSLOG_MSGTYPE_HINT, sDisplayMessage, '');
M_Update:
METHOD M_Update : BOOL VAR_INPUT stData : ST_ScannerStationData; END_VAR
_stData := stData;
FB_init :
METHOD FB_init : BOOL VAR_INPUT bInitRetains : BOOL; // if TRUE, the retain variables are initialized (warm start / cold start) bInCopyCode : BOOL; // if TRUE, the instance afterwards gets moved into the copy code (online change) iPublisher : I_Publisher; END_VAR
IF iPublisher <> 0 THEN _iPublisher := iPublisher; _iPublisher.M_AddSubscriber(iSubscriber := THIS^); END_IF
P_G_Data property:
PROPERTY P_G_Data : ST_ScannerStationData
Get:
VAR END_VAR P_G_Data := _stData;
To simulate the station data, we will have a function block called FB_Simulation. Every 2s it will increase the total scanned part count and randomly generate a scan result (PASS,FAIL). It will ouput the data and a new data update flag.
FB_Simulation:
FUNCTION_BLOCK FB_Simulation VAR_INPUT END_VAR VAR_OUTPUT stData : ST_ScannerStationData; bNewDataUpdate : BOOL; END_VAR VAR fbDelay : TON; fbDrand : DRAND; END_VAR
fbDelay(IN := TRUE, PT := T#2S); bNewDataUpdate := FALSE; IF (fbDelay.Q) THEN fbDelay(IN := FALSE); fbDrand(SEED := 1); stData.nTotalScannedParts := stData.nTotalScannedParts + 1; stData.bLastScanResult := fbDrand.Num > 0.5; bNewDataUpdate := TRUE; END_IF
In the MAIN program, declare a station data and simulation function block, and two HMI screen subscriber function blocks:
VAR fbSimulation : FB_Simulation; fbStationData : FB_StationData; //Publisher fbCurrentStationDataHMIScreen : FB_CurrentStationDataHMIScreen(fbStationData); // Subscriber 1 fbStationDataStatisticsHMIScreen : FB_StationDataStatisticsHMIScreen(fbStationData); // Subscriber 2 bRegister : BOOL; bUnregister: BOOL; END_VAR
fbSimulation(stData=> , bNewDataUpdate=> ); fbStationData(stStationData:= fbSimulation.stData); //provide data into the publisher from the simulation //If there is an update notify the users and call subscriber FBs IF fbSimulation.bNewDataUpdate THEN fbStationData.M_NotifySubscribers(); fbCurrentStationDataHMIScreen(); fbStationDataStatisticsHMIScreen(); END_IF IF bRegister THEN fbStationData.M_AddSubscriber(fbCurrentStationDataHMIScreen); fbStationData.M_AddSubscriber(fbStationDataStatisticsHMIScreen); bRegister := FALSE; END_IF IF bUnregister THEN fbStationData.M_RemoveSubscriber(fbCurrentStationDataHMIScreen); fbStationData.M_RemoveSubscriber(fbStationDataStatisticsHMIScreen); bUnregister := FALSE; END_IF
Whenever the simulation tells us that new data is available, we are calling the notify subcribers method of the fbStationData and subscriber function blocks.
Run the application and observer the message outputs. Also, set the bUnregister and bRegister to TRUE to unregister or register all the subscribers.
You can try adding more station data and new subscribers in order to comprehend this design better.
In this application, we have used the PUSH method, which means all the subscribers are getting notified with all the data. You can try implementing PULL method, which means a subscriber pulls the data from the publisher whenever it needs. You may use the I_Publisher reference that we pass in the FB_Init of the subscriber function blocks to achieve this.
In case you want to say thank you !)
We'd be very grateful if you could share this community with your colleagues and friends. You can also buy us a coffee to keep us fueled 😊 This is the best way to say thank you to this project and support your community.
twinControls - https://twincontrols.com/
Hello, thank you very much for another example of Design Patterns,
the M_Update method could not be deleted and in the P_G_Data property add the setter and what if the code of the M_Update method was there??...
going to the next level with Design Patterns...
https://github.com/runtimevic
https://github.com/TcMotion
https://www.youtube.com/playlist?list=PLEfi_hUmmSjFpfdJ6yw3B9yj7dWHYkHmQ
https://github.com/VisualPLC
@runtimevictor Hello, You're welcome! P_G_Data is there just in case if you need to get the subscribers data somewhere else. We want subscriber's data to be updated by only publishers using the M_Method and passing a subscriber reference into this method. If you add the setter, something else can set the data as well which would cause conflicts .
In case you want to say thank you !)
We'd be very grateful if you could share this community with your colleagues and friends. You can also buy us a coffee to keep us fueled 😊 This is the best way to say thank you to this project and support your community.
twinControls - https://twincontrols.com/
Thanks for sharing this useful design patterns.
May we use it to extend a MC_TouchProbe ? In that way, as soon as a new "RecoredPosition" is recorded, MC_TouchProbe infroms that a new data is available.
@alex Hi, you are welcome!
Yes, that's possible. You can either do;
FUNCTION_BLOCK FB_MyPublisher EXTENDS MC_TouchProbe IMPLEMENTS I_Publisher
//logic here to detect new recorded position, using SUPER^.RecordedPosition maybe IF bIsNewPositionRecorded THEN M_NotifySubscribers(); END_IF;
Or provide the MC_TouchProbeRecordedData and the notify flag to your publisher FB through inputs or properties.
Or you can inject your MC_TouchProbe function block to your Publisher FB using the FB_Init or a property.
I would do the third method to keep the responsibilities separate and be able to use a different MC_TouchProbe function block at runtime if I want to in the future.
It would be great if you can share your implementation with the community once you are done.
In case you want to say thank you !)
We'd be very grateful if you could share this community with your colleagues and friends. You can also buy us a coffee to keep us fueled 😊 This is the best way to say thank you to this project and support your community.
twinControls - https://twincontrols.com/
-
Abstract Factory Design Pattern
12 months ago
-
Decorator Design Pattern
2 years ago
-
Factory Method Design Pattern
2 years ago
- 17 Forums
- 265 Topics
- 932 Posts
- 1 Online
- 688 Members