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.