Join us today!

Factory Method Desi...
 
Notifications
Clear all

Factory Method Design Pattern

6 Posts
3 Users
10 Reactions
962 Views
twinControls
Posts: 115
Admin
Topic starter
(@twincontrols)
Member
Joined: 2 years ago

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: 

factory method 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: 

factory method

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. 

shapes

 

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: 

creator

 

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: 

drawResults

 

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. 

drawCustomShape

 

The new class diagram after adding a new custom shape function block and extending the FB_ShapeCreator: 

factory method extended

 

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. 

 

Reply
5 Replies
4 Replies
runtimevictor
(@runtimevictor)
Joined: 2 years ago

Estimable Member
Posts: 156

@twincontrols ,

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

Reply
twinControls
Admin
(@twincontrols)
Joined: 2 years ago

Member
Posts: 115

@runtimevictor 

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! 

 

Reply
runtimevictor
(@runtimevictor)
Joined: 2 years ago

Estimable Member
Posts: 156

@twincontrols ,

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...

Factory method pattern

 

Reply
twinControls
Admin
(@twincontrols)
Joined: 2 years ago

Member
Posts: 115

@runtimevictor 

You can click on the 'description' to toggle the ascending/descending order of the timestamp. Then it will show you the correct order. 

 

description

 

Reply
Posts: 3
(@trofimich)
Active Member
Joined: 2 years ago

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с...

 
 
 

 

 

Reply
Share: