Decorator Design Pattern. When and Why?
In this piece of article i am gonna talk about Decorator design pattern, it’s use-cases and why and when we should think of it’s presence and finally i am gonna show you it’s Kotlin implementation.
The decorator design pattern comes handy when you wanna add some functionality to an existing class/object without altering it’s current behavior.
When you want to add multiple functionality to a class/object and you want to have flexibility to add them whenever you need, it’s the Decorator time. if you choose inheritance and create a sub-class for each use-case, this way you will come up with lot’s of sub-classes that could blow your mind and make your code-base full of confusing parts. but with the Decorator Design Pattern you have your desired changes but still have a simple, understandable flow. your design is easily open for extension but close to modification with really simple logic. The best example could be the IO Streams that are implemented in Java (and C#). for example you have a File Stream and in one use-case You want to Encrypt that, then Zip that, then Log that and finally do something fancy with it. then in another class you decide to do something else You want to Convert that, then Encrypt that, then get the timing, bluh, bluh, bluh. again you have another flow in other classes. if you want to use Inheritance you have to create at least 3 sub-classes and if any other requirement is needed you have to add more sub-classes that in this case (Streams) you will end up dozens of sub-classes for mostly small changes.
class EncryptZipLogStream: Stream {
}
class ConvertEncryptTimeStream: Stream {
}
// this class declaration will continue for each use-case.
class ListOfSubClassesThatWillBeUnNecessaryIfIUseDecorator {
class EncryptZipLogStream{}
class ConvertEncryptTimeStream{}
class MoreStreamProcess{}
class OtherMoreStreamProcess{}
// and more classes to come in yet...
}
And in each use-case you have to remember what class is needed and try to use it. But let’s use Composition instead of Inheritance and that means we go for a design pattern instead of inheritance and you will end up having a Decorator class for each Stream process, you can easily combine your desired wrappers and have any desired processes with least amount of effort and max simplicity.
class WhereIWannaUseUseCaseOne {
EncryptStream(ZipStream(LogStream(FileStream("file name)))));
// rest of the code to use the combined streams.
}
// then you come up with another use-case:
class WhereIWannaUseUseCaseSecond {
ConvertStream(TimeStream(LogStream(FileStream("file name)))));
// rest of the code to use the combined streams.
}
And so on and so forth, you have the flexibility to do what ever you want at run-time with a simple flow and understandable logic.
Let’s bring some examples you can use Decorators/Wrappers for them.
One example could be text formatting, you have a base text and you wanna apply several arbitrary formatting styles to it. you can make it Bold, Strike-through, change the color, change the character-spacing, and so on.
LargeSpacing(ShadowText(StrikeThrough(BoldText(Text(“some text”))));
Then you need to create another style:
ColoredText(BorderText(StrikeThrough(ItalicText(Text(“some text”))));
And you have lot’s of other use-cases coming around. imagine you wanted to make sub-classes for each use-case, the amount of possible classes is enormous.
Another example could be GUI element attributes and features, you have a GUI environment and you want to add multiple combinations of GUI elements with their containers. like you have a text, then you want to wrap it around a scroll bar, then you want to add border shadows and so on. you will end up something like this using Decorators.
Window(Shadow(Border(ScrollBar(Text(“some text)))));
it’s out of mind to count the possible number of classes we need to create all the possible GUI element combinations with inheritance.
The Decorator Design Pattern has similarities with other Design Patterns especially Chain of Responsibility. the difference between Chain of Responsibility (COR) and Decorator is that when you want to add lot’s of functionality to an existing class the best choice is Decorator, but when you have lot’s of functionality but you want to use just one of them you will go for COR, for example when you want to check the correctness of an email you can handle multiple criteria using COR.
Now we know when and why to use Decorator, let’s dive into it’s implementation using Kotlin. here is the steps you have to take making a decorator.
- Ensure the context is: a single core component/object and an interface that is common to all.
- Create a “Lowest Common Denominator” interface that makes all classes interchangeable.
- Create a second level base class (Decorator) to support the optional wrapper classes.
- The Core class and Decorator class inherit from the LCD interface.
- The Decorator class declares a composition relationship to the LCD interface (meaning LCD contains the it) and this data member is initialized in its constructor.
- The Decorator class delegates to the LCD object.
- Define a Decorator derived class for each optional embellishment.
- Decorator derived classes implement their wrapper functionality — and — delegate to the Decorator base class.
- The client configures the type and ordering of Core and Decorator objects.
First off let’s define the Lowest Common Denominator that is our general interface.
interface Widget {
fun draw()
}
Then we need our core class, the object that is gonna be responsible for the main functionality.
class Button(val width: Int, val height: Int): Widget {
override fun draw(){
// do some fancy work here.
}
}
What we need then is the actual Decorator class that is responsible to wrap other classes and Decorator classes are gonna use this one as the parent class.
abstract class Decorator(private val widget: Widget): Widget {
override fun draw(){
widget.draw()
}
}
And finally we define our Wrappers/Decorators using the Base Decorator class.
class ScrollDecorator(private val widget: Widget): Decorator(widget) {
override fun draw() {
super.draw() // call the core class draw method.
// do some fancy decorative work here
// like making the core object scrollable.
widget.draw()
}
}
If we need other Decorators, just repeat above code and add desired functionality into “draw” method.
class BorderDecorator(private val widget: Widget): Decorator(widget) {
override fun draw() {
super.draw() // call the core class draw method.
// Adding border to the wrapee(the given object).
widget.draw()
}
}
And finally we are going to use our Wrappers/Decorators here.
class HowToUseAboveDecorators {
fun createGUI(){
val surface = SurfaceToDrawOn()
val widget = BorderDecorator(ScrollDecorator(Button()))
surface.addWidget(widget)
}
}
That’s it, hope you enjoy this article and please feel free to share this piece of article.