Design Patterns in Kotlin: Structural Patterns

Gang of Four’s Structural Patterns describe the way of composing classes and objects to form larger structures.

1. Adapter

Let’s continue with the car manufacturer example from the Creational Patterns. Assume now we have a ThirdPartyCarPartsFactory that doesn’t implements CarPartsFactory, but instead looks like this:

class ThirdPartyCarPartsFactory {
    fun createTire(): ThirdPartyTire = ThirdPartyTire()
    fun createEngine(): ThirdPartyEngine = ThirdPartyEngine()
}

The adapter pattern can be used here to solve the problem. It wraps the third party factory, so that we can continue using the existing buildCar() function.

class ThirdPartyCarPartsFactoryAdapter : CarPartsFactory {
    private val factory = ThirdPartyCarPartsFactory()

    override fun produceTire(): Tire {
        val thirdPartyTire = factory.createTire()
        val tire = ... // convert from thirdPartyTire
        return tire
    }

    override fun produceEngine(): Engine {
        val thirdPartyEngine = factory. createEngine()
        val engine = ... // convert from thirdPartyEngine
        return engine
    }
}

2. Bridge

Now let’s take a deeper look at CarPartFactory. In each factory, there are multiple assembly line types like below:

interface AssemblyLine {
    fun doOperationA()
    fun doOperationB()
    fun doOperationC()
}

class AssemblyLineOne : AssemblyLine { ... }
class AssemblyLineTwo : AssemblyLine { ... }

We want to avoid a permanent binding between a factory and a specific type of assembly line, especially that new assembly lines might be added in the future.

To solve this problem, we can use the bridge pattern, which decouples an abstraction (typically providing higher-level operations) from its implementation (usually only primitive operations), such that the two can evolve independently.

We can refactor the car part factories to be something like below:

abstract class CarPartsFactory {
    // to obtain an AssemblyLine instance is out of the scope 
    // for this pattern, we can use an abstract factory or any
    // other appropriate approach
    protected val assemblyLine: AssemblyLine = ...

    abstract fun produceTire(): Tire
    abstract fun produceEngine(): Engine

    protected fun doSomeHigherLevelOperation() {
        assemblyLine.doOperationA()
        assemblyLine.doOperationB()
        assemblyLine.doOperationA()
    }
}

class CarPartsFactoryA : CarPartsFactory {
    override fun produceTire(): Tire {
        assemblyLine.doOperationC()
        doSomeHigherLevelOperation
        val tire = ...
        return tire
    }
    
    ...
}

3. Composite

Now a car parts factory builds a brand new assembly line, which consists of several assembly lines. For good reasons, the car parts factory doesn’t want to add new logics to support using it.

The composite pattern can be used to solve this problem. It lets clients treat a group of instances the same way as a single instance.

We can define the new assembly line like this:

class CompositeAssemblyLine: AssemblyLine {
    private subAssemblyLines: List<AssemblyLine> = ...

    override fun doOperationA() {
        getNextIdleAssemblyLine().doOperationA()
    }

    private fun getNextIdleAssemblyLine(): AssemblyLine { ... }

    ...
}

4. Decorator

Now the car manufactories needs to add some new labels to the engines they produce, but this addition is likely temporary, so they don’t want to modify the CarPartsFactory they use and want to make it still easy to use.

The decorator pattern can be used in this case, to add or modify behaviors of an object dynamically.

We can introduce a new decorator car parts factory like below:

class DecoratorCarPartsFactory : CarPartsFactory(private val factory: CarPartsFactory) {
    override fun produceTire(): Tire = factory.produceTire()

    override fun produceEngine(): Engine {
        val engine = factory.produceEngine()
        // adds the labels
        return engine
    }
}

5. Facade

Think about the subsystem that produces engines, which is a very complicated subsystem with many components. It is very difficult and also tedious for the car manufacturer to use, especially when the manufacturer doesn’t need to make any customizations.

The facade pattern is used to hide all the complicated details, so that the client only needs to call one single higher-level function, e.g.:

class CarPartsFactoryA : CarPartsFactory {
    override fun produceEngine(): Engine {
        // utilize different assembly lines to produce an engine
    }

    ...
}

// the client just needs to call one function
carPartsFactory.produceEngine()

6. Flyweight

In the car parts factory, there are countless tire studs to be used. It will take way too much memory, if we always create a new instance for each tire stud.

The flyweight pattern provides a way to efficiently use a large number of objects by sharing as much as possible.

We can design the tire stud like below:

enum class TireStudType { ... }
class TireStud { ... }

class TireStudFactory {
    private val cache = mutableMapOf<TireStudType, TireStud>()

    fun getTireStud(type: TireStudType): TireStud = mutableMapOf.getOrElse(type) {
        createTire(type).also { cache[type] = it }
    }
}

Then the client, e.g. a tire assembly line, can get a reference to a tire stud through the factory, and use it to assemble a tire.

7. Proxy

Now in the car parts factory, there are two different assembly lines, one for tires, and the other for engines. However, it’s very expensive to create the one for engines, the factory doesn’t always need it, and it can only be used when certain conditions are met.

To make the object accessing easy, we can use the proxy pattern like below:

class EngineAssemblyLine : AssemblyLine { ... }


class EngineAssemblyLineProxy : AssemblyLine {
    private val engineAssemblyLine: EngineAssemblyLine by lazy { EngineAssemblyLine() }

    override fun doOperationA() {
        if (!conditionsMet()) throw IllegalStateException()
        engineAssemblyLine.doOperationA()
    }
}

// the client uses the proxy for access
class CarPartsFactoryA : CarPartsFactory {
    private val engineAssemblyLine: AssemblyLine = EngineAssemblyLineProxy()

    override fun produceEngine(): Engine {
        // uses engineAssemblyLine to produce engines
    }

    ...
}


See also

comments powered by Disqus