Kotlin - Delegation

Overview

Delegation in Kotlin allows objects to delegate some of their responsibilities to other objects. This powerful feature includes class delegation (composition over inheritance) and property delegation (lazy initialization, observable properties, etc.). This tutorial covers all delegation patterns with practical examples.

๐ŸŽฏ Learning Objectives:
  • Understand class delegation and the "by" keyword
  • Master property delegation patterns (lazy, observable, etc.)
  • Learn to create custom property delegates
  • Apply delegation for composition over inheritance
  • Implement practical delegation patterns

Class Delegation

Class delegation allows a class to implement an interface by delegating to another object that implements the same interface.

Basic Class Delegation

interface Printer {
    fun print(message: String)
}

class ConsolePrinter : Printer {
    override fun print(message: String) {
        println("Console: $message")
    }
}

class FilePrinter(private val filename: String) : Printer {
    override fun print(message: String) {
        println("Writing to $filename: $message")
        // File writing logic would go here
    }
}

// Logger delegates printing to another Printer
class Logger(printer: Printer) : Printer by printer {
    fun log(level: String, message: String) {
        print("[$level] $message")
    }
}

fun main() {
    val consoleLogger = Logger(ConsolePrinter())
    val fileLogger = Logger(FilePrinter("app.log"))
    
    consoleLogger.log("INFO", "Application started")  // Console: [INFO] Application started
    fileLogger.log("ERROR", "Something went wrong")   // Writing to app.log: [ERROR] Something went wrong
    
    // Direct delegation usage
    consoleLogger.print("Direct message")  // Console: Direct message
}

Property Delegation

Property delegation allows properties to delegate their getter/setter logic to another object.

Lazy Properties

class ExpensiveResource {
    init {
        println("ExpensiveResource created!")
    }
    
    fun doWork(): String = "Working hard..."
}

class Application {
    // Lazy initialization - created only when first accessed
    val expensiveResource: ExpensiveResource by lazy {
        println("Initializing expensive resource...")
        ExpensiveResource()
    }
    
    // Lazy with thread safety mode
    val threadSafeResource: ExpensiveResource by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
        ExpensiveResource()
    }
    
    // Lazy without thread safety (faster, single-threaded use)
    val unsafeResource: ExpensiveResource by lazy(LazyThreadSafetyMode.NONE) {
        ExpensiveResource()
    }
}

fun main() {
    val app = Application()
    
    println("Application created")
    println("About to access resource...")
    
    // Resource is created only when first accessed
    println(app.expensiveResource.doWork())
    println("Accessing again...")
    println(app.expensiveResource.doWork())  // Same instance, no re-creation
}

Observable Properties

import kotlin.properties.Delegates

data class User(private var _name: String, private var _age: Int) {
    // Observable property - notified on every change
    var name: String by Delegates.observable(_name) { property, oldValue, newValue ->
        println("Name changed from '$oldValue' to '$newValue'")
    }
    
    // Vetoable property - can reject changes
    var age: Int by Delegates.vetoable(_age) { property, oldValue, newValue ->
        val isValid = newValue >= 0 && newValue <= 150
        if (!isValid) {
            println("Invalid age: $newValue. Keeping old value: $oldValue")
        }
        isValid
    }
}

class Settings {
    var theme: String by Delegates.observable("light") { _, old, new ->
        println("Theme changed from $old to $new")
        applyTheme(new)
    }
    
    var fontSize: Int by Delegates.vetoable(12) { _, old, new ->
        (new in 8..72).also { valid ->
            if (!valid) println("Font size $new is invalid. Valid range: 8-72")
        }
    }
    
    private fun applyTheme(theme: String) {
        println("Applying $theme theme to UI...")
    }
}

