Design Patterns in Kotlin: Behavioral Patterns

Gang of Four’s Behavioral Patterns describe the way of communications between objects.

1. Chain of Responsibility

Now in one of the car parts factories, there might be one or more assembly lines. When an order arrives, the factory needs to dispatch operations to one of the assembly lines.

Instead of iterating over all the assembly lines each time, we can use the chain of responsibility pattern, where each assembly line will try to fulfill the request, or further forward it to its successor.

We can design the classes like below:

class ChainedAssemblyLine(
    private val nextAssemblyLine: ChainedAssemblyLine?
) : AssemblyLine {
    override fun doOpeationA() {
        if (!canHandleOperationA()) {
            nextAssemblyLine
                ?.doOpeationA() // forward the request
                ?:throw IllegalStateException() // throw if no successor
        }

        // execute the operation
    }

    ...
}

// the factory can act as if there is just one assembly line
class CarPartsFactoryA : CarPartsFactory {
    private assemblyLine: AssemblyLine = ...

    override fun produceTire(): Tire {
        assemblyLine.doOperationA()

        ...
    }
    
    ...
}

2. Command

Suppose the assembly line needs to provide transactional support that when one operation fails, the others shall be rolled back. However, only the car parts factory knows which operations should be grouped to form a transaction.

The command pattern can be used in this case to encapsulate the actions.

The classes can be extended as below:

interface AssemblyLine {
    fun doTransaction(block: AssemblyLine.() -> Unit)

    ...
}

class CarPartsFactoryA : CarPartsFactory {
    private assemblyLine: AssemblyLine = ...

    override fun produceTire(): Tire {
        // run the transaction here
        assemblyLine.doTransaction {
           doOperationA()
           doOperationB()
           ...
        }

        ...
    }
    
    ...
}

3. Interpreter

Now suppose the car parts factory wants to try different assembly line operations to make tires. One way to achieve this is to define a simple language so that the operators can specify the operations to be carried out.

The interpreter pattern provides a way to support this use case.

4. Iterator

Remember the CompositeAssemblyLine class that holds a list of assembly lines? Now that a car parts factory wants to iterate through the assembly lines, but the composite assembly line doesn’t want to expose how those lines are represented internally.

To achieve this, we can use the iterator pattern like below:

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

    fun assemblyLinesIterator(): Iterator<AssemblyLine>
        = subAssemblyLines.iterator()

    ...
}

class CarPartsFactoryA: CarPartsFactory() {
    private val assemblyLine: CompositeAssemblyLine = ...

    private fun randomFunction() {
        // the client can easily iterate, and doesn't need to
        // know anything of the internal representations
        assemblyLine.assemblyLinesIterator.forEach {
            ...
        }
    }

    ...
}

5. Mediator

Now that the car manufacturer is improving its car parts factories that they can talk to each other. However, this is creating a many-to-many relationships, which are difficult to understand and maintain.

To solve this problem, we can use the mediator pattern to simplify the interactions among the objects.

We can update the manufacturer as the mediator, taking care managing the interactions:

class CarManufacturer {
    private val carPartsFactories: List<CarPartsFactory> = ...

    fun requestOperationA() {
        // forwards the message to a peer
        carPartsFactories.firstOrNull()?.doOperationA()
    }

    ...
}

class CarPartsFactoryA: CarPartsFactory() {
    private val manufacturer: CarManufacturer = ...

    private fun randomFunction() {
        // instead of directly talking to other peers, it talks
        // to the car manufacturer (mediator)
        manufacturer.requestOperationA()
    }
}

6. Memento

For the assembly line, one way to support rollback when any operation fails during a transaction, is to remember the state before the transaction happens, and revert the object back to that state.

The memento pattern provides such an ability.

We can design the assembly line and related classes like below:

abstract class AssemblyLine {
    private data class State(...)

    // ignored synchronization for the sake of simplicity
    fun doTransaction(block: () -> Unit) {
        // records the internal state
        val state: State = currentState()
        try {
            block()
        } catch (e: Exception) {
            // restores to the recorded state when error happens
            restoreState(state)
            throw e
        }
    }

    ...
}

7. Observer

Now suppose the car parts factory needs to send some messages to its assembly lines every now and then. The observer pattern can be used in this case.

interface AssemblyLine {
    fun onEvent(event: Event)

