Abstract Class vs Interface

Ömer Sungur
10 min readJul 30, 2023

Yazılımda sık sık birbirine karıştırılan iki yapıyı inceleyeceğiz. Hem abstract sınıflar hem de interfaceler soyutlama amacıyla kullanılan yapılardır. Birçok benzer noktaları da olsa bazı kısımlarda ayrışıyorlar. Bu yazıda abstract sınıflara ve interface yapılarına detaylıca göz atıp, aralarındaki benzerliklere ve farklara değineceğiz. Kodları Kotlin dili için yazacağız ve bu kodları Java diline decompile edip arka plandaki çalışma mantığına bakacağız. Bu iki yapıyı anlayabilmek için ilk olarak soyutlama (abstraction) ne demek onu öğrenmeliyiz.

Soyutlama (Abstraction): Soyutlama, bir sınıfa veya fonksiyona ait temel işlevlerin tanımlanması, detayların ise tanımlanmaması anlamına gelir. Günlük hayattan bir örnek verelim hemen. Bilgisayarımızı kullanırken fişini prize takarız ve düğmesine basarız değil mi? Fakat biz sadece bu iki işlemi yaparak bilgisayarı kullanırız. Prizden gelen gücün kablolarla bilgisayara işlenmesini veya bilgisayarın donanım parçalarının birbiri arasındaki haberleşmeleri sayesinde bize sunduğu görüntünün nasıl gerçekleştiğini, klavyeyi kullandığımızda ekrana yazılan girdilerin nasıl oluştuğuyla ilgilenmeyiz. Biz sadece bilgisayarın çalışmasıyla ve kullanıcı ekranında yaptığımız şeylerle ilgileniriz. İşte bu soyutlamadır. Arka planda ne olduğuyla ilgilenmeyiz. Sadece ilgili olduğumuz konuya bağlı kalırız.

Programlamada soyutlama 2 şekilde yapılıyor. Bunlar abstract sınıflar ve interface yapılarıdır. Bu iki yapıyı derinlemesine inceleyelim ve aralarında kıyaslamalar yapalım.

Abstract (Soyut) Sınıf Nedir?

Abstract sınıflar bir sözleşme gibidir. Bu abstract sınıfları miras alarak tanımlanması gereken yapıları kullanırız. Kod örneklerine bakalım ve abstract sınıfların özelliklerini inceleyelim.

Diyelim ki elimizde üçgen, kare ve diktörtgen olmak üzere 3 tane geometrik şeklimiz var. Her şeklin alanını ve çevresini hesaplamak istiyorum. Her şekil sınıfında ayrı ayrı çevre ve alan fonksiyonları tanımlamak yerine bunları bir abstract sınıf içinde tanımlayalım ve bu şekilleri bu ana sınıftan miras aldıralım.

import kotlin.math.sqrt

abstract class Shape() {

abstract fun calculateArea() // Alan hesaplayan fonksiyon
abstract fun calculatePerimeter() // Çevre hesaplayan fonksiyon
}

class Square(private val edge: Int) : Shape() {

override fun calculateArea() {
val area = edge * edge
println("Karenin Alanı: $area br²")
}

override fun calculatePerimeter() {
val perimeter = edge * 4
println("Karenin Çevresi: $perimeter br")
}

}

class Triangle(private val edge: Int, private val edge2: Int, private val edge3: Int) : Shape() {

override fun calculateArea() {
val u: Double = (edge + edge2 + edge3) / 2.0
val area = sqrt(u * (u - edge) * (u - edge2) * (u - edge3))
println("Üçgenin Alanı: $area br²")
}

override fun calculatePerimeter() {
val perimeter = edge + edge2 + edge3
println("Üçgenin Alanı: $perimeter br")
}

}

class Rectangle(private val edge: Int, private val edge2: Int) : Shape() {

override fun calculateArea() {
val area = edge * edge2
println("Diktörgenin Alanı: $area br²")

}

override fun calculatePerimeter() {
val perimeter = (edge * 2) + (edge2 * 2)
println("Diktörgenin Çevresi: $perimeter br")
}
}

fun main() {

val square = Square(20)
square.calculatePerimeter()
square.calculateArea()
println()

val rectangle = Rectangle(10,20)
rectangle.calculatePerimeter()
rectangle.calculateArea()
println()

val triangle = Triangle (5,12,13)
triangle.calculatePerimeter()
triangle.calculateArea()
}
üstteki kodun çıktısı

Örneğimizi inceleyelim. Buradaki bütün şekillerin alanı ve çevresi olduğu için bunları bir üst sınıfta yazıp soyutladık. Bu soyut sınıfı miras alan bütün sınıflar, bütün abstract yapıları override etmek zorundadır. (İstisnalarına değineceğiz). Bu yapıyla beraber ne kazandık?

  • Bütün şekil sınıflarını bir üst sınıftan miras alarak tanımladık ve bir hiyerarşi kazandırdık. Böylece daha derli toplu bir yazım oldu. Ek olarak kodun modülerliğini ve sürdürülebilirliğini artırdık.
  • Abstract sınıfın içinde yer alan yapıları alt sınıflarda tekrar tekrar yazmadan override ederek oluşturduk. Bu bize hem zaman kazandırdı hem de kullanmamız gereken bütün yapıları unutmadan sınıfların içerisinde tanımlayabildik.
  • Abstract sınıflar, polimorfizm yapısını sağlar. Polimorfizm: Üst sınıfta tanımlanan yapıların child sınıflarda bu yapıların değiştirilip farklı formlarda kullanılmasıdır.
  • Abstract sınıflar, kendi alt sınıflarına ortak davranış uygulamak için bir yol sağlayarak her bir alt sınıfta yazılması gereken kod miktarını azaltır.
  • Abstract sınıfları birden fazla sınıf miras alabilir ve içindeki yapıları bütün sınıflar kullanabilir. Böylece aynı abstract methodları ve propertyleri paylaşarak bu yapıları tekrar tekrar kullanılabilir hale getirir.

Burada 1000 tane şekil sınıfı olduğunu ve her sınıf içerisinde aynı göreve sahip 1000 tane fonksiyon olduğunu düşünün. Şekil sınıflarını soyut bir sınıftan miras almasaydık tek tek fonksiyonları tanımlayacaktık. Bu durumda hem zaman kaybı yaşayacaktık hem de sınıf içerisinde tanımlamayı unuttuğumuz fonksiyon yapılarıyla karşılaşacaktık. Abstract sınıfı miras alarak bir hiyerarşi sistemi kurduk ve şimdi her şey daha basit bir hale geldi. :)

Şimdi gelelim abstract sınıfların özelliklerine.

  • Abstract sınıfların içindeki bütün yapıların abstract olmasına gerek yoktur. Fonksiyonları bodyli bir şekilde tanımlayabiliriz. Ayrıca değeri olan propertyler de tanımlanabilir. Yani abstract sınıfların içinde state tutabiliriz.
abstract class Shape() {

abstract fun calculateArea()
abstract fun calculatePerimeter()

fun calculateAreaWithBody() {

}

var edge = 10
abstract var edge2: Int
}

Buradaki edge bir değişken değildir. Sadece bir property yapısıdır. Eğer property ve field konularına hakim değilseniz, bu linkten gerekli bilgilere ulaşabilirsiniz, yazının birkaç bölümünde daha bu yapıyı göreceğiz. Shape soyut sınıfının içindeki bütün abstract yapıları alt sınıflarda override etme zorunluluğumuz vardır fakat değeri olan propertyler ve bodyli fonksiyonlar için bu zorunluluk kalkıyor. Bu yapıları alt sınıflarda isteğe bağlı şekilde override edebiliriz. Bunun için bu yapıların önüne “open” kelimesini yazmamız gerekir ki bunları override edebilelim. Bunun sebebi ise Kotlin’de hemen hemen bütün yapılar default olarak public ve final olarak oluşturulur. Final olan bir yapı da alt sınıflarda kullanılamaz. Üstte yazdığımız Kotlin kodunu Java koduna dönüştürelim ve neler oluyor görelim. Bu kodu Java koduna decompile edelim. 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.

Abstract sınıfın içinde tanımlanan yapıların Java dilindeki karşılıkları

