πŸš€ Β· Delegating interface implementation βš™οΈ

7 min read Β· Updated on by

Learn about delegation β€” an introduction to composition, the delegation pattern, and how Kotlin makes both of them easy, with non-trivial examples.

One of the problems with composition (which you should prefer over inheritance) is the verbosity related to accessing the underlying interfaces.

For example, consider the following FileSystemManager and DatabaseManager interfaces:


//sampleStart
data class Row(val id: Int, val cols: List<String>)

interface FileSystemManager {
    fun read(file: String): String?
    fun write(file: String, contents: String): Boolean
}

interface DatabaseManager {
    fun insert(row: Row): Boolean
    fun select(id: Int): Row?
}
//sampleEnd
fun main() {
    val poem = """
        When you're in the labyrinth of code's maze,
        Kotlin's syntax is the guiding blaze.
        With paths and twists, a journey so vast,
        In the coding labyrinth, it's steadfast!
    """.trimIndent()
    println(poem)
}

Now imagine that we want to create a class that is able to implement exporting and importing, which requires interaction with both the file system and the database:


data class Row(val id: Int, val cols: List<String>)

interface FileSystemManager {
    fun read(file: String): String?
    fun write(file: String, contents: String): Boolean
}

interface DatabaseManager {
    fun insert(row: Row): Boolean
    fun select(id: Int): Row?
}

//sampleStart
interface DbImportExportManager {
    fun import(id: Int, file: String)
    fun export(id: Int, file: String)
}
 
class DbImportExportManagerImpl(
    private val colSep: String,
    private val fsMan: FileSystemManager,
    private val dbMan: DatabaseManager
) : DbImportExportManager {
  
    override fun import(id: Int, file: String) {
        when(val contents = fsMan.read(file)) {
            is String -> dbMan.insert(Row(id, contents.split(colSep)))
            else -> false
        }
    }

    override fun export(id: Int, file: String) {
        when(val row = dbMan.select(id)) {
            is Row -> fsMan.write(file, row.cols.joinToString(colSep))
            else -> null
        }
    }
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the conductor in code's symphony,
        With extensions and functions, a harmonious glee.
        From notes to rhythms, in a coding trance,
        In the world of programming, it's a dance!
    """.trimIndent()
    println(poem)
}

So far, so good.

However, since DbImportExportManagerImpl contains references to both a FileSystemManager and a DatabaseManager, it only makes sense that it should be able to take on those interfaces as well, right?

Unfortunately, doing that is pretty tedious. We basically have two options:

  • Expose the underlying objects and access them directly. That defeats the whole purpose of composition, because the container object doesn’t implement the corresponding interfaces, and so all we’ve really done is just added a layer that achieves nothing. Adding insult to injury, we need to specify the property every time we want to call a given method.
  • Have the container object implement the interfaces of the objects it contains, and reimplement every single method by delegating it to the underlying object (see bellow). This is called the Delegation pattern.

Take a look at DbFsBridge (a renamed version of DbImportExportManagerImpl) in the following example, and notice how many lines are wasted on boilerplate delegation to the underlying objects:


data class Row(val id: Int, val cols: List<String>)

interface FileSystemManager {
    fun read(file: String): String?
    fun write(file: String, contents: String): Boolean
}

interface DatabaseManager {
    fun insert(row: Row): Boolean
    fun select(id: Int): Row?
}

interface DbImportExportManager {
    fun import(id: Int, file: String)
    fun export(id: Int, file: String)
}

//sampleStart
class DbFsBridge(
    private val colSep: String,
    private val fsMan: FileSystemManager,
    private val dbMan: DatabaseManager
) : FileSystemManager, DatabaseManager, DbImportExportManager {
  
    override fun import(id: Int, file: String) {
        when(val contents = fsMan.read(file)) {
            is String -> dbMan.insert(Row(id, contents.split(colSep)))
            else -> false
        }
    }

    override fun export(id: Int, file: String) {
        when(val row = dbMan.select(id)) {
            is Row -> fsMan.write(file, row.cols.joinToString(colSep))
            else -> null
        }
    }

    override fun read(file: String) = fsMan.read(file)
    override fun write(file: String, contents: String) = fsMan.write(file, contents)
    override fun insert(row: Row) = dbMan.insert(row)
    override fun select(id: Int) = dbMan.select(id)
}
//sampleEnd
fun main() {
    val poem = """
        In the tapestry of code, Kotlin's the thread,
        With extension properties, it's widely spread.
        From weaves to patterns, a design so clear,
        In the coding fabric, it's always near!
    """.trimIndent()
    println(poem)
}

That sux ballz, so to save us from this, Kotlin allows delegating interface implementations to object instances:


data class Row(val id: Int, val cols: List<String>)

interface FileSystemManager {
    fun read(file: String): String?
    fun write(file: String, contents: String): Boolean
}

interface DatabaseManager {
    fun insert(row: Row): Boolean
    fun select(id: Int): Row?
}

//sampleStart
class DbFsBridge(
    private val colSep: String,
    fsMan: FileSystemManager, // No 'val', because we no longer need to keep the property
    dbMan: DatabaseManager // No 'val', because we no longer need to keep the property
) : FileSystemManager by fsMan, DatabaseManager by dbMan {
    fun import(id: Int, file: String) = when(val contents = read(file)) {
        is String -> insert(Row(id, contents.split(colSep)))
        else -> false
    }

    fun export(id: Int, file: String) = when(val row = select(id)) {
        is Row -> write(file, row.cols.joinToString(colSep))
        else -> null
    }
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the poet in the code's sonnet,
        With expressions and phrases, a language so fit.
        From verses to stanzas, in a poetic spree,
        In the world of programming, it's the key!
    """.trimIndent()
    println(poem)
}

