Blog Infos
Author
Published
Topics
, , , ,
Published
When to use?

When an object needs to have behavior added and removed dynamically at Runtime, it’s also an alternative to extending classes by choosing Composition over Inheritance. It’s also commonly named Wrapper, as it says more about what it does in reality.

Decorator Class Diagram

As you can see in the diagram, there is a Component class, which is the default behaviour implementation, but there also are DecoratorA and DecoratorB , which both implement Decorator and aggregate on it. These are the additional behaviour classes that usually can’t work on their own and need additional Decorator , these are things like Logging, Caching, Compressing etc.

An example should make everything clearer:

Example

You’re writing a generic class that handles Request and Response from a server. You’ll want to add Logging behaviour in the Debug build and Caching behaviour depending on request(you don’t want to cache user email and password after all).

Example class diagram

 

In our example ResponseReader will be the Component meaning it will be able to read the responses while CacheDecorator and LoggerDecorator can’t work on their own so they need to aggregate on Reader .

Let’s start by coding our Reader along with Request and Response

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

data class Request(val data: String)
data class Response(val body: String)

interface Reader {
    fun read(request: Request): Response
}

Now, let’s create a ResponseReader that will be our primary handler for the Requests and Responses.

class ResponseReader : Reader {
    override fun read(request: Request): Response {
        // TODO get the real response by request
        return Response("success")
    }
}

Because it’s an essential class that’ll be used in every, Reader we don’t want to pass another Reader to it in the constructor. It’s a special case. Now let’s add Cache and Logger :

class CacheReader(
    private val reader: Reader,
) : Reader {
    private val cache = mutableMapOf<Request, Response>()

    override fun read(request: Request): Response {
        val response = cache[request]
        return if (response != null) {
            response
        } else {
            val readerResponse = reader.read(request)
            cache[request] = readerResponse
            readerResponse
        }
    }
}

class LoggerReader(private val reader: Reader) : Reader {
    override fun read(request: Request): Response {
        val response = reader.read(request)
        // TODO use logger of some kind instead of println in real project
        println("Request: $request || Response: $response")
        return response
    }
}

These 2 classes, on the other hand, can’t work by themself. They need next Reader to work correctly. So it’s added in the constructor. You might want it Reader to be nullable in cases where the Reader can handle it by itself, but it needs some functionality in edge cases.

How is it used in practice?
fun main() {
    val responseReader: Reader = ResponseReader()
    val logReader: Reader = LoggerReader(responseReader)
    val cacheReader: Reader = CacheReader(logReader)

    val request = Request("example")

    responseReader.read(request) // Nothing is printed
    logReader.read(request) // Request: Request(data=example) || Response: Response(body=success)
    // cacheReader will also log the data because LoggerReader is passed in its constructor
    cacheReader.read(request) // Request: Request(data=example) || Response: Response(body=success)
   
    // All of the returned responses are identical
}

As you can see, adding or removing behaviours in your project is a very flexible way.

Advantages
  • Combining multiple behaviours
  • Composition over Inheritance
  • Adding and removing behaviours at Runtime
Disadvantages
  • It’s hard to write a Decorator where the order of instances in Stack doesn’t matter.
  • The initialization code will look ugly.
More design patterns

https://medium.com/@michalankiersztajn/list/design-patterns-in-kotlin-12e52466affe?source=post_page—–8b3dd541f45b——————————–

Based on the book:

“Wzorce projektowe : elementy oprogramowania obiektowego wielokrotnego użytku” — Erich Gamma Autor; Janusz Jabłonowski (Translator); Grady Booch (Introduction author); Richard Helm (Author); Ralph Johnson (Author); John M Vlissides (Author)

This article is previously published on proandroiddev.com

 

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu