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

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

Effective Java Madde 4: Nesne Yaratılmasını İstemediğiniz Sınıfları Private Yapıcı Metot İle Güçlendirin

Bazen sadece statik metotları ve değişkenleri bir arada tutmak için bir sınıf yazmak isteyebilirsiniz. Bu tür sınıflar, bazı geliştiriciler tarafından nesne bazlı düşünmekten kaçınmak için suistimal edildiklerinden ötürü kötü bir üne kavuşmuşlardır. Buna rağmen, geçerli kullanım alanları bulunmaktadır. İlkel türler veya diziler üzerinde işlem yapan, birbiriyle alakalı metotları gruplamak için kullanılabilirler. java.lang.Math ve java.util.Arrays bu kullanıma örnek gösterilebilir. Bunun yanında java.util.Collections örneğinde olduğu gibi, belli bir arayüzü gerçekleştiren nesneler için tasarlanmış statik metotları – statik fabrika metotları da dahil (Madde 1) – bir arada tutmak için de kullanılabilirler. (Java 8 ile birlikte artık bu metotları arayüz içerisine de yazabilirsiniz, tabi arayüze erişiminiz varsa.) Son olarak, bu sınıflar final bir sınıf içerisindeki metotları gruplamak için de kullanılabilir, çünkü bunlar bir çocuk sınıfta tanımlanamaz.

Bu tarz yardımcı sınıflardan (utility class) nesne oluşturulması istenmez. Ancak herhangi bir yapıcı metot tanımlanmadığı zaman, Java derleyicisi otomatik olarak parametre almayan public bir varsayılan yapıcı metot (default constructor) tanımlayacaktır. Bir istemci açısından bu varsayılan yapıcı metodun, açıkça tanımlanmış diğer yapıcı metotlardan hiçbir farkı yoktur. Bu sebeple, birçok API içerisinde istemeden de olsa nesne yaratılabilen sınıflar bulunmaktadır.

Bunu engellemek için bir sınıfı abstract tanımlamak işe yaramaz. Bu sınıf katılılarak çocuk sınıftan bir nesne yaratılabilir. Dahası, kullanıcı bu sınıfın kalıtım amaçlı tasarlandığı yanılgısına kapılabilir. (Madde 19) Aslında bir sınıftan nesne yaratılmasını engellemek için çok basit bir yöntem vardır, private bir yapıcı metot tanımlamak. Biz açıkça bir yapıcı metot tanımladığımız zaman, derleyici varsayılan public yapıcı metodu tanımlamaz.

// Nesne yaratılamayan yardımcı sınıf
public class UtilityClass {
    // Varsayılan yapıcı metodu engelle
    private UtilityClass() {
        throw new AssertionError();
    }
    
    ...  // diğer kısımlar çıkartılmıştır
}

Tanımladığımız yapıcı metot private olduğu için sınıfın dışından görülebilmesi veya çalıştırılabilmesi mümkün değildir. Fırlatılan AssertionError ise yapıcı metodun sınıfın içerisinden yanlışlıkla çağırılması ihtimaline karşı önlem almak içindir. Böylece bu sınıftan asla bir nesne oluşturulamayacağını garanti etmiş oluruz.

Bir yan etki olarak, bu private yapıcı metot aynı zamanda sınıfın kalıtılmasını da engeller. Bütün yapıcı metotlar ilk önce kalıttıkları ata sınıfın (açıkça belirtilen bir kalıtım yoksa Object sınıfının) yapıcı metodunu çağırmak zorundadır, ata sınıftaki yapıcı metot açıkça tanımlanmış veya derleyici tarafından eklenen varsayılan yapıcı metot olabilir. Biz yukarıdaki yardımcı sınıfta hem varsayılan yapıcı metodun tanımlanmasını engellediğimiz için hem de kendi yapıcı metodumuzu private tanımladığımız için, bu sınıfı kalıtacak bir çocuk sınıfın çağırabileceği bir yapıcı metot bulunmamaktadır.

Share

Effective Java Madde 3: Singleton Sınıfları Private Yapıcı Metot veya Enum Türüyle Güçlendirin

Singleton en basit anlamıyla sadece bir kez somutlaştırılabilen (instantiate) sınıf anlamına gelir. Diğer bir değişle, singleton sınıflardan sadece bir kez nesne oluşturulabilir. Bu nesneler ya fonksiyon gibi durum içermeyen nesneleri, (Madde 24) ya da doğası itibariyle eşsiz olan bileşenleri temsil ederler. Bir sınıfı singleton yapmak, onu kullanan istemcileri test etmeyi zorlaştırır çünkü singleton nesneyi bir mock gerçekleştirim ile değiştirmek eğer bu nesne bir arayüzü gerçekleştirmiyorsa imkansızdır.

Singleton nesne oluşturmak için iki yöntem vardır. Her ikisi de yapıcı metotları private tutarak, singleton nesneyi public static bir üye olarak dışarı açma esasına dayanmaktadır. Bu yöntemlerden ilkinde, singleton nesne final olarak tanımlanmaktadır:

// public final olarak tanımlanmış singleton alan
public class Elvis {

public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}

>> DEVAMINI OKU
Share

Effective Java Madde 16: Kalıtım Yerine Komposizyonu Tercih Edin

Kalıtım kodların yeniden kullanımını sağlayabilen güçlü bir araçtır, ancak her zaman yapacağınız iş için en doğru araç olmayabilir. Yanlış kullanıldığında kırılgan yazılımlara yol açar. Kalıtımı, aynı paket içerisinde, ata sınıf ve çocuk sınıfın aynı programcının kontrolünde olduğu durumlarda kullanmak güvenlidir. Bunun yanında, özellikle kalıtılmak için tasarlanmış ve belgelenmiş sınıfları kalıtmak da güvenlidir (Madde 17). Ancak, farklı paketlerdeki sıradan somut sınıfları kalıtmak tehlikelidir. Hatırlatma olarak söyleyelim, burada kalıtım dediğimiz zaman bir sınıfın başka bir sınıfı kalıtmasından bahsetmekteyiz. Bahsedilen problemler arayüz (interface) kalıtırken geçerli değildir.

Metot çağırmanın aksine, kalıtım yapmak sarmalamayı (encapsulation) ihlal eder. Başka bir deyişle, bir çocuk sınıf işlevini gerçekleştirebilmek için ata sınıfın gerçekleştirim detaylarına bağımlıdır. Ata sınıfın içeriği zaman içerisinde yeni sürümlerle birlikte değişebilir, ve bu durumda çocuk sınıfın kodu hiç değişmemiş olsa bile çalışmayabilir. Sonuç olarak, eğer ata sınıf özel olarak kalıtılmak için tasarlanmamışsa, çocuk sınıf değişikliğe ihtiyacı olmasa bile ata sınıfla birlikte gelen değişikliklere uyum sağlayacak şekilde değiştirilmelidir.

Read more “Effective Java Madde 16: Kalıtım Yerine Komposizyonu Tercih Edin”

Share

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)

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

Share

Effective Java Madde 14: public sınıflarda erişim metotları kullanın, public alanlar değil

Zaman zaman birkaç tane alanı bir arada tutmaktan başka bir iş yapmayan sınıflar yazma eğiliminde olabilirsiniz:

// Bunun gibi sınıflar public olmamalıdır!
class Point {
    public double x;
    public double y;
}

Bu tür sınıfların veri alanları direk erişime açık olduğu için, kapsülleme prensibinin (encapsulation) faydalarını kullanamazlar. UPA’yı (API) değiştirmeden kod içerisinde değişiklik yapmak zorlaşır, sabit değerler tanımlayamazsınız ve bir alana erişildiğinde müdahale etme şansınız olmaz. Katı görüşlü programcılar bu tür sınıfları lanetli olarak görürler ve bunların yerine private alanları ve public erişim metotları olan sınıflar yazılması gerektiğini savunurlar.