Görüldüğü gibi abstract olarak tanımladığımız edge2 propertysinin backing fieldı oluşturulmuyor. Ayrıca bu propertye ait get ve set metotlarına baktığımızda bodylerinin bile oluşturulmadığını görüyoruz. Bu yüzden dolayı da bu abstract property yapısına, tanımlandığı abstract sınıf içerisinden direkt bir değer ataması yapılamıyor. Sınıf içinde override edileceği zaman get ve set metotları tanımlandığı için orada bir değer ataması yapabiliyoruz. Abstract olarak tanımlamadığımız ve open yazmadığımız yapıların default olarak final şeklinde tanımlandığına dikkat edelim. Soyut sınıflar içerisinde olabildiğince bu “open” yapıları tanımlamamalıyız. Çünkü bu soyut sınıfı miras alan child sınıflarda bir karmaşa yaratabilir ve abstract sınıfların amacı bu değildir.

  • Abstract sınıflardan nesne oluşturulamaz. Normal bir sınıfla abstract sınıfın en ayırıcı noktalarından biri budur. Genel olarak base sınıfları abstract yaparız ki bu sınıfların nesneleri oluşturulmasın ve içeriği değiştirilmesin.
abstract sınıflardan nesne oluşturulamaz
  • Bir abstract sınıf başka bir abstract sınıfı miras alabilir. Bu durumda üst sınıfta bulunan abstract yapıları override etme zorunluluğu kalkar.
abstract sınıfların abstract yapıları

Normal sınıflar miras aldığı soyut sınıfın soyut yapılarını override etmezse bir hata alırız. Bu durum üç şekilde çözülebiliyor. Ya bütün soyut yapıları override ederiz ya da o sınıfı abstract yaparız. Diğeri yolu da aşağıda açıklayalım.

  • Hiyerarşik bir sistem düşünelim. A sınıfı üst sınıf, B sınıfı bu A sınıfını miras alan bir sınıf ve C sınıfı da B sınıfını miras alan bir sınıf olsun. A sınıfında override edilmesi zorunlu bir yapı varsa ve bu yapılar B sınıfında override edilirse C sınıfının artık herhangi bir şeyi override etme zorunluluğu kalmaz.
Child sınıfın iki tane üst sınıfı varsa
  • Abstract propertyleri constructorda da override edebiliriz.
abstract class Shape() {

var edge = 10
abstract val edge2: Int
}

class Square(override val edge2: Int): Shape() {

}

Abstract sınıfların birkaç özelliği daha var fakat bunları interface yapılarından sonra görmemiz daha iyi olacaktır.

Interface (Arayüz) Nedir?

Interfaceler abstract sınıflara çok benzer. Abstract sınıflar için bir kopya kağıdıdır, bir şablondur demiştik. Interfacelerler ise bir beceri setidir yani bir yapının yeteneklerini yazıyormuşuz gibi düşünebilirsiniz.

Diyelim ki elimizde köpek, kedi ve at sınıfları var. Bu hayvanların beceri setleri nelerdir? Koşmak, yemek yemek ve yürümek gibi bir sürü yetenekleri, becerileri olabilir. İşte bunları bir interface içinde tanımlamamız gerekir. Bir başka örnek, elimizde bir veri tabanı olsun. Bu veri tabanına ait veri ekleme, veri silme, veri güncelleme gibi işlemleri de interface içinde yazarız. İşte interface yapılarının abstract sınıflardan en ayrışan kısmı budur. Abstract sınıflarda is-a ilişkisi vardır. Kare bir şekildir (square is a shape). Intefacelerde ise can-do ilişkisi vardır. At koşabilir-koşar (the horse can run - the horse runs) gibi. Yani kısaca, abstract sınıflarda bir şey olma, interfacelerde ise bir şeyi yapabilme ilişkisi vardır. Üstte bahsettiğimiz hayvan örneğiyle bir interface yazalım.

fun main() {

val dog = Dog()
dog.eat()
}

interface IAnimal {

fun run()
fun eat()
}

class Horse() : IAnimal {
override fun run() {
println("The horse is running")
}

override fun eat() {
println("The horse is eating")
}

}

class Dog() : IAnimal {
override fun run() {
println("The dog is running")
}

override fun eat() {
println("The dog is eating")
}
}

