Singleton Nedir? Object Expression ve Object Declaration Kullanımı

Ömer Sungur
10 min readSep 6, 2023

--

Kotlin’de sık sık ihtiyaç duyduğumuz object yapısını bu yazı içerisinde detaylı bir şekilde göreceğiz. Bununla kalmayıp bunlarla bağlantılı olan data object, companion object ve singleton yapılarına da göz atacağız. İlk olarak bütün bunların temeli olan singleton yapısıyla başlayalım.

- Singleton -

Singleton bir design patterndir. Design pattern denilen şey, yazılım geliştirmede yaygın olarak karşılaşılan problemler için kullanılan yöntemlere verdiğimiz isimdir. Eğer bir sınıfın sadece ve sadece bir tane nesnesinin olmasını istiyorsak singleton tasarım kalıbını kullanırız. Bu yapı neredeyse çoğu iş görüşmesinde sorulur. Kotlin dili için bu yapıyı object ve companion object keywordleri ile yapıyoruz fakat Java dilinde böyle bir kısa yol olmadığı için singleton yapısını kendimiz kuruyoruz. Java dilinde singleton yapısını thread safe biçimde yazalım.

public class SingletonDeneme {

private volatile static SingletonDeneme singletonDeneme; // static yapmazsak static fonksiyondan erişemiyoruz. (main fonksiyonu static)
// Ek olarak bu yapının memoryden silinmemesi gerekiyor bu yüzden yine static olmaya ihtiyacı var.
// volatile ile bir değişkende yapılan değişiklik diğer threadler tarafından görülebilir hale gelir.

private SingletonDeneme(){} // constructorı private yapıyoruz ki bu classtan bir nesne oluşturulmasın.

public synchronized static SingletonDeneme createInstance() { // class içinden bu classın nesnesini kendimiz oluşturacağız
if(singletonDeneme == null) { // eğer null geliyorsa elimizde nesne yoktur ve oluşturmamız gerekiyor.
singletonDeneme = new SingletonDeneme();
}
// synchronized ile bir fonksiyona aynı an da sadece 1 thread erişebilir. Güvenliği sağlar.

return singletonDeneme; // nesneyi oluşturduk ve bu fonksiyonun dönüşü olarak ayarladık.
}

public int count = 0;
}

class mainClass {
public static void main(String[] args) {

SingletonDeneme s1 = SingletonDeneme.createInstance();
s1.count++; // 1
s1.count++; // 2

System.out.println(s1.count);

SingletonDeneme s2 = SingletonDeneme.createInstance();
s2.count++; // 3
s2.count++; // 4
s2.count++; // 5

System.out.println(s2.count); // yeni nesne oluşturmuyoruz. var olan tek bir nesne var ve onun üzerinden değişiklik yapıyoruz.
System.out.println(s1.count); // s2 ve s1 aynı nesneyi arttırdı o yüzden sonuç aynı.
}
}

Singleton yapısını kurabilmek için şu 4 adımı takip ettik:

1 - Sınıf içinde bu sınıfa ait static bir değişken oluşturuyoruz. Bunu private yapıyoruz ki dışarıdan erişilemesin. volatile ile bu değişkende yapılacak bir değişikliğin diğer threadler tarafından da algılanmasını istiyoruz ki yaptığımız işlem programın her yerinde aynı olsun.

2 - Sınıfın constructorının erişim belirleyicisini private yapıyoruz ki bu sınıfın dışarıdan nesnesi oluşturulamasın.

3 - Sınıf içerisinde bir metot oluşturarak nesne oluşturma işlemini kendimiz yapacağız. Dışarıdan bu metota erişildiğinde eğer bu sınıfın nesnesi yoksa nesneyi oluşturuyoruz ve oluşturulan nesneyi return ediyoruz ama nesne önceden oluşturulduysa önceden oluşturulan nesneyi return ediyoruz. Yani bir kere nesne oluşturulduysa başka oluşturulmayacak, tam da singleton yapısının bize sağladığı durum gibi. Bu metotu synchronized ile tanımlıyoruz ki aynı an da birden fazla thread tarafından erişilemesin. Eğer erişilirse birden fazla nesne oluşturulabilir ve biz bunun olmasını hiç istemiyoruz. Bu kullanımla thread safe bir yapı oluşturmuş oluyoruz.

4 - Son olarak, bu oluşturulan metota eriştiğimiz zaman her zaman aynı nesneye eriştiğimiz için tek bir örneği olmuş olacak. İstediğimiz kadar farkı değişkenle nesne oluşturmaya çalışalım her zaman aynı nesne oluştuğuna dikkat çekerim. count değişkenine hangi nesneden erişirseniz erişin yapılan herhangi bir değişik hepsinde geçerli olacaktır. Çünkü her zaman aynı yapıya erişmiş olacağız.

İyi, güzel ama singleton yapısını nerede kullanacağız? Eğer ki bir nesnenin sadece bir örneği bulunması gerekiyorsa singleton yapısı işlerimizi inanılmaz kolaylaştıracaktır. Örneğin database yapısının sadece bir örneği olmalıdır, burada kullanabiliriz veya uygulamamızda retrofit kullanıyoruz ve bu retrofitin bir nesnesi oluşturulacak o zaman yine singleton yapısını kullanabiliriz.

Singleton yapısını Kotlin dilinde bu kadar uzun uzun yazmamıza gerek yok. Object, tam olarak bunun için oluşturulmuş bir yapıdır. Object kullanımının iki yolu vardır. Object declaration, singleton yapısı için kullanılırken object expression, kullan at sınıflar veya interfaceler için oluşturulur. Şimdi bunları detaylı bir şekilde inceleyelim.

Object Declaration

Kotlin’de singleton yapısından yararlanmak için object declaration kullanılır. Object tanımlamak için belirli durumları sağlamalıyız ve belirli kurallara uymalıyız.

  • Object kelimesinden sonra bir isimlendirme yapılır.
  • Object expressionlar gibi bir değişkene atanamazlar, sınıf gibi direkt olarak tanımlanmalılar.
  • Object declaration‘ın kendisi thread safetir fakat içindeki yapılar thread safe değildir. Nasıl thread safe yapılır, companion object ile ileride göreceğiz.
  • Herhangi bir constructor yapısına sahip olmadığı için nesneleri oluşturulamaz. Zaten bu yüzden tek kullanımlık yapılar oluşturulur diyoruz.
  • Tanımlama yapılırken sınıfları miras alabiliriz veya interfaceleri implement edebiliriz. Fakat sınıflar objectleri miras alamaz (“Cannot inherit from a singleton” hatası alınır). Ek olarak bir fonksiyonun dönüş tipi object tipinden bir ifade olabilir.
  • Local olarak (fonksiyon içinde) veya inner sınıfların içinde tanımlanamazlar fakat normal sınıfların veya objectlerin içerisinde tanımlama yapılabilir. Bu durumda objectlerin statik olarak tutulduğuna dikkat edelim. (Inner - Nested yapılarından dolayı)
  • İçindeki yapılara direkt olarak object ismiyle erişebiliyoruz (static). Herhangi bir nesne oluşturma durumu yok.
object SingletonDeneme {
var count = 0
}

fun main() {

SingletonDeneme.count++
}

İlk başta Java dilinde singleton yapısı kurmuştuk. Buradaki yapı oradakiyle neredeyse birebir aynıdır. Kotlin’de ne kadar basit bir şekilde tanımlıyoruz değil mi? Tek fark count değişkenin thread safe olmamasıdır. Java dilinde volatile ve synchronized ile bu problemi engellemiştik.

Data Object

Nasıl ki bir sınıfı data sınıfı yapıp belirli özellikler kazanabiliyorsak bir object yapısını da data object yapıp belirli özelikler kazanırız. Bu özellikler toString, hashCode ve equals metotlarıdır. Bunları data class isimli yazımda uzun uzun ele aldığım için burada tekrardan anlatmıyorum, üstteki linkten bu içeriğe ulaşabilirsiniz. Sınıflar için default olarak yazılan metotları bilmiyorsanız mutlaka okumanızı tavsiye ederim.

fun main() {

println(MyDataObject) // MyDataObject
println(MyObject) // MyObject@34a245ab
}

data object MyDataObject {
val count = 0
}

object MyObject {
val count = 0
}

Bunları data classlarda da görmüştük. Üstteki çıktılardan biri sınıf ismini verirken diğeri, bellekte nerede tutuluyorsa o bölgenin adresini bize veriyordu. Data object yapısının data classtan birkaç farkı vardır. Bunlar;

  • Data object içerisinde toString metotu özelleştirilebiliyor fakat equals ve hashCode metotlarını override edip değiştiremiyoruz.
  • Data classlar için oluşturulan copy ve component metotları data objectler için oluşturulmuyor. Copy ile elimizdeki yapının kopyalarını oluşturarak farklı nesneler yaratıyorduk. Bu başlı başına singleton yapısına ters zaten, elimizde o yapıya ait tek bir örnek olmalı. Component metotları da class constructorında bulunan parametreler için oluşturuluyordu, object tanımlamasında elimizde zaten constructor olmuyor. Bu sebeplerden ötürü data object içinde bu metotlara hiç ihtiyaç duymuyoruz.

