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
- Create a logging system using class delegation
- Implement a settings class with observable and vetoable properties
- Build a custom delegate for database-backed properties
- Design a caching delegate with expiration logic
Quick Quiz
- What's the difference between class delegation and property delegation?
- When should you use
lazy
vsobservable
delegates? - How do you create a custom property delegate?
Show Answers
- Class delegation delegates interface implementation to another object; property delegation delegates getter/setter logic to another object.
- Use
lazy
for expensive one-time initialization; useobservable
for properties that need side effects on every change. - Implement
getValue
and optionallysetValue
operator functions in a class.