Press "Enter" to skip to content

Effective Java Madde 21: Arayüzleri İleriyi Düşünerek Tasarlayın

Java 8’den önce arayüzlere yeni bir metot eklemek daha önce bu arayüzü uygulamış sınıfları bozmak anlamına geliyordu. Arayüze eklenen yeni metot, onu önceden uygulayan sınıflarda tesadüfi durumlar haricinde olmayacağı için kod derleme anında hata veriyordu. Bu sorunu gidermek için Java 8’le birlikte dile varsayılan metotlar (default method) eklenmiştir, ancak bu kullanım riskli bir yaklaşım olarak karşımıza çıkmaktadır.

Arayüz içerisinde bir varsayılan metot tanımlarken, arayüzü uygulayan ancak bu metodu geçersiz kılmamış (veya buna fırsat bulamamış, haberi olmayan) sınıflar tarafından kullanılmak üzere bir varsayılan gerçekleştirim (default implementation) yazarız. Her ne kadar bu bize arayüzlere yeni metotlar ekleme şansı veriyor olsa da, tanımladığımız varsayılan gerçekleştirimin arayüzü uygulamış mevcut sınıfların tamamında sorunsuz çalışacağının bir garantisi yoktur. Bu varsayılan gerçekleştirim, arayüzü önceden uygulamış sınıflara haberleri olmadan ”enjekte” edilmektedir. Java 8 var olmadan önce bu sınıfları yazan kişiler, sınıflarına tepeden inme bir metodun ileride enjekte edilebileceğini bilmeden kodlarını yazdılar.

Java 8’le dile eklenen lambda fonksiyonlarını daha işlevsel hale getirmek ve kullanımlarını kolaylaştırmak için, ana collection arayüzlerine çok sayıda varsayılan metot eklendi. Bu metotlar çok iyi düşünülüp tasarlanmış, genel amaçlı kullanımlarda sorun çıkarmayacak şekilde yüksek kaliteyle yazılmışlardır ve gerçekten de çoğu durumda sorunsuz çalışmaktadırlar. Ancak, bir arayüzü uygulayan sınıfların tamamında sorunsuz çalışabilecek bir varsayılan metot yazmak her zaman mümkün olmaz.

Örneğin, Java 8’de Collection arayüzüne eklenen removeIf metodunu ele alalım. Bu metot parametre olarak boolean değer döndüren bir Predicate fonksiyonu alır ve bu veri yapısı içerisindeki bütün elemanları iterator ile tarayarak bu fonksiyonu her bir eleman için çalıştırır. Fonksiyonun true değeri ürettiği elemanlar bu veri yapısından iterator.remove metoduyla silinir. Şimdi koda bakalım:

// Java 8'le Collection arayüzüne eklenen varsayılan metot
default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean result = false;
    for (Iterator<E> it = iterator(); it.hasNext(); ) {
        if (filter.test(it.next())) {
            it.remove();
            result = true;
        }
    }    
    return result;
}

Yukarıdaki varsayılan metot, genel amaçlı işler için yazılabililecek en iyi metottur. Ancak yine de, pratikte başarısız olduğu durumlar vardır. Mesela, org.apache.commons.collections4.collection.SynchronizedCollection sınıfını ele alalım. Bu sınıf Apache Commons kütüphanesinin bir parçası olup, java.util içerisindeki Collections.synchronizedCollection fabrika metodundan dönen sınıfla benzerdir. SynchronizedCollection sınıfı Collection arayüzünü uygulamaktadır ve çok threadli (multithreaded) ortamlarda çalışabilmesi için arayüz metotlarına senkronizasyon (synchronization) uygulamaktadır. Aynı zamanda kullanıcılarına kendi belirledikleri bir nesne ile kilitleme (locking) yapabilme imkanı da vermektedir.

