열거 타입을 확장하는 것은, 대부분 상황에서 좋지 않기 때문에 타입 안전 열거 패턴과 달리 불가능하다. 그런데 확장 가능한 열거 타입이, "연산 코드(operation code)"에는 쓰임새가 존재한다. 이따금 API가 제공하는 기본 연산 외에 사용자 확장 연산을 추가할 수 있도록 열여줘야 할 때가 있기 때문이다.
기본 아이디어는, 열거 타입이 임의의 인터페이스를 구현할 수 있다는 것을 이용하는 것이다. 열거 타입 자체는 확장 할 수 없지만, 인터페이스는 확장 가능하다.
▶️ 인터페이스를 이용해 확장 가능 열거 타입을 흉내
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;
}
}
이제 앞의 연산 타입을 확장해 지수 연산(EXP)와 나머지 연산(REMAINDER)를 추가해보자. 단지 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;
}
apply()가 인터페이스에 선언되어 있으니 앞서서처럼 열거 타입에 따로 추상 메서드를 선언하지 않아도 된다.(아이템 34)
다음과 같이 확장 열거 타입을 넘겨 확장된 열거 타입의 원소 모두를 사용하게 할 수도 있다. 첫 번째 방법은 한정적 타입 토큰(Class 객체)를 이용하는 것이다.
▶️ 첫 번째 대안 : 열거 타입의 Class 객체를 이용해 확장된 열거 타입의 모든 원소를 사용
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y); //class 리터럴
}
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));
}
메인 메서드에서는 확장 열거 타입의 class 리터러를 넘겨 확장된 연산들이 무엇인지 알려준다. (한정적 타입 토큰의 역할(아이템 33))
<T extends Enum<T> & Operation>
은 Class 객체가 열거 타입인 동시에 Operation의 하위 타입이어야 한다는 뜻이다. 열거 타입이어야 원소를 순회 가능하고, Operation이어야 원소가 뜻하는 연산을 수행 가능하기 때문이다.
Class 객체 대신, 한정적 와일드 카드 타입(아이템 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));
}
열거 값들을 리스트 컬렉션으로 만든 후, Collection<? extends Operation>
을 통해 하위 타입이어야 함을 보장하고 있다. 단, 특정 연산에서는 EnumSet과 EnumMap을 사용하지 못한다.
자바 라이브러리도 인터페이스를 구현한 패턴을 사용하고 있다. java.nio.file.LinkOption
열거 타입은 CopyOption과 OpenOption 인터페이스를 구현했다.
열거 타입끼리 구현을 상속할 수 없다는 사소한 문제가 존재한다.
문제 해결을 위해, 아무 상태에도 의존하지 않는 경우에는 디폴트 구현(아이템 20)을 이용해 인터페이스에 추가하는 방법이 있다. 만약 공유하는 기능이 많아 중복량이 높다면, 그 부분을 별도의 도우미 클래스나 정적 도우미 메서드로 분리하는 방식으로 코드 중복을 없앨 수 있을 것이다.
📚 핵심 정리
열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다. 이렇게 하면 클라이언트는 이 인터페이스를 구현해 자신만의 혹은 다른 열거 타입을 생성할 수 있다.