Home > Java > Effective Java Madde 15: Değişebilirliği (Mutability) Kısıtlayın

Effective Java Madde 15: Değişebilirliği (Mutability) Kısıtlayın

Basit olarak tanımlamak gerekirse değişmez (immutable) sınıflar, nesneleri üzerinde değişiklik yapılamayan sınıflardır. Her bir nesne içinde tanımlı olan değerler, nesne yaratılırken belirlenir ve nesnenin ömrü boyunca aynı kalır. Java platformu içerisinde tanımlı String, Integer, Boolean, BigInteger, BigDecimal gibi birçok değişmez sınıf bulunmaktadır. Bunun sebebi değişmez sınıfların daha kolay tasarlanabilmesi, yazılabilmesi ve kullanılabilmesidir. Ayrıca, değişmez sınıflar hata yapmaya daha az olanak verir ve daha güvenlidirler.

Bir sınıfı değişmez yapmak için aşağıdaki 5 kuralı uygulamalısınız:

1. Nesnenin durumunu değiştiren hiçbir metot tanımlamayın.
2. Sınıfı kalıtılamaz hale getirin. Böylece yazdığınız sınıfın değişmezliği, kötü niyetli veya dikkatsizce yazılmış çocuk sınıflar tarafından etkilenmemiş olur. Bunu yapmak için genellikle sınıf tanımlanırken final anahtar kelimesi kullanılır, ancak başka bir yol daha var ve buna ilerde değineceğiz.
3. Sınıftaki bütün alanları (field) final olarak tanımlayın. Böylece alanlara ilk değerlerini atadıktan sonra değiştirilmesi mümkün olmayacaktır.
4. Sınıftaki bütün alanları private olarak tanımlayın. Böylece sınıf içerisinde değişebilir nesnelere referanslar varsa, bu nesnelerin değiştirilmesini engellemiş olursunuz. Her ne kadar değişmez sınıflar içerisinde temel değerler taşıyan (başka nesnelere referans olmayan) public final alanlar tanımlamak mümkün olsa da, ileride sınıfın iç yapısının değiştirilmesini kısıtlayacağı için önerilmez. (Madde 13)
5. Değişebilir alanlara olan erişimi kısıtlayın. Eğer değişmez sınıf içerisinde değişebilir nesnelere referanslar varsa, istemcilerinizin bu referanslara erişemediğinden emin olun. Bu tür referansları istemciye döndürmeniz gerekiyorsa, gösterdiği nesnenin bir kopyasını oluşturup onu döndürün. Aynı şekilde, örneğin bir yapıcı metot aracılığıyla istemciden gelen bir referansı değişmez sınıf içerisinde kullanmanız gerekiyorsa, yine nesnenin bir kopyasını oluşturup onu kullanın. Böylece istemci nesnede değişiklik yapsa bile değişmez sınıftaki nesneyi değil kopyasını değiştirmiş olacaktır. (Madde 39, Madde 76)

Daha önceki maddelerde yazdığımız sınıfların birçoğu değişmezdir. Buna örnek olarak Madde 9’da tanımladığımız PhoneNumber sınıfı verilebilir. PhoneNumber sınıfında her bir alan için erişim metodu olsa da, değer değişikliği yapabilen hiçbir metot yazılmamıştır. Aşağıda daha karmaşık bir örnek verilmiştir:

public final class Complex {
    private final double re;
    private final double im;
    
    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
   
    // erişim sağlayan (getter) metotlar
    public double realPart() { 
        return re; 
    }

    public double imaginaryPart() { 
        return im; 
    } 
    
    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

    public Complex subtract(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }

    public Complex multiply(Complex c) {
        return new Complex(re * c.re - im * c.im, re * c.im + im * c.re);
    }

    public Complex divide(Complex c) {
        double tmp = c.re * c.re + c.im * c.im;
        return new Complex((re * c.re + im * c.im) / tmp, (im * c.re - re * c.im) / tmp);
    }

    @Override 
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Complex)) {
            return false;
        }

        Complex c = (Complex) o;
        return Double.compare(re, c.re) == 0 && Double.compare(im, c.im) == 0;
    }

    @Override 
    public int hashCode() {
        int result = 17 + hashDouble(re);
        result = 31 * result + hashDouble(im);
        return result;
    }

    private int hashDouble(double val) {
        long longBits = Double.doubleToLongBits(re);
        return (int) (longBits ^ (longBits >>> 32));
    }

    @Override 
    public String toString() {
        return "(" + re + " + " + im + "i)";
    }
}

Yukarıdaki sınıf sanal (imaginery) ve gerçel (real) olmak üzere iki parçadan oluşan bir karmaşık sayıyı ifade etmektedir. Bu sınıf Object ata sınıfından gelen metotlar haricinde, sanal ve gerçel değerleri döndüren erişim metotları ve toplama, çıkarma, çarpma ve bölme işlemlerini yapan 4 farklı metot tanımlamaktadır. Aritmetik işlemleri yapan metotların, var olan nesneyi değiştirmek yerine her seferinde yeni bir nesne yaratıp döndürmesi dikkat edilmesi gereken bir noktadır. Bu yöntem birçok değişmez sınıf içerisinde kullanılmaktadır ve fonksiyonel yaklaşım olarak bilinmektedir, çünkü metotlar değer değişikliği yapmadan bir fonksiyonun sonucunu direk olarak istemciye döndürmektedirler. Bunun tersi olan prosedürel yaklaşımda ise metotlar, gelen parametreler üzerinde değişiklik yaparak durumlarını değişmesine neden olurlar.

Eğer alışık değilseniz fonksiyonel yaklaşım ilk başta biraz garip görünebilir, ancak sınıflarda değişmezliği sağladığı için çok avantajlıdır. Değişmez nesneler çok basittir ve durumları yaratıldıktan sonra değiştirilemez, dolayısıyla sonradan nesne üzerinde değişiklik yapılıp yapılmadığıyla ilgilenmek zorunda kalmazsınız. Diğer taraftan değişebilir nesneler ise herhangi bir zamanda herhangi bir durumda bulunabilirler. Eğer sınıfta değişime neden olan metotlar nesnenin durumları arasındaki geçişleri iyi belgelemezse, bu nesneleri kullanmak güvenli bir biçimde kullanmak çok zorlaşır.

Değişmez nesneler doğası gereği thread güvenlidir (thread safe), senkronizasyon gerektirmezler. Bir başka deyişle birden fazla thread (iş parçacığı) aynı nesne üzerinde güvenli bir biçimde işlem yapabilir. Değişmez nesneler kullanmak thread güvenliğini sağlamak için açık ara en kolay yöntemdir. Nesne üzerinde değişiklik yapılamadığı için aynı nesne birçok thread arasında güvenli bir biçimde paylaşılabilir. Değişmez sınıflar bu özelliklerini kullanmaları için istemcileri teşvik etmelidirler. Bunu yapmanın bir yolu, sık kullanılan nesneleri public static final olarak aynı sınıf içerisinde tanımlamaktır. Örneğin, yukarıda tanımladığımız Complex sınıfı aşağıdaki nesneleri de içerisinde barındırabilir.

public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

Bu yaklaşım bir adım daha ileriye götürülebilir. Değişmez sınıf statik fabrika metotları (Madde 1) tanımlayarak sık kullanılan nesneleri tekrar tekrar yaratmak yerine bir kere yaratıp her istemciye aynı nesneyi döndürebilir. Java’da Integer, Boolean gibi temel türleri temsil eden sınıflar (boxed primitive types) ve BigInteger sınıfı bunu yapmaktadır. Bu yöntemi kullanmak gereksiz nesne yaratılmasını önlediği için bellek kullanımını azaltır ve çöp toplayıcının (garbage collector) işini kolaylaştırır. Bir sınıfı tasarlarken public yapıcı metotlar (constructor) yerine statik fabrika metotları kullanmak, daha sonra istemcileri hiç etkilemeden önbellek (cache) eklemenize de olanak sağlar.

Değişmez nesnelerin bir başka avantajı da kopyalamaya asla gerek kalmamasıdır. (Madde 39) Sonuç olarak kopyalananan nesne de orijinalinin aynısı olacak ve üzerinde değişiklik yapılamayacaktır. Bu sebeple, değişmez bir sınıf içerisinde clone metodu veya kopya yapıcı metot (copy constructor) tanımlamaya gerek yoktur (Madde 11). Bu durum Java’nın ilk zamanlarında çok iyi anlaşılmamış ve String sınıfı içerisinde bir kopya yapıcı metot tanımlanmıştır, bu yapıcı metodun kullanılması tavsiye edilmez. (Madde 5)

Değişmez nesneler başka nesnelerin oluşturulmasında yapı taşı görevi görürler.
Kendisi değişmez olsa da olmasa da, içerisinde değişmez bir nesne bulunduran karmaşık bir nesnenin durumu (state), değişebilir bir nesne bulunduran bir nesnenin durumuna göre çok daha tutarlı olacaktır. Örneğin, değişmez nesneler Map yapılarında harika bir anahtar görevi görürler ve Set yapılarına eleman olarak güvenle eklenebilirler. Bu değerler çalışma zamanında değişmeyeceği için, veri yapısına eklendikten sonra değer değişikliğinden kaynaklı bozulmalar yaşanmayacaktır.

Değişmez sınıfların tek dezavantajı, her bir değer için farklı bir nesne oluşturulması gerekmesidir. Bu yüzden büyük nesneler söz konusu ise değişmez nesneler kullanmak pahalı olabilir. Örneğin 1 milyon bitten oluşan bir BigInteger nesnesi üzerinde tek bir biti değiştirmek istediğimizi düşünelim:

BigInteger moby = ...;
moby = moby.flipBit(0);

2. satırdaki flipBit() metodu yine 1 milyon bit uzunluğunda başka bir BigInteger nesnesi yaratacaktır ve aradaki tek fark 1 bittir. BigSet sınıfı ise, BigInteger gibi istediğiniz uzunlukta bir bit kümesi tanımlamanızı sağlar ancak değişebilirdir. Bu sınıf, bit değiştirme işlemlerini aynı nesne üzerinde yaptığı için yukarıdaki aynı işlemi çok daha çabuk gerçekleştirir.

Bu performans problemi, her biri yeni bir nesne yaratılmasına neden olan birkaç farklı işlemi art arda yapıyorsanız daha da belirgin olacaktır. Çünkü en son yaptığınız işlem haricinde yaratılan bütün nesneler geçersiz kalacaktır. Bu durumla başa çıkmanın en pratik yolu değişebilir bir yardımcı sınıf yazmaktır. Bunun en güzel örneği Java platformu içerisinde bulunan String ve StringBuilder sınıflarıdır. String değişmez bir sınıf olarak tasarlanmış, ancak birden çok adımda oluşturulan String nesneleri performans kaybına yol açmasın diye değişebilir StringBuilder sınıfı da yazılımcılara sunulmuştur.

Artık bir sınıfı nasıl değişmez yapabileceğinizi ve bu yaklaşımın iyi ve kötü taraflarını öğrendiğinize göre, birkaç değişik tasarım üzerinde konuşabiliriz. Değişmezliği sağlayabilmek için sınıfın kalıtılmasını engellemek gerektiğini hatırlayın. Genel olarak bu sınıfı final tanımlayarak yapılabilir ancak ikinci ve daha esnek bir yol daha var. Sınıfı final tanımlamak yerine bütün yapıcı metotları private veya package private yazabilir, public yapıcı metotlar yerine de public statik fabrika metotları tanımlayabiliriz. (Madde 1)

Bu durumu örneklemek için aşağıdaki kodu verebiliriz:

// Yapıcı metot yerine statik fabrika metotu kullanılmış değişmez (immutable) sınıf 
public class Complex {
    private final double re;
    private final double im;

    private Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }

    public static Complex valueOf(double re, double im) {
        return new Complex(re, im);
    }

    ... // Sınıfın geri kalanında değişiklik yok..
}

Bu yaklaşım çok sık kullanılmasa da aslında çoğu zaman en iyi seçenektir. Aynı zamanda esnektir çünkü yapıcı metodu package private tanımlarsanız, kendiniz paket içerisinde sınıfı kalıtabilir ancak istemcilerinizin kalıtmasını engelleyebilirsiniz. Paket dışında kalan istemciler açısından bakıldığında bu sınıf final tanımlanmış gibidir, çünkü başka bir paketten gelen sınıfı eğer public veya protected bir yapıcı metodu yoksa kalıtmak mümkün değildir. Bu avantajın yanında, bu yaklaşım sonraki versiyonlarda statik fabrikanın geliştirilerek, istemci koduna dokunmadan önbellek (cache) eklenebilmesine de olanak sağlar.

Statik fabrika metotları Madde 1’de anlatıldığı üzere yapıcı metotlara göre birçok fayda sağlarlar. Örneğin yukarıdaki Complex sınıfına, karmaşık sayıları kutup koordinatları kullanarak yaratma özelliğini eklemek istediğimizi düşünelim. Bunu yapıcı metot kullanarak yapmak işleri çok karıştıracaktır çünkü kullanacağımız yapıcı metot imzası sınıfta zaten var olan yapıcı metotla çakışacaktır: Complex(double, double). Statik fabrika kullandığımızda ise çok kolaydır, tek yapmamız gereken farklı bir isimle ikinci bir metot eklemek olacaktır.

public static Complex valueOfPolar(double r, double theta) {
    return new Complex(r * Math.cos(theta), r * Math.sin(theta));
}

Değişmez sınıfların kalıtılmaması kuralı malesef BigInteger ve BigDecimal sınıfları yazılırken ihmal edilmiştir ve bugün bu sınıfların bütün metotları kalıtılabilir durumdadır. Bu durumun düzeltilmesi geriye uyumluluğu (backwards compatibility) bozacağı için mümkün değildir. Dolayısıyla, güvenmediğiniz istemcilerden gelen BigInteger veya BigDecimal türünden argümanları, eğer değişmezlik kriteri sizin için önemliyse gerçekten BigInteger veya BigDecimal türünden mi yoksa kalıtılmış bir tür mü olduğunu kontrol etmelisiniz. Eğer argüman kalıtılmış bir tür ise kendinize ata sınıftan yeni bir kopya oluşturmalısınız.

public static BigInteger safeInstance(BigInteger val) {
    if (val.getClass() != BigInteger.class) 
        return new BigInteger(val.toByteArray());
    return val;
}

Yukarıda bahsettiğimiz değişmezlik kurallarında sınıfın durum değişikliği yapan metot bulundurmaması ve bütün alanların final olması gerektiğinden bahsetmiştik. Bu kurallar performası artışı sağlayabileceğimiz yerlerde esnetilebilir. Değişmez sınıf, hiçbir koşulda istemcilerin görebileceği bir değişikliğe izin vermemelidir, ancak kendi içerisinde performansı artırmak için belli hesaplamalar yapıp saklayabilir. Aynı değer tekrar lazım olduğunda tekrar hesaplamak yerine sakladığı değeri döndürülebilir. Bu tekniğin işe yaramasının sebebi sınıfın değişmez olmasıdır, çünkü aynı hesaplama tekrar tekrar yapılsa bile her seferinde aynı sonucu üretecektir.

Örneğin Madde 9’da yazdığımız PhoneNumber sınıfının hashCode() metodu, ilk çağrıldığında hash kodunu hesaplamakta ve tekrar çağrıldığında daha önce hesapladığı değeri döndürmektedir. Bu teknik String sınıfı tarafından da kullanılmaktadır. (Madde 71)

Serileştirme ile ilgili bir uyarıdan da bahsedelim. Eğer değişmez sınıf değişebilir bir nesneye referans içeriyorsa, Serializable arayüzünü gerçekleştirirken bir readObject veya readResolve metodu tanımlamanız, veya ObjectOutputStream.writeUnshared ve ObjectInputStream.readUnshared metotlarını kullanmanız gerekmektedir. Aksi taktirde bir saldırgan sınıfınızdan değişebilir bir nesne yaratabilir. Bu konu Madde 76’da detaylı ele alınmaktadır.

Özetlemek gerekirse, alanların değerlerini değiştiren metotları mecbur değilseniz yazmayın. (setter metotlar) Sınıflar, değişebilir olması için mantıklı bir sebep yoksa değişmez yazılmalıdır. Değişmez sınıflar birçok yönden avantaj sağlarlar ve tek kötü tarafları bazı şartlar altında performans kaybına sebebiyet vermeleridir. Birkaç değer taşımaktan ibaret olan PhoneNumber ve Complex gibi sınıfları her zaman değişmez yapmalısınız. (Java platformu içerisinde java.util.Date ve java.awt.Point gibi değişmez olması gereken ama olmayan sınıflar vardır.) Büyük veriler taşıyabilen String ve BigInteger gibi sınıfları değişmez yapıp yapmama konusunda dikkatli karar vermelisiniz. Eğer performans kaybını engellemek için gerçekten gerekliyse, ek olarak bir de yardımcı değişebilir sınıf yazmalısınız.

Bazı sınıfları değişmez yapmak pratik olarak mümkün olmayabilir. Böyle durumlarda değişebilirliği mümkün olduğunca kısıtlamalısınız. Dolayısıyla, nesnelerin alanlarını aksi bir sebep yoksa final tanımlamalısınız.

Bunun dışında, yapıcı metotların nesneleri bütünüyle yaratması gerekmektedir. Yapıcı metot veya statik fabrika dışında, nesneye ilk değer atamaları yapan başka bir metot daha eklemeyin.

Son olarak, yukarıda yazdığımız Complex sınıfı sadece değişmezliği örneklemek için kabataslak yazılmıştır. Gerçek uygulamalarda kullanılabilecek sağlamlıkta ve güvenilebilirlikte değildir.

  1. No comments yet.
  1. No trackbacks yet.

Please leave these two fields as-is:

Protected by Invisible Defender. Showed 403 to 641.622 bad guys.

%d blogcu bunu beğendi: