RizTech Academy logo
RizTech Academy
Design Patterns, the Kotlin WayLesson 3 of 530 min

Adapter, decorator and delegation with extensions and by

Structural patterns are about how objects are composed — how you build bigger things out of smaller ones, and adapt or extend behaviour. The two you meet most, Adapter and Decorator, plus the idea of delegation, show Kotlin at its best: extension functions and the by keyword replace what would otherwise be layers of wrapper classes.

Adapter — making one interface fit another

The problem: you have an object with one interface, and you need it to fit a different interface that some other code expects. The classic example: a third-party library returns data in one shape, and your code wants it in another. An Adapter sits between them and translates.

The Java pattern is a wrapper class that implements the target interface and forwards to the adaptee. In Kotlin, an extension function is very often all you need (the functions module):

// a class you do not own, from some library
class LegacyTemperature(val fahrenheit: Double)

// your code wants Celsius — adapt with an extension, no wrapper class
fun LegacyTemperature.toCelsius(): Double = (fahrenheit - 32) * 5 / 9

val reading = LegacyTemperature(98.6)
println(reading.toCelsius())      // 37.0  — adapted, in one line

You did not write a TemperatureAdapter class wrapping the legacy object; you added the method you needed directly. For adapting a single method or a small translation, an extension function is the Kotlin Adapter — lighter, and the adapted call reads as if the method always existed. (For adapting a whole rich interface, a real wrapper class is still right; but the common small case is an extension.)

Decorator — adding behaviour without changing the original

The problem: you want to add behaviour to an object — logging, caching, validation — without modifying its class and without subclassing. A Decorator wraps the object, adding its behaviour before or after delegating to the wrapped one.

The classic form is a wrapper class that holds the original and forwards to it, adding a little. In Kotlin, extension functions handle the simple cases, and for wrapping a whole interface, the by keyword makes it almost free — which brings us to delegation.

Delegation with by — Kotlin's structural superpower

The problem behind several structural patterns: you want a class to have the behaviour of some interface, but implement most of it by handing off ("delegating") to another object that already does it — composition instead of inheritance (the interfaces module's advice).

In Java, delegating an interface means writing a forwarding method for every member — tedious boilerplate. Kotlin has by, which generates all that forwarding for you:

interface Repository {
    fun find(id: Int): String?
    fun all(): List<String>
}

class InMemoryRepository : Repository {
    private val data = mapOf(1 to "Kavita", 2 to "Ravi")
    override fun find(id: Int) = data[id]
    override fun all() = data.values.toList()
}

// a decorator that adds logging — delegates everything to `base` via `by`,
// and overrides only the one method it wants to change
class LoggingRepository(private val base: Repository) : Repository by base {
    override fun find(id: Int): String? {
        println("Looking up $id")
        return base.find(id)          // add behaviour, then delegate
    }
    // all() is NOT written — `by base` forwards it automatically
}

val repo = LoggingRepository(InMemoryRepository())
println(repo.find(1))     // prints "Looking up 1", then Kavita
println(repo.all())       // works, forwarded to base with no code written

Read what : Repository by base does: LoggingRepository is a Repository, and every method it does not override is automatically forwarded to base. You wrote find (to add logging) and got all for free. In Java this decorator would need a forwarding method for every member of the interface; Kotlin's by generates them. This is composition-over-inheritance made effortless — the Decorator and Adapter patterns, and the general principle the interfaces module preached, all supported by one keyword.

Why this matters beyond the patterns

The by delegation feature is worth internalising as more than a pattern trick. Recall the interfaces module's guidance: prefer composition to inheritance. The reason people reach for inheritance despite its fragility is often that composition used to be more work — you had to forward all those methods by hand. Kotlin removes that cost. When composition is as cheap as inheritance, you can make the right choice (composition) without paying a penalty for it. That is the deeper value: by does not just implement the Decorator pattern, it makes the healthier design habit the easy one.

Property delegation, briefly

by also works for properties, delegating how a property is stored or computed. You will meet this in Android constantly, even if you rarely write your own:

val lazyValue: String by lazy {
    println("Computed once")
    "the value"          // the block runs only on first access, then caches
}

by lazy { } delegates the property to a lazy object that computes the value on first access and caches it — a common, useful idiom. by is one keyword powering delegation for both interfaces and properties, and it is a distinctly Kotlin tool with no direct Java equivalent.

The scoreboard

Pattern / idea Java Kotlin
Adapter (small case) Wrapper class Extension function
Decorator Wrapper forwarding every method by delegation + override the one method
Composition over inheritance Manual forwarding boilerplate by (generated forwarding)
Lazy / computed property Getter with a cached field by lazy and property delegates

The pattern of the module holds: the structural patterns are about composing behaviour, and Kotlin's extensions and by make composition so cheap that the heavyweight wrapper-class versions are rarely needed.

Check your work

What an Adapter does, and Kotlin's light form. Makes one interface fit another; an extension function for the small, single-method case (a wrapper class for a whole rich interface).

What a Decorator does. Adds behaviour (logging, caching) by wrapping an object and delegating to it, without modifying or subclassing it.

What : Interface by base generates. Automatic forwarding of every interface method to base, so you override only the ones you want to change.

Why by matters beyond the pattern. It makes composition as cheap as inheritance, so you can follow "prefer composition" without paying a boilerplate penalty.

What property delegation with by does. Delegates how a property is stored or computed — e.g. by lazy { } computes once on first access and caches.

The overall scoreboard. Adapter → extension; Decorator/composition → by; lazy property → by lazy.

Practice

  1. Adapt a class you "do not own" (LegacyTemperature) to a method you want (toCelsius) with an extension function. Confirm the call reads naturally.
  2. Write a Repository interface and an InMemoryRepository. Then write a LoggingRepository using by base, overriding only find. Confirm all() works without writing it.
  3. Remove the by base and try to compile — note you now must implement every method. Add it back.
  4. Add a second decorator (CachingRepository) the same way and stack them: LoggingRepository(CachingRepository(InMemoryRepository())).
  5. Use by lazy { } for a property that prints when computed. Access it twice and confirm the block ran only once.
  6. Take a design where you were about to use inheritance and rewrite it with by delegation. Decide which is clearer.

Official documentation

Next: behavioural patterns — Strategy, Observer and State with lambdas, Flow and sealed classes.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship