Kotlin Sealed Sınıflar: Kapsamlı Bir Rehber
Sealed Sınıflara Giriş
Kotlin, modern yazılım geliştirmenin ihtiyaçlarına cevap veren güçlü ve ifade edici bir dildir. Bu ihtiyaçlardan biri de, belirli bir tür hiyerarşisini kapalı ve kontrollü bir şekilde tanımlayabilmektir. İşte bu noktada Kotlin’in “sealed class” (mühürlü sınıf) özelliği devreye girer. Sealed sınıflar, bir sınıfın alt sınıflarının sayısını ve türünü derleme zamanında kısıtlamamızı sağlayan özel bir sınıf türüdür. Bu özellik, özellikle durumları, olayları veya belirli bir veri setini temsil etmek için idealdir ve kodumuzun daha güvenli, okunabilir ve bakımı kolay olmasını sağlar.
Geleneksel olarak, Java gibi dillerde veya Kotlin’de enum sınıfları, belirli bir sabit değerler kümesini temsil etmek için kullanılır. Ancak enum sınıfları, her bir değerin kendi benzersiz verilerini taşıması gerektiğinde veya karmaşık davranışlar sergilemesi gerektiğinde yetersiz kalır. Örneğin, bir ağ isteğinin durumunu LOADING, SUCCESS, ERROR olarak temsil etmek istediğimizde, SUCCESS durumu başarılı yanıt verisini, ERROR durumu ise hata mesajını veya kodunu içermelidir. enum sınıfları bu tür farklı veri yüklerini doğal olarak desteklemez. Soyut sınıflar veya arayüzler ise bu esnekliği sunar ancak kalıtımı kısıtlamazlar; yani, herhangi bir yerde yeni bir alt sınıf tanımlanabilir, bu da when ifadeleri gibi durumlarda derleyiciye tüm olası durumların bilindiğini garanti etme imkanı vermez.
Sealed sınıflar, enum‘ların basitliğini ve soyut sınıfların esnekliğini bir araya getirerek bu boşluğu doldurur. Bir sealed sınıf, kendisi soyut olmakla birlikte, doğrudan alt sınıflarının sadece tanımlandığı dosya (Kotlin 1.5 öncesi) veya derleme birimi (module) (Kotlin 1.5 sonrası) içinde olabileceğini garanti eder. Bu kısıtlama sayesinde Kotlin derleyicisi, when ifadelerini kullanırken tüm olası alt sınıfların ele alındığından emin olabilir ve bir else dalı yazma zorunluluğunu ortadan kaldırır. Bu da, özellikle büyük kod tabanlarında veya uzun süreli projelerde geliştirici hatalarını önemli ölçüde azaltır ve kod refactoring’ini kolaylaştırır.
Bu makalede, Kotlin sealed sınıfların ne olduğunu, nasıl kullanıldığını, temel sözdizimini, başlıca kullanım senaryolarını, sunduğu avantajları, sınırlamalarını ve diğer benzer yapılarla (enum’lar, soyut sınıflar) karşılaştırmasını detaylı bir şekilde inceleyeceğiz. Ayrıca, Kotlin 1.5 ile gelen önemli değişikliklere de değineceğiz.
Temel Kavramlar ve Sözdizimi
Sealed sınıflar, Kotlin’in tür sistemine getirdiği güçlü bir ektir. Temel amacı, bir sınıf hiyerarşisinin kapalı bir set olmasını sağlamaktır; yani, bu sınıfın tüm doğrudan alt sınıflarının derleme zamanında bilinmesini garanti etmektir.
Sealed Sınıf Nedir?
Bir sealed sınıf, aslında alt sınıfları sınırlı olan soyut bir sınıftır. sealed anahtar kelimesiyle işaretlendiğinde, bu sınıfın doğrudan alt sınıfları yalnızca belirli bir kapsam içinde tanımlanabilir. Bu kapsam, Kotlin 1.5 öncesinde sealed sınıfın tanımlandığı dosya ile sınırlıyken, Kotlin 1.5 ve sonrasında aynı derleme birimi (module) içinde herhangi bir yerde olabilir. Bu kısıtlama, derleyicinin sealed sınıfın tüm olası alt tiplerini bilmesini sağlar, bu da özellikle when ifadelerinde çok değerli bir özelliktir.
sealed sınıflar örtük olarak soyuttur, yani doğrudan örneklenemezler. Ayrıca, constructor’ları varsayılan olarak private‘dır ve protected olarak da belirtilebilir.
Sözdizimi
Bir sealed sınıf tanımlamak oldukça basittir. class anahtar kelimesinin önüne sealed anahtar kelimesi eklenir. Alt sınıfları ise sealed sınıfı miras alarak tanımlanır.
sealed class Sonuc {
data class Basarili(val veri: String) : Sonuc()
data class Hata(val mesaj: String, val kod: Int) : Sonuc()
object Yukleniyor : Sonuc() // Tekil nesne
object Bos : Sonuc() // Tekil nesne
}
Yukarıdaki örnekte:
* Sonuc, bir sealed sınıftır.
* Basarili, Hata, Yukleniyor ve Bos ise Sonuc sınıfının doğrudan alt sınıflarıdır.
* Basarili ve Hata birer data class‘tır ve kendi benzersiz verilerini (veri, mesaj, kod) taşırlar.
* Yukleniyor ve Bos ise object‘tir, yani tekil nesnelerdir ve durumlarını temsil etmek için ek bir veriye ihtiyaç duymazlar.
* Tüm bu alt sınıflar, Sonuc sınıfını miras alır (: Sonuc()).
Bu yapı, bir işlemin farklı olası durumlarını (başarılı, hatalı, yükleniyor, boş) net ve tip güvenli bir şekilde temsil etmemizi sağlar.
Neden sealed?
sealed sınıfların temel faydası, kalıtımı kontrol etme ve derleme zamanı güvenliği sağlamasıdır. Bu sayede:
1. Kalıtımın Kısıtlanması: Sınıf hiyerarşisinin sadece belirli, önceden tanımlanmış alt sınıflardan oluşmasını sağlarız. Bu, beklenmeyen veya istenmeyen alt sınıf tanımlamalarını engeller.
2. Kapsamlı when İfadeleri: En önemli avantajlarından biridir. sealed sınıfın tüm alt sınıflarını when ifadesinde ele aldığımızda, Kotlin derleyicisi bir else dalı yazmamızı istemez. Eğer olası bir alt sınıfı unutursak, derleyici bize hata verir. Bu, kodun eksiksizliğini ve hata yakalama yeteneğini artırır.
3. Tip Güvenliği: Her bir durumun kendi tipine ve verisine sahip olabilmesi, güçlü tip güvenliği sağlar. Yanlış veri türüyle işlem yapma riskini azaltır.
Bu özellikler, özellikle karmaşık durum makineleri, olay tabanlı sistemler ve kullanıcı arayüzü durum yönetimi gibi senaryolarda sealed sınıfları vazgeçilmez kılar.
Kullanım Senaryoları
Sealed sınıflar, birçok farklı senaryoda kodun temizliğini, güvenliğini ve okunabilirliğini artırmak için kullanılabilir. İşte en yaygın kullanım senaryolarından bazıları:
Durum Temsili (State Representation)
Uygulamalarda, özellikle kullanıcı arayüzü (UI) geliştirme veya ağ istekleri gibi asenkron işlemler sırasında, bir bileşenin veya verinin farklı durumları olabilir. Sealed sınıflar bu durumları tip güvenli bir şekilde temsil etmek için mükemmeldir.
sealed class UiState {
object Loading : UiState()
data class Success(val data: T) : UiState()
data class Error(val message: String) : UiState()
object Empty : UiState()
}
fun handleUiState(state: UiState) {
when (state) {
UiState.Loading -> println("Veriler yükleniyor...")
is UiState.Success -> println("Veri başarıyla alındı: ${state.data}")
is UiState.Error -> println("Hata oluştu: ${state.message}")
UiState.Empty -> println("Gösterilecek veri bulunamadı.")
}
}
// Kullanım
fun main() {
handleUiState(UiState.Loading)
handleUiState(UiState.Success("Merhaba Dünya!"))
handleUiState(UiState.Error("Ağ bağlantısı hatası"))
handleUiState(UiState.Empty)
}
Bu örnekte, UiState sınıfı bir UI bileşeninin dört olası durumunu temsil eder: Loading, Success, Error, Empty. Her durum kendi verisini taşıyabilir (örneğin, Success veri, Error hata mesajı). handleUiState fonksiyonundaki when ifadesi, derleyici tarafından tüm durumların ele alındığı garanti edildiği için else dalına ihtiyaç duymaz.
Olay Temsili (Event Handling)
Kullanıcı etkileşimleri veya sistem olayları gibi durumlar, genellikle farklı parametreler gerektiren farklı olay türleri olarak temsil edilir. Sealed sınıflar, bu olayları tek bir hiyerarşi altında toplamak için idealdir.
sealed class UserEvent {
data class Click(val x: Int, val y: Int) : UserEvent()
data class TextChanged(val newText: String) : UserEvent()
object SubmitForm : UserEvent()
object Logout : UserEvent()
}
fun processEvent(event: UserEvent) {
when (event) {
is UserEvent.Click -> println("Tıklama olayı: x=${event.x}, y=${event.y}")
is UserEvent.TextChanged -> println("Metin değişti: ${event.newText}")
UserEvent.SubmitForm -> println("Form gönderildi.")
UserEvent.Logout -> println("Kullanıcı çıkış yaptı.")
}
}
// Kullanım
fun main() {
processEvent(UserEvent.Click(100, 200))
processEvent(UserEvent.TextChanged("Yeni değer"))
processEvent(UserEvent.SubmitForm)
}
Burada UserEvent sınıfı, farklı kullanıcı etkileşimlerini temsil eder. Her olay türü, kendi özel verilerini (örneğin, Click için koordinatlar, TextChanged için yeni metin) taşıyabilir. Bu yapı, olay işleme mantığını merkezi ve düzenli tutar.
Hata Yönetimi (Error Handling)
Bir uygulamanın farklı katmanlarında ortaya çıkabilecek hata türlerini standardize etmek ve yönetmek için sealed sınıflar kullanılabilir. Bu, hata mesajlarını veya kodlarını taşıyan özel hata nesneleri oluşturmayı sağlar.
sealed class AppError {
data class NetworkError(val code: Int, val message: String) : AppError()
data class DatabaseError(val exception: Exception) : AppError()
object Unauthorized : AppError()
object UnknownError : AppError()
}
fun handleError(error: AppError) {
when (error) {
is AppError.NetworkError -> println("Ağ hatası: Kod ${error.code}, Mesaj: ${error.message}")
is AppError.DatabaseError -> println("Veritabanı hatası: ${error.exception.localizedMessage}")
AppError.Unauthorized -> println("Yetkilendirme hatası.")
AppError.UnknownError -> println("Bilinmeyen bir hata oluştu.")
}
}
// Kullanım
fun main() {
handleError(AppError.NetworkError(500, "Sunucu hatası"))
handleError(AppError.DatabaseError(IllegalStateException("Tablo bulunamadı")))
}
AppError sınıfı, uygulamanın karşılaşabileceği çeşitli hata türlerini kapsar. Bu, hata işleme mantığını daha modüler ve anlaşılır hale getirir.
Dışlanmış Hiyerarşiler (Restricted Hierarchies)
Matematiksel şekiller, komut desenleri veya yapılandırma seçenekleri gibi belirli bir küme içindeki farklı ancak ilişkili türleri temsil etmek için sealed sınıflar kullanılabilir.
sealed class Shape {
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
data class Triangle(val side1: Double, val side2: Double, val side3: Double) : Shape()
}
fun calculateArea(shape: Shape): Double {
return when (shape) {
is Shape.Circle -> Math.PI shape.radius shape.radius
is Shape.Rectangle -> shape.width * shape.height
is Shape.Triangle -> {
val s = (shape.side1 + shape.side2 + shape.side3) / 2
Math.sqrt(s (s - shape.side1) (s - shape.side2) * (s - shape.side3))
}
}
}
// Kullanım
fun main() {
val circle = Shape.Circle(5.0)
val rectangle = Shape.Rectangle(4.0, 6.0)
val triangle = Shape.Triangle(3.0, 4.0, 5.0)
println("Daire alanı: ${calculateArea(circle)}")
println("Dikdörtgen alanı: ${calculateArea(rectangle)}")
println("Üçgen alanı: ${calculateArea(triangle)}")
}
Shape sınıfı, geometrik şekillerin kapalı bir hiyerarşisini tanımlar. calculateArea fonksiyonu, her şekil türü için doğru alanı hesaplamak için when ifadesini kullanır ve derleyici, tüm şekil türlerinin ele alındığını garanti eder.
Sealed Sınıfların Avantajları
Sealed sınıflar, Kotlin geliştiricilerine bir dizi önemli avantaj sunar ve modern, robust uygulamalar geliştirmeyi kolaylaştırır.
Derleme Zamanı Güvenliği (Compile-Time Safety)
Bu, sealed sınıfların en büyük ve en çok öne çıkan avantajıdır. when ifadesiyle bir sealed sınıfın tüm alt sınıflarını ele aldığınızda, derleyici bu durumun kapsamlı olduğunu (exhaustive) kontrol eder. Eğer bir alt sınıfı işlemeyi unutursanız, derleyici size bir hata verir ve kodu derlemenize izin vermez. Bu, runtime’da beklenmedik durumlarla karşılaşma riskini ortadan kaldırır ve hata ayıklama süresini önemli ölçüde azaltır.
sealed class TrafficLight {
object Red : TrafficLight()
object Yellow : TrafficLight()
object Green : TrafficLight()
}
fun getNextLight(currentLight: TrafficLight): TrafficLight {
return when (currentLight) {
TrafficLight.Red -> TrafficLight.Green
TrafficLight.Yellow -> TrafficLight.Red
TrafficLight.Green -> TrafficLight.Yellow
// Eğer yeni bir alt sınıf eklersek (örneğin, FlashingYellow),
// bu when ifadesi derleme hatası verecektir.
// Bu sayede eksik durumları kolayca tespit edebiliriz.
}
}
Daha Okunabilir ve Bakımı Kolay Kod (More Readable and Maintainable Code)
Sealed sınıflar, olası durumları veya türleri açıkça tanımlayarak kodun niyetini netleştirir. Bir sınıfın hangi alt tiplere sahip olabileceğini görmek için tüm kod tabanını aramanıza gerek kalmaz; her şey sealed sınıfın tanımında veya aynı derleme biriminde merkezi olarak bulunur. Bu, yeni bir geliştiricinin projeye adapte olmasını kolaylaştırır ve uzun vadede kodun bakımını basitleştirir. Bir alt sınıf eklendiğinde veya değiştirildiğinde, derleyici ilgili tüm when ifadelerini kontrol etmenizi sağlayarak refactoring sürecini daha güvenli hale getirir.
Tek Tip Kontrol (Type Checking)
is ve as operatörleriyle birlikte sealed sınıflar, güçlü ve sezgisel tip kontrolü sağlar. when ifadesi içinde, bir alt sınıfın tipi otomatik olarak akıllıca atılır (smart cast), bu da ek tip dönüşümlerine gerek kalmadan doğrudan alt sınıfın özelliklerine erişmenizi sağlar.
fun processData(data: Sonuc) {
when (data) {
is Sonuc.Basarili -> println("Veri: ${data.veri.length}") // data otomatik olarak Sonuc.Basarili tipine dönüştürülür
is Sonuc.Hata -> println("Hata mesajı: ${data.mesaj.toUpperCase()}")
// ... diğer durumlar
}
}
Performans (Performance)
Sealed sınıfların kullanımı, genel olarak uygulamanın performansı üzerinde ihmal edilebilir bir etkiye sahiptir. Derleyici, sealed sınıfları ve alt sınıflarını optimize edilmiş bir şekilde ele alır. when ifadeleri, runtime’da karmaşık yansıma (reflection) mekanizmaları yerine, doğrudan tip karşılaştırmaları ve dallanmalar kullanılarak verimli bir şekilde çalışır. Bu, enum sınıflarına benzer bir performans profili sunar, ancak çok daha fazla esneklik sağlar.
Sealed Sınıfların Sınırlamaları ve Dikkat Edilmesi Gerekenler
Sealed sınıflar güçlü bir araç olsa da, bazı kısıtlamaları ve dikkat edilmesi gereken noktaları vardır. Bu kısıtlamalar, sealed sınıfların temel tasarım felsefesinden kaynaklanır ve aslında onların gücünü de oluşturur.
Aynı Dosya/Paket Kısıtlaması (Kotlin 1.5 Öncesi) / Aynı Modül Kısıtlaması (Kotlin 1.5 ve Sonrası)
Bu, sealed sınıfların en belirgin kısıtlamasıydı ve Kotlin 1.5 ile önemli ölçüde gevşetildi.
* Kotlin 1.5 Öncesi: Bir sealed sınıfın tüm doğrudan alt sınıfları, sealed sınıfın tanımlandığı aynı .kt dosyası içinde bulunmak zorundaydı. Bu, büyük sealed hiyerarşileri için dosyaların çok uzun ve karmaşık hale gelmesine neden olabiliyordu.
* Kotlin 1.5 ve Sonrası: Artık doğrudan alt sınıflar, sealed sınıfın tanımlandığı aynı derleme birimi (module) içinde herhangi bir dosyada ve herhangi bir pakette tanımlanabilir. Bu, sealed sınıfların daha modüler ve düzenli bir şekilde kullanılmasını sağlar. Bu gevşeme, sealed sınıfların kullanım esnekliğini artırırken, derleyicinin tüm alt sınıfları derleme zamanında bilebilme temel ilkesini korur.
Bu kısıtlamanın temel nedeni, derleyicinin when ifadelerinin kapsamlı olup olmadığını kontrol edebilmesi için tüm olası alt sınıfları derleme zamanında bilmesi gerektiğidir. Eğer alt sınıflar rastgele farklı modüllerde tanımlanabilseydi, derleyici bu kontrolü yapamazdı.
Soyut Olma (Implicitly Abstract)
sealed sınıflar örtük olarak soyuttur, yani doğrudan örneklenemezler. Bir sealed sınıfın constructor’ı varsayılan olarak private‘dır ve protected olarak da belirtilebilir. Bu, sealed sınıfın yalnızca alt sınıfları aracılığıyla temsil edilmesini sağlar ve mantıksal olarak bir durum veya hiyerarşi kökü olarak işlev görmesini sağlar.
sealed class MySealedClass
// val instance = MySealedClass() // Hata: 'MySealedClass' soyut ve doğrudan örneklenemez.
final Olamaz (Cannot be final)
Bir sealed sınıfın final olması mantıksızdır, çünkü sealed anahtar kelimesi zaten alt sınıflara sahip olacağını ima eder. Bu nedenle, bir sealed sınıfı final olarak işaretleyemezsiniz.
Veri Sınıfları ile Karşılaştırma (Comparison with Data Classes)
Sealed sınıflar ve veri sınıfları (data classes) bazen karıştırılabilir çünkü her ikisi de veri taşıyan nesneler oluşturmak için kullanılır. Ancak temel farkları vardır:
* data class: Veri depolamak ve taşımak için tasarlanmış basit sınıflardır. equals(), hashCode(), toString(), copy() gibi standart fonksiyonları otomatik olarak sağlarlar. Bir data class bir hiyerarşi tanımlamaz, sadece tek bir veri yapısını temsil eder.
* sealed class: Bir sınıf hiyerarşisi tanımlamak ve bu hiyerarşinin kapalı bir set olmasını sağlamak için kullanılır. Alt sınıfları data class, object veya normal class olabilir.
Özetle, data class tek bir “şey”i temsil ederken, sealed class bir “durumlar kümesi” veya “türler hiyerarşisi”ni temsil eder. Genellikle sealed sınıfların alt sınıfları olarak data class‘lar kullanılır, bu da her bir durumun kendi verilerini taşımasını sağlar.
Sealed Sınıflar vs. Enum Sınıflar
Kotlin’de hem sealed sınıflar hem de enum sınıflar, sınırlı bir değerler veya durumlar kümesini temsil etmek için kullanılır. Ancak, aralarında önemli yapısal ve kullanım farklılıkları vardır. Doğru aracı doğru senaryoda seçmek, kodunuzun verimliliğini ve okunabilirliğini artırır.
Temel Farklar
1. Örnekleme ve Veri Yükü:
* Enum: Her bir enum sabiti, tekil (singleton) bir nesnedir. Yani Renk.KIRMIZI her zaman aynı nesneyi ifade eder. Enum sabitleri, kendi özelliklerine sahip olabilir ancak bu özellikler, tüm enum sabitleri için aynı yapıya sahiptir. Farklı enum sabitleri farklı tiplerde veri taşıyamaz; hepsi aynı enum tipindedir.
* Sealed Class: Alt sınıfları farklı tiplerde olabilir (data class, object, normal class). Her alt sınıfın kendi benzersiz verileri ve davranışları olabilir ve birden fazla örneği oluşturulabilir (eğer object değilse). Örneğin, Sonuc.Basarili("veri") ve Sonuc.Basarili("başka veri") iki farklı Basarili nesnesidir.
2. Kalıtım:
* Enum: Enum sınıfları örtük olarak Enum sınıfını miras alır ve başka bir sınıfı miras alamazlar.
* Sealed Class: Sealed sınıflar soyut sınıflardır ve kendi alt sınıfları olabilir. Alt sınıflar da kendi içlerinde başka sınıfları miras alabilir veya arayüzleri implement edebilir.
3. Kapsam:
* Enum: Enum sabitleri, enum sınıfının içinde tanımlanır.
* Sealed Class: Alt sınıflar, sealed sınıfın tanımlandığı aynı modül içinde herhangi bir yerde olabilir (Kotlin 1.5+).
Ne Zaman Hangisi?
* Enum Sınıfları Kullanın:
* Birbirinden ayrı, sabit ve tekil değerler kümesini temsil etmek istediğinizde.
* Her bir değerin kendi özel verisine veya davranışına ihtiyaç duymadığı durumlarda (veya tüm değerler aynı yapıya sahipse).
* Örnekler: Haftanın günleri (Pazartesi, Salı), renkler (Kırmızı, Mavi), basit durumlar (Açık, Kapalı), yönler (Kuzey, Güney).
* Sealed Sınıfları Kullanın:
* Birbirinden farklı veriler taşıyan veya farklı davranışlar sergileyen durumlar veya türler hiyerarşisi oluşturmak istediğinizde.
* when ifadelerinde derleme zamanı kapsamlılık kontrolünden (exhaustive check) faydalanmak istediğinizde.
* Örnekler: Ağ isteklerinin durumu (Yükleniyor, Başarılı(veri), Hata(mesaj)), UI durumları, farklı hata türleri, farklı şekiller (daire, kare, üçgen).
Örnek Karşılaştırma
Basit bir işlem sonucunu temsil etmek için hem enum hem de sealed sınıfı nasıl kullanabileceğimize bakalım:
Enum ile:
enum class Status {
LOADING,
SUCCESS,
ERROR
}
// Bu durumda SUCCESS veya ERROR durumlarına özel veri ekleyemeyiz.
// Eğer bir hata mesajı veya başarı verisi eklemek istersek,
// bu enum'un içinde tutulamaz veya çok çirkin çözümler gerektirir.
Sealed Class ile:
sealed class Result {
object Loading : Result()
data class Success(val data: T) : Result()
data class Error(val message: String) : Result()
}
fun handleResult(result: Result) {
when (result) {
Result.Loading -> println("Yükleniyor...")
is Result.Success -> println("Veri: ${result.data}")
is Result.Error -> println("Hata: ${result.message}")
}
}
Görüldüğü gibi, sealed sınıf, her bir duruma özgü verileri taşıma esnekliği sunarken, enum sadece sabit, veri içermeyen durumlar için daha uygundur. Bu nedenle, çoğu modern Android veya backend uygulamasında, özellikle asenkron işlemlerin sonuçlarını veya UI durumlarını temsil etmek için sealed sınıflar tercih edilir.
Sealed Sınıflar vs. Soyut Sınıflar/Arayüzler
Sealed sınıflar, soyut sınıflar ve arayüzler, Kotlin’de kalıtım ve polimorfizm için kullanılan temel yapılardır. Her birinin kendine özgü kullanım alanları ve özellikleri vardır. Sealed sınıflar, soyut sınıflar ve arayüzler arasında bir köprü görevi görür, belirli kısıtlamalarla esneklik sunar.
Temel Farklar
1. Kalıtım Kısıtlaması:
* Soyut Sınıf/Arayüz: Kalıtımı veya implementasyonu kısıtlamaz. Herhangi bir modülde veya dosyada, bu soyut sınıfı miras alan veya arayüzü implement eden yeni sınıflar tanımlanabilir. Derleyici, bu yapıların tüm olası alt tiplerini derleme zamanında bilemez.
* Sealed Class: Kalıtımı kısıtlar. Doğrudan alt sınıfları, sealed sınıfın tanımlandığı aynı derleme birimi (module) içinde olmak zorundadır. Bu, derleyicinin tüm olası alt tipleri bilmesini sağlar.
2. Örnekleme:
* Soyut Sınıf/Arayüz: Doğrudan örneklenemezler.
* Sealed Class: Doğrudan örneklenemezler (örtük olarak soyutturlar).
3. when İfadeleri ile Kapsamlılık (Exhaustiveness):
* Soyut Sınıf/Arayüz: Bir soyut sınıf veya arayüzün alt tipleriyle when ifadesi kullanıldığında, derleyici tüm olası alt tiplerin ele alındığını garanti edemez. Bu nedenle, genellikle bir else dalı veya is kontrolü eklemek gerekir. Yeni bir alt sınıf eklendiğinde, when ifadesinin güncellenmesi gerektiği konusunda derleyici bir uyarı veya hata vermez.
* Sealed Class: Sealed sınıfların en büyük avantajı buradadır. when ifadesiyle bir sealed sınıfın tüm alt sınıflarını ele aldığınızda, derleyici bunun kapsamlı olduğunu kontrol eder ve bir else dalı gerektirmez. Yeni bir alt sınıf eklendiğinde ve when ifadesi güncellenmediğinde, derleyici bir hata verir, bu da refactoring ve bakım sırasında büyük bir güvenlik sağlar.
Ne Zaman Hangisi?
* Arayüz (Interface) Kullanın:
* Farklı sınıflar arasında ortak bir davranış sözleşmesi tanımlamak istediğinizde.
* Soyut metotlar ve/veya varsayılan implementasyonlar içerebilir.
* Çoklu kalıtım (bir sınıf birden fazla arayüzü implement edebilir) için idealdir.
* Örnek: Clickable, Serializable, Runnable.
* Soyut Sınıf (Abstract Class) Kullanın:
* Bir temel sınıf hiyerarşisi tanımlamak ve alt sınıflar arasında ortak implementasyon veya durum paylaşımı sağlamak istediğinizde.
* Bazı metotların varsayılan implementasyonlarını sağlayıp, bazılarını alt sınıfların implement etmesini zorunlu kılabilirsiniz.
* Tekli kalıtım (bir sınıf sadece bir soyut sınıfı miras alabilir) için kullanılır.
* Örnek: BaseViewModel, Animal (alt sınıflar Dog, Cat gibi).
* Sealed Sınıf (Sealed Class) Kullanın:
* Bir durum kümesini, olay kümesini veya kapalı bir tür hiyerarşisini temsil etmek istediğinizde.
* Tüm olası alt tiplerin derleme zamanında bilinmesini ve when ifadelerinde kapsamlılık kontrolü yapılmasını istediğinizde.
* Sınıf hiyerarşisinin gelecekteki genişlemesini belirli bir modül içinde tutmak ve kontrol altında tutmak istediğinizde.
* Örnek: Result, UiState, Shape.
Exhaustive when Avantajı
Sealed sınıfların en belirgin avantajı, yukarıda bahsedildiği gibi when ifadelerinin kapsamlılığıdır. Bu özellik, soyut sınıflar veya arayüzler için mevcut değildir.
// Abstract class örneği
abstract class Animal {
abstract fun makeSound()
}
class Dog : Animal() { override fun makeSound() = println("Hav hav") }
class Cat : Animal() { override fun makeSound() = println("Miyav") }
fun describeAnimal(animal: Animal) {
when (animal) {
is Dog -> println("Bu bir köpek.")
is Cat -> println("Bu bir kedi.")
// Yeni bir alt sınıf (örneğin, Bird) eklediğimizde,
// derleyici burada bir hata vermez.
// Bu yüzden genellikle bir 'else' dalı eklemek gerekir:
// else -> println("Bilinmeyen hayvan türü.")
}
}
Yukarıdaki describeAnimal fonksiyonuna Bird adında yeni bir Animal alt sınıfı eklediğimizde, derleyici bize when ifadesini güncellememiz gerektiğini söylemez. Bu, runtime’da beklenmeyen davranışlara yol açabilir. Sealed sınıflar bu riski ortadan kaldırır.
Gelişmiş Kullanım ve İpuçları
Sealed sınıfların temel kullanımının ötesinde, daha karmaşık senaryolarda ve modern Kotlin özellikleriyle birlikte nasıl kullanılabileceğine dair bazı gelişmiş ipuçları bulunmaktadır.
İç İçe Sealed Sınıflar (Nested Sealed Classes)
Daha karmaşık durum hiyerarşileri veya daha fazla detay gerektiren durumlar için iç içe sealed sınıflar kullanmak mümkündür. Bu, bir ana durumu daha spesifik alt durumlara ayırmanıza olanak tanır.
sealed class AuthenticationState {
object Idle : AuthenticationState()
object Loading : AuthenticationState()
sealed class Authenticated : AuthenticationState() {
data class Success(val userToken: String) : Authenticated()
data class TwoFactorRequired(val userId: String) : Authenticated()
}
sealed class Error : AuthenticationState() {
data class NetworkError(val message: String) : Error()
data class InvalidCredentials(val attemptsLeft: Int) : Error()
object UserNotFound : Error()
}
}
fun handleAuthState(state: AuthenticationState) {
when (state) {
AuthenticationState.Idle -> println("Kimlik doğrulama bekleniyor.")
AuthenticationState.Loading -> println("Kimlik doğrulanıyor...")
is AuthenticationState.Authenticated.Success -> println("Giriş başarılı: ${state.userToken}")
is AuthenticationState.Authenticated.TwoFactorRequired -> println("İki faktörlü kimlik doğrulama gerekli: ${state.userId}")
is AuthenticationState.Error.NetworkError -> println("Ağ hatası: ${state.message}")
is AuthenticationState.Error.InvalidCredentials -> println("Yanlış kimlik bilgileri. Kalan deneme: ${state.attemptsLeft}")
AuthenticationState.Error.UserNotFound -> println("Kullanıcı bulunamadı.")
}
}
Bu örnekte, AuthenticationState hem genel durumları (Idle, Loading) hem de daha spesifik, kendi içlerinde sealed olan Authenticated ve Error durumlarını içerir. Bu yapı, durum yönetimini çok daha detaylı ve okunabilir hale getirir.
Generics ile Kullanım (Using with Generics)
Sealed sınıflar, generic tiplerle birlikte kullanıldığında daha da esnek hale gelir. Bu, farklı veri tipleriyle çalışabilen genel durum veya sonuç yapıları oluşturmanızı sağlar.
sealed class AsyncResult {
object Loading : AsyncResult()
data class Success(val data: T) : AsyncResult()
data class Failure(val exception: Throwable) : AsyncResult()
}
fun processGenericResult(result: AsyncResult) {
when (result) {
AsyncResult.Loading -> println("Veri bekleniyor...")
is AsyncResult.Success -> println("Başarılı veri: ${result.data}")
is AsyncResult.Failure -> println("Hata oluştu: ${result.exception.localizedMessage}")
}
}
// Kullanım
fun main() {
processGenericResult(AsyncResult.Success("Generic Başarı"))
processGenericResult(AsyncResult.Failure(RuntimeException("Generic Hata")))
}
Burada AsyncResult generic bir sealed sınıftır. out T kullanımı (kovaryans), AsyncResult‘in bir AsyncResult yerine geçebilmesini sağlar ve daha fazla tip esnekliği sunar.
Kotlin 1.5 ve Sonrası Değişiklikler
Daha önce de bahsedildiği gibi, Kotlin 1.5 sürümüyle birlikte sealed sınıfların alt sınıflarını tanımlama kısıtlaması önemli ölçüde gevşetildi.
* Öncesi: Tüm doğrudan alt sınıflar, sealed sınıfın tanımlandığı aynı .kt dosyası içinde olmalıydı.
* Sonrası (Kotlin 1.5+): Doğrudan alt sınıflar, sealed sınıfın tanımlandığı aynı derleme birimi (module) içinde herhangi bir dosyada ve herhangi bir pakette tanımlanabilir.
Bu değişiklik, büyük projelerde sealed sınıfların kullanımını çok daha pratik hale getirmiştir. Artık ilgili alt sınıfları kendi dosyalarına veya paketlerine ayırarak kod tabanını daha düzenli tutmak mümkündür. Bu, modülerliği artırır ve dosya boyutlarını yönetilebilir kılar, özellikle çok sayıda alt sınıfı olan sealed hiyerarşilerinde büyük bir kolaylık sağlar. Bu gevşeme, sealed sınıfların derleme zamanı kapsamlılık kontrolü gibi temel faydalarını korurken, geliştirici deneyimini önemli ölçüde iyileştirmiştir.
Sonuç
Kotlin’deki sealed sınıflar, modern yazılım geliştirmede karşılaşılan yaygın sorunlara zarif ve etkili çözümler sunan güçlü bir dil özelliğidir. Durumların, olayların veya sınırlı tür hiyerarşilerinin tip güvenli, okunabilir ve bakımı kolay bir şekilde temsil edilmesini sağlarlar. Özellikle when ifadelerinde derleme zamanı kapsamlılık garantisi sunmaları, geliştirici hatalarını azaltır ve kod refactoring’ini çok daha güvenli hale getirir.
Enum sınıflarının sınırlamalarını aşarak her bir durumun kendi verisini taşımasına olanak tanıması ve soyut sınıfların/arayüzlerin esnekliğini kontrollü bir kalıtım mekanizmasıyla birleştirmesi, sealed sınıfları Kotlin ekosisteminde vazgeçilmez bir araç haline getirir. UI durum yönetimi (örneğin, Android’de MVI veya MVVM mimarilerinde), ağ isteklerinin sonuçlarını modelleme, hata işleme ve domain’e özgü dışlanmış hiyerarşiler oluşturma gibi birçok alanda yaygın olarak kullanılırlar.
Kotlin 1.5 ile gelen “aynı modül” kısıtlamasının gevşetilmesi, sealed sınıfların daha büyük ve karmaşık projelerde modülerliği artırarak kullanımını daha da kolaylaştırmıştır. Bu özellik, temiz mimariler oluşturmak, kodu daha anlaşılır kılmak ve uzun vadede yazılım kalitesini artırmak isteyen her Kotlin geliştiricisinin araç setinde bulunması gereken bir yapıdır. Sealed sınıfları doğru bir şekilde anlamak ve uygulamalarınıza entegre etmek, daha güvenilir ve bakımı kolay uygulamalar geliştirmenizin anahtarı olacaktır.