Read more “Effective Java Madde 14: public sınıflarda erişim metotları kullanın, public alanlar değil”

Share

Effective Java Madde 13: Sınıfların ve Üyelerinin Erişilebilirliğini Kısıtlayın

İyi tasarlanmış bir yazılım modülünü kötü tasarlanmış bir modülden ayıran en önemli faktör, içindeki verileri ve gerçekleştirim (implementation) detaylarını diğer modüllerden ne kadar iyi saklayabildiğidir. İyi tasarlanmış bir modül, gerçekleştirim detaylarını net bir biçimde diğer modüllerden gizler. Modüller birbirleriyle tanımladıkları API (Uygulama Programlama Arabirimi, UPA) üzerinden konuşurlar ve birbirlerinin iç dünyasından habersiz olmaları gerekir. Bu konsept, çok temel bir yazılım tasarım ilkesidir ve bilgi saklama yada kapsülleme (encapsulation) olarak bilinir.

Bilgi saklama konsepti birçok açıdan önemlidir. En önemli faydası ise yazılım modüllerini birbirinden ayrıştırarak (decoupling) birbirinden bağımsız bir şekilde geliştirilebilmesini, test edilebilmesini ve kullanılabilmesini sağlamaktır. Bu durum, bir sistemin geliştirilmesini hızlandırır çünkü farklı modüller paralel bir biçimde aynı anda geliştirilebilir. Ayrıca bakım yapmayı ve modülün anlaşılmasını kolaylaştırır, bir problem olduğunda diğer modülleri bozma riski olmadan hata araştırılıp, çeşitli çözümler denenebilir. Bilgi saklama kendi başına bir sistemin yüksek performansla çalışmasını sağlamasa da, birbirinden bağımsız modüllerin diğerlerini etkilemeden optimize edilebilmesini sağlar. Dolayısıyla sistemi test ederek hangi modülün yavaş çalıştığını tespit edebilir, sistemin diğer parçalarına zarar vermeden yavaş çalışan modülü optimize edebilirsiniz. Birbirinden bağımsız modüller aynı zamanda tekrar kullanılabilirliği (reusability) yüksek olan modüllerdir. Bilgi saklama tekniği büyük sistemler geliştirmenin riskini önemli ölçüde azaltır, çünkü sistem bir bütün olarak doğru çalışmasa da bağımsız modüllerin doğru çalıştığı kanıtlanabilir.

Read more “Effective Java Madde 13: Sınıfların ve Üyelerinin Erişilebilirliğini Kısıtlayın”

Share

Effective Java Madde 12: Comparable Arayüzünü Gerektiğinde Uygulayın

Bu bölümde gördüğümüz diğer metotların aksine, compareTo() metodu Object içerisinde tanımlanmış değildir. Bunun yerine Comparable arayüzünün tek metodu olarak karşımıza çıkmaktadır. Karakter olarak Object sınıfındaki equals() metoduna benzer ancak eşitlik karşılaştırması yanında sıralama da yapabilir ve üreyseldir (generic). Comparable arayüzünü uygulayarak, sınıfınızın nesneleri arasında sıralama yapılabileceğini belirtmiş olursunuz. Bu arayüzü uygulayan nesnelerden oluşan bir diziyi (array) sıralamak aşağıdaki gibi son derece kolaydır:

Arrays.sort(a);

Benzer şekilde, Comparable arayüzü uygulandığında arama yapma, uç değerleri bulma, kendiliğinden sıralı veri yapılarını yönetme gibi problemler de çok kolaylaşır. Örneğin, aşağıdaki program komut satırından girilen parametreleri alfabetik sırada yazdırmakta ve tekrar edilen değerleri elemektedir. Bu kodun doğru çalışması String sınıfının Comparable arayüzünü uygulamasıyla mümkün olmaktadır.

public class WordList {
        public static void main(String[] args) {
        Set<String> s = new TreeSet<String>();
        Collections.addAll(s, args);
        System.out.println(s);
    }
}

Read more “Effective Java Madde 12: Comparable Arayüzünü Gerektiğinde Uygulayın”

Share

Effective Java Madde 10: toString() Metodunu Her Zaman Geçersiz Kılın

Her ne kadar java.lang.Object bir toString() gerçekleştirimi sunsa da, geri döndürdüğü karakter dizisi genellikle sizin sınıfınızı kullanmak isteyen bir yazılımcının görmek istediği şey değildir. Bu değer sınıfın adı, ardından gelen ‘@’ karakteri ve nesnenin hash kodunun onaltılık sistemde (hexadecimal) ifadesini içermektedir, örneğin “PhoneNumber@163b91”. toString sözleşmesi, bu metottan döndürülen değerin “kolay okunup anlaşılabilen, kısa ama bilgi verici” olması gerektiğini söyler. “PhoneNumber@163b91” değerinin kısa ve okuması kolay olduğunu savunabilirsiniz ancak “(707) 867-5309” ile karşılaştırıldığında bilgi verici olmadığı açıktır. toString sözleşmesi bunların haricinde “Bu metodu bütün alt sınıfların geçersiz kılması tavsiye edilir” demektedir. Kesinlikle uyulması gereken bir tavsiye!

Her ne kadar equals (Madde 8) ve hashCode (Madde 9) sözleşmeleri kadar kritik olmasa da, güzel bir toString gerçekleştirimi sağlamak sınıfınızı çok daha kolay kullanılabilir hale getirir. toString metodu, bir nesne printf, println gibi metotlara parametre olarak geçildiğinde, karakter dizilerine eklendiğinde veya assert ile birlikte kullanıldığında otomatik olarak işletilir.

Read more “Effective Java Madde 10: toString() Metodunu Her Zaman Geçersiz Kılın”

Share

Effective Java Madde 9: equals() ile Birlikte Mutlaka hashCode() Metodunu da Geçersiz Kılın

Önemli Not: Hash tabanlı veri yapılarının Java’da nasıl çalıştığını bilmiyorsanız bu yazıyı okumadan önce araştırmanızı şiddetle tavsiye ederim.

Object sınıfından gelen hashCode metodunu gerektiği yerde geçersiz kılmamak (override) birçok hatanın kaynağını oluşturur. equals metodunu geçersiz kıldığınız her sınıfta hashCode metodunu da geçersiz kılmanız gerekir. Bunu yapmamak Object.hashCode metodunun sözleşmesini ihlal etmek demektir, bu da sınıfınızın hash tabanlı HashMap, HashSet, HashTable gibi veri yapılarıyla birlikte kullandığında yanlış çalışmasına yol açar. Bunun sebebi ise Object sınıfından gelen hashCode metodunun hash kodunu hesaplarken nesnenin o anda bulunduğu bellek adresini kullanmasıdır. Her nesnenin bellek adresi farklı olacağı için hesaplanan hash kodu da farklı olacaktır. Object sınıfı belirtiminde tarif edildiği üzere sözleşme genel olarak şu şekildedir:

  • equals karşılaştırmasında kullanılan alanlar sabit kaldığı sürece, hashCode metodu aynı uygulama içerisinde üst üste çağrıldığında her zaman aynı sonucu üretmelidir.
  • Eğer iki nesne equals metoduna göre birbirine eşitse, bu iki nesnenin hashCode metotları da aynı integer değerini üretmelidir.
  • Eğer iki nesne equals metoduna göre eşit değilse, hashCode metodu bu iki nesne için farklı integer sonuçları üretmek zorunda değildir. Ancak yazılımcı bilmelidir ki eşit olmayan nesneler için farklı hash kodları üretmek hash table performansını artırabilir.

Read more “Effective Java Madde 9: equals() ile Birlikte Mutlaka hashCode() Metodunu da Geçersiz Kılın”

Share