Interface yapılarının ismini yazarken I harfiyle başlamak best practicedir (işin en iyi yapılma yolu). “I” yazarak o yapının bir interface olduğunu anlamamız daha hızlı oluyor. Şimdi interface yapılarının özelliklerine geçelim ve bu bölümde abstract sınıflarda görmediğimiz ek özelliklere de değinelim.

  • Interfacelerin abstract sınıflara göre en büyük farklarından biri state tutamıyor oluşlarıdır. Abstract sınıflarda bir property tanımlayıp ona ilk değer atamasını yapabiliyorduk, bunu interfacelerde yapamıyoruz.
interface içinde propertylere direkt değer ataması yapamıyoruz

Bu yapıyı şununla karıştırmayalım:

val myNumber: Int
get() = 10

Burada bir propertynin get metotuna bir değer atıyoruz, propertynin kendisine değil. Yani bu üstteki kodun şundan hiçbir farkı yoktur.

fun myNumberFunction(): Int {
return 10
}

Peki neden değer atayamıyoruz. Hemen aşağıdaki kodumuzu decompile edelim ve bakalım.

interface IAnimal {

fun run()
fun eat()

val myNumber: Int
get() = 20

fun myNumberFunction(): Int {
return 10
}

val myNumber2: Int
}

Abstract sınıflarda abstract propertylerin backing fieldı oluşturulmuyordu. Görüyoruz ki burada da backing field oluşturulmuyor. Sebebi ise interface içindeki yapılar default olarak abstract şeklinde tanımlanıyor. Buna sonraki maddelerde değineceğiz.

  • Bu kod bloğundan da anlaşıldığı gibi interface içinde bodyli fonksiyonlar tanımlanabilir. Bodysiz tanımlanan fonksiyonlar ve getter fonksiyonu yazılmayan propertyler alt sınıflarda override edilmelidir (Yine istisnalara bakacağız). Fakat bodyli fonksiyonlar ve get metotu yazılan propertylerin alt sınıflarda override edilmesine gerek yoktur.
  • Interface içinde tanımlanan yapılar default olarak abstract yapıdadır. Eğer fonksiyonun bodysini oluşturursak veya propertynin get metotunu yazarsak işte o zaman abstract durumu ortadan kalkar. Bu kez default olarak open hale geçerler. Yani isteğe bağlı olarak override edilebilirler.

Interfacelerin kendisi de default olarak abstract yapıda tanımlanırlar. Abstract olarak tanımlanan fonksiyonları ve propertyleri alt sınıflarda override etmek zorunda kalıyorduk fakat open kelimesini kullanmıyoruz nasıl oluyor diyecek olursanız, abstract kelimesi zaten yapıların (sınıf, interface) miras alınmasını ve fonksiyon, propertylerin de override edilmesini sağlıyor. Bu sebepten ötürü de abstract yapıları open yapmak gereksizdir. Kısaca, abstract yazarak open kelimesini kullanmış gibi oluyoruz fakat ek olarak override zorunluluğu geliyor. Buna abstract sınıflarda değinmemiştik. Abstract sınıflardaki bodyli fonksiyonların default hali interfacelerden farklı olarak open değildir. Bu yüzden override etmek istiyorsak open kelimesini yazmamız gerekiyor. Eğer işler çorba olmaya başladıysa merak etmeyin. Bu farkların ve benzerliklerin hepsine son bölümde bir tablo üzerinden ulaşacaksınız ve kafalara tam oturmuş olacak. Şu an sadece açıklamalar yapıyoruz :)

  • Abstract sınıf ve interface arasındaki bir diğer ayırıcı fark ise, soyut sınıflar inherit edilir ve Kotlin dilinin yapısı gereği sadece bir tane sınıfı miras alabiliriz. Interfaceler ise implement edilir ve bir child sınıf birden çok interfacei implement edebilir. Bu inherit etmek ve implement etmek Java dilinde kullanılan kalıplardır. Kotlin dilinde direkt olarak iki nokta koyup yapıyı yazıyoruz. Eğer nasıl ayırt ediliyor diye soracak olursanız. Bir diğer farka geçiyoruz.
  • Abstract sınıflar constructora sahipken interfacelerde constructor yoktur. Bir child sınıf bu yapıları kullanırken parantez kullanıyorsa o yapı bir sınıftır, ama parantez yoksa o yapı bir interfacedir. İkisi arasındaki ayrımı bu şekilde yapabiliriz.
  • Nasıl ki bir child abstract sınıf miras aldığı abstract sınıfın abstract yapılarını override etmek zorunda kalmıyorsa, aynı şekilde bir interface de implement ettiği interface yapılarını override etmek zorunda değildir.
  • Bir abstract sınıf bir abstract sınıfı miras alabilir. Bir abstract sınıf bir interfacei implement edebilir. Bir interface bir interfacei implement edebilir. Fakat bir interface bir abstract sınıfı miras alamaz.
  • Çok karşılaşmasakta şöyle bir özellik var: Bir sınıf iki tane interfacei implement ederse ve bu iki interfacede de birebir aynı fonksiyon varsa süper tip kullanarak çağrım yapılması zorunlu hale geliyor. Ek olarak normalde bir interface içinde bodyli fonksiyonları override etmemize gerek kalmıyordu. Eğer üstteki gibi iki interface içinde birerbir aynı fonksiyon varsa ve bu fonksiyonun bodysi varsa artık onu override etmemiz zorunlu hale geliyor.
