The first structural pattern that we will discuss is the decorator pattern. It introduces you to the object substitution by adding new functionalities or behaviors.
The main objective of this pattern is to dynamically add new functionalities to an object. The interface of the object will not be modified, so from the client's perspective, this is fully transparent.
This pattern is an alternative to the addition of a subclass that adds functionalities to its parent class. A key implementation point in the decorator pattern is that decorators both inherit the original class and contain an instantiation of it.
This pattern can be used when:
The generic UML class diagram of the decorator pattern is quiet simple: The ConcreteComponent
and AbstractDecorator
classes share the same interface that have the same method name. Our AbstractDecorator
class defines a constructor where we pass our Abstractcomponent
class as an argument. Then, in our ConcreteDecorator
class, we reroute the operation call to the additionalOperation
methods to add new functionalities or behaviors to the original component, as shown in the following diagram:
In the preceding diagram, there are four participants in this pattern:
AbstractComponent
: This is the common interface to components and decorators.ConcreteComponent
: This is the main object to which we want to add behaviors and/or functionalities.AbstractDecorator
: This abstract class contains a reference to a component.ConcreteDecoratorA
and ConcreteDecoratorB
: These are the concrete subclasses of AbstractDecorator
. These classes implement the functionalities added to the component.When a decorator receives a message that must reach the component, it redirects the message by making a prior or posterior operation to that redirection.
To illustrate this pattern, we will take a simple example. Let's suppose that you have a drawing software that enables you to draw some shapes on the screen: a rectangle and square.
You already know how to draw these shapes. Now, you need to add a new functionality that will add a rounded angle to your shapes. To do this, you need to decide which decorator pattern you need to use that will allow you to not to interfere with the existing class method signature.
First, we will create our interface that defines the shape. We will simulate a Draw()
operation. In fact, the method will return a string that tells us what is drawn:
protocol IShape { func draw() -> String }
Now, we will create our two concrete classes that implement the IShape
interface. We will have the Square
and Rectangle
classes. They both implement the draw
function. This function returns the shape that is currently drawn:
class Square: IShape { func draw() -> String{ return "drawing Shape: Square" } } class Rectangle: IShape { func draw() -> String { return "drawing Shape: Rectangle" } }
Our classes are ready; now, we prepare our abstract ShapeDecorator
class that defines the structure of our future concrete decorators. This class implements the IShape
interface too, so the Draw()
function must be present. Nevertheless, Swift doesn't have an abstract class, so we implement the draw method, but we force an exception to tell us that this method must be implemented. The ShapeDecorator
class will not be used by the client itself. The client will call the ConcreteDecorator
object to add a new functionality to its shape:
class ShapeDecorator: IShape { private let decoratedShape: IShape required init(decoratedShape: IShape){ self.decoratedShape = decoratedShape } func draw() -> String { fatalError("Not Implemented") } }
Now, we add our concrete decorator class that inherits from the ShapeDecorator
abstract class. We add our new setRoundedCornerShape
functionality to this class and override the draw
function to return the shape that is drawn, but with rounded corners:
class RoundedCornerShapeDecorator: ShapeDecorator{ required init(decoratedShape: IShape) { super.init(decoratedShape: decoratedShape) } override func draw() ->String{ //we concatenate our shape properties return decoratedShape.draw() + "," + setRoundedCornerShape(decoratedShape) } func setRoundedCornerShape(decoratedShape: IShape) -> String{ return "Corners are rounded" } }
Now, here is the easy part that shows us how to use all of the code, we already have written, from the client perspective.
We first create our two concrete shapes:
let rectangle = Rectangle() let square = Square()
Now, we want to have some shapes with rounded corners. To do this, we simply call the ConcreteDecorator
class that interests us, the RoundedCornerShapeDecorator
class, and pass a new shape (Rectangle
or Square
) as an argument of the constructor:
let roundedRectangle = RoundedCornerShapeDecorator(decoratedShape: Rectangle()) let roundedSquare = RoundedCornerShapeDecorator(decoratedShape: Square())
Now, we simulate the Draw()
method on the screen of our shapes by calling the draw
operation:
print("rectangle with Normal Angles") rectangle.draw() print("square with Normal Angles") square.draw() //rounded corners shapes roundedRectangle.draw() roundedSquare.draw()
The Playground will return the following result:
Swift allows you to implement the decorator pattern using the concept of extensions. This allows you to add additional methods to concrete classes or constructs, without having to subclass or alter the original one. With extensions, you can add new methods but no new properties, as opposed to a subclass.