fun main() {
    val user = User("John", 25)
    
    println("Initial: ${user.name}, ${user.age}")
    
    user.name = "Jane"      // Name changed from 'John' to 'Jane'
    user.age = 30           // Valid change
    user.age = -5           // Invalid age: -5. Keeping old value: 30
    user.age = 200          // Invalid age: 200. Keeping old value: 30
    
    println("Final: ${user.name}, ${user.age}")
    
    println("\n--- Settings Example ---")
    val settings = Settings()
    settings.theme = "dark"      // Theme changed from light to dark
    settings.fontSize = 16       // Valid change
    settings.fontSize = 100      // Font size 100 is invalid. Valid range: 8-72
    
    println("Settings: theme=${settings.theme}, fontSize=${settings.fontSize}")
}

Map Delegation

class User(map: Map) {
    val name: String by map
    val age: Int by map
    val email: String by map
}

class MutableUser(map: MutableMap) {
    var name: String by map
    var age: Int by map
    var email: String by map
}

fun main() {
    // Read-only user from map
    val userMap = mapOf(
        "name" to "Alice",
        "age" to 28,
        "email" to "[email protected]"
    )
    
    val user = User(userMap)
    println("User: ${user.name}, ${user.age}, ${user.email}")
    
    // Mutable user
    val mutableUserMap = mutableMapOf(
        "name" to "Bob",
        "age" to 32,
        "email" to "[email protected]"
    )
    
    val mutableUser = MutableUser(mutableUserMap)
    println("Before: ${mutableUser.name}, ${mutableUser.age}")
    
    mutableUser.name = "Robert"
    mutableUser.age = 33
    
    println("After: ${mutableUser.name}, ${mutableUser.age}")
    println("Map contents: $mutableUserMap")
}

Custom Property Delegates

import kotlin.reflect.KProperty

// Custom delegate for uppercase strings
class UppercaseDelegate {
    private var value: String = ""
    
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return value
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        this.value = value.uppercase()
        println("Set ${property.name} to: ${this.value}")
    }
}

// Custom delegate with validation
class ValidatedDelegate(
    private var value: T,
    private val validator: (T) -> Boolean
) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        if (validator(newValue)) {
            value = newValue
            println("${property.name} set to: $value")
        } else {
            println("Invalid value for ${property.name}: $newValue")
        }
    }
}

// Custom delegate for caching expensive computations
class CachedDelegate(private val computation: () -> T) {
    private var cached: T? = null
    private var isComputed = false
    
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        if (!isComputed) {
            println("Computing ${property.name}...")
            cached = computation()
            isComputed = true
        }
        return cached!!
    }
}

class Example {
    var title: String by UppercaseDelegate()
    
    var score: Int by ValidatedDelegate(0) { it in 0..100 }
    
    val expensiveCalculation: Double by CachedDelegate {
        Thread.sleep(1000)  // Simulate expensive operation
        Math.PI * Math.E
    }
}

fun main() {
    val example = Example()
    
    example.title = "hello world"    // Set title to: HELLO WORLD
    println("Title: ${example.title}")  // Title: HELLO WORLD
    
    example.score = 85               // score set to: 85
    example.score = 150              // Invalid value for score: 150
    println("Score: ${example.score}")  // Score: 85
    
    println("First access to calculation:")
    println(example.expensiveCalculation)  // Computing expensiveCalculation...
    
    println("Second access (cached):")
    println(example.expensiveCalculation)  // No computation, returns cached value
}

Real-World Example: Configuration System

import kotlin.properties.Delegates
import kotlin.reflect.KProperty

// Custom delegate for environment variables with defaults
class EnvironmentDelegate(private val default: String) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return System.getenv(property.name.uppercase()) ?: default
    }
}

// Custom delegate for configuration with type conversion
class ConfigDelegate(
    private val key: String,
    private val default: T,
    private val converter: (String) -> T
) {
    private val properties = mutableMapOf()
    
    init {
        // Load from system properties, environment, or config file
        loadConfiguration()
    }
    
    private fun loadConfiguration() {
        // Simulate loading configuration
        properties["DATABASE_URL"] = "localhost:5432"
        properties["MAX_CONNECTIONS"] = "10"
        properties["CACHE_ENABLED"] = "true"
        properties["LOG_LEVEL"] = "INFO"
    }
    
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        val value = properties[key] ?: return default
        return try {
            converter(value)
        } catch (e: Exception) {
            println("Error converting $key: $value, using default: $default")
            default
        }
    }
}