Data objectler genellikle sealed classlar veya sealed interfaceler ile kullanılıyor.

Object Expression

Object expression yapısını, isimsiz (anonymous) sınıflar oluştururken veya bir parametrede interface, class gibi yapılara ihtiyacımız oluyorsa onları sağlayabilmek için kullanıyoruz.

Lambda ifadeleri veya isimsiz fonksiyonlar ile bir değişkene fonksiyonu atayabiliyorduk. Sınıflar için val a = class MyClass diye bir atama yapılamıyor. Bunun yerine object expression yapısından yararlanıyoruz.

val myClass = object {
val number = 10
}

Object expression yapısıyla değişken içine sınıfı atayabildik. Tanımlama yapılırken ne gibi durumlarla karşılaşabiliriz onlara göz atalım.

  • Object expression ile bir sınıfı miras alabiliriz veya bir interface yapısını implement edebiliriz.
abstract class MyAbstractClass() {
abstract fun x()
}

val myClassX = object: MyAbstractClass() {

val number = 10

override fun x() {
println("x")
}
}

fun main() {
myClassX.x()
}
  • Objeyi birden fazla yapıdan türeteceksek yani bir class ve interface veya birden fazla interface yapısını kullanacaksak değişkenimizin tipini belirtmek zorundayız. Belirtmediğimiz takdirde “Right-hand side has anonymous type. Please specify type explicitly” hatası alacağız.
abstract class MyAbstractClass() {
abstract fun x()
}

interface MyInterface {
fun y()
}

val myClassX: MyInterface = object: MyAbstractClass(), MyInterface {

val number = 10

override fun x() {
println("x")
}

override fun y() {

}
}
  • Burada abstract sınıf ve interface yapılarının özellikleri aynı şekilde geçerlidir. Abstract yapılarını override etmek zorundayız.
  • Object içinde oluşturulan propertylere veya fonksiyonlara dışarıdan erişilemez. Ek olarak bir object birden fazla yapıdan türetiliyorsa, tipi türetildiği yapılardan hangisi ise onun özelliklerine dışarıdan erişilebilir. Diğer yapıların içindekilere ulaşılamaz.
abstract class MyAbstractClass() {
abstract fun x()
}

interface MyInterface {
fun y()
}

val myClassX: MyInterface = object: MyAbstractClass(), MyInterface {

val number = 10

override fun x() {
println("x")
}

override fun y() {

}
}

fun main() {
// myClassX.number erişilemez.
// myClassX.x() erişilemez.
myClassX.y() // erişilir. Çünkü myClassX MyInterface tipinde.
}

Buradaki myClassX adlı değişkenimiz MyInterface tipine sahip olduğu için dışarıdan sadece MyInterface yapısının içindeki özelliklere erişilebilir.

  • Anonim object local veya private olarak (fonksiyon veya property) kullanılırsa, nesnenin tüm üyeleri bu fonksiyon veya property’e erişilebilir hale gelir.
open class MyClass {

private var x = object {
fun onPrint() {
println("x")
}
}

private var getObject = object { // public yaparsak printX içinden erişemeyiz çünkü tipi any olur.
val y: String = "y"
}

fun printFunc() {
println(getObject.y) // erişilebilir.
println(x.onPrint()) // erişilebilir.
}
}

Görüyoruz ki object expression kullanımında object yapısının atandığı değişkenin tipi oldukça önemli. Bu tipler neye göre şekilleniyor bakalım.

1 - Eğer hiçbir tip belirtimi yapılmadan object tanımlaması yapılırsa o değişkenin tipi Any olur. Any, normal sınıflar gibi özel metotlar (toString, hashCode, equals) barındırır. Bunları objectin süslü parantezleri arasında kullanabildiğimize dikkat çekerim.

2 - Eğer object yapısı tek bir türden kalıtım alıyorsa direkt olarak değişkenin tipi de o yapıdandır.

3 - Eğer object yapısı birden fazla türden oluşuyorsa değişkenin yanında bu tiplerden birini belirtmemiz gerekiyor ve belirttiğimiz tip bizim direkt olarak değişkenimizin tipi haline gelir.

