22
Decorator Pattern in Kotlin
The Decorator
allows you to dynamically add or change the behavior of a specific object of a given class, without affecting other objects of the same class. In some cases, this allows you to significantly reduce the number of classes by moving the shared behavior to the Decorator
, rather than extending the inheritance structure.
The decorator's job is to "wrap" (hence another name: "Wrapper") the original object and modify or overwrite its behavior. The decorator class has the same interface as the decorated object, so it doesn't matter to the client whether the object has been decorated or not. As a rule, Decorator
is unlikely to add new public methods, as the client using the original object's interface would not have access to them anyway. Having the same interface as the original object allows decorators to nest and wrap each other freely.
Decorating is done dynamically while the program is running, not at the compile time. The decorated object is usually passed in the Decorator constructor because the 'Decorator' instance makes no sense by itself, without the object it wraps around.
There are few key pieces in the Decorator
pattern:
-
Client - a class that uses the
Component
object, knows only generic interface, not concrete classes, so it does not need to know about the existence of a decorator -
Component - interface of the object that
Decorator
decorates -
ConcreteComponent - a specific implementation of the
Component
, which then is passed in the decorator's constructor -
Decorator - an abstract class that implements the
Component
interface and takes aComponent
object in the constructor. Using an abstract class, rather than an interface allows forcing the constructor on inheriting classes without the possibility of creating an instance. IfDecorator
accepts objects with a genericComponent
interface rather than a concrete class, it can decorate entire families of objects. - ConcreteDecorator - a specific decorator implementation
The above diagram can look like this in code:
interface Component {
// example methods of decorated interface
fun methodA()
fun methodB()
}
// a single concrete implementation but there can be many
class ConcreteComponent : Component {
override fun methodA() {}
override fun methodB() {}
}
// Decorator `is-a` Component and `has-a` Component
// field `component` is `protected` which makes it available to inheriting classes
abstract class Decorator(protected val component: Component) : Component
// concrete `Decorator` implementation with forced constructor requiring the `Component` instance
class ConcreteDecorator1(component: Component) : Decorator(component) {
// methods has to be overridden
// in this case, `Decorator` is calling wrapped instance methods without any changes
// so it's basically a Proxy
override fun methodA() = component.methodA()
override fun methodB() = component.methodB()
}
// another implementation of `Decorator`
class ConcreteDecorator2(component: Component) : Decorator(component) {
override fun methodA(){
// in this implementation you can't use methodA()
// it may be related to checking `Component` parameters for example
throw Exception("you can't do this")
}
override fun methodB(){
println("running methodB")
component.methodB()
}
}
fun main(){
// "naked" `Component``
val component: Component = ConcreteComponent()
// first Decorator wrapping a component
val dec1: Component = ConcreteDecorator1(component)
// second Decorator, wrapping already wrapped component
val dec2: Component = ConcreteDecorator2(dec1)
}
You can see here how easy it is to nest Decorators. In this implementation, ConcreteDecorator1
is basicallyProxy
because it simply calls the methods from the passed Component
object. Note that the ConcreteDecorator2.methodA()
throws an exception. The decorator can also return a constant and not use the passed Component
object at all. The 'decorator' decides for himself how to behave. Based on the properties of the wrapped object, it may decide to raise an exception instead of returning some value.
Kotlin has built-in support for the delegation pattern, another pattern that sets composition above inheritance. The general idea is to delegate the task resulting from the class interface to some component object, passed in the constructor, or injected by DI. This way class can be composed of reusable delegates instead of duplicating the implementation or creating strange inheritance structures. A bit like the template method, only that for the class - a class template :)
Delegates can be used to implement the Decorator
pattern. In Kotlin, delegates are created using the 'by' keyword:
// basic interface to be decorated
interface Component {
fun sayHello(): String
}
class ConcreteComponent1 : Component {
override fun sayHello() = "hello from ${javaClass.simpleName}"
}
// delegating the `Component` interface behavior to the `component` instance passed in constructor
abstract class Decorator(protected val component: Component): Component by component
class Decorator1(component: Component) : Decorator(component) {
// component method overridden by decorator, extending the message
override fun sayHello() = "${component.sayHello()} and from ${javaClass.simpleName}"
}
// no need to override `sayHello()` every time
// it was "delegated" to the `component` instance from constructor
class Decorator2(component: Component) : Decorator(component)
fun main() {
val first: Component = ConcreteComponent1()
val decOne: Component = Decorator1(first)
val decTwo: Component = Decorator2(decOne)
println(first.sayHello()) // hello from ConcreteComponent1
println(decOne.sayHello()) // hello from ConcreteComponent1 and from Decorator1
println(decTwo.sayHello()) // hello from ConcreteComponent1 and from Decorator1 and from Decorator2
}
Alternatively, component
may not even be a protected field in the abstract Decorator
. Then the inheriting specific decorators can use super
, which will delegate the method call to the comm
object from the constructor.
abstract class AltDecorator(component: Component): Component by component
class Decorator1(component: Component) : Decorator(component) {
override fun sayHello() = "${super.sayHello()} and from ${javaClass.simpleName}"
}
You can delegate as many interfaces as you want, but only interfaces not classes:
interface Component {
fun sayHello(): String
}
interface Component2{
fun sayBye(): String
}
abstract class Decorator(
protected val component: Component,
protected val component2: Component2
) :
Component by component,
Component2 by component2
By using delegates, a particular Decorator
can only contain methods that it actually wants to change. The abstract base class Decorator
could also contain proxy calls to all methods of the decorated object, in which case concrete decorators would overwrite only the methods they require. But if you can do the same thing with a single word, why overpay? :)
Let's take a system that sends text messages. Messages can be sent via Bluetooth or TCP. We want to be able to send pure text, JSON and JSON with the Base64 encoded message. Any type of message should be transferable by any means. And from the client level, it shouldn't matter what type of message and medium has been selected.
Ok lets start from communication means:
// generic communication interface
interface Comm {
fun sendMessage(text: String): Result
}
// class handling Bluetooth communication with the use of passed BtModule
class BtComm(val bt: BtModule) : Comm {
override fun sendMessage(text: String): Result {
return bt.send(text)
}
}
// class handling TCP communication with the use of passed TcpModule
class TcpComm(val tcpModule: TcpModule) : Comm {
override fun sendMessage(text: String): Result {
return tcpModule.send(text)
}
}
class TcpModule {
fun send(text: String): Result {
println("sending message via TCP: $text")
return Result.Success() // sealed class
}
}
class BtModule {
fun send(text: String): Result {
println("sending message via BT: $text")
return Result.Success()
}
}
Clients will only know the general Comm
interface, without knowing exactly how the message is processed. Separate modules will handle the details of sending via Bluetooth and TCP. From the point of the communication interface, its a facade for potentially complicated messaging, framing, device pairing, address finding, etc.
BtComm
andTcpComm
are specific classes that implement the interface to be decorated, not decorators.
Using it with Bluetooth may look like that:
val btModule = BtModule()
val message = "hello"
val btComm: Comm = BtComm(btModule)
btComm.sendMessage(message) // sending message via BT: hello
Which causes sending simple text message via Bluetooth. Same story with TCP.
We'd like to send JSON formatted messages with BT and TCP. A decorator like this can be used:
// `comm` passed in constructor is not a class field, so inheriting classes can't access it`
abstract class CommDecorator(comm: Comm) : Comm by comm
class JsonDecorator(comm: Comm) : CommDecorator(comm) {
override fun sendMessage(text: String): Result {
// with `super` method from abstract `CommDecorator` is called,
// that then delegates the call to the `comm` instance
return super.sendMessage("{\"message\":\"$text\"}")
}
}
And usage:
val tcpModule = TcpModule()
val btModule = BtModule()
val message = "hello"
// wrapping TCP communication in JSON decorator
val tcpJsonComm: Comm = JsonDecorator(TcpComm(tcpModule))
tcpJsonComm.sendMessage(message) // sending message via TCP: {"message":"hello"}
// for BT:
val btJsonComm: Comm = JsonDecorator(BtComm(btModule))
btJsonComm.sendMessage(message) // sending message via BT: {"message":"hello"}
If there was another way of communication now (Websockets, Firebase, carrier pigeon) and the messages should have the same format, no problem. You just decorate the new Comm
implementation with an already made decorator. Likewise, if a new message format is created, familiar transfer methods can be used with the decorator.
Last decorator will help creating Base64 encoded message:
class Base46Decorator(comm: Comm) : CommDecorator(comm) {
override fun sendMessage(text: String): Result {
return super.sendMessage(prepareMessage(text))
}
private fun prepareMessage(text: String): String {
return Base64.getEncoder().encodeToString(text.toByteArray())
}
}
It has a new private method to prepare messages. It does not extend the original interface, so it makes no difference whether the client knows about Comm
or Base46Decorator
- it has access to the same methods, so will rather choose to use Comm
due to the easily interchangeable implementation. Such a subliminal encouragement of good behavior.
All together is used like:
val tcpBase64JsonComm: Comm = JsonDecorator( // put message in JSON structure
Base46Decorator( // encode everything in Base64
TcpComm( // send using TCP
tcpModule
)
)
)
tcpBase64JsonComm.sendMessage(message) // sending message via TCP: eyJtZXNzYWdlIjoiaGVsbG8ifQ==
// different decorating order
val tcpJsonBase64Comm: Comm = Base46Decorator( // encode just the message with Base64
JsonDecorator( // put it in JSON structure
TcpComm( // send using TCP
tcpModule
)
)
)
tcpJsonBase64Comm.sendMessage(message) // sending message via TCP: {"message":"aGVsbG8="}
As you can see, the order of the decorators is of colossal importance. What exactly happened?
-
tcpBase64JsonComm
- wrap the message in a JSON structure
- encode everything with Base64
- Send via TCP
-
tcpJsonBase64Comm
- encode just the message with Base64
- wrap everything in JSON structure
- Send via TCP The deeper the decorator is, the later it will be called with the result of processing data from earlier decorators.
As I wrote earlier, one of the decorators could also throw an exception or not use the method from the decorated object at all and return a constant. This can be helpful, for example, when we want to obscure some data depending on where we send it. For example, application logs using the class
data class LogEntry (val timestamp: Timestamp, val tag: String, val message: String)
- if they are stored locally, they should contain all the information
- if they are sent to our website via HTTPS, only sensitive data should be obfuscated
- if for some external monitoring, we send a minimum of the information, maybe even just the TAG and timestamp.
There is no point in extending the implementation of logging or sending data with these functionalities. Decorators on LogEntry
can be created which, using internal logic, would clean up the message before its sent. Changing the logging policy or adding new aggregators will not cause the necessity to change the interfaces of the classes responsible for collecting or sending logs, but only the replacement of decorators.
It seems that instead of adding more classes and playing with delegates, you can add few extension methods
to the interface and thus prepare the message to be sent as JSON and/or Base64.
fun String.toJsonMessage(): String = "{\"message\":\"$this\"}"
fun String.toBase64(): String = Base64.getEncoder().encodeToString(this.toByteArray())
btComm.sendMessage(message.toBase64().toJsonMessage()) // sending message via BT: {"message":"aGVsbG8="}
But now it is the clients who need to know what form of a message to use, rather than using a decorated object with a generic interface that can be injected into them. Additionally, the extension method
can be implicitly overridden elsewhere in the project, so you can never be sure that calling toJsonMessage()
will produce the same result. I have already written about this in the context of the Adapter Pattern. In this case, I'm using the String
class, but if toJsonMessage()
would return some JSON object, the toBase64()
method would have to take this into account - when using decorators, the interface is always the same and the decorators don't need to know about each other.
Code using extension methods
is much simpler than using a series of decorators. However, if extension methods become used in multiple places and with multiple interfaces, maintaining them can be cumbersome, as well as testing. And if they will not be used anywhere else, then maybe the usual methods in the class will suffice instead of extension methods
. Although I will admit that I sometimes prefer the instance.extMethod()
syntax over extMethod(instance)
.
I usually don't like suffixes derived from design patterns in class names, but in the case of Decorator
it is good to have clear information about the purpose of the class. Although the class interface is the same as the object being decorated, an instance of 'Decorator' itself does not make sense, unlike the decorated object.
The Decorator
pattern is used where creating separate classes which are a combination of all possibilities would result in their explosion. This pattern focuses on creating object layers to transparently and dynamically complement objects with new tasks. The decorator provides an object with the same interface as the decorated object.
While it is possible to add new public methods in the decorator, it may encourage clients to cast up or use the interface of a concrete decorator instead of a general component. The main advantage of the Decorator is its transparency to the customer, which is achieved by using the generic interface of the wrapped component.
In the example with communication interfaces, if not for the decorator, you would probably need to extend clients with new functionalities or introduce all variations of message processing and the way of sending it in separate classes.
Kotlin allows you to elegantly create decorators with delegates. Extension methods also kind of decorate the original class with new features, but are not a replacement or alternative to this pattern. Instead, they are an alternative to using the simple Java Wrappers
, which add new public methods to objects that we don't want or can't extend.
-
changing object behavior without inheritance - inheritance is appropriate only in cases where the derived class is a subtype of the base class (relation on the principle of generalization-> specialization) - "Effective Java". You could probably find such a connection between the
String
and theTCP
packet, but it would be a bit of a stretch and inflexible. Especially adding JSON formatting and Base64 encoding along the way. It would also be difficult to re-use the code for a message over Bluetooth. By arranging layers, the decorator allows you to flexibly change the behavior of an object, without changing what the object is. - dynamic changes in the behavior of the object - the object can be decorated while the program is running because the new behaviors do not change the interface, but only fit into it. The same object can be decorated with one class in one place and with another in different one.
- SRP - a large monolithic class with multiple responsibilities, can be transformed into a set of decorators used only where they are needed. Unfortunately, this can also lead to upcasting to the decorator interface or calling an internal decorated object from the client (Demeter law violation).
-
the order in which the decorators are applied is important - example with sending a message. The order is important, and at the same time the decorators themselves don't know anything about other decorators. The responsibility for creating the correct set of decorators should rest with some, for example,
Factory
orBuilder
- as long as there is an extensive hierarchy.
22