Skip to main content
9 min read

OOP: Nesne Yönelimli Programlama: Encapsulation (Kapsülleme) Rehberi

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

Encapsulation (Kapsülleme) Nedir?

Key Takeaways

  • Kapsülleme, alanları private yapıp her biri için getter/setter üretmek değildir; o yaklaşım alanı fazladan iki metotla yine dışarıya açar.
  • Asıl iş, nesnenin değişmezlerini (invariant) korumak: geçersiz bir duruma hiçbir yoldan girilememesi.
  • private bir koleksiyonu olduğu gibi döndürmek durumu sızdırır; dışarı veri verirken kopya ya da değiştirilemez görünüm verin.
  • En güçlü kapsülleme değişmezliktir. Değiştirilemeyen bir nesnenin bozulacak durumu da yoktur.
  • Java’da protected yalnızca alt sınıflara değil, aynı pakete de açar. Sanılandan daha geniş bir izindir.

Serinin ilk yazısında Araba sınıfının alanlarına araba1.marka = "Toyota" diyerek dışarıdan doğrudan değer atamıştık. Oraya bir not düşmüştüm: gerçek kodda alanlar private yapılır, erişim metotlar üzerinden verilir. Sebebine şimdi geliyorum.

Kapsüllemenin ders kitabı tanımı şöyle: veriyi ve o veriyi işleyen kodu bir arada tutmak, iç detayları dışarıdan gizlemek.

Doğru ama eksik. Eksik olan kısım, neyin neden gizlendiği.

Getter ve setter yazmak kapsülleme değildir

Kapsülleme anlatılırken en sık verilen örnek şudur: alanı private yap, getX() ve setX() ekle. Bu örneğin sorunu, hiçbir şey kapsüllememesi.

java
public class BankaHesabi {    private double bakiye;     public double getBakiye() {        return bakiye;    }     public void setBakiye(double bakiye) {        this.bakiye = bakiye;    }}

Bu sınıfta bakiye teknik olarak private. Ama dışarıdaki herhangi bir kod hala istediği değeri yazabiliyor: hesap.setBakiye(-5000) sorunsuz çalışır ve müşterinin hesabında eksi 5000 lira görünür. Alanı doğrudan public yapsaydınız sonuç değişmezdi, sadece iki metot daha az yazardınız.

Bir sınıfın her alanı için otomatik getter/setter üretiyorsanız o sınıf kapsüllenmiş olmuyor. Sadece uzuyor. Joshua Bloch, Effective Java’nın 15. maddesinde aynı noktaya değinir: erişilebilirliği en aza indirin, çünkü açtığınız her üye ileride uymak zorunda kalacağınız bir sözleşmeye dönüşür.

Asıl mesele değişmezleri korumak

Değişmez (invariant), bir nesnenin ömrü boyunca doğru kalması gereken kural. Banka hesabı için basit: bakiye negatif olamaz.

Kapsülleme, bu kuralın hiçbir yoldan çiğnenememesini sağlamak demek. Alanı gizlemek tek başına yetmez; dışarıya sunduğunuz işlemleri de değiştirmeniz gerekir. Kural hem kurulum anında hem de her para çekme işleminde yeniden denetlenir:

java
public class BankaHesabi {    private double bakiye;     public BankaHesabi(double acilisBakiyesi) {        if (acilisBakiyesi < 0) {            throw new IllegalArgumentException("Açılış bakiyesi negatif olamaz");        }        this.bakiye = acilisBakiyesi;    }     public void paraCek(double tutar) {        if (tutar <= 0) {            throw new IllegalArgumentException("Tutar pozitif olmalı");        }        if (tutar > bakiye) {            throw new IllegalStateException("Yetersiz bakiye");        }        bakiye -= tutar;    }     public double getBakiye() {        return bakiye;    }}

Örnekte bakiyeyi double tuttum, çünkü konu aritmetik değil erişim. Gerçek kodda parayı asla kayan noktalı sayıyla tutmayın: 0.1 + 0.2 ikilik tabanda tam olarak 0.3 etmez ve bu fark faiz hesabında birikir. Doğrusu BigDecimal ya da en küçük birimi tutan bir long. Yazının sonundaki Para örneğinde BigDecimal kullanacağım.

İki sınıfı yan yana koyun. İlkinde setBakiye vardı, kural yoktu. İkincisinde setBakiye hiç yok; yerine paraCek geldi ve kuralı kendi içinde denetliyor. getBakiye yerinde kaldı, çünkü bakiyeyi okumak zararsız. Kaldırılan şey yazma yetkisi.

Buradan çıkan pratik kural şu: setter yazmadan önce sorun, bu alanı dışarıdan değiştirebilmek gerçekten gerekiyor mu? Cevap çoğu zaman hayır çıkar. Gereken şey alanı değiştirmek değil, bir iş yapmaktır; ve o iş kendi kuralını taşır.