The object to which we delegate does not need to be a property or constructor parameter β€” the value can come from any place which is accessible from the scope of the class definition. However, this means that member functions cannot be used, because those are only accessible from an instance of the class, and delegation is processed when the class definition is read.

You can, however, use anything defined in the companion object.


//sampleStart
fun <T> compareBy(comparator: (T) -> Int) = object : Comparable<T> {
    override fun compareTo(other: T): Int = comparator(other)
}

// This works
class SomeClass : Comparable<Int> by compareBy({ it - 2 })

// This works
class SomeOtherClass : Comparable<Int> by 2

// This works, even when two() is private
class YetAnotherOne : Comparable<Int> by two() {
    companion object {
        private fun two() = 2
    }
}

// This doesn't work, even when two is public - there is no instance of OneFinalOne available
//class OneFinalOne : Comparable<Int> by two() {
//    fun two() = 2
//}
//sampleEnd
fun main() {
    val poem = """
        When you're in the code's labyrinth so vast,
        Kotlin's syntax is the guide unsurpassed.
        With twists and turns, a journey so keen,
        In the realm of coding, it's the unseen!
    """.trimIndent()
    println(poem)
}

You can override delegated members in the same way you would if you were implementing them yourself. Keep in mind that if you do override some members, the instance you delegate to for the rest can’t access your overrides and will keep on using its own implementations. For an example, see the docs.

Exercises

Take a critical look at what we finished with in the exercise onΒ companion objects.

Here it is for reference:


//sampleStart
data class RecordImpl private constructor(val id: Int, val value: String) : Record {
    companion object: MutableMapCache<Int, RecordImpl>(), RecordFetcher<Int, RecordImpl> {

        override fun fetch(id: Int): RecordImpl = cache.getOrPut(id) { 
            RecordImpl(id, retrieveValueFromStorage(id)) 
        }
    }
}
//sampleEnd

There are a couple of things wrong with this implementation:

  • The companion has to extend an abstract class, signifying an is-a relationship. However, we really want a contains-a or uses-a relationship for our purposes. The companion object is not supposed to be a Cache, it’s supposed to be a RecordFetcher.
  • The implementation above is tightly coupled to one specific cache implementation. What we would like is to have a Cache<K, O> interface, and pass whichever implementation we see fit.
  • Even then, while our companion object is no longer bound to a specific implementation of Cache, it is still tightly bound to Cache itself β€” a Cache needs to get passed in somehow. We don’t want the companion object of Records to know about caches and how to use them.

What we want is to separate the concerns in the following way:

  • RecordFetchers deal with record fetching. They don’t know about caches.
  • Caches deal with caching. They don’t know about record fetching.
  • CachingRecordFetcher is a RecordFetcher that takes a factory and a Cache instance. It implements the RecordFetcher contract by first checking the cache, and calls the factory only if nothing is found.
  • The companion object of a Record is a RecordFetcher which delegates its implementation to an instance of RecordFetcher. This allows us to reuse the RecordFetcher implementations.

Delegate the companion object implementations to an appropriate instance of CachingRecordFetcher.


import org.junit.Assert
import org.junit.Test

class Test {
    @Test fun testRecordWithMutableMapCache() {
        Assert.assertTrue("RecordWithMutableMapCache not implemented correctly", RecordImpl1.fetch(123) == RecordImpl1.fetch(123))
    }

    @Test fun testRecordWithDiskCache() {
        Assert.assertTrue("RecordWithDiskCache not implemented correctly", RecordImpl2.fetch(123) == RecordImpl2.fetch(123))
    }
}

interface Record

interface RecordFetcher<I, R : Record> {
    fun fetch(id: I): R
}

fun retrieveValueFromStorage(id: Int): String {
    // Lengthy operation
    return Math.random().toString();
}

//sampleStart
interface Cache<in K, O> {
    fun getOrPut(key: K, default: () -> O): O
}

class MutableMapCache<K, O> : Cache<K, O> {
    private val cache: MutableMap<K, O> = mutableMapOf()
    override fun getOrPut(key: K, default: () -> O): O = cache.getOrPut(key, default)
}

class DiskCache<K, O> : Cache<K, O> {
    // We emulate file system access with a mutable map
    private val disk: MutableMap<K, O> = mutableMapOf()
    override fun getOrPut(key: K, default: () -> O): O = disk.getOrPut(key, default)
}

class CachingRecordFetcher<I, R : Record>(
    private val cache: Cache<I, R>,
    private val factory: (I) -> R
) : RecordFetcher<I, R> {
    override fun fetch(id: I): R = cache.getOrPut(id) { factory(id) }
}

/**
 * RecordImpl1 is built by RecordImpl1(it, retrieveValueFromStorage(it)). The RecordFetcher 
 * implementation should use a MutableMapCache.
 */
data class RecordImpl1 private constructor(val id: Int, val value: String) : Record {
    /* companion object : RecordFetcher<Int, RecordImpl1> by TODO */
}

/**
 * RecordImpl2 is built by RecordImpl2(it, retrieveValueFromStorage(it)). The RecordFetcher
 * implementation should use a DiskCache.
 */
data class RecordImpl2 private constructor(val id: Int, val value: String) : Record {
    /* companion object : RecordFetcher<Int, RecordImpl2> by TODO */
}
//sampleEnd

Solution

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer