Effective Java Madde 2: Çok Sayıda Parametreyle Karşılaştığınızda Builder Kullanın

Daha önce birinci maddesini işlediğimiz Effective Java kitabının ikinci maddesiyle devam ediyoruz.

Sınıf yapıcılar (constructor) ve statik fabrika metotlarının (static factory methods) paylaştığı ortak bir kısıt vardır: çok sayıda parametre geçmeniz gerektiği zaman kullanışsız bir duruma gelirler. Örneğin, paket gıdaların üzerinde yer alan ve besin değerlerini gösteren bir etiketi Java’da bir sınıf kullanarak ifade etmeye çalıştığımızı düşünelim. Bu etiketin üzerinde kalori, kolesterol, protein, karbonhidrat, yağ, doğmamış yağ, kalsiyum, demir ve daha birçok besin türü görürüz. Ancak besinlerin bir çoğu için bu değerlerin büyük bir kısmı opsiyonel olacaktır, yani bu alanlara hiçbir atama yapmamıza gerek olmayacaktır.

Böyle bir sınıf yazmamız gerektiği zaman nasıl bir sınıf yapıcı veya statik fabrika metodu kullanırız? Genelde yazılımcıların aklına gelen ilk yöntem iç içe geçmiş yapıcı metotlar (telescobing constructors) kullanmak olur, yani sadece zorunlu parametreleri içeren bir yapıcı metot, ardından zorunlu parametreleri ve sadece bir opsiyonel parametre içeren başka bir yapıcı metot, ardından iki opsiyonel parametreli üçüncü bir yapıcı metot ve bu şekilde devam eder. Şimdi örnek kodu inceleyelim, kolaylık olması açısından sadece 2 tane zorunlu 4 tane de opsiyonel değişkenimiz olsun.

// iç içe geçmiş yapıcı metotlar - ölçeklenebilir değil
public class BesinDegeri{

    private final int kalori;         // zorunlu
    private final int kolesterol;     // zorunlu
    private final int protein;        // opsiyonel
    private final int yag;            // opsiyonel
    private final int karbonhidrat;   // opsiyonel
    private final int demir;          // opsiyonel

    public BesinDegeri(int kalori, int kolesterol) {
        this(kalori, kolesterol, 0);
    }

    public BesinDegeri(int kalori, int kolesterol, int protein) {
        this(kalori, kolesterol, protein, 0);
    }

    public BesinDegeri(int kalori, int kolesterol, int protein, int yag) {
        this(kalori, kolesterol, protein, yag, 0);
    }

    public BesinDegeri(int kalori, int kolesterol, int protein, int yag, int sodium) {
        this(kalori, servings, protein, yag, sodium, 0);
    }

    public BesinDegeri(int kalori, int kolesterol, int protein, int yag, int karbonhidrat, int demir) {
        this.kalori = kalori;
        this.kolesterol = kolesterol;
        this.protein = protein;
        this.yag = yag;
        this.karbonhidrat = karbonhidrat;
        this.demir = demir;
    }
} 

Böyle bir sınıftan nesne yaratmak istediğiniz zaman geçmek istediğiniz parametleri içeren en kısa yapıcı metodu bulup kullanırsınız. Örneğin:

BesinDegeri besin = new BesinDegeri(120, 8, 0, 12, 26, 3);

Burada nesne yaratırken “demir” parametresini geçebilmek için 6 parametreli sınıf yapıcıyı kullanmamız gerek ancak parametre değerlerinden bir tanesinın değeri 0 olmasına rağmen yine de geçmek zorunda kaldık. Sadece “demir” değeri geçmeye çalışsaydık bu sefer ilk 5 parametreyi 0 olarak geçmek zorunda kalacaktık. Burada küçük bir örnekle yola çıktığımız zaman kolayca zorlayabildiğimiz bir tasarım gerçek projelerde daha büyük sorunlara yol açacaktır. Sonuç olarak bu kod çalışacaktır ama parametre sayısı arttıkça kodu yazmak ve daha da önemlisi okumak zorlaşacaktır. Kodu okuyan kişi hangi parametre “protein” hangisi “kolesterol” anlamakta zorluk çekecektir.

Bu duruma ikinci bir alternatif çözüm JavaBeans yöntemini kullanmaktır. Bu yöntemde parametre almayan bir yapıcı metot çağrılır ve daha sonra değer atamak istediğimiz alanlara atama metotları (setter method) kullanarak değer atarız. Şimdi kodu inceleyelim:

// JavaBeans yöntemi - tutarsız ve sonradan değiştirilebilen nesneler yaratmaya olanak sağlar
public class BesinDegeri {

    // Parametrelere varsayılan değerler atıyoruz
    private int kalori         = -1;  // Zorunlu alan
    private int kolesterol     = -1;  // Zorunlu alan
    private int protein        = 0;
    private int yag            = 0;
    private int karbonhidrat   = 0;
    private int demir          = 0;

    public BesinDegeri() { 
    
    }

    // Atama metotları (setter methods)
    public void setKalori(int deger) { 
        kalori = deger; 
    }
    public void setKolesterol(int deger) {
        kolesterol = deger; 
    }
    public void setProtein(int deger) {
        protein = deger; 
    }
    public void setYag(int deger) {
        yag = deger; 
    }
    public void setKarbonhidrat(int deger) {
        karbonhidrat = deger; 
    }
    public void setDemir(int deger) { 
        demir = deger; 
    }
}

Bu kod bir önceki çözümün kısıtlarını barındırmıyor ve birkaç satır fazla yazsak da daha anlaşılır bir kod yazmamızı sağlıyor. Aşağıda kodu görebilirsiniz:

BesinDegeri besin = new BesinDegeri();
besin.setKalori(120);
besin.setKolesterol(8);
besin.setYag(12);
besin.setKarbonhidrat(26);
besin.setDemir(3);

Malesef, JavaBeans yönteminin de kendine göre dezavantajları var. Nesne yaratma işlemini birkaç satırda yaptığımız için, yaratma esnasında nesnenin tutarsız olduğu durumlar olabilir. (örneğin kalori haricinde hiçbir değeri atanmamış) Böyle bir nesne programınızın başka bir yerinde kullanıldığı taktirde -multithreaded bir ortamda çalıştığınızı düşünürsek- istenmeyen sonuçlara sebep açacaktır. Ayrıca JavaBeans yöntemi kullanarak oluşturulan nesneler değiştirilemez (immutable) değildir. Yani nesne yaratma işlemi bittikten sonra bile atama metotlarını (setter methods) çağırarak nesnelerin durumunu değiştirebilirsiniz. Nesnelerin değiştirilemez olmadığı ortamlarda paralel programlama yapmak zorlaşır ve kolay kolay çözemeyeceğiniz program hatalarıyla uğraşmak zorunda kalabilirsiniz.

Hem “iç içe geçmiş yapıcılar” yönteminde avantaj olan nesnenin tek seferde yaratılması ve bununla gelen güvenlik, hem de JavaBeans yönteminde öne çıkan okunabilirlik özelliklerini beraber kullanabileceğimiz başka bir yöntem daha var. Builder tasarım deseninin bir örneği olan bu yöntemde istemci ilk başta bir yapıcı metot veya statik fabrika metodu çağırarak bir Builder nesnesi yaratır, ardından builder nesnesini kullanarak atama metodu (setter) benzeri metotlarla opsiyonel alanları doldurur. Sonunda, istemci builder nesnesi üzerinden build() metodunu çağırarak değiştirilemez (immutable) bir nesne yaratır. Koda bakınca daha rahat anlayabiliriz:

// Builder tasarım deseni
public class BesinDegeri {

    private final int kalori;
    private final int kolesterol;
    private final int protein;
    private final int yag;
    private final int karbonhidrat;
    private final int demir;

    public static class Builder {

        // Zorunlu parametreler, yapıcı metota geçilmeleri lazım
        private final int kalori;
        private final int kolesterol;

        // Opsiyonel parametreler, varsayılan değerler atıyoruz
        private int protein = 0;
        private int yag = 0;
        private int karbonhidrat = 0;
        private int demir = 0;

        public Builder(int kalori, int kolesterol) {
            this.kalori = kalori;
            this.kolesterol = kolesterol;
        }
        public Builder protein(int deger) {
            protein = deger;
            return this; 
        }
        public Builder yag(int deger) {
            yag = deger; 
            return this; 
        }
        public Builder karbonhidrat(int deger) {
            karbonhidrat = deger;  
            return this; 
        }
        public Builder demir(int val) { 
            demir = deger;
            return this; 
        }
        public BesinDegeri build() {
            return new BesinDegeri(this);
        }
    }

    private BesinDegeri (Builder builder) {
        kalori = builder.kalori;
        kolesterol = builder.kolesterol;
        protein = builder.protein;
        yag = builder.yag;
        karbonhidrat = builder.karbonhidrat;
        demir = builder.demir;        
    }
}

Gördüğünüz gibi Builder aslında sınıf içerisinde tanımlanmış başka bir statik sınıf ve build() harici bütün metotlarından this nesnesini döndürüyor. Yani istemci builder nesnesini geri aldığı için zincirleme metot çağrıları yaparak asıl oluşturmak istediğimiz BesinDegeri nesnesini yaratabilir. İstemci kodu aşağıdaki gibi olacaktır:

BesinDegeri besin = new BesinDegeri.Builder(120, 8).yag(12).karbonhidrat(26).demir(3).build();

İstemci kodu da hem yazması hem okuması kolay bir kod oldu. Burada dikkat edilmesi gereken şey, zincirleme metot çağrılarını yaparken tamamen Builder nesnesini kullanıyoruz, son ana kadar build() metodunu çağırmadan BesinDegeri nesnesi yaratılmıyor. İstemci yaratmak istediği nesnenin alanlarını doldurduktan sonra build() metodunu çağırınca BesinDegeri yapıcı metodu çalıştırılarak bir daha değiştirilmesi mümkün olmayan (immutable) bir nesne oluşuyor. Böyle hem okuması/yazması kolay bir kod hem de her ortamda sorunsuz çalışacak değiştirilemez (immutable) bir nesne yaratmış oluyoruz.

Eğer alanlar için sınırlama getirmek isterseniz (kalori 0’dan küçük olamaz gibi) bunu ister atama metotlarında isterseniz yapıcı metot içerisinde kontrol edebilirsiniz. İstemci uygun olmayan bir değer geçerse IllegalStateException fırlatarak nesnenin oluşmasını engelleyebilirsiniz.

Builder deseni çok sayıda opsiyonel parametre gerektiren nesneleri yaratmada büyük esneklik sağlar, aynı Builder sınıfını kullanarak çok sayıda nesne oluşturabilirsiniz. Buna karşılık olarak yazdığınız kod satırı sayısı biraz artmakta ama sağladığı avantajlar düşünülünce bu durum gözardı edilebilir. Sadece 3 parametreli sınıflarda Builder kullanmak çok gerekli olmayabilir ancak parametre sayısının ileride artacağını düşünüyorsanız sonradan değiştirmektense ilk başta builder kullanmak daha mantıklı olabilir.

Özetleyecek olursak builder tasarım deseni hem JavaBeans yönteminden çok daha güvenli olduğu için hem de çok sayıdaki parametreden dolayı sınıf yapıcı veya statik fabrika metodu kullanmanın getireceği okunabilirlik problemlerini çözdüğü için sıklıkla tercih edilir. Böylece Effective Java’nın ikinci maddesini de işlemiş olduk, sonraki yazılarda görüşmek üzere.

Share

2 Replies to “Effective Java Madde 2: Çok Sayıda Parametreyle Karşılaştığınızda Builder Kullanın”

  1. Çok karışık gelen bir deseni sizin anlatımınızda rahatlıkla anladım. Çok güzel anlatmışsınız. Teşekkürler.
    Küçük bir düzeltme sanırım ilk sınıftaki NutritionFacts BesinDegeri olacaktı.

Bir Cevap Yazın