Kotlin Design Patterns: Using Cool Features To Simplify Other Design Patterns

Originally posted to my blog, at: https://fugisawa.com/kotlin-design-patterns-using-cool-features-to-simplify-other-design-patterns/

Published on
7 read
Kotlin Design Patterns: Using Cool Features To Simplify Other Design Patterns

In the previous chapters, we’ve seen cases where Kotlin’s features can profoundly alter traditional design patterns to a simpler, more concise and expressive solution.

In this final article of the series, we'll explore how Kotlin's capabilities can help simplify other patterns. The changes and impact shown here can seem to be not so expressive as those previously shown. But using those features can really help declutter patterns implementation and make them less complex, more readable and mantainable.

Kotlin Features and Characteristics Enhancing General Design Patterns

Conciseness and Readability

Kotlin's syntax is designed to be concise and expressive. This reduces boilerplate code, making implementations of design patterns more readable and maintainable.

Null Safety:

Kotlin's type system is designed to eliminate the null pointer exceptions. This feature can simplify implementations of patterns that involve object creation and interaction, reducing the need for extensive null checks.

Extension Functions:

These allow adding new methods to existing classes without modifying them. Extension functions can be particularly useful to replace patterns like Decorator with a simpler solution, as they can add functionalities to objects without inheritance.

Check this example:

// Base interface

interface Coffee {

    fun getCost(): Double

    fun getDescription(): String

}
// Concrete implementation

class SimpleCoffee : Coffee {

    override fun getCost() = 10.0

    override fun getDescription() = "Simple Coffee"

}
// Extension functions as decorators

fun Coffee.withMilk() = object : Coffee {

    override fun getCost() = this@withMilk.getCost() + 2

    override fun getDescription() = "${this@withMilk.getDescription()}, Milk"

}

fun Coffee.withSugar() = object : Coffee {

    override fun getCost() = this@withSugar.getCost() + 1

    override fun getDescription() = "${this@withSugar.getDescription()}, Sugar"

}

// Using the extension functions

fun main() {

    val myCoffee: Coffee = SimpleCoffee()

    val myCoffeeWithMilk = myCoffee.withMilk()

    val myCoffeeWithMilkAndSugar = myCoffeeWithMilk.withSugar()

    println(myCoffeeWithMilkAndSugar.getDescription())  // Output: Simple Coffee, Milk, Sugar

}

In this example, withMilk and withSugar are extension functions applied to the Coffee interface. They effectively "decorate" the Coffee object with additional features, mimicking the Decorator Pattern, but without the need for explicit subclassing or interface implementation.

Data Classes:

With automatically generated getters, setters, equals()hashCode()toString()copy(), data classes are ideal for patterns like Builder and Prototype, simplifying the creation and management of complex data objects. Many patterns can slightly benefit from data classes. Some examples are:

  • The copy method of a data class makes it easy to create a memento that represents a snapshot of an object's state.

  • When different strategies can be represented as data, data classes can be used to encapsulate the varying parts of the algorithm.

  • Data classes are perfect for creating DTOs, as they often just carry data and require simple boilerplate methods like equalshashCode, and toString.

Property Delegation and Class Delegation:

Kotlin allows delegating the implementation of a class or property to a separate class. This feature can be used in patterns like Strategy, Proxy or Adapter to delegate certain functionalities to different objects. The example below shows an ideia of a Strategy-like solution that makes use of Kotlin class delegation to delegate the implementation of the sort(list: List<Int>) method to the constructor argument strategy (this is done through by strategy):

// Strategy Interface
interface SortingStrategy {

    fun sort(list: List<Int>): List<Int>

}

// Concrete Strategies

class AscendingSort : SortingStrategy {

    override fun sort(list: List<Int>) = list.sorted()

}

class DescendingSort : SortingStrategy {

    override fun sort(list: List<Int>) = list.sortedDescending()

}

// Context Class with Delegation

class SortedList(private val strategy: SortingStrategy) : SortingStrategy by strategy

fun main() {
    val numbers = listOf(3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5)

    val ascendingSorted = SortedList(AscendingSort())

    println("Ascending: ${ascendingSorted.sort(numbers)}")

    val descendingSorted = SortedList(DescendingSort())

    println("Descending: ${descendingSorted.sort(numbers)}")

}

In this example, SortedList delegates the sorting behavior to a SortingStrategy. The actual sorting algorithm can be changed at runtime by providing different strategy implementations, showcasing the flexibility of the Strategy Pattern achieved through class delegation.

