아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

대부분의 경우 열거 타입은 타입 안전 열거 패턴(typesafe enum pattern)보다 좋지만 한 가지 단점이 있다. 타입 안전 열거 패턴은 확장에 열려 있어 열거 값들을 추가 할 수 있는 반면 열거 타입은 확장에 닫혀있다.

열거 타입 확장

열거 타입을 확장하는건 대부분 좋지 않은 생각이다. 확장한 타입의 원소는 기반 타입의 원소를 취급하지만 그 반대는 성립하지 않고 기반 타입과 확장된 타입의 원소들을 모두 순회할 방법도 없기 때문이다.

확장할 수 있는 열거 타입

연산 코드(operation code 혹은 opcode)만 확장할 수 있는 열거 타입에 어울린다. 연산 코드의 각 원소는 특정 기계가 수행하는 연산을 뜻한다.

확장 할 수 있는 열거 타입을 만들고 싶으면 인터페이스를 만들어서 열거 타입이 해당 인터페이스를 구현하게 하면 된다. 자바 라이브러리에서 java.nio.file.LinkOption 열거 타입은 CopyOptionOpenOption 인터페이스가 구현 되어 있다.

인터페이스를 이용해 확장 기능 열거 타입을 흉내 냈다.

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;
	}
}

열거 타입인 BasicOperation은 확장할 수 없지만 인터페이스인 Operation은 확장할 수 있다. 그리하여 타입을 확장하고 싶으면 다른 열거 타입으로 Operation을 구현하면 된다.

확장 가능 열거 타입

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;
	}
}

확장된 열거 타입 사용 방법

첫 번째 방법은 한정적 타입 토큰인 class 리터럴을 활용하면 확장된 열거 타입을 사용할 수 있다. <T extends Enum<T> & Operation> Class<T> 로 매개변수를 선언하면 Class 객체가 열거 타입이면서 Operation의 하위 타입이게 된다.

public class OperationTest {  
  
    @Test  
    void extendedOperationTest() {  
        // given  
        double x = 3.0;  
        double y = 2.0;  
  
        // when & then  
        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));  
    }  
}
3.000000 ^ 2.000000 = 9.000000
3.000000 % 2.000000 = 1.000000

두 번째 방법은 한정적 와일드카드 타입인 Collection<? extends Operation>을 활용하는 방법이다. 이 방법을 통해 여러 구현 타입의 연산을 조합해 호출할 수 있어 더 유연해지지만, 특정 연산에서 EnumSetEnumMap을 사용하지 못한다.

public class OperationTest {  
  
    @Test  
    void extendedOperationTest2() {  
        // given  
        double x = 3.0;  
        double y = 2.0;  
  
        // when & then  
        test(List.of(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));  
    }
}
3.000000 ^ 2.000000 = 9.000000 
3.000000 % 2.000000 = 1.000000 

정리

열거 타입 기본적으로 확장할 수 없다. 하지만 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 선언하면 열거 타입을 확장하는 효과를 낼 수 있다.

Last updated