Join us today!
Decorator Design Pattern
In this tutorial, we will go through the power of object composition, which enable us to decorate our function blocks easily at runtime rather than having to make changes at compile time. The decorator design pattern is used to modify an object's functionality at runtime. When you learn this technique, you can give your objects new roles without modifying the base classes' source code.
To expand an object's behavior, we utilize inheritance, but this is done at compile time and is applicable to all instances of the class. Inheriting behavior through subclassing sets that behavior statically at compile time. Also, all subclasses must inherit the same behavior. However, if the behavior of an object can be extended through composition, it can be extended dynamically at runtime. Decorator pattern comes into play to solve the problem of not being able to add any new functionality or remove any existing behavior at runtime. The decorator pattern adds additional responsibilities on an object. For additional functionalities, decorators offer a versatile alternative to subclassing.
By assembling objects dynamically, you can write new code to add new functionality instead of modifying existing code. Since this technique doesn't change existing code, it greatly reduces the possibility of bugs or unintended side effects.
The decorator pattern class diagram:
Let's assume we are trying to draw different types of shapes and we want to be able to paint the border of these shapes with green color and change the border dash type. When we think about it the implementation, at first we can think about having base shape classes (e.g. Circle , Square, Rectange etc..) and subclasses (Green Circle, Green Square, Dash Border Green Square, Round Dot Border Rectangle etc.). The problem with this solution is that there might be many shape style subclasses and this would cause maintenance issues. Inheritance technique is powerful, however it doesn't always provide the most maintainable and flexible designs.
Instead of overusing inheritance, we will have decorator function blocks, which will decorate our shape function blocks. We can think of these decorator function blocks as wrappers.
According to the UML diagram above, we will have one I_Shape interface and our shape function blocks and the decorator function block will implement this interface. We will define a decorator function block as abstract so that each decorator function block has to implement their own draw shape methods. For demonstration purposes, we will have a green shape decorator and border dash type decorator.
The class diagram for our sample application can be seen below:
With this technique, we can easily wrap our shape function blocks and give them new functionalities easily. As an example, we will first draw a shape, then decorate (wrap) it with green border and change the border dash type.
Our sample project structure:
Let's add the I_Shape interface :
INTERFACE I_Shape
M_Draw:
METHOD M_Draw : BOOL
Then we will have FB_Circle and FB_Square implementing the I_Shape interface. We are just printing the name of the shape in the M_Draw method of these function blocks.
FB_Circle:
FUNCTION_BLOCK FB_Circle IMPLEMENTS I_Shape
M_Draw:
METHOD M_Draw : BOOL
ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Draw circle', ''); M_Draw := TRUE;
FB_Square:
FUNCTION_BLOCK FB_Square IMPLEMENTS I_Shape
M_Draw:
METHOD M_Draw : BOOL
ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Draw Square', ''); M_Draw := TRUE;
We now need to create the abstract decorator function block. In the FB_init method, we will get the shape reference that user wants to decorate.
FB_ShapeDecorator:
FUNCTION_BLOCK ABSTRACT FB_ShapeDecorator IMPLEMENTS I_Shape VAR_INPUT END_VAR VAR_OUTPUT END_VAR VAR _iDecoratedShape: I_Shape; END_VAR
M_Draw:
METHOD ABSTRACT M_Draw : BOOL
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) iDecoratedShape : I_Shape; END_VAR
_iDecoratedShape := iDecoratedShape;
Now we can extend the abstract decorator function block and create our concrete decorator function blocks. We will then add the decorating implementations in the M_Draw method. In the FB_init method, we will store the reference shape function block in the _iDecoratedShape variable that's declared in the parent function block.
FB_GreenShapeDecorator:
FUNCTION_BLOCK FB_GreenShapeDecorator EXTENDS FB_ShapeDecorator
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) iDecoratedShape : I_Shape; END_VAR
SUPER^._iDecoratedShape := iDecoratedShape;
M_SetGreenBorder:
METHOD PRIVATE M_SetGreenBorder VAR_INPUT iDecoratedShape : I_Shape; END_VAR
ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Border Color : Green', '');
M_Draw:
METHOD M_Draw : BOOL
M_Draw := SUPER^._iDecoratedShape.M_Draw(); M_SetGreenBorder(SUPER^._iDecoratedShape);
In the M_Draw Method, we are first calling the M_Draw method of the reference shape function block and then the private decorating method (M_SetGreenBorder).
FB_BorderDashTypeDecorator:
FUNCTION_BLOCK FB_BorderDashTypeDecorator EXTENDS FB_ShapeDecorator VAR_INPUT END_VAR VAR_OUTPUT END_VAR VAR _eBorderDashType : E_BorderDashType; END_VAR
In the FB_init method, we are getting the reference shape function block and the border dash type (E_BorderDashType) from the user.
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) iDecoratedShape : I_Shape; eBorderDashType : E_BorderDashType; END_VAR
SUPER^._iDecoratedShape := iDecoratedShape; _eBorderDashType := eBorderDashType;
M_SetBorderDashType:
METHOD M_SetBorderDashType VAR_INPUT iDecoratedShape : I_Shape; END_VAR
CASE _eBorderDashType OF E_BorderDashType.Solid: ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Border Dash Type : Solid', ''); E_BorderDashType.SquareDot: ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Border Dash Type : Square Dot', ''); E_BorderDashType.RoundDot: ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Border Dash Type : Round Dot', ''); E_BorderDashType.Dash: ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Border Dash Type : Dash', ''); E_BorderDashType.LongDashDot: ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Border Dash Type :Long Dash Dot', ''); ELSE ADSLOGSTR(ADSLOG_MSGTYPE_HINT, 'Border Dash Type :Unkown', ''); END_CASE
M_Draw:
METHOD M_Draw : BOOL
M_Draw := SUPER^._iDecoratedShape.M_Draw(); M_SetBorderDashType(SUPER^._iDecoratedShape);
We also added a property to be able to change the border dash type after initialization as well.
PROPERTY P_GS_BorderDashType : E_BorderDashType
Get:
P_GS_BorderDashType := _eBorderDashType;
Set:
_eBorderDashType := P_GS_BorderDashType;
E_BorderDashType:
{attribute 'qualified_only'} {attribute 'strict'} TYPE E_BorderDashType : ( Solid := 0, RoundDot, SquareDot, Dash, LongDashDot ); END_TYPE
Now we can declare the function blocks in the MAIN program and initialize the decorator function blocks with the reference shape function blocks.
PROGRAM MAIN VAR fbCircle : FB_Circle; fbGreenCircle : FB_GreenShapeDecorator(fbCircle); fbRoundDotBorderGreenCircle : FB_BorderDashTypeDecorator(fbGreenCircle,E_BorderDashType.RoundDot); fbSquare : FB_Square; fbGreenSquare : FB_GreenShapeDecorator(fbSquare); fbDashBorderGreenSquare : FB_BorderDashTypeDecorator(fbGreenSquare,E_BorderDashType.Dash); END_VAR
fbRoundDotBorderGreenCircle.M_Draw(); fbDashBorderGreenSquare.M_Draw();
We first declared a shape function block, then passed this function block to our first decorator function block(FB_GreenShapeDecorator). To modify the border dash type, we passed the first decorated function block to our second decorator function block.
When you call the fbRoundDotBorderGreenCircle.M_Draw() method, it first prints the shape name, then decorates it with green color, then wraps it with round dot dash border.
To comprehend this technique more, you can try adding more decorator function blocks and modifying the FB_GreenShapeDecorator function block to allow more color options.
The Decorator pattern contains a set of decorator classes used to wrap concrete components and allows us to change the behavior of the components dynamically by adding new functionalities. A decorator class reflects the type of component it decorates and they are the same type of objects they decorate. You can create as many as decorators you need and wrap a component with all of them if you like, however having many decorates in the design can complicate the project.
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/
Kudos for adding the "implements" and "aks" etc labels in the UML. I'm always confused by what each line means :D.
In case you don't like shapes, but do like pizza. There is another example of the decorator pattern.
My blog: https://cookncode.com/twincat
My code: https://github.com/roald87
@rruiter Thank you! Yes, it gets really confusing especially when the UML diagram is quite big. Love pizza!!! 🍕 But not when it is decorated with pineapple on it 😃
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/
-
Operator Overloading in TwinCAT using OOP
11 months ago
-
Abstract Factory Design Pattern
1 year ago
-
A few difficulties while learning OOP
1 year ago
-
Fluent Interface and Method Chaining in TwinCAT 3
2 years ago
-
Observer Design Pattern
2 years ago
- 17 Forums
- 267 Topics
- 924 Posts
- 1 Online
- 737 Members