Design Patterns in Kotlin: Advanced Level with Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Design Patterns in Kotlin with Examples: Advanced Level

At the initial level, developers master classic patterns: Singleton, Factory, Observer. However, when building complex, scalable systems, more subtle architectural tuning is required. In this article, we will analyze four advanced design patterns in Kotlin: Composite, Visitor, State, and Bridge. Each pattern will be illustrated with a practical code example and an explanation of when to apply it.



1. Composite Pattern

The Composite pattern allows clients to work uniformly with individual objects and their groups. This is an ideal solution for building hierarchical structures such as file systems, interface menus, or graphical scenes.

In Kotlin, we often use sealed classes to represent tree nodes. Consider an example of building a "Files and Folders" structure:

sealed class FileSystemNode(open val name: String) {    data class File(override val name: String, val size: Long) : FileSystemNode(name)    data class Directory(        override val name: String,        val children: MutableList<FileSystemNode> = mutableListOf()    ) : FileSystemNode(name) {        fun add(node: FileSystemNode) {            children.add(node)        }        fun remove(node: FileSystemNode) {            children.remove(node)        }        fun getTotalSize(): Long = children.sumOf {            when (it) {                is File -> it.size                is Directory -> it.getTotalSize()            }        }    }}

// Usagefun main() { val root = FileSystemNode.Directory("root") root.add(FileSystemNode.File("readme.txt", 1024)) val subDir = FileSystemNode.Directory("src") subDir.add(FileSystemNode.File("main.kt", 4096)) root.add(subDir) println("Total size: ${root.getTotalSize()} bytes") // 5120}

Pros: simplifies client code, supports recursive operations. Cons: can complicate the system if the tree is too deep.



2. Visitor Pattern

The Visitor pattern allows adding new operations to objects without changing their classes. This is especially useful when you have a stable class hierarchy but frequently changing processing algorithms.

In Kotlin, it works well with sealed classes. Let's implement an example of calculating area for shapes:

sealed class Shapeclass Circle(val radius: Double) : Shape()class Rectangle(val width: Double, val height: Double) : Shape()

interface ShapeVisitor<T> { fun visit(circle: Circle): T fun visit(rectangle: Rectangle): T}

class AreaCalculator : ShapeVisitor<Double> { override fun visit(circle: Circle) = Math.PI * circle.radius * circle.radius override fun visit(rectangle: Rectangle) = rectangle.width * rectangle.height}

// Usagefun <T> Shape.accept(visitor: ShapeVisitor<T>): T = when (this) { is Circle -> visitor.visit(this) is Rectangle -> visitor.visit(this)}

fun main() { val shapes = listOf(Circle(5.0), Rectangle(3.0, 4.0)) val calculator = AreaCalculator() shapes.forEach { println("Area: ${it.accept(calculator)}") }}

When to apply: if the object structure rarely changes, but operations on them change frequently. In Kotlin, you can use when for simple cases, but Visitor remains indispensable for complex logic with many types.



3. State Pattern

The State pattern allows an object to change its behavior depending on its internal state. This is an elegant alternative to cumbersome if-else or switch-case statements.

In Kotlin, we can use sealed classes to represent states. Example — an order in an online store

Blogs

Book Recommendations