Press "Enter" to skip to content

Effective Java Madde 5: Bağımlılıkları Kendiniz Yaratmak Yerine Dependency Injection Kullanın

Last updated on September 10, 2019

Birçok sınıf işlevlerini yerine getirebilmek için çeşitli kaynaklara ihtiyaç duyar. Bu kaynaklar, başka bir deyişle ihtiyaç duyulan diğer sınıflar yazılımda bağımlılık (dependency) olarak adlandırılır. Örneğin, bir yazım denetleme uygulaması (spell checker) işlevlerini yerine getirebilmek için bir sözlüğe bağımlı olabilir. Bu tarz sınıfların statik yardımcı sınıf (utility class) olarak gerçekleştirildiğini zaman zaman görmekteyiz. (Madde 4)

// Statik yardımcı sınıfların yanlış kullanımı 
// esnek değildir ve test edilemez!
public class SpellChecker {
    private static final Lexicon dictionary = ...;
    private SpellChecker() {} 
    public static boolean isValid(String word) { 
        ... 
    }
    public static List<String> suggestions(String typo) { 
        ... 
    }
}

Benzer olarak, bunların Singleton sınıflar olarak gerçekleştirildiğini görmek de mümkündür. (Madde 3)

// Yanlış singleton kullanımı, esnek değildir ve test edilemez!
public class SpellChecker {
    private final Lexicon dictionary = ...;
    private SpellChecker(...) {}
    public static INSTANCE = new SpellChecker(...);
    public boolean isValid(String word) { 
        ... 
    }
    public List<String> suggestions(String typo) { 
        ... 
    }
}

Bu iki yaklaşım da tatmin edici değildir, çünkü ikisi de kullanmaya değer sadece bir tane sözlük (dictionary) olduğu varsayımında bulunmaktadır. Ancak pratikte her bir dilin farklı bir sözlüğü bulunmaktadır, hatta aynı dil içinde farklı amaçlar için farklı sözlükler de kullanılabilir (deyimler sözlüğü, bilişim terimleri sözlüğü gibi). Bu sebeple tek bir sözlüğün yeterli olacağını düşünmek yanlış olur.

Bunu çözmek için SpellChecker sınıfına kullanılan sözlüğü değiştirmeyi sağlayan bir metot eklemeyi düşünebilirsiniz. Ancak bu son derece hata yapmaya müsait ve kırılgan bir uygulamaya sebebiyet verecektir. Daha ötesi birden çok thread kullanılan (concurrent) uygulamalarda başınızı çok ağrıtacaktır. Statik yardımcı sınıflar ve singleton sınıflar, davranışlarını parametre olarak geçilen bir bağımlılığa (dependency) göre değiştiriyorlarsa, istenmeyen sonuçlar doğabilir.

Burada yapılması gereken şey SpellChecker sınıfını, birden fazla nesnesini oluşturacak şekilde tasarlamak ve her nesne için kullanılacak sözlüğün istemci tarafından belirlenmesini istemek olacaktır. Bunu yapmanın en basit yolu istemcinin nesneyi yaratırken kullanılmasını istediği sözlüğü yapıcı metoda göndermesidir. Bu, dependency injection dediğimiz prensibi uygulamanın bir biçimidir. Sözlük, burada SpellChecker sınıfı için bir bağımlılıktır (dependency). İstemci ise, kullanılmasını istediği sözlük referansını SpellChecker sınıfına dışarıdan ”enjekte” etmektedir. (injection)

// Dependency injection esneklik ve test edilebilirliği sağlar
public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker(Lexicon dictionary) { 
        this.dictionary = dictionary;
    }
    public boolean isValid(String word) { 
        ... 
    }
    public List<String> suggestions(String typo) { 
        ... 
    }
}

Dependency injection prensibi o kadar basit ve yaygındır ki birçok yazılımcı bunu kullanır ama bir ismi olduğunu bilmez. Her ne kadar bizim örneğimizde tek bir bağımlılık olsa da (sözlük), bu prensibi kaç tane bağımlılık olursa olsun kullanabilirsiniz. Değişebilirliği (mutability) kısıtlama prensibini de sağladığı için (Madde 17) istendiği taktirde birden fazla istemci aynı bağımlılığı kullanabilir. Dependency injection sadece yapıcı metotlara değil, statik fabrika metotlarına (Madde 1) ve builder kullanan sınıflara (Madde 2) da uygulanabilir.

Bu prensibin bir başka uygulama şekli ise yapıcı metoda bir fabrika referansı geçmektir. Fabrika, çağırdıkça belirli bir sınıfın nesnelerini üretip döndüren sınıflara verilen addır. Java 8 ile kullanıma sunulan Supplier<T> arayüzü, fabrika sınıflarını ifade etmek için çok uygundur. Bir Supplier<T> referansını girdi olarak kabul eden fabrika metotlarının, tür parametresini sınırlı joker türü (bounded wildcard type) kullanarak kısıtlamaları gerekir (Madde 31). Böylece istemci sadece istenen türü değil, bu türün alt türlerini üreten fabrika referanslarını da parametre olarak geçebilir. Örneğin aşağıda, istemcinin gönderdiği ve gerekli taşları üreten bir fabrika referansı kullanarak mozaik döndüren bir metodun imzasını görüyorsunuz:

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

Dependency injection tekniği, her ne kadar uygulamanızın test edilebilirliğini ve esnekliğini çok büyük oranda artırsa da, büyük projeler birbirine bağımlı binlerce sınıftan oluştuğu için uygulaması kolay olmayabilir. Bu sorunu çözmek için Dagger, Guice, Spring gibi dependency injection framework denilen uygulama çatıları geliştirilmiştir. Bu çatıların kullanımı bu yazının kapsamında değildir. Ancak şunu söyleyebiliriz ki, bu prensibi kendiniz uygulamış olsanız bile daha sonra bu çatılardan birine geçiş yapmak son derece kolaydır.

Özetle, sınıfınızın bağımlılıkları varsa ve bu bağımlılıklar sınıfın davranışını etkiliyorsa, bu sınıfı statik yardımcı (utility) veya singleton olacak şekilde yazmayın! Bu sınıfın kendi bağımlıklıklarını yaratmasına da izin vermeyin! Bunun yerine istemci tarafında bu bağımlılıkları yaratıp sınıfa gönderin veya bu bağımlılıkları yaratan bir fabrika referansı gönderin. Bunları sadece yapıcı metodu çağırırken değil, statik fabrika veya builder kullanan sınıflara da gönderebilirsiniz. Bu şekilde uyguladığınız taktirde, dependency injection sınıfların esnekliğini, yeniden kullanılabilirliğini ve test edilebilirliğini çok büyük oranda artırır.

Share

Leave a Reply

%d bloggers like this: