ITEM 38 "확장할 수 있는 열거타입이 필요하면 인터페이스를 사용하라"
enum 열거 타입은 확장할 수 없다. 확장이 가능하다면 기반이 되는 타입과 확장이 되는 타입들의 원소 모두를 순회하는 방법이 있어야 하는데 방법이 마땅치 않다.
그런데도 확장이 가능한 enum 타입이 필요할 때가 있는데 대표적인 예가 연산코드이다. 연산코드 같이 명령어들을 열거타입으로 만들 때 보통 사용자가 확장 연산을 추가할 수 있도록 열어주기 때문에 확장이 용이해야 한다.
다행히 enum 타입이 인터페이스를 구현할 수 있어, 행위 자체를 인터페이스에 명명하고 그 인터페이스를 구현하면 된다.
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
@Override
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 Enum 자체는 확장할 수 없지만, 인터페이스인 Operation 은 확장할 수 있기 때문에, 이 인터페이스를 타입으로 사용하면 된다. 사칙연산에 이어서 지수 곱, 나머지 연산자를 추가하고 싶다면 아래와 같이 만들 수 있다.
public enum ExtendedOperation implements Operation {
EXP("^") {
@Override
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
@Override
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;
}
}
사용부에서 인터페이스를 사용하도록 작성되어 있다면, BasicOperation Enum 을 ExtendedOperation Enum 으로 교체할 수도 있다. 이미 인터페이스 내부에 메서드들이 명명되어 있어 열거 타입에 따로 추상 메서드를 선언하지 않아도 된다. (Enum 상수별 메서드 구현)
사용할 때는 아래 두 가지 방법이 존재한다.
1) 클래스 타입을 인자로 전달
public enum ExtendedOperation implements Operation {
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 operation : opEnumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n", x, operation, y, operation.apply(x, y));
}
}
}
한정적 타입 토큰 역할을 하는 Class 리터럴을 전달하고, 그 리터럴의 getEnumConstants() 함수를 사용해서 접근한다.
Class 객체가 열거 타입인 동시에 Operation 의 하위 타입어야 하기 때문에 함수의 타입은 <T extends Enum<T> & Operation> 이어야 한다. 열거 타입이어야 원소를 순회할 수 있고, Operation 이어야 원소가 뜻하는 연산을 할 수 있기 때문이다.
2) 한정적 와일드카드 타입을 인자로 전달
public enum ExtendedOperation implements Operation {
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> operations, double x, double y) {
for (Operation operation : operations) {
System.out.printf("%f %s %f = %f%n", x, operation, y, operation.apply(x, y));
}
}
}
enum 타입의 값들을 컬렉션 형태로 전달하면 여러 구현 타입의 연산을 조합해 호출할 수 있다는 장점이 있다.
다만 특정 연산(EnumSet, EnumMap) 은 사용하지 못 한다.
인터페이스를 이용해 확장 가능한 열거 타입을 흉내내는 이 방식에도 아래와 같은 문제점이 존재한다.
- 열거 타입끼리는 구현을 상속할 수 없다.
- 아무 상태에도 의존하지 않는 경우라면, 인터페이스를 구현한 enum 타입들에 직접 로직들을 작성해야 한다. (중복되는 로직이 많아진다면 정적 도우미 메서드나 클래스로 분리해야 한다.)
"enum 열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현한
기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다."
'독후감 > Effective JAVA' 카테고리의 다른 글
[Effective JAVA] 40 "@Override 애너테이션을 일관되게 사용하라" (1) | 2023.10.29 |
---|---|
[Effective JAVA] 39 "명명 패턴보다 애너테이션을 사용하라" (1) | 2023.10.24 |
[Effective JAVA] 37 "ordinal 인덱싱 대신 EnumMap 을 사용하라" (0) | 2023.08.15 |
[Effective JAVA] 36 "비트 필드 대신 EnumSet 을 사용하라" (0) | 2023.05.01 |
[Effective JAVA] 35 "ordinal 메서드 대신 인스턴스 필드를 사용하라" (0) | 2023.05.01 |