    ...
}

class CarPartsFactoryA : CarPartsFactory {
    private assemblyLines: List<AssemblyLine> = ...

    private fun notifyAssemblyLines(event: Event) {
        assemblyLines.forEach { it.onEvent(event) }
    }
    
    ...
}

8. State

Now that one of the assembly lines behaves differently when its internal state is changed.

Instead of writing lots if if-else or when statements in different places, we can also use the state pattern. It creates different classes to be used in different states, e.g.:

class StatefulAssemblyLine : AssemblyLine {
    private var assemblyLine: State = State.Uninitialized()

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

    private fun updateState(state: State) {
        // this will be the only place where we have conditional statements
        when (state) {
            is State.StateA: assemblyLine = State.StateA()
            ...
        }
    }

    private sealed class State: AssemblyLine {
        class Uninitialized : State { ... }
        class StateA: State { ... }
    }

    ....
}

9. Strategy

The car manufacturer has developed different algorithms to produce tires in different scenarios. The strategy pattern provides a way to select the algorithm flexibly at runtime.

abstract AbstractCarPartsFactory : CarPartsFactory { ... }

class CarPartsFactoryA : AbstractCarPartsFactory {
    override fun produceTire(): Tire { ... }
}

class CarPartsFactoryB : AbstractCarPartsFactory {
    override fun produceTire(): Tire { ... }
}

// the client needs to specify the car parts factory to be used
class CarManufacturerA(
    private val carPartsFactory: CarPartsFactory
) : CarManufacturer { ... }

Alternatively, we can use higher-order functions instead of subclassing:

class StrategyCarPartsFactory(private val createTire: () -> Tire) : CarPartsFactory {
    override fun produceTire(): Tire = createTire()
}

10. Template Method

Now that one of the algorithms the car manufacturer developed can be split into several parts, some are identical, and some are different.

The template method pattern can be used here. It provides a way to vary part of an algorithm.

// the base class implements the template for the algorithm
abstract AbstractCarPartsFactory : CarPartsFactory {
    override fun produceTire(): Tire {
        commonPart1()
        differentPart1()
        differentPart2()
        commonPart2()
        differentPart3()
    }

    abstract fun differentPart1()
    abstract fun differentPart2()
    abstract fun differentPart3()

    ...
}

class CarPartsFactoryA : AbstractCarPartsFactory {
    override fun differentPart1() { ... }
    override fun differentPart2() { ... }
    override fun differentPart3() { ... }
}

class CarPartsFactoryB : AbstractCarPartsFactory {
    override fun differentPart1() { ... }
    override fun differentPart2() { ... }
    override fun differentPart3() { ... }
}

Alternatively, we can use higher-order functions instead of subclassing:

class TemplateCarPartsFactory(
    private val differentPart1: () -> Unit,
    private val differentPart2: () -> Unit,
    private val differentPart3s: () -> Unit
) : CarPartsFactory {
    override fun produceTire(): Tire {
        commonPart1()
        differentPart1()
        differentPart2()
        commonPart2()
        differentPart3()
    }
}

11. Visitor

Now suppose that after a car is fully assembled, the car manufacturer needs to go through each of the car part, run some final testing, and potentially do some other operations in the future.

In this case, the visitor pattern can be used to separate the algorithm from the object structure on which it operates.

We can design the classes like below:

interface CarElementVisitor {
    fun visit(carElement: CarElement)
}

interface CarElement {
    fun accept(carElementVisitor: CarElementVisitor)
}

class Tire: CarElement {
    private val subElements: List<CarElement> = ...

    override fun accept(carElementVisitor: CarElementVisitor) {
        // each element makes sure sub-elements are visited
        subElements { it.accept(carElementVisitor) }

        // also itself is visited
        carElementVisitor.visit(this)
    }
}

class Engine: CarElement {
    override fun accept(carElementVisitor: CarElementVisitor) { ... }
}

// makes it easy for the client to use
class Car: CarElement {
    private val elements: List<CarElement> = ...

    override fun accept(carElementVisitor: CarElementVisitor) {
        elements { it.accept(carElementVisitor) }
        carElementVisitor.visit(this)
    }
}

// the client can use it like below
car.accept(object : CarElementVisitor {
    override fun visit(carElement: CarElement) {
        ...
    }
})


See also

comments powered by Disqus