Higher-Order Functions and Lambdas:

These features are essential for functional programming in Kotlin and are useful in simplifying patterns like Strategy, Command, or Template Method by replacing interfaces implementations with lambda expressions.

In the following example, the Context class takes a strategy as a higher-order function. We define different strategies (lowercase and uppercase) as lambda expressions and pass them to the context. This approach allows for a flexible and concise way to change the behavior of the executeStrategy method.

// Strategy interface using higher-order function
class Context(private val strategy: (String) -> String) {

    fun executeStrategy(text: String): String = strategy(text)

}

fun main() {

    // Defining strategies as lambda expressions

    val lowercaseStrategy = { text: String -> text.lowercase() }

    val uppercaseStrategy = { text: String -> text.uppercase() }



    // Using different strategies

    val contextLower = Context(lowercaseStrategy)

    println(contextLower.executeStrategy("Kotlin IS Awesome!")) // Output: kotlin is awesome!

    val contextUpper = Context(uppercaseStrategy)

    println(contextUpper.executeStrategy("Kotlin IS Awesome!")) // Output: KOTLIN IS AWESOME!

}

Domain-Specific Languages (DSLs):

Kotlin's ability to create internal DSLs can simplify complex configurations and setups, potentially reducing the need for some creational and structural patterns.

In the following example, we'll create a DSL to build a tree-like structure of graphical objects. The Composite Pattern will be used to treat individual objects (leaf nodes) and compositions of objects (composite nodes) uniformly.

Each Composite will include several Point or Composite instances, showcasing how Kotlin DSLs can elegantly manage collections of elements within each composite.

(If you prefer a more practical view of how this Kotlin DSL works, feel free to skip straight to the main() function. There you can see the usage and how it simplifies the construction of complex hierarchical structures.)

// Component Interface
interface Graphic {
    fun draw()

}

// Leaf

class Point(private val x: Int, private val y: Int) : Graphic {

    override fun draw() = println("Drawing point at ($x, $y)")

}

// Composite

class CompositeGraphic(val name: String) : Graphic {

    private val children = mutableListOf<Graphic>()

    fun graphic(graphic: Graphic) = children.add(graphic)

    override fun draw() {

        println("Drawing Composite: $name")

        children.forEach(Graphic::draw)

    }

}

// DSL Builder for CompositeGraphic

fun composite(name: String, init: CompositeGraphic.() -> Unit): CompositeGraphic = 

 CompositeGraphic(name).apply(init)

fun CompositeGraphic.point(x: Int, y: Int) = graphic(Point(x, y))

fun main() {

    composite("Root") {

        point(1, 2)

        point(2, 3)

        composite("Child 1") {

            point(4, 5)

            point(5, 6)

        }

        point(3, 4)        

        composite("Child 2") {

            point(6, 7)

            point(7, 8)

            point(8, 9)

        }

    }.draw()

}

In this implementation, the composite function is a DSL for creating CompositeGraphic objects. Within this DSL, we can easily nest composites within one another, mimicking a tree structure. This approach allows for a clean and intuitive setup of complex hierarchical structures.

Final Thoughts

Kotlin's emphasis on conciseness, readability and pragmatism principles simplifies the implementation of traditional design patterns and also enhances their flexibility and maintainability.

It's not just about writing less code: it's about writing more expressive, safer, and more maintainable code. As developers and architects, embracing these features can lead to more enjoyable, cleaner and efficient development experiences.

Wrapping Up Our Kotlin Design Patterns Series

We've come to the end of our journey exploring how Kotlin can simplify traditional design patterns. Throughout this series, you've seen how Kotlin's unique features can make your code more concise, readable, and enjoyable to write. We've tackled complex patterns together, turning them into elegant and practical solutions.

As you continue coding in Kotlin, remember the insights from this series. Experiment with these patterns in your projects, embrace the simplicity Kotlin offers, and enjoy the process of crafting cleaner, more efficient code. Keep exploring and pushing the boundaries of what you can achieve with Kotlin's powerful features. Let's keep innovating and evolving our coding practices together! 🚀🎉

--

This article was originally posted to my Lucas Fugisawa on Kotlin blog, at: https://fugisawa.com/kotlin-design-patterns-using-cool-features-to-simplify-other-design-patterns/

To explore more about Kotlin-related topics, subscribe to my newsletter on https://fugisawa.com/ and stay tuned for more insights and updates.

Author
Lucas Fugisawa
Lucas Fugisawa

-- Lucas Fugisawa

Discussion (0)

Subscribe