Bu üç durumlardaki object bildirimleri içerisinde yer alan üyelere (direkt olarak object içinde tanımlanan) dışarıdan erişilemez. Sadece ve sadece değişken tipi ne ise o yapının içindeki override yapılara dışarıdan erişilebilir.

Üstteki 3 durumu kodlamada görelim.

// 1

val myClass1 = object { // myClass1 : Any (Any tipindedir)

}

// 2

abstract class MyAbstractClass2() {
abstract fun x()
}


val myClass2 = object: MyAbstractClass2(){
// myClass2 yanına tip belirtmemize gerek yok.
// Tek bir yapıdan kalıtım aldığı için tipi direkt bellidir. (MyAbstractClass2 tipinde)
override fun x() {

}
}

// 3

abstract class MyAbstractClass3() {
abstract fun x()
}

interface MyInterface3 {
fun y()
}

val myClass3: MyInterface3 = object: MyAbstractClass3(), MyInterface3 {

// myClass3 yanına tip bildirmek zorundayız. (MyInterface3 tipinde)
// Dışarıdan sadece MyInterface3 yapısının içeriğine erişilebilir. Onun haricinde buradaki başka bir yapıya erişilemez.
val number = 10

override fun x() {
println("x")
}

override fun y() {
println("y")
}
}

Bu yapıyı, parametrede sınıf veya interface istendiği zaman kullanıyoruz.

abstract class A

interface B {
fun myFunc()
}

class ExampleClass {
fun exampleObjectDeclaration(param: B) {
param.myFunc()
}
}

fun main() {
val exampleClass = ExampleClass()

exampleClass.exampleObjectDeclaration(object : B {
override fun myFunc() {
println("My Function")
}
})
}

ExampleClass isimli sınıfın içinde bir fonksiyon var ve bu fonksiyon parametresine bakacak olursak interface tipinde bir tanımlama görüyoruz. Bu fonksiyon çağrıldığında o interface yapısını nasıl vereceğiz? İnterface yapılarından bir nesne oluşturulamadığını biliyoruz. İşte burada yardımımıza object expression koşuyor. Tek kullanımlık veya bir başka bir tabirle kullan-at yapı oluşturuyoruz ve istenilen parametreyi burada paslıyoruz.

Bu arada bu kullanımda çok ilginç bir şey var. object: B yazarak aslında arka planda interface yapısının bir nesnesini oluşturuyoruz. Bu kodu Java diline decompile edelim ve görelim. Bunun için shift+shift kısa yolu ile açılan pencereye Show Kotlin ByteCode yazıp ilgili aracı seçelim ve sağ tarafta açılan pencereden decompile butonuna basalım.

public final class ObjectExampleKt {
public static final void main() {
ExampleClass exampleClass = new ExampleClass();
exampleClass.exampleObjectDeclaration((B)(new B() {
}));
}

Gerçekten de interface’in nesnesi oluşturuluyor :). Bu arka planda böyle yapılsa da geliştiriciler olarak kodlama yaparken böyle bir şey yapmamıza dil açısından izin verilmiyor. Bu yapının android alanında kullanıldığı bir örneği daha görüp geçelim.

object : CountDownTimer(10000, 1000) { // 10 saniye içinde 1 saniye aralıklarla işimizi yapıyoruz.

override fun onTick(millisUntilFinished: Long) {
textView.text = "Left: " + millisUntilFinished/1000
}

override fun onFinish() {
Toast.makeText(this@MainActivity,"Süre Doldu",Toast.LENGTH_LONG).show()
textView.text = "Finished"

}
}.start()

Android geliştirmede yer alan en temel yapılardan biri olan CountDownTimer’ı böyle kullanabiliriz. Yapısı gereği abstract bir sınıf olduğu için kendisinden nesne oluşturulamıyor. Bu sebepten ötürü object expression ile kullanıyoruz.

Object expression yazıldığı yerde direkt olarak çalıştırılır fakat object declaration’ın çalışması için bir erişim gerekmektedir. Çağrım yapılmadıkça çalışmaz. Yani object expression hemen çalışır, (initialized immediately) object declaration erişildiğinde çalışır (initialized lazily).

Son olarak companion object yapısına da bakıp bu yazıyı bitirelim.

Companion Object

Bir sınıfın tamamını değil de belli bir kısmını singleton yapmak istiyorsak companion object kullanırız.

class MyClass {
companion object {
var count = 0
}

val number = 0
}

fun main() {
MyClass.count
// MyClass.number // Nesne oluşturulmadan erişilemez.
MyClass.Companion
}

MyClass içinde oluşturduğumuz companion object ile singleton yapısını kurmuş olduk. companion object yapısı dışındaki bölüm singleton bir yapı içermez.

  • Companion object yapısına bir isim vermeye gerek yoktur fakat yine de verebiliriz. Direkt olarak companion object’in kendisine erişmek istiyorsak sınıf ismi ve Companion yazarak erişebiliriz (MyClass.Companion). Companion object’in ismi varsa Companion yerine onu yazarak erişiriz.
  • Companion object içerisindeki yapılara içinde bulunduğu sınıf ismiyle direkt erişebiliriz. Nesne oluşturmamıza gerek yoktur (static).
  • Companion object içerisindeki private alanlara companion object dışarısından ve sınıf içinde kalmak şartıyla erişilebilir.
class MyClass {
companion object {
private var count = 0

}

fun myFunc() {
count // erişilebilir.
}
}
  • Companion object yapısı sınıfları miras alabilir veya interface yapılarını implement edebilir. Bu durumda interface ve abstract sınıf gibi yapıların özellikleri aynen geçerlidir. Fakat diğer yapılar bu companion object yapısını miras alamaz çünkü singleton bir yapıdır.
class MyClass {
companion object: A, B() {

override fun myFuncA() {

}

override fun myFuncB() {

}
}
}

interface A {
fun myFuncA()
}

abstract class B {
abstract fun myFuncB()
}

Class içinde object yazmak yerine companion object yapısını kullanmalıyız.

Bu konuyla ilgili son olarak, Kotlin’deki singleton yapısının tam olarak thread safe olmadığını söylemiştim. Şimdi o durumu halledelim. Kotlin’de de bu yapıyı kurarken Java dilindeki benzer işlemleri yapacağız.

class Singleton private constructor() {

companion object {

@Volatile private var instance: Singleton? = null

fun getInstance() =
instance ?: synchronized(this) {
instance ?: Singleton().also { instance = it }
}
}
}

Java dilinde 4 adımda oluşturduğumuz singleton yapısının bir benzerini Kotlin dili içinde oluşturduk.

Bu arada bundan bahsetmeli miyim? Bilmiyorum ama Kotlin dilinin ne kadar esnek bir dil olduğunu da göstermek istiyorum. Normal de çoklu kalıtım birçok programlama dilinde yoktur. Kotlin’de de bunu yapamıyoruz zaten. Ek olarak interface yapılarının içinde state tutulamadığını da biliyoruz. Bu iki senaryo bir şekilde çürütülebiliyor. Çoklu kalıtım şöyle yapılabiliyor: Başka sınıftan miras alan bir sınıfımız olsun. Bu sınıfın içinde object expression kullanımıyla bir yapı oluşturup eğer o da farklı bir sınıftan miras alırsa burada çoklu kalıtım yapmış oluyoruz. Interfaceler içinde state tutmak için ise interface içinde comanion object tanımlamasıyla beraber propertyler tanımlanabiliyor. Fakat bu iki durum da kullanılmamalı. Sadec Kotlin’in ne kadar esnek bir dil olduğunu anlatmaya çalışıyorum.

class MyClass : Y(){

val MyClass2 = object: X() { // hem X hem Y sınıfının özelliklerine erişilebilir.

fun myClass2Func() {
numberX // erişilebilir.
numberY // erişilebilir.
}
}
}

open class X {
val numberX = 10
}
open class Y {
val numberY = 10
}

interface A {
companion object {
val number = 10 // interface içinde property.
}
}

fun main() {
A.number // interface içinde state tutmuş oluyoruz. Bunu yapmaktan kaçınmalıyız!
}

Fakat bahsettiğim gibi, bu senaryolar OOP yapısına ters. Bunları kullanmamalıyız. Bu yapılarla Kotlin dilinin ne kadar esnek olduğunu görüyoruz.

Evet, yazının sonuna geldik. Umarım okuyanlar için faydalı bir içerik olmuştur. 😊 Bu yazıyı yazarken kaynaklarından yararlandığım Gökhan Öztürk’e (KeKod) çok teşekkür ederim. Ayrıca Kotlin dökümantasyonunu incelemek isterseniz buraya linkini bırakıyorum. Başka bir yazıda görüşmek üzere…

Bana Linkedin üzerinden ulaşabilirsiniz.

Son Güncelleme: 06.09.2023

--

--