Press "Enter" to skip to content

Effective Java Madde 38: Enumlarda Kalıtımı Arayüz Kullanarak Taklit Edin

Madde 34’de enumlardan bahsederken enum türlerinin kalıtılamayan (final) sınıflara dönüştürüldüğü söylemiştik. Dolayısıyla bir enum türü başka bir enum türünü kalıtamaz. Enum türleri zaten dolaylı olarak java.lang.Enum sınıfından türetildikleri için başka sınıfları kalıtması da mümkün değildir.

Bu durum çoğu zaman bir sorun teşkil etmese de bazı enumları kalıtma ihtiyacı duyabiliriz veya kullanıcılara bu imkanı vermek isteyebiliriz. Buna örnek olarak işlem kodlarını (operation code) temsil eden enumları verebiliriz. İşlem kodları bir makina üzerinde yapılabilecek işlemleri temsil ederler, tıpkı Madde 34’de yazdığımız dört işlem yapabilen bir hesap makinasını tanımlayan Operation gibi. Bu tür enumlar için kullanıcılara esneklik sağlayarak yeni işlem kodları eklemelerine izin vermek isteyebiliriz.

Enumlar kalıtımı desteklemese de arayüzleri kullanarak böyle bir etki yaratabiliriz. Aşağıdaki gibi tek metotlu bir arayüz tanımlayıp enum türünde bu arayüzü uygulayabiliriz. Şimdi BasicOperation enum türünü bu şekilde yazalım:

// Arayüz uygulayarak kalıtılabilirliği taklit eden enum
public interface Operation {
    double apply(double x, double y);
}

public enum BasicOperation implements Operation {
    PLUS("+") {
        public double apply(double x, double y) { 
            return x + y; } 
        },
    MINUS("-") {
        public double apply(double x, double y) { 
            return x - y; 
        }
    },
    TIMES("*") {
        public double apply(double x, double y) { 
            return x * y; } 
        },
    DIVIDE("/") {
        public double apply(double x, double y) { 
            return x / y; 
        }
    };

    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override 
    public String toString() {
        return symbol;
    } 
}

Eğer bir kullanıcı BasicOperation türünde tanımlı aritmetik işlemlere eklemeler yapmak isterse bu enumu kalıtamaz ancak Operation arayüzünü uygulayan ikinci bir enum yazabilir. Örneğin, bu programa üst alma ve bölme işleminde kalan hesaplama işlemlerini de eklemek isteyelim. Bunun için yapmamız gereken bu iki işlemi gerçekleştiren yeni bir enum türünü Operation arayüzünü uygulayarak yazmak olacaktır:

// Kalıtımı taklit eden enum
public enum ExtendedOperation implements Operation {
    EXP("^") {
        public double apply(double x, double y) {
            return Math.pow(x, y);
        } 
    },
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x % y; 
        }
    };
    
    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override 
    public String toString() {
        return symbol;
    } 
}

Böylece tür olarak Operation bekleyen metotlara hem BasicOperation nesnelerini hem de yeni yazdığımız ExtendedOperation nesnelerini geçebiliriz. Hatta sadece nesneleri değil ExtendedOperation türünün kendisini de BasicOperation yerine metotlara geçip elemanlarını kullanabiliriz. Aşağıdaki program komut satırından verilen iki argümanı kullanarak ExtendedOperation enumunda tanımlı bütün aritmetik işlemleri gerçekleştiriyor ve sonuçları ekrana yazdırıyor:

public static void main(String[] args) { 
    double x = Double.parseDouble(args[0]); 
    double y = Double.parseDouble(args[1]); 
    test(ExtendedOperation.class, x, y);
}

private static <T extends Enum<T> & Operation> void test( 
    Class<T> opEnumType, double x, double y) {
    
    for (Operation op : opEnumType.getEnumConstants()) {
        System.out.printf("%f %s %f = %f%n",
                             x, op, y, op.apply(x, y));
    }
}

Burada main metodunun test metodunu çağırırken ExtendedOperation.class geçmesi sayesinde EXP ve REMAINDER işlemleri gerçekleşmektedir. Bu kullanım sınırlandırılmış tür belirtecine bir örnektir. (Madde 33) Metottaki opEnumType parametresinin <T extends Enum<T> & Operation> Class<T> biçimindeki tür tanımı da geçilebilecek Class nesnesinin hem bir enum olması hem de Operation arayüzünü uygulaması gerektiğini belirliyor. Bize lazım olan da tam olarak budur.

Burada ikinci bir seçenek olarak metoda Class nesnesi yerine Collection<? extends Operation> geçebiliriz. Bu kullanım sınırlandırılmış joker tür olarak bilinir. (Madde 31)

public static void main(String[] args) {
    double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]); 
    test(Arrays.asList(ExtendedOperation.values()), x, y);
}

private static void test(Collection<? extends Operation> opSet, 
        double x, double y) {

    for (Operation op : opSet) {
        System.out.printf("%f %s %f = %f%n",
                          x, op, y, op.apply(x, y));
    }
}

Bu kod biraz daha anlaşılır durumdadır ve test metodu biraz daha esnektir çünkü metodun istemcisi Operation türünü uygulayan farklı enum türlerinden nesneleri birleştirip gönderebilir. Diğer taraftan ise enum nesneleri aynı türden olmadığı için EnumSet (Madde 36) ve EnumMap (Madde 37) ile kullanılması mümkün olmaz. Buradaki iki yaklaşım da API tanımlarken gerçek enum türlerini değil bunların ortak olarak uyguladığı arayüz türünü kullandığı için bize bu esnekliği sağlayabilmektedir.

Bu iki program da komut satırından 2 ve 4 değerleri geçildiğinde aşağıdaki sonucu üretecektir:

4.000000 ^ 2.000000 = 16.000000 
4.000000 % 2.000000 = 0.000000

Arayüz kullanarak kalıtılabilirliği taklit etme yönteminin bir dezavantajı, bir enumda tanımlı metotların diğer metot tarafından kullanılamamasıdır. Bu metodun davranışı nesnenin durumuna bağımlı değilse arayüz içinde varsayılan metot (default method) olarak yazılabilir. (Madde 20) Bizim örneğimizde BasicOperation ve ExtendedOperation enumları symbol alanının saklanması ve okunması amacıyla yazılmış bölümleri tekrar etmektedirler. Bu örnekte tekrar edilen kod miktarı çok az olduğu için farketmez ama artarsa ortak kısımları bir yardımcı sınıf veya statik bir yardımcı metot içine taşıyabilirsiniz.

Bu maddede anlatılan teknik Java kütüphanelerinde de kullanılmaktadır. Örneğin java.nio.file.LinkOption enum türü CopyOption ve OpenOption arayüzlerini uygulamaktadır.

Özetle, kalıtılabilen enum türleri yazamasak da bu etkiyi verebilmek için bir arayüz tanımlayıp yazdığımız enumda bunu uygulayabiliriz. Bu istemcilere kendi enum türlerini (veya sınıflarını) aynı arayüzü uygulayarak yazmalarına izin verir. Bu farklı türlerin nesneleri de referans olarak arayüz türünü kabul eden her yerde kullanılabilir.

Share

Leave a Reply

%d bloggers like this: