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

    아이템 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 확장의 한계점이다. 극복 방안은 다음과 같다. 

    1. 만약 상태를 가지지 않은 경우, 인터페이스에 default 메서드를 선언해서 공유해서 사용한다.  
    2. 만약 상태를 가진 경우, UtilClass로 공통 메서드를 뺀다. 

     

     

    댓글

    Designed by JB FACTORY