아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
요약하면 다음과 같다.
- 인터페이스를 이용해 열거 타입을 확장하는 효과를 가져올 수 있음. (대부분 필요없음)
- 효과를 보려면, 인터페이스 타입을 매개변수로 가지는 곳이 많아야 함.
- 이렇게 확장하는 경우 열거 타입의 한계로 인해 공통된 메서드가 반복될 수 있음. 이는 인터페이스의 default 메서드나 유틸 클래스로 분리할 수 있음.
대부분의 상황에서 열거 타입을 확장하는 건 좋지 않은 상황이다. 다음 세 가지 이유가 존재하기 때문이다.
- 확장한 타입의 원소는 기반 타입의 원소로 취급하지만, 그 반대는 성립하지 않기 때문이다.
- 기반 타입 / 확장된 타입들의 원소 모두를 순회할 방법도 마땅하지 않음.
- 이렇게 확장성을 높이려면 설계와 구현이 복잡해진다.
대부분의 상황에서는 어울리지 않지마느 연산 코드 (Operation Code) 같은 곳에서는 어울린다. 개발자가 개발한 기본 연산 외에도 확장 연산을 추가하는 경우가 있을 수 있다. 예를 들어 사칙연산만 구현되어 있는데 지수 연산을 추가하고 싶은 경우가 되겠다.
Enum을 확장하기 위한 Operation 인터페이스다.
public interface Operation {
double apply(double x, double y);
}
BasicOperation은 기본 사칙 연산만 구현한 열거 타입이다.
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; }
}
ExtendedOperation은 확장 연산을 구현한 열거 타입이다.
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 String symbol;
ExtendedOperation(String symbol) {this.symbol = symbol;}
@Override public String toString() { return symbol;}
}
이렇게 구현했을 때의 장점은 무엇일까? 둘다 Operation 인터페이스를 구현했기 때문에 매개변수의 타입이 Operation이 메서드에서는 둘다 같이 사용할 수 있으며, 잘못된 타입 전달 시에 컴파일 시점에 에러로 찾을 수 있게 된다.
사용하는 방법
Enum 확장한 것들을 사용할 때는 다음과 같이 사용하면 된다. 두 가지 방법이 존재한다
- 한정 타입 토큰을 매개변수로 사용해서 순회하는 경우
- Collection을 넘겨 직접 순회하는 경우
두 가지 경우가 모두 사용 가능하다. 하지만 한정 타입 토큰을 넘기는 경우에는 복잡하게 T extends Enum<T> & Operation을 사용해야한다. 이 때 extends가 괄호로 묶인다고 이해하는게 더 편하다. T extends (Enum<T> & Operation)라고 보고, 자기 자신을 타입으로 가지는 Enum 클래스이면서 Operation을 구현한 클래스라는 것이다.
public class Calculator {
public static void main(String[] args) {
double x = 0.2;
double y = 0.3;
test(ExtendedOperation.class, x, y);
testCollections(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void testCollections(List<ExtendedOperation> asList, double x, double y) {
for (ExtendedOperation exp : asList) {
System.out.printf("%f %s %f = %f%n",
x, exp, y, exp.apply(x, y));
}
}
private static <T extends Enum<T> & Operation> void test(
Class<T> enumType, double x, double y) {
for (T exp : enumType.getEnumConstants()) {
System.out.printf("%f %s %f = %f%n",
x, exp, y, exp.apply(x, y));
}
}
아무튼 이렇게 사용할 경우, Operation 인터페이스 타입을 매개변수로 받는 곳에 한해서는 Enum을 확장해서 사용할 수 있다.
Enum의 확장의 한계점.
인터페이스를 이용해 Enum의 확장을 흉내내더라도 한계는 존재한다. 확장된 Enum 타입 각각이 공통된 기능을 사용한다고 하더라도, Enum 타입끼리는 이 코드를 공유할 수 없기 때문에 중복된 코드가 이곳저곳에서 발생할 수 있다.
예를 들면 ExtendedOperation, BasicOperation 열거 타입에서 다음 메서드를 공통으로 가지고 있다고 가정해보자. 그런데 이 메서드가 ExtendedOperatio, BasicOperation에 각각 있어야 한다는 것이다.
public double getPiNumber(){
return 3.14;
}
이것이 인터페이스를 이용한 Enum 확장의 한계점이다. 극복 방안은 다음과 같다.
- 만약 상태를 가지지 않은 경우, 인터페이스에 default 메서드를 선언해서 공유해서 사용한다.
- 만약 상태를 가진 경우, UtilClass로 공통 메서드를 뺀다.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Effective Java : 아이템 42. 익명 클래스보다는 람다를 사용하라. (0) | 2023.08.06 |
---|---|
Effective Java : 아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라. (0) | 2023.08.04 |
Effective Java : 아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라 (0) | 2023.08.04 |
Effective Java : 아이템 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2023.08.01 |
Effective Java : 아이템 34. int 상수 대신 열거 타입을 사용하라 (0) | 2023.07.31 |