Delegation ve Delegated Properties Nedir?
Merhabalar, bu yazıda Kotlin dilindeki delegation, delegated properties kavramlarını ve birçok yapıyı inceleyeceğiz. Delegation yapısının gücü ve delegated properties kullanımının sağladığı katkılara değineceğiz. Delegation ile başlayalım.
Delegation Nedir?
Delegation (Delegasyon), bir işlemi yapması gereken birinin o işi başka birine devretmesidir. Mesela bir yönetici, yapması gereken bir işi çalışanına devrediyorsa bu bir delegasyondur. Yazılımdaki delegasyon da böyledir. Birçok boilerplate (pek çok yerde kullanılan ve değişmeyen) kod yazmaktan bizi kurtarır ve bir nevi çoklu kalıtımı da sağlar. 2 çeşit delegasyon vardır.
1 - Explicit Delegation
interface Base {
fun sum(): Int
}
class BaseImpl(private val num: Int, private val num2: Int) : Base {
override fun sum(): Int = num + num2
}
class MyClass(private val base: Base) {
fun myClassSum() {
println(base.sum() + 100)
}
}
fun main() {
val baseImpl = BaseImpl(10,20)
val myClass = MyClass(baseImpl)
myClass.myClassSum() // 130
}
Elimizde bir base yapı var. Sınıfımız (BaseImpl) bu base yapıyı implement ediyor ve kendi kurallarını uygulayarak bu fonksiyonu kullanıyor. Diyelim ki bambaşka bir sınıfa (MyClass) ihtiyacımız var ve bu base yapının fonksiyonlarını kullanmamız gerekiyor. Fakat kullanırken de önceden oluşturulan sınıfın yaptığı işlevlere benzer bir kullanım yapıyorum. Yani MyClass sınıfı içindeki fonksiyonumuz hem base interface içindeki fonksiyonu kullanacak hem de bu sonuca ek olarak 100 sayısını ekleyecek. Bunun için iki tane int tipinde parametre alıp bu değerleri toplayıp sonradan 100 sayısını ekleyebilirim. Ek olarak interface içindeki fonksiyonları da override etmek zorunda kalırım. Fakat bunlara ne gerek var? Zaten BaseImpl sınıfı 2 tane sayıyı topluyor. İşte tam olarak bu 2 sayıyı toplama işlemini delege etmemiz gerekiyor ki boilerplate kod yazmayalım. baseImpl sınıfından bir nesne oluşturup bunu MyClass parametresine gönderiyoruz ve iki sayının toplanma işlemini, önceden yazılan bir fonksiyona delege ediyoruz.
Eğer delegasyon yapmazsak… Buradaki fonksiyon sayıları artsaydı ne kadar fazla boilerplate kod yazacağımızı düşünelim :) Aşağıdaki örnekte delegasyon yapmadan bu işlevi gerçekleştiriyoruz.
interface Base {
fun sum(): Int
}
class BaseImpl(private val num: Int, private val num2: Int) : Base {
override fun sum(): Int = num + num2
}
class MyClass(private val num: Int, private val num2: Int) : Base {
override fun sum(): Int = (num + num2) + 100
}
fun main() {
val myClass = MyClass(10,20)
println(myClass.sum()) // 130
}
Bu sadece bir örnekti. Çok daha büyük bir yapı üstünden örnek verelim. Interface içinde 1000 tane fonksiyon olduğunu düşünelim ve yine aynı şekilde BaseImpl sınıfı içinde bunları override edip kullanalım. Başka bir sınıfa daha ihtiyacımız oldu ve base interface içindeki 500 fonksiyona ihtiyacımız var. Bu fonksiyonları kullanacağım fakat gerekli değişikliklerin bir kısmı zaten yapılmış (BaseImpl içinde). Bu zaten yapılmış değişikliklerin üstüne kendi yapacağımız işlevleri ekleriz ve işimiz biter. Delegasyon yapmazsak hem ihtiyacımız olmayan fonksiyonları kullanmamız gerekiyor hem de önceden yazılan bir yapıyı kullanmadığımız için çok daha fazla kod yazmış oluyoruz. İhtiyacımız olan 500 tane fonksiyonu içeren bir interface tanımlasak? O zaman yine boilerplate kod yazmış oluyoruz. Çünkü ihtiyacımız olan fonksiyonlar bir interface içinde zaten yazılmış, neden bu interface yapısına benzer bir yapı oluşturalım ki?
2 - Implicit Delegation
Explicit delegation herhangi nesne yönelimli bir dilde uygulanabilirken implicit delegation ın uygulanabilmesi için dilin bu özelliği sağlaması gerekiyor.
interface Base {
fun sum(): Int
fun minus(): Int
fun times(): Int
}
class BaseImpl(private val num: Int, private val num2: Int) : Base {
override fun sum() = num + num2
override fun minus() = num - num2
override fun times() = num * num2
}
class BaseImpl2(private val num: Int, private val num2: Int, private val num3: Int) : Base {
override fun sum() = num + num2 + num3
override fun minus() = num - num2 - num3
override fun times() = num * num2 * num3
}
class MyClass(private val base: Base) : Base by base {
override fun sum(): Int = base.sum() + 100
}
fun main() {
val baseImpl = BaseImpl(10, 20)
val baseImpl2 = BaseImpl2(3, 3, 5)
val myClass = MyClass(baseImpl)
val myClass2 = MyClass(baseImpl2)
println(myClass.sum()) // 130
println(myClass2.sum()) // 111
println(myClass.times()) // 200
println(myClass2.times()) // 45
}
Implicit delegation’da by kelimesini kullanıyoruz. Böylece delegasyon işlemini sağlamış oluyoruz. Burada hem interface yapısını implement ediyoruz hem de by kelimesini kullanıyoruz. Örnekte yine sadece sum fonksiyonunun sonucuna 100 sayısını ekliyoruz. MyClass içerisinde hiçbir fonksiyonu override etmek zorunda değiliz. Hangisinde bir değişiklik yapmak istiyorsak onu override edip kullanabiliriz. MyClass sınıfında override etmediğimiz yapıları da kullanabiliriz. O haliyle hangi sınıfı parametre olarak veriyorsak o sınıfın fonksiyonu çalışacaktır.
Bu sefer ek olarak BaseImpl2 sınıfını da kullandık ve bu sayede MyClass sınıfı hem BaseImpl sınıfının hem de BaseImpl2 sınıfının içindeki fonksiyonu kullanabildiğini görüyoruz. Bu bize çoklu kalıtım gibi bir yapıyı sunuyor. Tabii ki Kotlin dilinde bir sınıf sadece bir sınıfı miras alabilir ama buradaki yaptığımız şey ile bunu aşmış gibi oluyoruz. MyClass sınıfından oluşturulan nesnelerle iki sınıftaki fonksiyonu da kullanabildik.
Son olarak MyClass parametresine “Base” tipini verdik ve bu şekilde bu interface i implement eden bütün sınıfları buraya parametre olarak geçebiliriz. Eğer ki spesifik olarak sadece bir sınıf içindeki parametreleri kullanmak istiyorsak oraya base yapıyı implement eden bir sınıfı verebiliriz.
class MyClass(private val baseImpl: BaseImpl) : Base by baseImpl {
override fun sum(): Int = baseImpl.sum() + 100
}
MyClass sınıfına artık BaseImpl2 sınıfının nesnesini parametre olarak veremeyiz. Delegasyonu BaseImpl sınıfına yaptık ve sadece onun fonksiyonlarını kullanacağız.
Delegasyonun ne kadar önemli bir şey olduğunu anladık. Artık bir diğer kavrama bakma zamanı geldi.
Delegated Properties Nedir?
Delegated properties konusunun iyi anlaşılması için property konusunu bildiğimizi varsayıyorum. Eğer yoksa lütfen resmi dökümantasyondan gerekli bilgileri okuyun veya bu linkten property hakkında yazdığım yazıyı okuyabilirsiniz.
Kotlin’de bir property tanımlandığı zaman arka planda bu yapının getter ve setter metotlarının oluşturulduğunu biliyoruz. Eğer ki bu get set işlemlerinin manuel olarak yapılmasını istemiyorsak, bir property yapısına yeni değer atandığı zaman bu değişiklikten haberdar olmak istiyorsak (observable properties), bir property tanımlandığı zaman eğer ki ona direkt erişim yoksa bellekte yer tutmamasını istiyorsak (lazy properties) veya tanımlanan bir property yapısına başka bir sınıf üzerinden değer ataması yapmak, değere erişmek (ReadWriteProperty ve ReadOnlyProperty) istiyorsak burada devreye delegated properties kavramı giriyor. Kotlin’de bu işlevleri sağlayabileceğimiz yapılar vardır. İşte bu hazır yapılara delegated properties denir.
- Read Only Property
Tanımlanan propertynin değerine başka bir sınıf üzerinden getValue fonksiyonuyla erişilmesini sağlayan delegated propertydir. Adından da anlaşılacağı gibi sadece get (okuma) işlemi yapabiliriz.
ReadOnlyProperty interface yapısı bizden iki tip ister. Birincisi, delege edilen property yapısının sahibine ait tipi, ikincisi ise bu delege edilen propertynin tipini belirtir. Implementasyonu yaptıktan sonra getValue fonksiyonunu override etmemiz gerekiyor. Bu fonksiyonun parametresindeki thisRef objenin kendisini verir. property isimli parametresi ise propertye ait özelliklerin kullanılmasını sağlar.
Diyelim ki bir sınıfımız var ve bu sınıfta bir property oluşturuyoruz ve bu propertynin bize dönderdiği değeri manuel olarak ayarlamak istiyoruz. O halde ReadOnlyProperty interface yapısını implement edebiliriz.
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class MyClass {
val myNumber: Int by MyClassDelegation()
}
class MyClassDelegation : ReadOnlyProperty<MyClass, Int> {
override fun getValue(thisRef: MyClass, property: KProperty<*>): Int {
println("$thisRef ${property.name}")
println("getValue")
return 10
}
}
fun main() {
val myClass = MyClass()
println(myClass.myNumber)
}
Artık main içinde bu fonksiyonu yazdırmaya çalıştığımızda getValue içindeki değerleri görüyoruz. Manuel bir get işlemi yaptık. MyClass içindeki propertyi var yaparsak hata alırız. Çünkü var yaptığımızda set işlemini de delege etmemiz gerekiyor fakat biz sadece get işleminin yapıldığı ReadOnyProperty interfaceini implement ettik.
- Read Write Property
Eğer ki sadece get işlemi yapmak istemiyorsak ek olarak set işlemi içinde manuel bir ayarlama yapacaksak ReadWriteProperty isimli delegated property yapısını kullanırız. ReadlWriteProperty interfacei ReadOnlyProperty interfaceini implement eder.
Burada ReadOnlyProperty interfaceinden gelen getValue fonksiyonunun override edildiğini görüyoruz. Ek olarak setValue yapısı da eklenmiş. setValue fonksiyonunun value parametresi ile yaptığımız atama değerine sahip olabiliyoruz.
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class MyClass {
var myNumber: Int by MyClassDelegation()
}
class MyClassDelegation : ReadWriteProperty<MyClass, Int> {
private var myNumber = 0
override fun getValue(thisRef: MyClass, property: KProperty<*>): Int {
println("$thisRef ${property.name}")
println("getValue")
return myNumber
}
override fun setValue(thisRef: MyClass, property: KProperty<*>, value: Int) {
myNumber = value
}
}
fun main() {
val myClass = MyClass()
println(myClass.myNumber)
myClass.myNumber = 10
println(myClass.myNumber)
}
Read Only yapısından farklı olarak set işlemini de gerçekleştirdik. Diğer yapılar tamamen aynı.
Bu getValue ve setValue işlevlerini her zaman bu şekilde yapmak zorunda değiliz. Extension fonksiyon veya operatör fonksiyonları ile de kullanabiliriz.
Extension kullanımı:
import kotlin.reflect.KProperty
private var myNumber = 0
class MyClass {
var myNumberProperty: Int by this
}
operator fun MyClass.getValue(thisRef: MyClass, property: KProperty<*>): Int {
println("$thisRef ${property.name}")
println("getValue")
return myNumber
}
operator fun MyClass.setValue(thisRef: MyClass, property: KProperty<*>, value: Int) {
myNumber = value
}
fun main() {
val myClass = MyClass()
println(myClass.myNumberProperty)
myClass.myNumberProperty = 10
println(myClass.myNumberProperty)
}
by this ifadesiyle beraber sınıfın kendisine bir delegasyon yapıyoruz. Sınıfın getValue ve setValue operaratör fonksiyonları ile bu işlevleri gerçekleştirebiliyoruz. Bu kullanımda bir önceki çıktının aynısını alıyoruz.
Operator kullanımı:
import kotlin.reflect.KProperty
class MyClass {
var myNumber: Int by MyClassDelegation()
}
class MyClassDelegation {
private var myNumber = 0
operator fun getValue(thisRef: MyClass, property: KProperty<*>): Int {
println("$thisRef ${property.name}")
println("getValue")
return myNumber
}
operator fun setValue(thisRef: MyClass, property: KProperty<*>, value: Int) {
myNumber = value
}
}
fun main() {
val myClass = MyClass()
println(myClass.myNumber)
myClass.myNumber = 10
println(myClass.myNumber)
}
Bu son 2 kullanımda interface yapısını implement etmiyoruz.
- Lazy
Projenin büyüklüğüne göre uygulamalarımızda birçok property tanımlaması yapabiliriz. Her bir property bellekte bir yer işgal eder (nesnesinin oluşturulmasına göre). Eğer uygulamada direkt olarak o propertye ihtiyacımız yoksa bunu lazy fonksiyonuna delege edebiliriz. Bu sayede property, kendisine erişim yapılana kadar bellekte yer tutmayacaktır. Direkt olarak kullanmadığımız propertyler için bu delege işlemini kullanabiliriz. Böylece çok daha performanslı bir uygulama yapmış oluruz.
fun main() {
val myClass = MyClass()
println(myClass.myNumber)
println(myClass.myNumber)
println(myClass.myNumber)
}
class MyClass() {
val myNumber: Int by lazy {
println("First Call!")
println("My Number!")
10
}
val myNumber2 = 20
val myNumber3 = 30
}
/*
binding.button.setOnClickListener {
binding.textView.text = myClass.myNumber.toString()
}
*/
İlk olarak MyClass nesnemizi oluşturduk. Nesne oluşturulduğu gibi içerisindeki propertyler bellekte yer tutar. Burada MyClass sınıfının nesnesi oluştuğu gibi myNumber2 ve myNumber3 propertyleri bellekte yer tutacaktır. Fakat myNumber propertysine erişim yapılmadığı için herhangi bir initial işlemi olmayacaktır. Ardından println ile myNumber propertysine eriştiğimiz gibi direkt bu sınıfın içine girilir ve lazy ile tanımlanan property initial edilir (başlatılır). İlk initial işleminden sonra lazy bloğu çalışır. Bu blok içindeki son satır o fonksiyonun dönüş tipidir, lambda fonksiyonlarından bunu biliyoruz. Ardından 2 kez daha bu propertye erişiyoruz fakat bu sonraki erişimlerde lazy bloğu çalışmayacaktır. Lazy bloğu ilk erişimde çalışır. Eğer bu kodu debug ederek çalıştırırsanız bütün bu sonuçları görebilirsiniz. Burada println değil de setOnClickListener içinde bu erişimi yaptığımızı düşünelim. Click yapmadığımız sürece myNumber değişkenine erişilmeyecek ve bellekte yer tutmayacak.
Lazy yapısını kullanırken thread safe modunu da ayarlayabiliyoruz. Bir lazy propertye değer atarken bunu thread safe bir biçimde yapılmasını isteyebiliriz veya her thread ayrı ayrı işlem yapsın isteyebiliriz.
-Eğer sadece tek bir thread bu değerin atamasını yapsın ve bu değişiklik diğer threadler tarafından görünür olsun istersek LazyThreadSafetyMode.SYNCHRONIZED parametresini geçebiliriz ki zaten default olarak bu parametre veriliyor. Bu parametreyle beraber lazy bloğuna tek bir thread erişebilir ve propertye atama yapar. Daha sonra diğer threadler erişmeye çalıştığında bu blok çalışmaz.
-Lazy fonksiyonunun initial bloğuna birden fazla thread erişebilsin istiyorsak LazyThreadSafetyMode.PUBLICATION parametresini geçebiliriz. Bu parametreyle birlikte lazy yapısına ilk erişen thread atamayı yapar ve sonradan erişenler de bu değeri kullanabilir.
-Hem birden fazla thread erişsin hem de her thread değişiklik yapabilsin istiyorsak LazyThreadSafetyMode.NONE parametresini geçebiliriz. Bu en esnek moddur. Bu kullanım tehlikeli olabilir ve bunu sadece lazy bloğuna tek bir thread tarafından erişildiğinden eminsek kullanmalıyız.
- Lateinit
Lazy kullanımına ek olarak bir de lateinit yapısı bulunmaktadır. İkisi de geç değer atama amacıyla kullanılır. Aralarındaki belirgin farkları listeleyelim:
1 - Lazy kullanımı val propertylerle beraber yapılırken, lateinit yapısını var propertylerle birlikte yapabiliyoruz. Bu yüzden dolayı lazy propertye sadece bir kere değer atayabilirken lateinit propertylere birden fazla değer ataması yapabiliriz.
2 - Lateinit kullanımında propertyler primitive tipte olamaz (Int, Boolean, Long, Float, Double vs.). Lazy için böyle bir şey söz konusu değil.
3 - Lazy için thread safety mod ayarlayabiliyoruz fakat lateinit için böyle bir şey söz konusu değil. Thread safety özelliğinden dolayı singleton pattern için lazy propertyler kullanır.
4- Lazy kullanımlarda tipler nullable olabilir fakat lateinit yapısında nullable tipler kullanılamaz.
5 - Lateinit bir property oluşturduğumuzda bu propertye ait değeri sonradan atayacağımızın sözünü veriyoruz. Eğer ki değer atamadan bu propertye erişirsek exception oluşur. Lazynin kendi inital bloğu olduğu için değeri orada zaten veriyoruz. Bu yüzden böyle bir hata oluşmuyor.
class MyRecyclerViewAdapter
class MyFragment {
private lateinit var myRecyclerViewAdapter: MyRecyclerViewAdapter
fun onViewCreated() {
if(this::myRecyclerViewAdapter.isInitialized.not()) {
myRecyclerViewAdapter= MyRecyclerViewAdapter()
println("Initial call!")
}
}
}
fun main() {
val myFragment = MyFragment()
myFragment.onViewCreated()
myFragment.onViewCreated()
myFragment.onViewCreated()
}
isInitialized.not() fonksiyonuyla atamanın yapılıp yapılmadığını kontrol ettiriyoruz. İlk erişimde herhangi bir atama yapılmadığı için bu bloğun içine girilecek fakat diğer erişimlerde bu blok çalışmayacaktır.
- Delegates.notNull
Peki ya hem primitive tiplerini kullanmak istersek hem de değerini lateinit gibi geç vermek istersek ne yapmalıyız? Bu propertyi ya nullable olarak tanımlarız ve null değer veririz ki bu durumda sürekli null check yapmak zorunda kalırız ya da daha iyisi notNull() fonksiyonuna delege edebiliriz. Bu kullanımda da eğer propertye değer atamadan erişirsek exception oluşacaktır.
var notNull: Int by Delegates.notNull()
fun main() {
notNull = 10
println(notNull)
}
NOT: Uygulamanın ilk ayağa kalktığı yerlerde lazy ve lateinit yapılarını kullanırsak bunun hiçbir anlamı kalmaz. Bir aktivite içinde bulunan onCreate fonksiyonunda veya bir sınıfın init bloğunda bu yapılara erişmemeliyiz. Erişeceksek tanımlamamalıyız. Örneğin bir aktivite açıldığı gibi onCreate, bir sınıf çağrıldığı gibi init bloğu çalışır. Bunların içinde propertylere erişirsek bellekte yer tutmuş olacaklar ve lazy, lateinit kullanmanın bir amacı kalmayacak. Bu yapıları bir şey tetiklendiğinde kullanmalıyız. Örneğin kullanıcı bir butona basınca bir propertye ihtiyaç duyuyorsak orada kullanabiliriz. Kullanıcı o butona basacak mı bilmediğimiz için propertynin bellekte yer işgal etmesine hiç gerek yok. Yani kısacası bir yapı ayağa kalkarken değil de sonradan kullanacağımız propertyleri lazy ve lateinit ile tanımlayabiliriz.
Bir başka delegated property yapısı olan observable’a geçmeden önce observable pattern nedir, görelim.
Observable Pattern
Bir yapının state i değiştiği an da bu değişikliği bize otomatik olarak bildiren tasarım desenidir. Bir propertynin değeri her değiştiğinde bundan haberdar olmak istiyorsak bu yapıyı kullanırız. Örneğin bir bankacılık uygulamasında bir para biriminin son 24 saatlik değişimini gösterdiğini düşünelim. Eğer buradaki değer pozitifse yeşil yukarı ok veya buna benzer bir UI çizdirilir. Burada sürekli paranın durumu kontrol edilir ve değiştiğinde yeni değer eski değere kıyasla işlem yapılır. Ayrıca android geliştirmede kullanılan live data yapısının arka planında bu pattern vardır.
Kotlin dilinde tam olarak bu işlemi gerçekleştirebileceğimiz delegated propertyler vardır. Kotlin dilinde Delegates isimli bir obje bulunmaktadır bunun içinde observable ve vetoable isminde iki tane observable pattern içeren yapı vardır.
- Delegates.observable
observable fonksiyonu, parametresine bir adet başlangıç değeri alıyor ve bir de lambda bloğu alıyor. Buradaki observable bir higher order fonksiyondur ve parametresindeki onChange fonksiyonu, propertynin değeri değiştiğinde çağrılıyor.
fun main() {
var lastValue: Int by Delegates.observable(0) { property, oldValue, newValue ->
println("old:$oldValue new:$newValue")
}
lastValue = 10
lastValue = 55
lastValue = 100
}
- Delegates.vetoable
delegates.vetoable yapısının delegates.observable’dan farkı, bir koşula göre gerçekleşmesidir. Örneğin sadece eski değer yeni değerden büyük olduğu zaman bir observable pattern kullanmak istiyorsak bu yapıyı kullanabiliriz. Lambda bloğu içindeki son satırın boolean olması gerekiyor. Eğer ifade true dönerse gerekli atama yapılacaktır. False ise isminden de anlaşılacağı gibi o işlem veto edilecektir.
fun main() {
var lastValue: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
println("old:$oldValue new:$newValue")
newValue > oldValue
}
lastValue = 10
lastValue = 20
lastValue = 15
lastValue = 5
}
Eğer yeni atadığımız değer bir öncekinden düşükse bu old value içine aktarılmayacaktır. Burada 20 değerini verdikten sonra 15 ve 5 için yaptığımız değişiklikler geçersiz sayılacaktır ve old value değişmeyecektir. Hatta direkt println ile lastValue değerini yazdıracak olursak 20 sayısından sonra verilen değerlerin bir işlevi olmadığını ve yine 20 yazıldığını görebiliriz.
- by map
Sınıf propertylerinin isimlerini ve değerlerini mapleyerek kullanabiliriz. Yani propertyleri map içinde saklıyoruz. Json verilerini parse ederken bu yapı işimize yarayabilir.
fun main() {
val myClass = MyClass(
mapOf(
"myString" to "Hello World",
"myNumber" to 10,
"myName" to "Ömer"
)
)
println(myClass.myString)
println(myClass.myNumber)
println(myClass.myName)
}
class MyClass(private val map: Map<String, Any?>) {
val myString: String by map
val myNumber: Int by map
val myName: String by map
}
Erişmeye çalıştığımız propertye bir değer ataması yapılmamışsa exception oluşur.
- Başka bir propertye delegasyon yapmak
Bir propertyi başka bir propertye delege edebiliriz.
fun main() {
val myClass = MyClass()
myClass.oldName = 10
println(myClass.newName) // 10
}
class MyClass {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
Diller, teknolojiler geliştikçe bazı yapıların deprecated olduğunu görürüz. IDE içerisinde bu yapının altı çizilidir. Üstüne geldiğimizde bize deprecated parametresindeki mesajı gösterir ve ikinci parametresinde de yeni yapının ismini verir. Bunlar genellikle arka planda tanımlanır. Biz burada manuel bir tanımlama yaptık.
Evet, nihayet bir yazının daha sonuna geldik :) Umarım okuyanlar için faydalı olmuştur. Bir başka yazıda görüşmek üzere…
Bana Linkedin üzerinden ulaşabilirsiniz.
Son Güncelleme: 06.11.2023