class DatabaseConnection(private val config: DatabaseConfig) {
    fun connect(): String = "Connected to ${config.url} with ${config.maxConnections} connections"
}

class CacheService(private val enabled: Boolean) {
    fun get(key: String): String? = if (enabled) "cached_$key" else null
}

class AppConfig {
    // Environment-based configuration
    val environment: String by EnvironmentDelegate("development")
    
    // Typed configuration with conversion
    val databaseUrl: String by ConfigDelegate("DATABASE_URL", "localhost:5432") { it }
    val maxConnections: Int by ConfigDelegate("MAX_CONNECTIONS", 5) { it.toInt() }
    val cacheEnabled: Boolean by ConfigDelegate("CACHE_ENABLED", false) { it.toBoolean() }
    
    // Observable configuration that triggers actions
    var logLevel: String by Delegates.observable("INFO") { _, old, new ->
        println("Log level changed from $old to $new")
        updateLoggerConfiguration(new)
    }
    
    private fun updateLoggerConfiguration(level: String) {
        println("Updating logger to $level level")
    }
}

data class DatabaseConfig(val url: String, val maxConnections: Int)

class Application {
    private val config = AppConfig()
    
    // Lazy initialization of services based on configuration
    private val databaseConnection: DatabaseConnection by lazy {
        DatabaseConnection(DatabaseConfig(config.databaseUrl, config.maxConnections))
    }
    
    private val cacheService: CacheService by lazy {
        CacheService(config.cacheEnabled)
    }
    
    fun start() {
        println("Starting application in ${config.environment} environment")
        println(databaseConnection.connect())
        
        val cachedValue = cacheService.get("user_123")
        println("Cache result: ${cachedValue ?: "Not cached"}")
        
        // Change configuration at runtime
        config.logLevel = "DEBUG"
    }
}

fun main() {
    val app = Application()
    app.start()
}

Advanced Delegation Patterns

Multiple Interface Delegation

interface Reader {
    fun read(): String
}

interface Writer {
    fun write(data: String)
}

class FileReader(private val filename: String) : Reader {
    override fun read(): String = "Content from $filename"
}

class ConsoleWriter : Writer {
    override fun write(data: String) {
        println("Writing: $data")
    }
}

// FileProcessor delegates to both Reader and Writer
class FileProcessor(
    reader: Reader,
    writer: Writer
) : Reader by reader, Writer by writer {
    
    fun process() {
        val data = read()
        val processedData = data.uppercase()
        write(processedData)
    }
}

fun main() {
    val processor = FileProcessor(
        FileReader("input.txt"),
        ConsoleWriter()
    )
    
    processor.process()  // Content from input.txt -> CONTENT FROM INPUT.TXT
}

Best Practices

โœ… Best Practices:
  • Use class delegation to favor composition over inheritance
  • Use lazy for expensive object initialization
  • Use observable for properties that need side effects
  • Create custom delegates for repeated property patterns
  • Use map delegation for dynamic property access
โŒ Common Pitfalls:
  • Don't overuse delegation where simple properties suffice
  • Be careful with thread safety in custom delegates
  • Avoid circular dependencies in delegated properties
  • Remember that lazy properties are computed only once

Practice Exercises

  1. Create a logging system using class delegation
  2. Implement a settings class with observable and vetoable properties
  3. Build a custom delegate for database-backed properties
  4. Design a caching delegate with expiration logic

Quick Quiz

  1. What's the difference between class delegation and property delegation?
  2. When should you use lazy vs observable delegates?
  3. How do you create a custom property delegate?
Show Answers
  1. Class delegation delegates interface implementation to another object; property delegation delegates getter/setter logic to another object.
  2. Use lazy for expensive one-time initialization; use observable for properties that need side effects on every change.
  3. Implement getValue and optionally setValue operator functions in a class.