Skip to main content
Yazılım Tasarımı•10 dk okuma

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

Mustafa Kürşad BaşerMustafa Kürşad BAŞER
••

Seri: Nesne Yönelimli Programlama · Bölüm 3/5

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

Encapsulation (Kapsülleme) Nedir?

Özet (TL;DR)

  • 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; söylenmeyen 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ı ve hiçbir kural yoktu; ikincisinde setBakiye hiç yok, yerine kuralı kendi içinde denetleyen paraCek geldi. getBakiye yerinde kaldı çünkü bakiyeyi okumak zararsız; kaldırılan şey yalnızca yazma yetkisiydi.

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 belirleyicilerine serinin kalıtım yazısında değinmiştim, ama oradaki örnekler C# ile yazılmıştı ve bu konuda iki dil ayrışıyor. Java tarafında sık gözden kaçan bir ayrıntı var.

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. Farklı bir paketteki alt sınıf içinse ek bir kısıt devreye girer: protected üyeye ancak kendi tipi üzerinden ulaşabilir, elindeki referans üst sınıf tipindeyse derleyici izin vermez. Alanlarda varsayılan tercihiniz private olsun; protected yalnızca 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) {        Objects.requireNonNull(urun, "Ürün zorunlu");        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.

Bir noktayı atlamayalım: List.copyOf listeyi dondurur, içindeki nesneleri değil. Urun sınıfı değiştirilebilirse çağıran kod hâlâ siparis.getUrunler().get(0).fiyatiDegistir(0) yazabilir. Garantinin tam olması için elemanların da değişmez olması gerekir.

Aynı sızıntı diziler ve java.util.Date gibi 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 sonradan 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.

Uyarı

record alanları final yapar ama içlerindeki nesneleri dondurmaz. Bir record içinde List tutuyorsanız o liste hâlâ değiştirilebilir. Kompakt kurucuda urunler = List.copyOf(urunler); yazmanız yeterli: kompakt kurucuda parametreyi yeniden atamak serbesttir ve alana giden şey artık değiştirilemez bir kopya olur. Bunu atlarsanız 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ç

Bir de çerçevelerin işi karıştırdığı yer var, çünkü ilk duvara orada çarpılıyor. JPA entity’leri parametresiz bir kurucu ister ve alanları final olamaz. Jackson ya varsayılan kurucu ister ya da @JsonCreator. Lombok’un @Data anotasyonu ise bu yazının açılışta eleştirdiği şeyi, yani her alan için setter üretmeyi tek satırda geri getirir.

Çözüm, kapsüllemeden vazgeçmek değil, sınırı doğru yere çekmek: entity sınıfını veri tabanı eşlemesi olarak bırakıp iş kurallarını taşıyan asıl modeli ayrı tutmak. Aynı sınıfın hem ORM’e hem de iş mantığına hizmet etmesini beklerseniz ikisinden birinden ödün verirsiniz.

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ımdan ibaret: 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 da değişmez yapın.

Serinin bir sonraki yazısı Polymorphism (Çok Biçimlilik): 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ığı.

Sıkça Sorulan Sorular

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

Bu yazıyı paylaş

Mustafa Kürşad Başer
Yazar

Mustafa Kürşad Başer

Kıdemli Yazılım Mühendisi

Karmaşık sorunlara zarif çözümler üretmekten keyif alan, tutkulu bir yazılım mühendisi. Kodlamanın ötesinde, teknoloji, sanat ve insan bilincinin kesişim noktalarını keşfetmekle derinden ilgileniyor.