Erişim belirleyicilerinde bir hatırlatma

Erişim belirleyicilerini serinin kalıtım yazısında ele almıştım, o yüzden burada yalnızca sık gözden kaçan bir ayrıntıya değineceğim.

Java’da protected, adının çağrıştırdığından daha geniş davranır. Yalnızca alt sınıflara değil, aynı paketteki bütün sınıflara da erişim verir. Yani hiç türetme yapmayan ama aynı pakette duran bir sınıf da o alanı okuyup yazabilir. Dil belirtiminde açıkça yazılı olmasına rağmen sürpriz olmaya devam ediyor. Alanlarda varsayılan tercihiniz private olsun; protected sadece alt sınıfın gerçekten ihtiyaç duyduğu ve bilinçli olarak sözleşmeye dahil ettiğiniz üyelere kalsın.

Kaçan referanslar

Bu, kapsüllemenin en sessiz kırılma noktası.

Alan private, setter yok, kod incelemesinden sorunsuz geçiyor. Ama nesne bir koleksiyon tutuyorsa hikaye değişir.

java
public class Siparis {    private final List<Urun> urunler = new ArrayList<>();     public void urunEkle(Urun urun) {        if (urunler.size() >= 50) {            throw new IllegalStateException("Bir siparişte en fazla 50 ürün olabilir");        }        urunler.add(urun);    }     public List<Urun> getUrunler() {        return urunler;   // buradaki sorunu fark ettiniz mi?    }}

urunEkle metodu 50 ürün sınırını özenle koruyor. Ama getUrunler listenin kendisini döndürüyor. Dışarıdaki kod siparis.getUrunler().add(urun) yazarak 51. ürünü ekleyebilir, hatta clear() çağırıp siparişi boşaltabilir. Alan hem private hem final; yine de durum dışarıya sızmış oluyor.

Çözüm, dışarıya listenin kendisini değil bir kopyasını ya da değiştirilemez bir görünümünü vermek:

java
    public List<Urun> getUrunler() {        return List.copyOf(urunler);    }

List.copyOf Java 10 ile geldi, o anki içeriğin değiştirilemez bir kopyasını üretiyor. Alternatifi Collections.unmodifiableList, ama davranışı farklı: kopya değil canlı bir görünüm döndürüyor, içerideki liste değişirse görünüm de değişiyor. İkisi arasında bilinçli seçim yapın.

Aynı sızıntı diziler ve java.util.Date gibi değiştirilebilir nesnelerde de olur. Kural genel: değiştirilebilir bir nesneyi dışarıya olduğu gibi vermeyin, dışarıdan alıp olduğu gibi saklamayın. İkincisi de en az birincisi kadar tehlikelidir, çünkü size listeyi veren kod onu daha sonra değiştirmeye devam edebilir.

En güçlü kapsülleme değişmezliktir

Buraya kadar saydığım bütün problemlerin tek bir ortak kaynağı var: nesnenin durumunun değişebilmesi. Değişmiyorsa bozulacak bir şey de kalmıyor.

Değişmez bir sınıf alanlarını final yapar, hepsini kurucuda doldurur, hiçbir setter sunmaz. Elinize bir Para nesnesi geçtiğinde onu kimsenin, hiçbir iş parçacığında değiştiremeyeceğini bilirsiniz. Eşzamanlı kod yazarken bu güvence tek başına bir synchronized bloğundan daha çok iş görüyor: paylaşılan ama değişmeyen veriyi korumaya gerek kalmıyor.

Java 16’dan beri bu kalıp için record var. Alanları final, kurucuyu, equals, hashCode ve toString metotlarını derleyici üretir:

java
public record Para(BigDecimal tutar, String paraBirimi) {     public Para {        Objects.requireNonNull(tutar, "Tutar zorunlu");        Objects.requireNonNull(paraBirimi, "Para birimi zorunlu");        if (tutar.signum() < 0) {            throw new IllegalArgumentException("Tutar negatif olamaz");        }    }}

Parametre listesi olmayan bu kurucuya kompakt kurucu deniyor ve alanlar atanmadan hemen önce çalışıyor. Null denetimlerinin signum() çağrısından önce gelmesi de tesadüf değil; sırayı bozarsanız kullanıcı sizin yazdığınız mesaj yerine çıplak bir NullPointerException görür. record kullanmak doğrulamadan vazgeçmek demek değil: geçersiz bir Para nesnesi hiç var olamıyor.

Warning

record alanları final yapar ama içlerindeki nesneleri dondurmaz. Bir record içinde List tutuyorsanız o liste hala değiştirilebilir. Hem kompakt kurucuda List.copyOf ile kopyalayın, hem de erişim metodunu elle yazıp kopya döndürün; aksi halde bu yazının başındaki sızıntı aynen geri gelir.

Sık yapılan hatalar

  • Her alan için otomatik getter/setter üretmek. IDE bunu tek tuşla yapıyor diye doğru olduğu anlamına gelmiyor.
  • Doğrulamayı çağıran tarafa bırakmak. Kural sınıfın içinde değilse, onu unutan ilk çağrı noktasında bozulur.
  • private koleksiyonu doğrudan döndürmek. En sık rastlanan ve en geç fark edilen sızıntı.
  • Kurucuyu boş bırakıp nesneyi setter’larla doldurmak. Bu, nesnenin bir süre geçersiz durumda yaşaması demektir.
  • Test edebilmek için alanı public yapmak. Test, sınıfın davranışını sınamalı; iç alanlarını okumak zorunda kalıyorsanız sorun testte değil tasarımdadır.

Sonuç

Kapsüllemeyi tek bir soruya indirebilirsiniz. Bu nesne, geçersiz bir duruma düşürülebilir mi?

Cevap evetse, alanların private olması hiçbir şey ifade etmiyor. Uygulaması ise üç adım. Alanları gizleyin, değiştirmeyi metotlar üzerinden yaptırın ve kuralı o metotların içine koyun. Dışarıya veri verirken değiştirilebilir bir referans kaçırmadığınızdan emin olun. Değişebilir olması gerekmeyen ne varsa değişmez yapın.

Serinin bir sonraki yazısında Polymorphism (çok biçimlilik) konusunu ele alacağım: aynı çağrının farklı sınıflarda farklı davranması, bunun derleme ve çalışma zamanındaki iki ayrı biçimi ve equals metodunun neden bu kadar sık yanlış yazıldığı.

Frequently Asked Questions

Kapsülleme için her alanı private yapmak yeterli mi?
Hayır. Alanı private yapıp her biri için setter eklerseniz, alanı yine dışarıdan istediği değere ayarlanabilir bırakmış olursunuz. Kapsülleme, nesnenin geçersiz bir duruma düşememesini sağlamaktır; bu da kuralın sınıfın içindeki metotlarda yaşamasını gerektirir.
Getter kullanmak yanlış mı?
Hayır, okuma genelde zararsızdır. Sorun setter tarafında. Bir alanı dışarıdan değiştirmek gerektiğini düşündüğünüzde, aslında bir iş yaptırmak istediğinizi fark edersiniz; o işi adıyla bir metot haline getirin, doğrulamayı da içine koyun.
private bir listeyi döndürmenin nesi sakıncalı?
Döndürdüğünüz şey listenin kopyası değil kendisidir. Çağıran kod ona ekleme yapabilir, temizleyebilir ve sınıfınızın koruduğu bütün kuralları atlayabilir. List.copyOf ile değiştirilemez bir kopya, Collections.unmodifiableList ile değiştirilemez bir görünüm döndürün.
Java record kullanmak kapsüllemeyi bozar mı?
Bozmaz, aksine güçlendirir. record alanları final olur ve setter üretilmez. Doğrulamayı kompakt kurucuya yazabilirsiniz. Tek dikkat edilecek nokta, record içindeki koleksiyonların hala değiştirilebilir olması; onları kurucuda kopyalamanız gerekir.
Kapsülleme ile soyutlama aynı şey mi?
Değil. Kapsülleme, durumu koruyup dışarıya kontrollü erişim vermekle ilgilidir. Soyutlama ise gereksiz detayı gizleyip yalnızca anlamlı bir arayüz sunmakla. İkisi birlikte çalışır ama farklı problemleri çözer; soyutlamayı serinin son yazısında ele alacağım.

Referanslar

Bloch, J. (2018). Effective Java (3rd ed.). Addison-Wesley. Madde 15 erişilebilirliği en aza indirmeyi, Madde 16 public sınıflarda alan yerine erişim metodu kullanmayı, Madde 17 değişmezliği, Madde 50 ise savunmacı kopyalamayı ele alıyor. Bu yazıdaki dört bölümün de karşılığı orada.

Goetz, B., Peierls, T., Bloch, J., Bowbeer, J., Holmes, D. ve Lea, D. (2006). Java Concurrency in Practice. Addison-Wesley. Üçüncü bölüm, değişmez nesnelerin paylaşılan durumu neden kilit gerektirmeden güvenli kıldığını anlatıyor.

Evans, E. (2003). Domain-Driven Design. Addison-Wesley. Değer nesneleri ve bir iş kuralının modelin neresinde yaşaması gerektiği üzerine.

Meyer, B. (1997). Object-Oriented Software Construction (2nd ed.). Prentice Hall. Sözleşmeye dayalı tasarımın ve sınıf değişmezlerinin ilk kapsamlı anlatımı.

Records, Oracle Java Documentation

Controlling Access to Members of a Class, Java Tutorials

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.