Skip to main content
11 min read

OOP: Nesne Yönelimli Programlama: Abstraction (Soyutlama) Rehberi

Mustafa Kürşad BaşerMustafa Kürşad BAŞER
OOP: Nesne Yönelimli Programlama: Abstraction (Soyutlama) Rehberi

Abstraction (Soyutlama) Nedir?

Key Takeaways

  • Soyutlama, bir şeyin ne yaptığını anlatıp nasıl yaptığını saklamaktır. Kapsülleme durumu korur, soyutlama karmaşıklığı gizler.
  • Abstract sınıf ortak durum ve ortak kod taşıyabilir; arayüz sadece sözleşme sunar. Java’da bir sınıf tek bir sınıftan türer ama istediği kadar arayüz uygulayabilir.
  • Java 8 ile gelen default metotlar arayüzlere davranış ekleyebilme imkanı verdi ve iki yapı arasındaki farkı daralttı.
  • İki arayüzden aynı imzalı default metot gelirse Java derleme hatası verir. Çözümü açıkça override edip Arayuz.super.metot() ile hangisini istediğinizi söylemektir.
  • Her soyutlama bir miktar sızdırır. Erken kurulan, tek uygulaması olan soyutlama ise çözdüğünden fazla karmaşıklık üretir.

Araba kullanmayı öğrenirken motorun içinde ne olduğunu öğrenmezsiniz. Direksiyon, pedallar ve vites yeterlidir. Bu üçü size arabanın ne yapabileceğini söyler, nasıl yaptığını değil.

Soyutlama tam olarak bu. Kullanıcıya anlamlı bir yüzey sunup geri kalan her şeyi arkada bırakmak.

Serinin son yazısındayız. Sınıf, nesne, kalıtım, kapsülleme ve çok biçimlilik konularını daha önce ele almıştım; bu yazıda hem soyutlamayı hem de altı kavramın birlikte nasıl çalıştığını göreceğiz.

Soyutlama ile kapsülleme aynı şey değil

İki kavram sürekli birbirine karıştırılıyor, çünkü ikisi de "gizlemek" fiiliyle anlatılıyor. Gizledikleri şey farklı.

Kapsülleme durumu gizler. Amacı, nesnenin geçersiz bir duruma düşmesini engellemek. Bir banka hesabının bakiyesini private yapıp değişikliği paraCek metoduna bağlamanız kapsüllemedir; konuya serinin kapsülleme yazısında girmiştim.

Soyutlama ise karmaşıklığı gizler. Amacı, kullanan tarafın ilgilenmesi gerekmeyen detayı görmemesi. List arayüzünü kullanırken elemanların dizide mi bağlı listede mi tutulduğunu bilmezsiniz ve bilmeniz de gerekmez.

Kısaca: kapsülleme "bunu bozamazsın" der, soyutlama "bunu bilmene gerek yok" der. İkisi birlikte çalışır ama farklı problemleri çözer.

Abstract Class

Abstract sınıf, doğrudan nesnesi üretilemeyen sınıftır. Türetilmek için vardır. İçinde gövdesi olan metotlar da, gövdesiz bırakılıp alt sınıfa devredilen metotlar da bulunabilir.

