Enum으로 다형성을 구현하는 방법

bp.chys·2020년 3월 10일
2

메서드 오버라이드

각 Enum 요소는 추상 메서드를 오버라이드하여 다형성을 구사할 수 있는데, 다음 Operator 예제를 살펴보자.

각 연산자 enum 요소는 calculate이라는 추상메서드를 오버라이드하여 같은 메서드 시그니처로 다른 결과값을 만들어 낼 수 있다.

public enum Operator {
    PLUS("+") {
        @Override
        public int calculate(int num1, int num2) {
            return num1 + num2;
        }
    },
    MINUS("-") {
        @Override
        public int calculate(int num1, int num2) {
            return num1 - num2;
        }
    },
    MULTIPLY("*") {
        @Override
        public int calculate(int num1, int num2) {
            return num1 * num2;
        }
    },
    DIVIDE("/") {
        @Override
        public int calculate(int num1, int num2) {
            return num1 / num2;
        }
    };

    private String symbol;

    Operator(String symbol) {
        this.symbol = symbol;
    }

    public abstract int calculate(int num1, int num2);
}

Lambda 식

java 8 이상에서는 람다식을 활용하면 추상 메서드를 오버라이드 하지 않고도 함수를 상태값을 갖는 enum으로 변경하여 다형성을 커버할 수 있다.

public enum Operator {
    PLUS("+", (num1, num2) -> num1 + num2),
    MINUS("-", (num1, num2) -> num1 - num2),
    MULTIPLY("*", (num1, num2) -> num1 * num2),
    DIVIDE("/", (num1, num2) -> num1 / num2);

    private String symbol;
    private BiFunction<Integer, Integer, Integer> expression;

    Operator(String symbol, BiFunction<Integer, Integer, Integer> expression) {
        this.symbol = symbol;
        this.expression = expression;
    }

    public int calculate(int num1, int num2) {
        return expression.apply(num1, num2);
    }
}

BiFunction은 두 개의 인자를 받아서 새로운 값을 리턴하는 함수 프로시저 자체를 자료형으로 지정할 수 있다. 이 프로시저를 상수값으로 갖도록 enum을 변경한다면 apply()를 통해서 두 인자를 받아 연산자마다 적절한 연산 결과를 리턴하도록 만들 수 있다.


Enum 활용한 리팩토링 예제

이번엔 다음 주어진 FigureFactory 에서 숫자 2, 3, 4 대신 enum을 사용해서 리팩토링 하는 과정을 살펴보자.

public class FigureFactory {
    private static Map creators = new HashMap<>();

    static {
        creators.put(2, new LineCreator());
        creators.put(3, new TriangleCreator());
        creators.put(4, new RectangleCreator());
    }

    static Figure getInstance(List points) {
        FigureCreator figureCreator = creators.get(points.size());
        if (figureCreator == null) {
            throw new IllegalArgumentException("유효하지 않은 도형입니다.");
        }
        return figureCreator.create(points);
    }
}

class LineCreator implements FigureCreator {
    @Override
    public Figure create(List points) {
        return new Line(points);
    }
}

class TriangleCreator implements FigureCreator {
    @Override
    public Figure create(List points) {
        return new Triangle(points);
    }
}

class RectangleCreator implements FigureCreator {
    @Override
    public Figure create(List points) {
        return new Rectangle(points);
    }
}

현재는 2, 3, 4라는 상수값을 기준으로 FigureCreator의 각 creator 인스턴스를 Map의 형태로 저장하고 있다. 하지만 Enum은 관련된 상수들을 묶어서 하나의 멤버로 사용할 수 있다는 장점이 있다. 점의 갯수를 가리키는 상수값과 FigureCreator를 묶어서 enum으로 만들어보자.

public enum FigureManager {
    LINE(2, new LineCreator()),
    TRIANGLE(3, new TriangleCreator()),
    RECTANGLE(4, new RectangleCreator());

    private int dots;
    private FigureCreator figureCreator;

    FigureManager(int dots, FigureCreator figureCreator) {
        this.dots = dots;
        this.figureCreator = figureCreator;
    }