interface A {

fun foo() {
print("A")
}

fun bar()

fun boo() {

}
}

interface B {

fun foo() {
print("B")
}

fun bar() {
print("Bar")
}

fun boo() {

}
}

class C() : A, B {

override fun foo() {
// super.foo() kullanımı yapılamaz. IDE hangi fonksiyonu çağıracağını anlamaz.
super<A>.foo()
super<B>.foo()
}

override fun bar() {
// super.bar() // A interface'i içindeki bar fonksiyonunun bodysi boş olduğu için burada super type kullanıma gerek yok.
}

override fun boo() { // iki interface içinde de bodyli bir şekilde bulunduğu için override etmek zorundayız!

}
}
  • Hem abstract sınıflar hem de interface yapıları parametre olarak kullanılabilir. Böylece bu yapıların içerisinde tanımlanan fonksiyonlara ve propertylere erişebiliriz.
interface BB {
val b: Int
get() = 10
}

abstract class AA: BB {
var a = 5
}

fun main() {

}

fun myFunc(param: AA) {
param.a = 10
println(param.b) // Abstract class BB'yi implement ettiği için onunda değişkenlerini kullanabiliyorum.
}
  • Son olarak, interface yapıları arayüz belirteci olarakta kullanılabilirler.
interface IAnimal {
var animalProperty: Array<IAnimal>

fun myFunc() {
animalProperty = arrayOf(Dog(), Cat())
}
}

class Dog : IAnimal {
override var animalProperty: Array<IAnimal> = arrayOf()
}

class Cat : IAnimal {
override var animalProperty: Array<IAnimal> = arrayOf()
}

Buradaki animalProperty bir arraydir. Normal de bir liste yazarken belirtecine Int veya String gibi, liste hangi elemanları içeriyorsa onun tipini verirdik. Burada da bir interface verdik.

Bu anlatılanları topladım ve abstract sınıflar ve interface yapıları arasındaki en temel farklardan oluşan bir tablo hazırladım. Bu tabloya çok daha fazla fark yazılabilir fakat bu iki yapı arasındaki farkı anlamak için yeterlidir.

Soyut sınıflar ve arayüzler arasındaki temel farklar

Tabloya eklemeyi unuttuğum önemli bir şey daha var o da, abstract sınıfların constructora sahip olması fakat interface yapılarının bir constructora sahip olmaması.

Bir de benzerlikler için bir tablo oluşturdum. Yine buraya birçok benzerlik yazabiliriz ama temel olarak aklıma gelenler bunlar.

Soyut sınıflar ve arayüzler arasındaki temel benzerlikler

Artık abstract sınıflar ve interface yapılarının ne olduğunu ve ne için kullanıldığını biliyoruz. Umarım faydalı bir yazı olmuştur. Başka bir yazıda görüşmek üzere…

Bana Linkedin üzerinden ulaşabilirsiniz.

Son Güncelleme: 30.07.2023

--

--