[Effective Java] 6 - item38 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라.
일반적으로 열거 타입은 확장이 불가능하다.
- 확장한 타입 원소는 기반 타입 원소로 취급되지만 반대가 성립하지 않는다면 이상할 것이다.
- 기반 타입 - 확장 타입 전체를 순환하는 코드를 작성하기도 어려울 것이다.
- 확장성을 높이려면 고려할 요소가 늘어나므로 설계와 구현이 더 복잡해질 것이다.
인터페이스를 구현한 열거 타입
하지만 연산 코드 등, API가 제공하는 기본 원소(연산 코드) 외에 추가 원소(사용자 확장 연산)을 추가할 필요가 있는 경우가 있다. 이러한 경우에 인터페이스를 구현한 열거 타입을 사용한다.
열거 타입인 BasicOperation
은 확장 불가능하나, 인터페이스인 Operation
은 확장이 가능하다. 따라서 이 인터페이스에 연산 메서드를 정의하고, 구현체에서 이를 구현하도록 하면 된다.
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;
}
}
public interface Operation {
double apply(double x, double y);
}
꼭 BasicOperation
클래스가 아니더라도, 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; }
}
개별 인스턴스 수준에서 뿐만 아니라, 타입 수준에서도 확장된 열거 타입을 넘겨 확장된 열거타입 원소 모두를 사용하게 할 수 있다.
클래스 리터럴 (한정적 타입 토큰)
파라미터로 클래스 리터럴을 넘겨 구현할 수 있다,
// ExtendedOperation의 모든 확장된 열거 타입을 테스트한다.
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y); // 한정적 타입 토큰 역할
}
// T가 열거 타입인 동시에 Operation의 구현체여야 한다.
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));
}
한정적 와일드카드 타입
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));
}
여러 구현 타입의 연산을 조합해 호출할 수 있으나, 특정 연산에서 EnumSet
, EnumMap
사용이 불가능하다.
결론
인터페이스와 이 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 확장 가능한 열거 타입과 같은 효과를 낼 수 있다. 다만 이 방식은 열거 타입간 구현을 상속할 수 없다.
- 아무 상태에도 의존하지 않는 경우에는 디폴트 구현을 이용해 인터페이스에 추가할 수 있다.
- 앞선 예시 코드는 연산 기호를 저장하고 찾는 로직이 두 구현체 모두에 들어가야만 한다. 예시에선 중복량이 적어 문제가 되지는 않지만, 공유 기능이 많다면 별도 클래스나 정적 도우미 메서드로 분리하는 방식을 고려할 수 있다.
Leave a comment