    public static FigureCreator getCreatorByDots(int size) {
        return Arrays.stream(FigureManager.values())
                .filter(f -> f.dots == size)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 도형입니다."))
                .figureCreator;
    }
}

일단 임시로 FigureManager라는 약간 애매한 이름의 Type으로 enum을 만들었다.
이 enum type과 함께 getCreatorByDots라는 메소드를 정의하니 FigureFactory에서 map을 제거할 수 있고, 코드도 많이 단순해 졌음을 확인할 수 있다.

public class FigureFactory {
    public static Figure getFigure(List<Point> points) {
        FigureCreator figureCreator = FigureManager.getCreatorByDots(points.size());
        if (figureCreator == null) {
            throw new IllegalArgumentException("유효하지 않은 도형입니다.");
        }
        return figureCreator.create(points);
    }
}

표면적으로 creator를 enum화 시키는데는 성공했다.
하지만 FigureFactory 에서 getFigure메서드를 살펴보자.
figureCreator를 enum에서 getCreatorByDots를 통해 받아고 있는데 이미 호출됨과 동시에 null 체크를 하기 때문에 외부에서 null을 또 체크할 필요가 없어진다.

public class FigureFactory {
    public static Figure getFigure(List<Point> points) {
        FigureCreator figureCreator = FigureManager.getCreatorByDots(points.size());
        return figureCreator.create(points);
    }
}

사실 더 중요한 부분이 있다.
FigureCreator는 도형을 create하는 메서드만을 갖고 있다. 그렇다면 FigureManager에서 상수값으로 생성된 FigureCreator를 갖고 create를 따로 호출하는 대신 create 메서드를 바로 오버라이드하여 FigureManager가 바로 create을 호출하는 것은 어떨까?

현재 FigureCreator도 세 개의 클래스로 분리되어 구현되어 있기 때문에 구현한 메서드가 하나 밖에 없다면 FigureManager로 통합하여도 무방할 것 같다. 리팩토링 해보자.

public enum FigureManager {
    LINE(2) {
        @Override
        public Figure create(List<Point> points) {
            return new Line(points);
        }
    },
    TRIANGLE(3) {
        @Override
        public Figure create(List<Point> points) {
            return new Triangle(points);
        }
    },
    RECTANGLE(4) {
        @Override
        public Figure create(List<Point> points) {
            return new Rectangle(points);
        }
    };

    private int dots;

    FigureManager(int dots) {
        this.dots = dots;
    }

    public static FigureManager getManagerByDots(int size) {
        return Arrays.stream(FigureManager.values())
                .filter(f -> f.dots == size)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 도형입니다."));
    }
    
    public abstract Figure create(List<Point> points);
public class FigureFactory {
    public static Figure getFigure(List<Point> points) {
        FigureManager figureManager = FigureManager.getManagerByDots(points.size());
        return figureManager.create(points);
    }
}

이 역시도 추상 메서드의 오버라이드 보다 람다식을 활용하면 더 간편하게 다형성을 구현할 수 있다.

public enum FigureManager {
    LINE(2, Line::new),
    TRIANGLE(3, Triangle::new),
    RECTANGLE(4, Rectangle::new);

    private final int dots;
    private final Function<List<Point>, Figure> creation;

    FigureManager(final int dots, final Function<List<Point>, Figure> creation) {
        this.dots = dots;
        this.creation = creation;
    }

    public static FigureManager find(int size) {
        return Arrays.stream(FigureManager.values())
                .filter(f -> f.dots == size)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 도형입니다."));
    }

    public Figure create(final List<Point> points) {
        return creation.apply(points);
    }
}

처음부터 보면 입력받은 점의 갯수와 점들을 가지고 원하는 도형을 생성하는 기능에 대해서 리팩토링을 진행해보았다.
마지막에 리팩토링한 방식과 같이, FigureCreator를 제거하고 기존의 creator가 구현했던 create를 람다식으로 구현하면 한개의 enum만으로도 불필요한 코드를 제거하고 좀 더 코드를 단순하고 읽기 쉽게 작성할 수 있다.

profile
하루에 한걸음씩, 꾸준히

0개의 댓글