java
public abstract class Rapor {    private final String baslik;    private final LocalDateTime olusturulma;     protected Rapor(String baslik) {        if (baslik == null || baslik.isBlank()) {            throw new IllegalArgumentException("Başlık zorunlu");        }        this.baslik = baslik;        this.olusturulma = LocalDateTime.now();    }     // Her rapor tipi kendi biçimini bilir.    public abstract String uret();     // Ortak davranış burada bir kez yazılır.    protected String basligiBicimlendir() {        return "[" + olusturulma.toLocalDate() + "] " + baslik;    }}

Bu sınıf iki iş yapıyor. uret metodunu gövdesiz bırakarak alt sınıfları onu yazmaya zorluyor. basligiBicimlendir metodunu ve baslik alanını da bir kez tanımlayıp bütün alt sınıflara devrediyor. Yani hem sözleşme hem ortak kod taşıyor; arayüzün yapamadığı da tam olarak bu ikincisi.

Kurucunun protected olmasına dikkat edin. Abstract bir sınıfın nesnesi zaten üretilemez, dolayısıyla bu teknik bir zorunluluk değil. Niyeti belli etmek için: bu kurucu yalnızca super(...) çağrısıyla, alt sınıftan kullanılacak.

Interface

Arayüz ise saf sözleşmedir. Ne yapılabileceğini söyler, nasıl yapılacağına karışmaz.

java
public interface Gonderilebilir {    void gonder(String mesaj);} public class Epostaci implements Gonderilebilir {    @Override    public void gonder(String mesaj) {        // SMTP üzerinden gönderim    }}

Aradaki en belirleyici fark Java’nın kalıtım kuralında saklı. Bir sınıf yalnızca tek bir sınıftan türeyebilir, ama istediği kadar arayüz uygulayabilir. Kalıtım kotanız bir tane; arayüz kotanız sınırsız.

Arayüzler durum tutamaz. İçlerine yazdığınız alanlar örtük olarak public static final olur, yani sabit sayılır. Nesneye ait bir alan tanımlayamazsınız.

Hangisini seçmeli

Karar tek bir soruya bakarak verilebiliyor: paylaşmak istediğiniz şey ortak bir durum mu, yoksa yalnızca bir yetenek mi?

ÖlçütAbstract ClassInterface
Ortak alan (durum) taşır mıEvetHayır, sadece sabit
Kurucu metodu var mıVarYok
Aynı anda kaç tanesi kullanılabilirBir taneSınırsız
Erişim belirleyicileriprivate dahil hepsiVarsayılan public, Java 9’dan beri private metot da yazılabilir
Uygun olduğu ilişki"bir çeşididir""şunu yapabilir"
Tipik örnekAbstractListComparable, Runnable
Abstract class ile interface arasındaki pratik ayrımlar

İlişki adlarındaki fark yön gösterici. Kartal bir Kus çeşididir, bu kalıtımdır. Ama Kartal aynı zamanda Ucabilir yeteneğine sahiptir ve o yetenek Ucak sınıfında da bulunur, aralarında hiçbir akrabalık olmadan. İkincisi arayüz işidir.

Kararsız kaldığınızda arayüzle başlayın. Sonradan ortak koda ihtiyaç duyarsanız arayüzü uygulayan bir abstract sınıf eklemek kolaydır. Ters yön, yani mevcut bir kalıtım hiyerarşisini sökmek, çok daha pahalıdır.

Java 8 denklemi değiştirdi

Yukarıdaki ayrım 2014’e kadar keskindi. Java 8, arayüzlere gövdeli metot yazabilme imkanı getirdi.

java
public interface Gonderilebilir {     void gonder(String mesaj);     // Uygulayan sınıflar yazmak zorunda değil.    default void topluGonder(List<String> mesajlar) {        mesajlar.forEach(this::gonder);    }}

Bunun asıl amacı tasarımı zenginleştirmek değildi, geriye dönük uyumu korumaktı. Collection arayüzüne stream() metodunu eklemek gerekiyordu ve o arayüzü uygulayan milyonlarca satır kod vardı. default olmasaydı Java 8’e geçen her proje derlenmeyi bırakırdı.

Yan etkisi de oldu: çoklu kalıtımın klasik problemi arayüzler üzerinden geri döndü. İki arayüzden aynı imzalı birer default metot gelirse derleyici hangisini seçeceğini bilemez ve hata verir. Java bunu sessizce çözmek yerine kararı size bırakır:

java
interface A {    default String selam() { return "A"; }} interface B {    default String selam() { return "B"; }} class C implements A, B {    @Override    public String selam() {        return A.super.selam();   // hangisini istediğinizi açıkça yazarsınız    }}

Java 9 ile arayüzlere private metot da eklendi, böylece birden fazla default metodun paylaştığı yardımcı kod dışarıya sızmadan yazılabiliyor. Arayüzler yıllar içinde sözleşme olmaktan çıkıp davranış da taşıyan yapılara dönüştü. Yine de tek bir şeyi hala yapamıyorlar: nesneye ait durum tutmak. Karar verirken bakılacak asıl ölçüt bugün de bu.

Soyutlamanın bedeli

Soyutlama bedava değil ve bu, tanıtım yazılarında pek geçmez.

Joel Spolsky’nin 2002’de yazdığı bir kural var: bütün önemsiz olmayan soyutlamalar, bir dereceye kadar sızdırır. Detayı gizlersiniz ama detay bir gün kendini hatırlatır. Ağ üzerindeki bir dosyayı yerel dosya gibi okuyabilirsiniz, ta ki bağlantı kopana kadar. List arayüzü size tekdüze bir yüzey sunar, ama LinkedList üzerinde indeksle dolaşırsanız performans çöker. Soyutlama arkasındaki gerçeklik yok olmaz, sadece görünmez olur.

İkinci bedel daha yaygın: erken soyutlama. Tek uygulaması olan bir arayüz, kod tabanına gezinilecek fazladan bir dosya ekler ve karşılığında hiçbir esneklik vermez. "İleride başka bir sağlayıcı gelirse" diye kurulan katmanların çoğunda o sağlayıcı hiç gelmez.

Kullanışlı bir eşik var: ikinci gerçek uygulama ortaya çıkana kadar bekleyin. İki somut örneği yan yana gördüğünüzde aralarındaki ortak yüzeyin ne olduğunu tahmin etmeniz gerekmez, okuyabilirsiniz.

Altı kavram tek örnekte

Seriyi kapatırken hepsini bir arada görelim. Bildirim gönderen küçük bir tasarım, altı kavramın da yerini gösteriyor:

java
// SOYUTLAMA: çağıran taraf yalnızca bunu bilir.public interface Bildirim {    void gonder(String mesaj);} // SINIF: bildirim şablonu. KAPSÜLLEME: alanlar private, doğrulama kurucuda.public abstract class TemelBildirim implements Bildirim {    private final String alici;     protected TemelBildirim(String alici) {        if (alici == null || alici.isBlank()) {            throw new IllegalArgumentException("Alıcı zorunlu");        }        this.alici = alici;    }     protected String getAlici() {        return alici;    }} // KALITIM: ortak doğrulama ve alan buradan gelir.public class EpostaBildirimi extends TemelBildirim {     public EpostaBildirimi(String eposta) {        super(eposta);    }     @Override    public void gonder(String mesaj) {        System.out.println("E-posta -> " + getAlici() + ": " + mesaj);    }} public class SmsBildirimi extends TemelBildirim {     public SmsBildirimi(String telefon) {        super(telefon);    }     @Override    public void gonder(String mesaj) {        System.out.println("SMS -> " + getAlici() + ": " + mesaj);    }} // NESNE ve ÇOK BİÇİMLİLİK: aynı çağrı, farklı davranış.public class Uygulama {    public static void main(String[] args) {        List<Bildirim> kanallar = List.of(            new EpostaBildirimi("kullanici@ornek.com"),            new SmsBildirimi("+90 555 000 00 00")        );         kanallar.forEach(kanal -> kanal.gonder("Siparişiniz kargoya verildi."));    }}

main metodunda hangi kanalın kullanıldığına dair tek bir if yok. Yeni bir kanal eklemek, Bildirim arayüzünü uygulayan bir sınıf yazmaktan ibaret; mevcut hiçbir dosyaya dokunulmuyor. Altı kavramın birlikte ürettiği sonuç bu.

Sonuç

Soyutlama, nesne yönelimli programlamanın en kolay yanlış kullanılan kavramı. Az yaparsanız kod tabanınız somut sınıflara çakılı kalır; fazla yaparsanız üç dosya gezmeden tek bir satırın ne yaptığını anlayamazsınız.

Dengeyi bulmanın kestirme yolu yok, ama iki soru yardımcı oluyor. Bu detayı kullanan tarafın gerçekten bilmesi gerekiyor mu? Ve bu soyutlamanın bugün kaç uygulaması var?

Seri burada tamamlanıyor. Sınıf ve nesneyle başladık, kalıtımla devam ettik, kapsülleme ve çok biçimlilikle sürdürdük, soyutlamayla kapattık. Bu altı kavram tek başına iyi tasarım üretmiyor; onları ne zaman kullanmayacağınızı bilmek de gerekiyor. Bir sonraki adım için S-O-L-I-D Prensipleri yazısına göz atmanızı öneririm, orada bu kavramların hangi kurallar çerçevesinde bir araya geldiğini ele aldım.

Frequently Asked Questions

Soyutlama ile kapsülleme arasındaki fark nedir?
İkisi de gizleme yapar ama farklı şeyleri. Kapsülleme bir güvenlik önlemidir; nesnenin kurallarını dışarıdan çiğnenemez hale getirir. Soyutlama bir sadeleştirmedir; ilgilenilmesi gerekmeyen detayı kullanıcının önünden kaldırır. Bir sınıf kapsüllenmiş olup hiç soyutlanmamış olabilir, tersi de mümkün.
Abstract class mı interface mi kullanmalıyım?
Paylaşmak istediğiniz şey ortak alan ve ortak kodsa abstract class, yalnızca bir yetenekse interface. Java’da bir sınıf tek bir sınıftan türeyebildiği için kalıtımı gerçekten gerektiren durumlara saklayın. Kararsızsanız arayüzle başlayın; sonradan abstract sınıf eklemek, kurulmuş bir hiyerarşiyi sökmekten kolaydır.
Java 8 default metotlar geldikten sonra abstract class gereksiz mi kaldı?
Kalmadı. Arayüzler artık davranış taşıyabiliyor ama hala nesneye ait durum tutamıyor; alanları örtük olarak public static final oluyor. Ortak bir alanı ve onu doğrulayan bir kurucuyu paylaşmanız gerekiyorsa abstract class hala tek seçenek.
İki arayüzden aynı default metot gelirse ne olur?
Derleyici hata verir ve sizi seçim yapmaya zorlar. Metodu uygulayan sınıfta açıkça override edip Arayuz.super.metot() söz dizimiyle hangi sürümü istediğinizi belirtirsiniz. Java bu belirsizliği sessizce çözmeyi bilinçli olarak reddeder.
Her sınıf için arayüz yazmalı mıyım?
Hayır. Tek uygulaması olan bir arayüz esneklik değil, gezilecek fazladan bir dosya üretir. İkinci gerçek uygulama ortaya çıkana kadar bekleyin; o noktada soyutlamanın nasıl görünmesi gerektiğini tahmin etmek yerine bilerek tasarlarsınız.

Referanslar

Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley. Madde 20 abstract sınıf yerine arayüzü, Madde 21 default metotların geriye dönük uyum için tasarlandığını, Madde 22 ise arayüzlerin yalnızca tip tanımlamak için kullanılması gerektiğini anlatıyor.

Ousterhout, J. (2018). A Philosophy of Software Design. Yaknyam Press. Derin ve sığ modül ayrımı üzerine kurulu; bu yazıdaki "tek uygulaması olan arayüz" eleştirisinin kaynağı da orası.

Gamma, E., Helm, R., Johnson, R. ve Vlissides, J. (1994). Design Patterns. Addison-Wesley. Template Method abstract sınıfın, Strategy ise arayüzün kanonik kullanımını gösterir.

Martin, R. C. (2017). Clean Architecture. Prentice Hall. Soyutlama sınırlarının bağımlılık yönünü nasıl belirlediği ve bir sınırın ne zaman çizilmeye değdiği üzerine.

Abstract Methods and Classes, Java Tutorials

Default Methods, Java Tutorials

Spolsky, J. (2002). The Law of Leaky Abstractions

Share this post

Link copied!
Mustafa Kürşad Başer
Author

Mustafa Kürşad Başer

Senior Software Engineer

A passionate software engineer who enjoys creating elegant solutions to complex problems. Beyond coding, I am deeply interested in exploring the intersections of technology, art, and human consciousness.