Join us today!
Factory Method Design Pattern
In this tutorial, we will go through the factory method design pattern implementation in TwinCAT 3. Factory pattern is one of the creational design patterns which is concerned with object creation issues during software design and development.
Factory/Factory method pattern states that define an interface or abstract class for creating an object, but let the subclasses decide which class to instantiate/create. Instead of creating the instances of classes in the main application, subclasses are responsible to do this task.
The factory method is also known as Virtual Constructor.
The factory method class diagram:
In the diagram above, program in the application asks Creator to create and return the object based on the given input.
Let's assume, we have a 2 or 3 axes CNC Machine and we are dealing with different types of shapes. Function block of each shape will be responsible for controlling the motion and drawing the shape. According to the diagram above, we will create an interface called 'I_Shape' and our products will be the shape function blocks. Let's say our current shape designs are Circle, Rectangle, Square, Star and Triangle. These will be our products and they will implement the 'I_Shape' interface. Then instead of instantiating the needed shape function block in MAIN Program, we will create a function block called 'FB_ShapeCreator' which would instantiate a shape function block and return it so that we can access the methods of the object and execute the 'Draw' method in the MAIN application.
The class diagram for our sample application can be seen below:
By doing this way, if need more shape designs in the future, we can extend the FB_ShapeCreator and add our code in the new function block instead of modifying the FB_ShapeCreator . We may not even have access to source code of FB_ShapeCreator, but we would be still be able to add the new shape product in our application. In the software design world, the goal is to have "open for extension", but "closed for modification" design.
Let's start with creating the interface first:
I_Shape:
INTERFACE I_Shape
Add a method called 'M_Draw'. Each shape function block will use this method to control the motion and draw the requested shape.
M_Draw:
METHOD M_Draw : BOOL; VAR_INPUT END_VAR
Add the function blocks below, which implement the I_Shape interface.
Add the {attribute 'enable_dynamic_creation'} on the top of the each function block's declaration section. This will enable us to instantiate the each function block dynamically using _NEW operator in the FB_ShapeCreator function block.
FB_Circle:
{attribute 'enable_dynamic_creation'} FUNCTION_BLOCK FB_Circle IMPLEMENTS I_Shape VAR_INPUT END_VAR VAR_OUTPUT END_VAR VAR END_VAR
M_Draw method of FB_Circle:
METHOD M_Draw : BOOL
ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Draw circle', ''); M_Draw := TRUE;
For demonstration purposes , we are just logging a message. Repeat this step for other shape function blocks and modify the message in accordance with the name of the shape.
Now we will add our 'Creator' function block called FB_ShapeCreator:
FB_ShapeCreator:
{attribute 'hide_all_locals'} FUNCTION_BLOCK FB_ShapeCreator VAR_INPUT eShape : E_Shape; END_VAR VAR_OUTPUT END_VAR VAR pCircle : POINTER TO FB_Circle; pRectangle : POINTER TO FB_Rectangle; pSquare : POINTER TO FB_Square; pStar : POINTER TO FB_Star; pTriangle : POINTER TO FB_Triangle; iShape : I_Shape; END_VAR
We have declared pointers to each corresponding shape function block, which would allow us to instantiate the requested shape function block dynamically and save memory space. If we had declared them as fbCircle : FB_Circle instead and let's assume we would never need to draw Circle, we would still occupy the memory space for it in this function block.
In the main program, user will provide an input for the desired shape and 'M_GetShapeObject' will decide which shape function block to instantiate and return it.
Enumeration for the shapes:
E_Shape:
{attribute 'qualified_only'} {attribute 'strict'} TYPE E_Shape : ( init := 0, Circle, Rectangle, Sqaure, Star, Triangle, end ); END_TYPE
M_GetShapeObject:
METHOD M_GetShapeObject : I_Shape VAR_INPUT END_VAR
CASE eShape OF E_Shape.Circle: //Dynamic instantiation pCircle := __NEW(FB_Circle); iShape := pCircle^; //Return the object IF iShape <> 0 THEN M_GetShapeObject := iShape; END_IF //Release the memory IF (pCircle <> 0) THEN __DELETE(pCircle); pCircle := 0; END_IF eShape := E_Shape.init; E_Shape.Rectangle: pRectangle := __NEW(FB_Rectangle); iShape := pRectangle^; //Return the object IF iShape <> 0 THEN M_GetShapeObject := iShape; END_IF IF (pRectangle <> 0) THEN __DELETE(pRectangle); pRectangle := 0; END_IF eShape := E_Shape.init; E_Shape.Sqaure: pSquare := __NEW(FB_Square); iShape := pSquare^; //Return the object IF iShape <> 0 THEN M_GetShapeObject := iShape; END_IF IF (pSquare <> 0) THEN __DELETE(pSquare); pSquare := 0; END_IF eShape := E_Shape.init; E_Shape.Star: pStar := __NEW(FB_Star); iShape := pStar^; //Return the object IF iShape <> 0 THEN M_GetShapeObject := iShape; END_IF IF (pStar <> 0) THEN __DELETE(pStar); pStar := 0; END_IF eShape := E_Shape.init; E_Shape.Triangle: pTriangle := __NEW(FB_Triangle); iShape := pTriangle^; //Return the object IF iShape <> 0 THEN M_GetShapeObject := iShape; END_IF IF (pTriangle <> 0) THEN __DELETE(pTriangle); pTriangle := 0; END_IF eShape := E_Shape.init; END_CASE
If we look at this logic, we are first checking which shape is requested by the user, then we are dynamically creating the requested shape function block instance;
pCircle := __NEW(FB_Circle);
pCircle is a pointer to FB_Circle. If __NEW() operation is successful, the pointer is a valid memory address to the FB. If it is zero, then __NEW() operation wasn't successful.
Then we are de referencing the pointer and assigning to the iShape which is declared as I_Shape.
iShape := pCircle^;
We can now return this object to the user. In the main application, we are able to access to the methods (M_Draw) and properties of the function block instance since all the shape function blocks are implementing I_Shape interface.
//Return the object IF iShape <> 0 THEN M_GetShapeObject := iShape; END_IF
We are then deleting the created pCircle since we are done with it. It no longer needs to occupy a memory space after we return it to the user.
//Release the memory IF (pCircle <> 0) THEN __DELETE(pCircle); pCircle := 0; END_IF
In the FB_Exit method of FB_ShapeCreator, we are deleting all the pointers. FB_exit will be called implicitly, for example when downloading.
FB_Exit:
METHOD FB_exit : BOOL VAR_INPUT bInCopyCode : BOOL; // if TRUE, the exit method is called for exiting an instance that is copied afterwards (online change). END_VAR
IF (pCircle <> 0) THEN __DELETE(pCircle); pCircle := 0; END_IF IF (pRectangle <> 0) THEN __DELETE(pRectangle); pRectangle := 0; END_IF IF (pSquare <> 0) THEN __DELETE(pSquare); pSquare := 0; END_IF IF (pStar <> 0) THEN __DELETE(pStar); pStar := 0; END_IF IF (pTriangle <> 0) THEN __DELETE(pTriangle); pTriangle := 0; END_IF
If you want to perform this occasionaly, you can also add another method called M_ReleaseMemory and put the same memory release code as FB_Exit.
Now in the MAIN program, declare and instantiate the FB_ShapeCreator:
VAR fbShapeCreator : FB_ShapeCreator; eShape : E_Shape := E_Shape.init; iShape : I_Shape; END_VAR
To demonstrate the application, we will execute the M_Draw methods of shape function blocks one after other starting from Circle.
fbShapeCreator(eShape:= eShape); CASE eShape OF E_Shape.Circle: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Rectangle; E_Shape.Rectangle: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Sqaure; E_Shape.Sqaure: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Star; E_Shape.Star: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Triangle; E_Shape.Triangle: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.end; END_CASE
We are providing an input the shape creator function block. In the logic, when we assign E_Circle to eShape enum, it will draw the each shape one by one.
We are assigning the returned object to iShape : I_Shape.
iShape := fbShapeCreator.M_GetShapeObject();
Then we are calling the M_Draw method through the interface:
IF iShape <> 0 THEN iShape.M_Draw(); END_IF
Login the plc and assing E_Circle to eShape to run the logic. The result can be seen below:
Now let's assume, we need to add another shape function block called FB_CustomShape. Add the function block which would also implement the I_Shape interface.
FB_CustomShape:
{attribute 'enable_dynamic_creation'} FUNCTION_BLOCK FB_CustomShape IMPLEMENTS I_Shape VAR_INPUT END_VAR VAR_OUTPUT END_VAR VAR END_VAR
METHOD M_Draw : BOOL
ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Draw custom shape', ''); M_Draw := TRUE;
We won't change anything in the FB_ShapeCreator function block. We will add a new function block called FB_ShapeCreatorEX, which would extend the FB_ShapeCreator .
FB_ShapeCreatorEX:
FUNCTION_BLOCK FB_ShapeCreatorEX EXTENDS FB_ShapeCreator VAR_INPUT END_VAR VAR_OUTPUT END_VAR VAR pCustomShape : POINTER TO FB_CustomShape; END_VAR
We have added a pointer to new shape function block. Add the M_GetShapeObject method which returns the I_Shape:
METHOD M_GetShapeObject : I_Shape VAR_INPUT END_VAR
IF eShape = E_Shape.Custom THEN //Dynamic instantiation pCustomShape := __NEW(FB_CustomShape); iShape := pCustomShape^; //Return the object IF iShape <> 0 THEN M_GetShapeObject := iShape; END_IF //Release the memory IF (pCustomShape <> 0) THEN __DELETE(pCustomShape); pCustomShape := 0; END_IF eShape := E_Shape.init; ELSE M_GetShapeObject := SUPER^.M_GetShapeObject(); END_IF
In the method body, we have added the logic for the new shape design. If it is not the custom shape, we are calling the parent's M_GetShapeObject.
M_GetShapeObject := SUPER^.M_GetShapeObject();
So now, a user can just change the declaration of the fbShapeCreator in the MAIN program and use the FB_ShapeCretorEX which would allow user to get any shape function block (Circle, Rectangle, Square, Star, Triangle and CustomShape).
Make the changes in the MAIN program to see the result:
VAR fbShapeCreator : FB_ShapeCreatorEX; eShape : E_Shape := E_Shape.init; iShape : I_Shape; END_VAR
fbShapeCreator(eShape:= eShape); CASE eShape OF E_Shape.Circle: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Rectangle; E_Shape.Rectangle: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Sqaure; E_Shape.Sqaure: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Star; E_Shape.Star: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Triangle; E_Shape.Triangle: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.Custom; E_Shape.Custom: iShape := fbShapeCreator.M_GetShapeObject(); IF iShape <> 0 THEN iShape.M_Draw(); END_IF eShape := E_Shape.end; END_CASE
We have added calling the M_Draw method of the new custom shape function block. If we run the logic again, we would see that M_Draw method of FB_CustomShape will be executed as well.
The new class diagram after adding a new custom shape function block and extending the FB_ShapeCreator:
Factory design pattern provides approach to code for interface rather than implementation. We didn't need to insatiate to shape function blocks in the MAIN program an easily added a new shape design without changing the code in FB_ShapeCreator.
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 for your time to make a tutorial on Design Patterns and specifically on the Factory Method, can you put the link to download the project?, so I can study it and follow it...
I would also be very grateful if you make a tutorial on each Design Pattern little by little according to free time...,
right now I am making one on the TCP-IP communication stack, with the Tc2_TcpIp library, I already have the OOP wrapper .., and now the next step I think would be a better approach to apply the state pattern, what do you think?..
I found this link where at the end it puts as an application example: A TCP communication stack is a good example of using the state pattern.
https://stefanhenneken.net/2018/11/17/iec-61131-3-the-state-pattern/
Thank you very much for everything and for the good work and time of dedication..., Víctor
https://github.com/runtimevic
https://github.com/TcMotion
https://www.youtube.com/playlist?list=PLEfi_hUmmSjFpfdJ6yw3B9yj7dWHYkHmQ
https://github.com/VisualPLC
Hi,
Thank you for kind words and feedback, we truly appreciate it! The reason we don't usually upload the code/project is that we want our readers to follow the whole article and use the code given in the article. We believe that it would help them to learn better and stay focused on the topic. We will consider your feedback and think about sharing the project as well.
Our goal is to cover the major design patterns and share tutorials about them with time.
Yes, that's a good approach! You can apply the state pattern.
Thank you for being an active member and contributing to our community!
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,
I have carried out the project..., and the order that it should have according to the main carried out does not appear in the log list, do you know why it is?
I attached a picture so you can see...
https://github.com/runtimevic
https://github.com/TcMotion
https://www.youtube.com/playlist?list=PLEfi_hUmmSjFpfdJ6yw3B9yj7dWHYkHmQ
https://github.com/VisualPLC
You can click on the 'description' to toggle the ascending/descending order of the timestamp. Then it will show you the correct order.
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/
Hi. Tell me if you are satisfied that you cannot assign the result of work __NEW to a variable of another type. This is bullying, I have asked to fix it more than once, but they don't care. Does it suit others?
I mean this:
shape : POINTER TO Shape;
...
shape := __NEW(Circle);
shape := __NEW(Square);
etс...
-
Abstract Factory Design Pattern
1 year ago
-
Decorator Design Pattern
2 years ago
-
Observer Design Pattern
2 years ago
- 17 Forums
- 268 Topics
- 925 Posts
- 5 Online
- 738 Members