Effective Java Madde 23: Sınıf Hiyerarşilerini İşaretli Sınıflara Tercih Edin

Nesnelerinin mantıksal olarak birbirlerine yakın şeyler ifade etmesi sebebiyle tek bir sınıf içerisinde tanımlanan, bir işaret (tag) alanı ile nesnenin tam olarak neyi temsil ettiğini gösteren sınıflara işaretli sınıf diyebiliriz. Aşağıda, her ikisi de birer geometrik şekil olması sebebiyle hem bir daireyi hem de dikdörtgeni ifade edebilmek için yazılmış bir sınıf görüyoruz. shape alanı bize nesnenin bir daireyi mi yoksa dikdörtgeni mi temsil ettiğini söylemektedir.

// işaretli sınıf - problemli! 
class Figure {

    enum Shape { RECTANGLE, CIRCLE };

    // işaret alanı, bu şeklin türünü belirtiyor
    final Shape shape;

    // bu alanlar sadece shape=RECTANGLE ise kullanılıyor
    double length;
    double width;

    // bu alan sadece shape=CIRCLE ise kullanılıyor
    double radius;

    // daire için kullanılan yapıcı metot
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // dikdörtgen için kullanılan yapıcı metot
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        } 
    }
}

Bu tarz işaretli sınıfların birçok dezavantajı vardır. Birincisi, gereksiz işaret alanı, enum türü ve switch bloğu yüzünden çok karışıktır. Birden fazla türü aynı sınıfa sıkıştırmaya çalıştığı için okunabilirliği çok zayıftır. Bellekte kapladığı alan olması gerekenden fazladır çünkü ihtiyaç olmadığı halde bir daire için (Shape.CIRCLE) length ve width, bir dikdörtgen içinse (Shape.RECTANGLE) radius alanını tutmaktadır. Üstelik bu alanları final olarak tanımlamak isteseydik yapıcı metotlar içerisinde lazım olmayan alanlara bile atama yapmamız gerekirdi. Yapıcı metotların, derleyiciden yardım almaksızın doğru shape türünü ataması ve doğru alanlara atama yapması gerekir, burada bir hata yapılırsa program çalışma zamanında çökecektir. Yeni bir tür eklemek isterseniz (örneğin üçgen), bütün sınıfı elden geçirmeniz ve switch bloğuna eklemeler yapmanız gerekir, eğer unutursanız program yine çalışma zamanında çökecektir. Bu çok basit bir örnek olduğu için tek bir switch bloğu olsa da, pratikte bundan çok daha fazlası olabilir. Bu da hata yapma riskini çok artırır. Son olarak, bir istemci açısından nesnenin türü (Figure), onun tam olarak neyi ifade ettiği konusunda hiçbir fikir vermemektedir. Bunu için istemcinin shape alanını da kontrol etmesi gerekir. Kısacası, işaretli sınıflar hem çok karmaşık, hem hata yapmaya müsait hem de verimi düşük sınıflardır.

Şanslıyız ki, Java gibi nesne tabanlı dillerde bu durumlarda kullanabileceğimiz çok daha iyi bir alternatif var: tür hiyerarşisi kurmak. İşaretli sınıflar aslında sınıf hiyerarşilerinin başarısız bir taklididir.

İşaretli bir sınıfı sınıf hiyerarşisine çevirmek için öncelikle soyut bir sınıf (abstract class) tanımlayın. Bu sınıfa metot olarak her bir tür için uygulanması gereken ve gerçekleştirimi türe göre değişiklik gösteren metotları yazın. Yukarıdaki örnekte area metodu buna örnektir çünkü alan hesaplama yöntemi türe göre değişiklik göstermektedir. Bu soyut sınıf, hiyerarşinin tepesinde olacaktır. Gerçekleştirimi türe göre değişmeyen, başka bir deyişle bütün türler için kodu aynı olan metotları bu sınıfa somut metot olarak ekleyin. Yine bütün türler için kullanılan ortak alanlar varsa onları da ekleyin. Bizim Figure örneğimizde bütün türler tarafından ortak olarak kullanılabilecek bir metot veya alan yoktur.

Sonra, gerçekleştirmek istediğiniz her tür için bir somut sınıf yaratıp soyut sınıfınızı kalıtın. Bizim örneğimize göre Circle ve Rectangle olmak üzere iki tane somut sınıf oluşturulması gerekir. Bu sınıflar için kullanılacak alanları da ekleyin: yani Circle sınıfında sadece radius, Rectangle sınıfında ise length ve width alanları olması gerekir. Son olarak ata sınıfta soyut tanımladığınız metodu geçersiz kılarak o tür için olması gereken kodu yazın. Yukarıdaki örneği buna çevirecek olursak:

// işaretli sınıfın tür hiyerarşisi ile yazılışı
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) { 
        this.radius = radius; 
    }

    @Override 
    double area() { 
        return Math.PI * (radius * radius); 
    } 
}

class Rectangle extends Figure {
    final double length;
    final double width;
  
    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }
       
    @Override 
    double area() { 
        return length * width; 
    }
}

Bu sınıf hiyerarşisi, daha önceki işaretli sınıf için anlattığımız bütün eksiklikleri çözmektedir. Kod çok basittir ve anlaşılması kolaydır. İşaretli sınıfta kullanmak zorunda kaldığımız gereksiz alanlar ve switch bloğu yoktur. Her tür kendi sınıfında tanımlandığı için kendisini ilgilendirmeyen alanlarla ilgilenmek zorunda değildir. Bütün alanları final tanımlamak mümkün olmuştur. Bu sayede, yapıcı metot kendi türü için bütün gerekli alanlara atama yapmak zorundadır, aksi taktirde derleyici hata verecektir. Ata sınıfta tanımlı soyut metotların çocuk sınıflarda da tanımlanması yine derleyici tarafından garanti edilir. Yani, işaretli sınıfta switch bloğuna eklenmeyen türlerin çalışma zamanında hata vermesi durumu artık mümkün değildir. Birden fazla programcı, ata sınıfa erişimleri olmasa bile çocuk sınıflar yaratarak birbirlerinden bağımsız bir biçimde kodlarını geliştirebilirler.

Tür hiyerarşilerinin başka bir avantajı da, alt türlerin birbiri arasındaki hiyerarşik düzeni kurabilmektir. Örneğin, buraya bir kare türü eklemek istersek, kareyi dikdörtgenden kalıtıp özel bir dikdörtgen olduğunu aşağıdaki gibi hiyerarşik düzene yansıtabiliriz:

class Square extends Rectangle {
    Square(double side) {
        super(side, side);
    }
}

Şunu da belirtelim ki, anlatılmak isteneni vurgulamak için yukarıda erişim metotları kullanmak yerine alanlara direk erişiyoruz. Eğer bu hiyerarşi dışarıdan erişilebilir (public) olsaydı, burada bir tasarım hatası yapmış olurduk. (Madde 16)

Özetle, işaretli sınıfların kullanımı birçok durum için uygunsuzdur. Eğer böyle bir sınıf yazmayı aklınızdan geçiriyorsanız, işaret alanını kaldırıp bunu bir tür hiyerarşisine çevirip çeviremeyeceğinizi ciddi olarak düşünün. Böyle bir sınıfla karşılaşırsanız da, tür hiyerarşisine dönüştürmeyi düşünün.

Share

Bir Cevap Yazın