SynchronizedCollection aktif olarak bakımı yapılan bir sınıf olsa da, bu yazı yazılırken removeIf metodunu geçersiz kılmış değildir. Eğer bu sınıf Java 8 ile birlikte kullanılırsa, uygulamış olduğu Collection arayüzünden removeIf metodunu otomatik olarak alacaktır. Bu yeni metot, SynchronizedCollection sınıfının kullanıcılarına sunduğu işlevselliği bozacaktır çünkü varsayılan gerçekleştirimde metot senkronizasyonu bulunmamaktadır. Zaten bu mümkün olamaz çünkü arayüz içerisinde sınıf bilgisi olmadığı için, hem bu sınıfın işlevlerinden habersizdir hem de sınıfta kilitleme yapmak için kullanılan nesneye (lock object) erişimi yoktur. Bu durumda, çok threadli bir ortamda, bir thread SynchronizedCollection nesnesi üzerinden removeIf metodunu çağırdığında başka bir thread de bu veri yapısı üzerinde değişiklik yapıyorsa ConcurrentModificationException veya başka bir bozuklukla karşılaşırız.

Bu tarz problemlerin Collections.synchronizedCollectionJava gibi Java kütüphanesi sınıflarında görülmesini önlemek için, varsayılan metotlar arayüzlere eklenirken bu sınıflar da elden geçirilerek varsayılan metotlar geçersiz kılınmış ve doğru gerçekleştirimler sağlanmıştır. Ancak, Java kütüphanelerinin dışındaki sınıfları kontrol eden programcıların bu şekilde eşzamanlı bir değişiklik yapmaları mümkün olmamıştır. Bunların bir kısmı varsayılan metotları sonradan geçersiz kılarak sorunu çözmüşlerdir ancak bazıları hala bozuk olan bu varsayılan metotları kullanmaktadır.

Varsayılan metotlar söz konusu olduğunda, önceden yazılmış arayüz gerçekleştirimleri derleme sırasında hata veya uyarı vermese de, çalışma zamanında çökebilir. Çok yaygın olmamakta birlikte böyle vakalar görülmüştür. Collections arayüzüne eklenen metotların küçük bir kısmı böyle bir risk taşımaktadır, bu durumdan etkilendiği bilinen sınıflar da mevcuttur.

Mevcut arayüzlere yeni varsayılan metotlar eklemekten mümkün olduğunda kaçınmalıyız. O zamana kadar arayüzü uygulamış olan sınıflarda bozukluklara yol açabileceğinizi bilerek, ciddi şekilde düşünüp taşındıktan sonra böyle bir kararı almalısınız. Bunun yanında varsayılan metotlar eğer arayüz tanımlanırken en başta eklenirse, arayüzü uygulayacak programcılara çok büyük kolaylıklar sağlarlar.(Madde 20)

Şunu da belirtmek gerekir ki, varsayılan metotlar arayüzlerde metot silmek veya mevcut metotların imzasını (method signature) değiştirmek için kullanılamazlar. Bu tür değişiklikler mevcut gerçekleştirimleri bozmadan yapılamaz.

Buradaki prensibimiz çok açık: varsayılan metotlar artık bize arayüzlere sonradan metot ekleme imkanı verse de, arayüzleri çok dikkatli ve ileriyi düşünerek tasarlamak halen çok önemlidir. Tasarımda bir sorun olsa bile sonradan bir varsayılan metot ekleyip durumu çözeriz gibi bir yaklaşım çok tehlikeli olacaktır. Yukarıda anlatıldığı gibi, arayüzlere sonradan metot eklemenin riskleri vardır. Arayüzdeki basit bir kusur kullanıcılarını rahatsız edebilir ancak bunu düzelteyim derken eklenecek hatalı bir metot yüzünden arayüz bazı kullanıcılar için kullanılamaz hale gelebilir.

Bu sebeple, yeni veya üzerinde değişiklik yapılmış arayüzleri kullanıma sunmadan önce test etmek çok kritiktir. Birden fazla programcı bu arayüzleri uygulayarak test etmelidirler. En azından bu şekilde üç tane farklı gerçekleştirim yapılmalıdır. Ayrıca istemci programlar oluşturarak, arayüzü uygulayan test sınıfları üzerinden bütün işlevsellik doğrulanmalıdır. Bu testler arayüzde hedeflenen kullanımların sağlanıp sağlanmadığını anlamanıza çok yardımcı olacaktır. En önemlisi de eğer bir kusur varsa bunu arayüzü kullanıma açmadan önce fark etmenizi sağlayacaktır. Arayüz kullanıma açıldıktan sonra fark edilen kusurları çözmek bir ihtimal mümkün olsa da, buna bel bağlamamak gerekir.

Share

One Comment

Leave a Reply

%d bloggers like this: