Enum Class vs Sealed Class
Kotlin’de birbirine benzeyen ve çok kullanışlı olan 2 yapıyı detaylarıyla inceleyeceğiz. Hem enum classların hem de sealed classların özelliklerine göz atıp aralarındaki farkları karşılaştıracağız. Ek olarak sealed interfaceler nedir, onlara da değineceğiz. Baştan şunu da söylemem gerekiyor ki yapıları gereği bu konuları iyi anlayabilmek için kalıtım, interface ve abstract classları yani kısacası OOP’e hakim olmamız gerekiyor.
Enum Class Nedir? Ne İşe Yarar?
Enumlar, aynı veri kümesindeki elemanları gruplamamızı sağlayan sınıflardır. İlk olarak nasıl kullanılıyor, bir örnek verelim ve sonra özellikleriyle birlikte detaylara dalalım.
enum class RGB {
RED, GREEN, BLUE
}
fun main() {
println(RGB.RED) // RED
}
Enum classların kullanımı bu şekilde yapılmaktadır. Oluşturduğumuz enum sınıfının içerisine aynı veri kümesindeki elemanları yazarız ve istediğimiz yerde bu değerleri direkt kullanabiliriz. Nesne oluşturulmadan bir çağrım yapıldığını görüyoruz, enumun özelliklerine geçelim ve neden olduğunu detaylı bir şekilde görelim.
- Enum classların içinde oluşturulan değerlere enum sabitleri denir ve bu sabitler birer nesnedir. Kotlin’de obje dediğimiz şeyler arka planda statik sınıflardır. Yani enum içerisinde oluşturulan enum sabitleri birer statik sınıftır. Statik bir yapıya nesne oluşturmadan direkt erişebiliyorduk, aynı üstte yaptığımız gibi. Ek olarak bu statik sınıfların tamamı yani enum sabitleri, içerisinde oluşturulduğu enum sınıfını miras alır.
Bir enum class oluşturalım ve bu Kotlin kodunu Java koduna dönüştürüp arka planda ne oluyor bakalım. Bunun için IntelliJ IDEA shift+shift kısa yolu ile açılan pencerede show kotlin byte code yazalım ve ilgili aracı seçelim. Sağ tarafta açılan pencereden decompile butonuna basalım.
Her enum sabiti bir sınıf olduğu için normal sınıflar gibi constructorı ve bodysi olabilir. Enum sabitleri arasına virgül koyarak tanımlama yapıyoruz. Enum sabitlerinin bütün harflerini büyük yazmak yaygın bir kullanımdır. Eğer ki enum sabitlerinden sonra bir property veya fonksiyon tanımlaması yapacaksak enum sabitlerinin bittiğini belirtmek için en sondaki enum sabitinin sonuna noktalı virgül koyarak diğer yapılardan ayırmamız gerekir.
enum class RGB() {
RED() {
var number = 5
},
GREEN() {
override fun colorValue(value: String) {
super.colorValue(value)
}
},
BLUE();
open fun colorValue(value: String) {
}
}
Görüyoruz ki enum sabitleri içerisinde bir fonksiyon veya property tanımlanıyorsa arka planda direkt olarak o enum sabiti aslında static sınıf olarak tanımlanıyor ve içinde tanımlandığı enum sınıfını da miras alıyor.
- Enum classlardan nesne oluşturulamaz. Çünkü arka planda constructorları private olarak oluşturulur. Primary constructor veya secondary constructorlara private dışında herhangi bir erişim belirleyicisi (public, protected, internal) vermeye kalkarsak hata alırız (Constructor must be private in enum class).
- Enum classlar abstract, sealed, open ve inner yapısında olamazlar. Bu kelimeleri enum classına getirdiğimizde hata alırız.
- Enum classlar interface yapılarını implement edebilirler fakat sınıfları miras alamazlar.
- Enum sınıflarının içinde abstract fonksiyonlar ve abstract propertyler tanımlanabilir. Nasıl ki normal bir sınıf abstract sınıfı miras aldığında içindeki bütün abstract yapıları override etmesi gerekiyorsa enum içinde de bu durum aynı şekilde geçerlidir. Bunun sebebi, enum sabitleri, içinde tanımlandığı enum sınıfını miras almasıdır. Eğer ki abstract sınıfların özelliklerini bilmiyorsanız bu link, ne olduklarını anlamanıza yardımcı olabilir.
Open yapıların override edilmesi zorunlu değildir fakat abstract yapılar kesinlikle override edilmek zorunda.
- Enum sınıfı constructorına parametreler verebiliriz ve bu parametreleri enum sabitlerinde de vermek zorunda kalırız. İşte bu yüzden yazının başında demiştik ki bu durumları iyi anlayabilmek için OOP mantığını kavramak gerekiyor. Bir sınıf bir sınıfı miras aldığında miras alınan sınıfın constructorlarından biri kullanılmak zorundadır. Bu yüzden de enum sabitleri parametrelerine bu değeri vermek zorunda kalıyoruz çünkü bütün enum sabitleri enum sınıfını miras alıyor.
enum class RGB(val colorValue: String) {
RED("#FF0000"),
GREEN("#008000"),
BLUE("#0000FF")
}
fun main() {
println(RGB.RED.colorValue) // #FF0000
}
Her enum sabiti colorValue değişkenine uygun bir parametre değeri vermek zorundadır. Buradaki değerlere, direkt oluşturulduğu sabit üzerinden ulaşabiliriz. Çünkü kalıtımda üst sınıfa ait özellikler child sınıflarında özelliğidir.
- Enum sabitlerinin en önemli özelliği, tip güvenliğini sağlamasıdır.
enum class RGB {
RED, GREEN, BLUE
}
fun changeColor(): RGB {
return RGB.RED
}
Fonksiyonumuz dönüş tipi bir enum ve biz bu fonksiyondan sadece ve sadece enum sabitlerini dönerek çıkabiliriz. Bu sayede enum sabitlerinde yer alan ifadeler dışında bir değer veremeyiz. Yanlış verileri return etmemizi engeller. Bu da bize tip güvenliğini sağlar.
- Her enum sabiti için default olarak name ve ordinal metotu vardır. Name metotu ile enum sabitinin ismi alınır. ordinal metotu, enum sabiti kaçıncı sırada tanımlandıysa onun indisini verir.
enum class RGB {
RED, GREEN, BLUE
}
fun main() {
println(RGB.RED.name) // RED
println(RGB.RED.ordinal) // 0 (0.indiste tanımlandı)
}
- Her enum sabiti bir sınıf olduğu için bunları virgül ile ayırarak yazıyoruz, normal sınıfa ait özellikler enum sabitleri için de vardır. toString, equals gibi metotları kastediyorum. Bu metotları kendimiz override edipte kullanabiliriz. Sınıftan bir nesne oluşturup o nesneyi yazdırdığımızda toString metotu çağrılıyor. Aşağıdaki kod bloğunda override edip kullandığımız için artık bizim dönderdiğimiz değer yazılıyor. toString metotunu enum sabitinin ismini kullanmak istemediğimiz zaman kullanabiliriz. Burada bahsettiğim sınıflar için default olarak oluşturulan metotlar hakkında bilgileriniz eksikse bu linkten yarlarlanabilirsiniz.
enum class RGB(val colorValue: String) {
RED("#FF0000"),
GREEN("#008000"),
BLUE("#0000FF") {
override fun toString(): String {
return "BLUE VALUE"
}
}
}
fun main() {
println(RGB.BLUE) // BLUE VALUE
}
- Enum sınıflar eğer bir interface yapısını implement ediyorsa o interface içindeki gövdesiz fonksiyonları veya get değeri yazılmamış propertylerini override etmemiz gerekiyor. Bu override işlemini ister tek bir kez enum sınıfı içinde yaparız ister bütün enum sabitlerinin içinde tek tek override ederiz. Enum sınıfının içinde override ettikten sonra enum sabitleri için bu zorunluluk kalkıyor. Bu durumlardan abstract sınıflar ve interface yapılarını karşılaştırdığım yazımda detaylıca bahsetmiştim. Aşağıdaki kod blokları bize 2 kullanım seçeneği sunuyor. Eğer bu fonksiyonu enum sabitleri için özelleştireceksek tabii ki enum sabitleri içerisinde override etmeliyiz.
enum class RGB(): OnClick {
RED(),
GREEN(),
BLUE() {
};
override fun changeUI() { // enum sınıfı içerisinde bir kez override ediyoruz.
}
}
interface OnClick {
fun changeUI()
}
enum class RGB(): OnClick {
RED() {
override fun changeUI() { // bütün enum sabitleri içerisinde override ediyoruz
TODO("Not yet implemented")
}
},
GREEN() {
override fun changeUI() {
TODO("Not yet implemented")
}
},
BLUE() {
override fun changeUI() {
TODO("Not yet implemented")
}
};
}
interface OnClick {
fun changeUI()
}
- Enum sınıfların içindeki bütün enum sabitlerini listelemek gibi bir seçeneğimiz var. Önceden bunun için values() metotu kullanılıyordu fakat Kotlin 1.9.0 versiyonu ile birlikte gelen entries yapısını kullanarak bütün enum sabitlerini listeleyebiliriz. Bu maddeyle beraber enum sınıfları içerisindeki bütün enum sabitlerinin açık bir şekilde görülebildiğini anlıyoruz.
enum class RGB() {
RED(),
GREEN(),
BLUE()
}
fun main() {
println(RGB.entries) // [RED, GREEN, BLUE]
}
Buradaki enum sınıfları için hep RGB örneğini verdik fakat birçok yapı için kullanabiliriz. Yani enum sınıfına ColorType diye bir isim tanımlayıp birçok renk değerini de içine verebilirdik, tamamen kullanıma bağlı şekilde istediğimiz yapıyı kurabiliriz.
Sonuç olarak;
- Enumlar, aynı kümedeki verilerin gruplanmasında rol oynar.
- Enumlar, tip güvenliğini sağlar. (Type safety)
- Enum içindeki bütün opsiyonları açık bir şekilde görebiliriz (entries ile).
- Enum sabitleri enum sınıfına bağlı olduğu için bu sabitler üzerinde oldukça fazla kontrole sahip oluruz.
Sealed Class Nedir?
Sealed classlar da enum classlar gibi gruplama yapmak için kullanılan yapılardır. Enumlar verileri gruplarken sealed classlar sınıfları gruplar. Sealed classların amacı, bir hiyerarşi sistemi oluşturmak ve bu hiyerarşiye ait sınıfları bir arada toplayarak bu sınıflar üstünde kontrol sağlamaktır. Sealed classların içinde tanımlanan yapılar enum classlara göre çok daha özelleştirilebilir yapıdadır. Örneğin bu subclassların tiplerini değiştirebiliriz, bu subclasslara özel parametreler ekleyebiliriz. Ek olarak bu subclasslar interfaceleri impelement edebilir
Sealed class için yapılan bu özelleştirmeleri enumlarda yapamıyoruz. Fakat bu demek değildir ki her yerde sealed class kullanalım. Enum classlarında sealed classlara göre avantajları vardır. Sealed classın subclassları sealed classı miras alması gerekir ki bu classlar sealed class çatısı altında toplansın.
sealed class RGBSealed(colorValue: String) {
abstract class Red(colorValue: String, message: String) : RGBSealed(colorValue)
open class Green(colorValue: String, number: Int) : RGBSealed(colorValue)
data class White(val colorValue: String) : RGBSealed(colorValue), Example
interface Extra
}
interface Example
Görüldüğü gibi sealed class içindeki yapılarını özelleştirebiliyoruz. Bu sınıfları data sınıf, abstract sınıf gibi yapılara çevirebiliyoruz ve ek olarak parametrelerine sealed classtan bağımsız değerler verebiliyoruz. Bunu enum sabitleri üzerinde yapamayız. Sealed classların en çok kullanıldığı yapı, internetten veri çekildiğinde oluşabilecek cevapları ele aldığımız durumlardır. Bu classları when bloğuna sokup bize dönen cevabı alırız ve buna göre işlem yaparız. Sealed classların içindeki yapıları kontrol etmek için yaygın olarak when yapısı kullanılır.
Bu arada sealed class içinde genellikle object, class ve data classlar tanımlanır. Diğer yapıların kullanılması duruma göre değişmekle beraber çok mantıklı değildir.
sealed class HttpError(val code: Int) {
class NotFound(val resource: String, code: Int) : HttpError(code) // 404
class Unauthorized(val message: String, code: Int) : HttpError(code) // 401
class InternalServerError(code: Int) : HttpError(code) // 500
}
fun handleError(error: HttpError) {
when (error) {
is HttpError.NotFound -> println("Resource not found: ${error.resource}")
is HttpError.Unauthorized -> println("Unauthorized: ${error.message}")
is HttpError.InternalServerError -> println("Internal Server Error: ${error.code}")
}
}
fun main() {
val error1 = HttpError.NotFound("Not Found",404)
val error2 = HttpError.Unauthorized("Invalid credentials",401)
val error3 = HttpError.InternalServerError(500)
handleError(error1) // Resource not found: Not Found
handleError(error2) // Unauthorized: Invalid credentials
handleError(error3) // Internal Server Error: 500
}
Android projelerinde de çok sık karşılaştığımız sealed classlar, internetten veri çektiğimizde bize dönen cevabın başarılı, başarısız veya verinin hala yüklendiği bilgilerini içeren sınıflar ile kuruluyor. Bu gelen cevaba göre kullanıcı arayüzünü güncelliyoruz.
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
class Success<T>(data: T) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T>(data: T? = null) : Resource<T>(data)
}
Android detaylarına çok dalmadan bu yapının nasıl kullanıldığını kısaca anlatalım. İnternetten verileri retrofit ile çekiyorsak bunun için oluşturduğumuz @GET fonksiyonları için bir dönüş tipi yazarız. İşte bu dönüş tipi üstte tanımladığımız Resource sınıfı olur ki internetten çektiğimiz veriyi gözlemleyip kullanıcıya bildirelim. Buradaki generic type kullanılmasının sebebi uygulamada kullandığımız veri modelinin türünü bilmememizden kaynaklanır. İnternetten veri çekmek için tanımladığımız fonksiyonu viewModel içerisinde çağrırız oluşacak sonuçları da fragmentta gözlemleriz ve kullanıcı arayüzünü güncelleriz.
Sealed classların da özelliklerine bakalım.
- Sealed classlar default olarak abstract yapılardır. Bu da demek oluyorki abstract sınıflar hangi özelliklere sahipse sealed classlarda sahiptir.
sealed class ExampleSealed() {
class Example() : ExampleSealed() {
override fun printFunction() {
}
}
abstract class Example2() : ExampleSealed()
abstract fun printFunction()
}
Abstract sınıflarda bu durumları çok konuştuğumuz için detaylarına tekrardan girip yazıyı uzatmayalım.
- Sealed classlardan nesne oluşturulamaz çünkü constructorları default olarak protected şeklinde oluşturulur. Sadece private erişim belirleyicisini kullanabiliriz. Public ve internal için hata alırız.
Buradaki kritik durum şudur: Eğer sealed sınıfına ait süslü parantezler dışına çıkarsak private constructor ile miras alınamaz. Bu yüzden dolayı constructorlar default olarak protected yapıdadır. Yani herhangi bir sınıfın sealed sınıfı miras alması için ille de o sealed sınıfa ait body içinde yer alması gerekmiyor.
- Sealed classın subclassları arka planda statik olarak tutulmazlar. Enumlar ve sealed classların en büyük farklarından birisi de budur. Sealed classları Java koduna decompile edelim ve bakalım.
sealed class ColorSealed() {
abstract class Red() : ColorSealed()
open class Green() : ColorSealed()
data class White(val colorValue: String) : ColorSealed()
class Blue : ColorSealed()
inner class Black : ColorSealed()
object Pink : ColorSealed()
}
Bu selaed classı Java koduna decompile edelim ve arka planda neler oluyor görelim.
O da ne? Black isimli sınıf dışında bütün sınıfların statik olarak tanımlandığını görüyoruz, ama demiştik ki sealed classın subclassları statik olarak tanımlanmaz. Gerçekten de statik tanımlanmazlar, şöyle bir durum var ki bu karmaşaya sebep oluyor: Eğer sealed classın subclasslarını sealed bodysi içinde tanımlarsak, nested class ve inner class kavramına girmiş oluyoruz. Nested ve inner classlar bu yazının konusu değil fakat kısaca şöyle diyebiliriz. Sınıf içerisinde bir sınıf tanımlandığı zaman ona nested sınıf denir. Eğer sınıf içerisinde tanımlanan sınıfa inner kelimesini getirirsek o inner sınıf olur. Bunların birbirinden birkaç farkı daha var ama bu yazı için bilmemiz gereken şey, nested classların arka planda statik olarak oluşturulduğu ve bu classlara nesne oluşturulmadan erişebilmemizdir. İşte tam da bu yüzden sealed classların bodysi içine tanımladığımız yapıları inner yapmıyorsak o yapılar default olarak nested sınıf gibi davranıyor ve statik bir durum oluşuyor. Bu sealed sınıfın subclasslarını body içine yazmayalım ve farkı görelim.
Görüyoruz farkı değil mi? Olay tamamen süslü parantezler içerisinde tanımlayıp tanımlamamaktan kaynaklanıyor. Body içinde yazarsak aklınıza nested ve inner classlar gelsin :)
- Sealed classların subclassları tanımlanırken bu yapıların aynı paket ve aynı modül içinde olması gerekiyor. Bu sayede üçüncü taraf istemciler bu sealed classları genişletemezler. Yani kısacası ne gördüysen onu kullanabilirsin :) Bu da bize güvenli bir sistem sağlıyor.
Sealed Interface Nedir?
Sealed classların bir hiyerarşi sistemi oluşturup alt sınıfları bir arada tuttuğunu görmüştük. Sealed interface yapılarının da bundan bir farkı yok. Nasıl ki normal bir sınıfı sealed yapıp istediğimiz sınıfları bu yapı altında topluyorsak sealed interface içinde de aynısını yapabiliriz. Abstract class ile normal interface arasındaki farklar ne ise bu yapılar arasındaki farkları da benzetebiliriz. Sealed interfaceler state tutamaz, içerisindeki bodysiz fonksiyonlar default olarak abstracttır vs. vs. Bunun içinde bir örnek verelim ve yazıyı sonlandıralım.
sealed interface StateUI {
fun OnClick()
fun SwipeLeft()
fun SwipeRight()
}
class UI(): StateUI {
override fun OnClick() {
println("Clicked!")
}
override fun SwipeLeft() {
println("Swiped Left")
}
override fun SwipeRight() {
println("Swiped Right")
}
}
İşin özü, eğer ki yapıları bir hiyerarşi sisteminde tutacaksanız, constructor kullanmayacaksanız, stateler tutmayacaksanız ve ek olarak bu yapılar normal bir interface içinde tutulacak gibi yapılarsa (tetiklenmeyi bekleyen) bunları sealed interface içinde tutmak daha mantıklıdır. Diğer durumlarda hiyerarşi sağlamak için sealed class kullanabiliriz.
Enum classların ve sealed classların ne için ve nasıl kullanıldığını anlatmaya çalıştım. Umarım bu yazı sizin için faydalı olmuştur…
Bana Linkedin üzerinden ulaşabilirsiniz.
Son Güncelleme: 05.08.2023