Kotlin'de Delegasyon
Bir class’ı extend ettiğimizde sub-class ve super-class arasında kırılgan bir bağımlılık oluşur. Bu bağımlılığın kırılgan olmasının sebebi super-class’ta yapılan değişikliklerin sub-class’ların çalışmasını bozabilme riskidir. Inheritance’ın bu yan etkisi yüzünden Kotlin’de class’lar final
‘dır (yani extend edilemezler). Eğer bir sınıfı genişletilebilir (extendable) yapmak istiyorsanız sınıfı open
olarak tanımlamanız gerekir. open
sınıflarda değişiklik yaparken bu değişikliklerden sub-sınıfların da etkilenebileceğini aklımızdan çıkartmamamız lazım.
Class Delegation
Bazen open
olmayan, yani extend etmeyi düşünmediğimiz sınıflara yeni özellikler eklemek isteyebiliriz. Böyle durumlarda Decorator
tasarım kalıbı uygulanabilir. Decorator tasarım kalıbı şöyle gerçeklenebilir:
- Yeni özellik eklemek istediğimiz orjinal sınıf ile aynı
interface
‘e sahip yeni bir sınıf oluşturulur. - Orjinal sınıfın bir instance’ı yeni sınıfa bir
field
olarak eklenir. - Değişmesini istediğimiz interface metodu dışındakı tüm metodlar orjinal sınıf instance’ına paslanır.
Aşağıda FilteringList adında bir MutableList
implementation’ı var. Bu listeye sadece bizim istediğimiz filtreye uygun elemanların girmesini istiyoruz. addXXX
metodlarını değiştirerek istediğimiz yeni özelliği ekleyebiliriz. Fakat MutableList interface’inde bulunan diğer metodlar için de orjinal listenin ilgili metodunu çağıran implementasyonlar eklememiz gerekiyor.
class FilteringList<T>(val filter: (T) -> Boolean) : MutableList<T> {
private val original = ArrayList<T>()
override val size
get() = original.size
override fun add(element: T): Boolean {
if (!filter(element)) return false
return original.add(element)
}
override fun add(index: Int, element: T) {
if (filter(element)) original.add(index, element)
}
override fun addAll(index: Int, elements: Collection<T>): Boolean = original.addAll(index, elements.filter(filter))
override fun addAll(elements: Collection<T>): Boolean = original.addAll(elements.filter(filter))
//Boilerplate code alarm !!!
override fun contains(element: T): Boolean = original.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = original.containsAll(elements)
override fun get(index: Int): T = original[index]
override fun indexOf(element: T): Int = original.indexOf(element)
override fun isEmpty(): Boolean = original.isEmpty()
override fun iterator(): MutableIterator<T> = original.iterator()
override fun lastIndexOf(element: T): Int = original.lastIndexOf(element)
override fun clear() = original.clear()
override fun listIterator(): MutableListIterator<T> = original.listIterator()
override fun listIterator(index: Int): MutableListIterator<T> = original.listIterator(index)
override fun remove(element: T): Boolean = original.remove(element)
override fun removeAll(elements: Collection<T>): Boolean = original.removeAll(elements)
override fun removeAt(index: Int): T = original.removeAt(index)
override fun retainAll(elements: Collection<T>): Boolean = original.retainAll(elements)
override fun set(index: Int, element: T): T = original.set(index, element)
override fun subList(fromIndex: Int, toIndex: Int): MutableList<T> = original.subList(fromIndex, toIndex)
}
Yukarıdaki örnek Kotlin’de yazılmış olmasına rağmen bir sürü boilerplate
kod var. Neyse ki Kotlin compiler bizim için bu laf kalabalığı kodu otomatik olarak üretebiliyor. Bunun için class delagation kullanmamız gerekiyor. Kotlin’de bir interface’i implement ettiginizde, bu interface’in implementasyonunu by
keyword’u ile başka bir sınıfa delegate
edebilirsiniz.
Class delegation kullanıldığında FilteringList aşağıdaki gibi boilerplate
kodlardan arınmış oluyor.
class FilteringList<T>(val original: ArrayList<T>, val filter: (T) -> Boolean)
: MutableList<T> by original {
override val size
get() = original.size
override fun add(element: T): Boolean {
if (!filter(element)) return false
return original.add(element)
}
override fun add(index: Int, element: T) {
if (filter(element)) original.add(index, element)
}
override fun addAll(index: Int, elements: Collection<T>): Boolean =
original.addAll(index, elements.filter(filter))
override fun addAll(elements: Collection<T>): Boolean =
original.addAll(elements.filter(filter))
}
FilteringList sınıfını aşağıdaki gibi kullanabiliriz:
val evenNumbers = FilteringList(ArrayList(), { x: Int -> x % 2 == 0 })
for (i in 1..10) {
evenNumbers.add(i)
}
evenNumbers.forEach { print(" $it") }
Delagated Properties
Kotlin programla dilinde var
ile mutable val
ile immutable class property’leri tanımlayabiliriz. Property’leri tanımlarken optional olarak initializer
, getter
ve setter
kullanılabilir.
class Date {
var dayOfWeek = 1 // initializer
get() = field // default getter'i explicit olarak yazmaya gerek yok
set(value) {
if (value in 1..7) field = value
}
}
Property getter ve setter metodları karmaşık logic’ler içerebilir ve başka sınıflarda ve başka property’lerde yeniden kullanmak isteyebiliriz. Örneğin bir field’ın lazy
bir şekilde (getter ilk kez çağırıldığında) oluşturulması ya da bir field’in değeri değiştirildiginde (setter) istediğimiz bir kod bloğunun çalıştırılması gibi karmaşık işlemleri Delegated Properties
olarak yazıp reuse
edebiliriz.
Örneğin aşağıdaki kod parçası name property’sinin getter ve setter işlemlerinin LoggingDelegate
adında bir sınıf tarafından yapılacağını gösteriyor.
class User {
var name : String by LoggingDelegate("")
}
LoggingDelegate sınıfını Kotlin convention’larına göre aşağıdaki gibi yazabiliriz. Bu delegate sınıfı bir property’nin get/set metodları çağırıldığında işlemi konsola yazıyor:
class LoggingDelegate<T>(initial: T) {
private var value = initial
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("${property.name} property of $thisRef is requested")
return value
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
println("Updating ${property.name} of $thisRef to $value")
this.value = value
}
}
Kotlin standard library’de yukarıda örnek olarak verilen LoggingDelegate’ten daha kullanışlı Delegate sınıfları var.
Delegates.vetoable
Property’nin değerini belirli bir kurala göre değiştirmek istediğimizde bu delegate’i kullanabiliriz. Örneğin User sınıfının age
property’si sadece pozitif tam sayılar alabiliyorsa aşağıdaki gibi bir vetoable
delegate kullanılabilir.
class User {
var age: Int by Delegates.vetoable(0) { property, old, new ->
new > 0
}
}
val user = User()
user.age = 37
user.age = -5
println(user.age) // prints 37
Delegates.observable
Bir property’nin değişimlerinden haberdar olmak istiyorsak observable
delegate kullanabiliriz. Örneğin:
data class Location(val lat: Double, val long: Double)
class User {
var location: Location by Delegates.observable(Location(0.0, 0.0)) { property, old, new ->
println("User location changed from $old to $new")
}
}
lazy
Bazen bir propery’nin değerini hesaplamak masraflı bir işlem olabilir. Böyle durumlarda property değerinin ilk erişimde hesaplanması mantıklı olur. Böyle durumlarda lazy delegation
kullanılabilir. Aşağıdaki örnekte kullanıcının tüm satın almaları allPayments
lazy property’sine ilk kez erişildiğinde hesaplanıyor. Daha sonraki erişimlerde daha önce hesaplanan payment listesi kullanılır, database’e tekrar gidilmez.
class User {
val allPayments : List<Payment> by lazy { fetchPaymentsFromDatabase() }
}
fun fetchPaymentsFromDatabase() : List<Payment> {
//costly operation
println("fetching payments from database")
return listOf(Payment(1), Payment(2), Payment(3))
}
Yukarıdaki örnekte kullanılan lazy delegation
thread-safe’tir. Eğer çalıştığınız ortamda birden fazla thread yoksa senkronizasyon maliyetleri ile uğraşmamak için alağıdaki gibi LazyThreadSafetyMode.NONE
kullanabilirsiniz.
val allPayments : List<Payment> by lazy (LazyThreadSafetyMode.NONE) { fetchPaymentsFromDatabase() }
Bir Kotlin yazısının daha sonuna geldik. Detaylara indikçe bu dili daha çok sevmeye başlıyorum :)