다형성을 사용해 if/else를 제거하는 리팩토링을 하라.

2017-11-03 09:45

오늘 리팩토링할 코드는 사칙연산을 계산하는 메소드이다.

Number라는 클래스는 int를 생성자의 인자로 가지며, 사칙연산을 실행하는 calculate() 메소드가 있다. 이 메소드는 사칙연산 기호에 따라 덧셈, 뺄쎔, 곱셈, 나눗셈 연산을 하는 메소드이다. 나눗셈 결과는 int로 가정하고 구현했다.

이 코드에 대한 리팩토링 요구사항은 calculate() 메소드에서 발생하는 if 문을 제거하는 것이다. 일반적으로 if/else로 구현하겠지만 else를 사용하지 않고 if만으로 구현했다.

어떻게 제거할 수 있을까?

public class Number {
    private int no;

    public Number(int no) {
        this.no = no;
    }

    public Number calculate(String expression, Number number) {
        if ("+".equals(expression)) {
          return new Number(this.no + number.no);
        }

        if ("-".equals(expression)) {
          return new Number(this.no - number.no);
        }

        if ("*".equals(expression)) {
          return new Number(this.no * number.no);
        }

        if ("/".equals(expression)) {
          return new Number(this.no / number.no);
        }

        throw new IllegalArgumentException();
    }
}

힌트 - 자바의 다형성, enum 등을 활용해 제거해 본다.

위 샘플 예제는 코드스쿼드 에서 새롭게 진행 중인 마스터즈 코스에서 발췌한 코드입니다. 코드스쿼드의 마스터즈 코스는 코드 리뷰 방식의 개인별 맞춤 학습 방법입니다.

0개의 의견 from FB

BEST 의견 원본위치로↓
2017-11-03 17:06

저도 비슷한 작업을 해봤는데요. Enum에 List에 대한 Sort를 처리를 하도록 하는..Comparator를 직접 "(o1, o2) -> o1.getxxx() > o2.getxxx();" 써도 되기는 하는데.. 여튼 만들어보면..

public enum SortType {
    ASC("d-asc", o -> {
       o.sort(Comparator.comparingInt(CObj::getDay));
    },
    DESC("d-desc", o -> {
       o.sort(Comparator.comparingInt(CObj::getDay).reversed());
    };

    private String description;

    private CObjSorter<CObj> cObjSorter;

    SortType(String description, CObjSorter<CObj> cObjSorter) {
        this.description = description;
        this.cObjSorter = cObjSorter;
    }

    public String getDescription() {
        return this.description;
    }

    public void sort(List<CObj> cObjs) {
        cObjSorter.apply(cObjs);
    }
}
// 또는 T
@FunctionalInterface
public interface CObjSorter<CObj> {

    void apply(List<CObj> cObjs);

}

뭐 대충 이런식. 근데 음.. 뭐랄까 enum이 좀 애매해 보이기도하고 이뻐보이지가 않기도 하고.. 해서 다른 방법을 고민중였는데, 공부되네요.~

6개의 의견 from SLiPP

2017-11-03 10:29

자신만의 연산을 가지고 있는 Enum 을 만들어서 구현해보았습니다.

java 8 이전

public class Number {
    private int no;

    public Number(int no) {
        this.no = no;
    }

    public Number calculate(String expression, Number number) {
        return Calculation.findCalculation(expression).calculate(this, number);
    }

    private enum Calculation {
        ADDITION("+", new Calculator() {
            @Override
            public int calculate(int number1, int number2) {
                return number1 + number2;
            }
        }),
        SUBTRACTION("-", new Calculator() {
            @Override
            public int calculate(int number1, int number2) {
                return number1 - number2;
            }
        }),
        MULTIPLICATION("*", new Calculator() {
            @Override
            public int calculate(int number1, int number2) {
                return number1 * number2;
            }
        }),
        DIVISION("/", new Calculator() {
            @Override
            public int calculate(int number1, int number2) {
                return number1 / number2;
            }
        });
        
        private String expression;
        private Calculator calculator;

        Calculation(String expression, Calculator calculator) {
            this.expression = expression;
            this.calculator = calculator;
        }
        
        private Number calculate(Number number1, Number number2) {
            return new Number(this.calculator.calculate(number1.no, number2.no));
        }
        
        private boolean isMatchedExpression(String expression) {
            return this.expression.equals(expression);
        }
        
        private static Calculation findCalculation(String expression) {
            for(Calculation calculation : Calculation.values()) {
                if(calculation.isMatchedExpression(expression)) {
                    return calculation;
                }
            }
            throw new IllegalArgumentException();
        }

        private interface Calculator {
            int calculate(int number1, int number2);
        }
    }
}

java 8부터는 java.util.function에서 제공되는 BiFunction 인터페이스를 사용하여 별도의 인터페이스를 만들지 않고 구현해보았습니다!

java 8 이후

public class Number {
    private int no;

    public Number(int no) {
        this.no = no;
    }

    public Number calculate(String expression, Number number) {
        return Calculation.findCalculation(expression).calculate(this, number);
    }

    private enum Calculation {
        ADDITION("+", (number1, number2) -> number1 + number2),
        SUBTRACTION("-", (number1, number2) -> number1 - number2),
        MULTIPLICATION("*", (number1, number2) -> number1 * number2),
        DIVISION("/", (number1, number2) -> number1 / number2);
        
        private String expression;
        private BiFunction<Integer, Integer, Integer> calculator;

        Calculation(String expression, BiFunction<Integer, Integer, Integer> calculator) {
            this.expression = expression;
            this.calculator = calculator;
        }
        
        private Number calculate(Number number1, Number number2) {
            return new Number(this.calculator.apply(number1.no, number2.no));
        }
        
        private boolean isMatchedExpression(String expression) {
            return this.expression.equals(expression);
        }
        
        private static Calculation findCalculation(String expression) {
            for(Calculation calculation : Calculation.values()) {
                if(calculation.isMatchedExpression(expression)) {
                    return calculation;
                }
            }
            throw new IllegalArgumentException();
        }
    }
}

아래는 for문을 Map 탐색으로 변경해본 방식입니다.

public class Number {
    private int no;

    public Number(int no) {
        this.no = no;
    }

    public Number calculate(String expression, Number number) {
        return Calculation.findCalculation(expression).calculate(this, number);
    }

    private enum Calculation {
        ADDITION("+", (number1, number2) -> number1 + number2),
        SUBTRACTION("-", (number1, number2) -> number1 - number2),
        MULTIPLICATION("*", (number1, number2) -> number1 * number2),
        DIVISION("/", (number1, number2) -> number1 / number2);
        
        private String expression;
        private BiFunction<Integer, Integer, Integer> calculator;
        private static Map<String, Calculation> calculationMap = Arrays.stream(Calculation.values()).collect(toMap(o -> o.expression, o -> o));

        Calculation(String expression, BiFunction<Integer, Integer, Integer> calculator) {
            this.expression = expression;
            this.calculator = calculator;
        }
        
        private Number calculate(Number number1, Number number2) {
            return new Number(this.calculator.apply(number1.no, number2.no));
        }
        
        private static Calculation findCalculation(String expression) {
            return Optional.ofNullable(calculationMap.getOrDefault(expression, null)).orElseThrow(IllegalArgumentException::new);
        }
    }
}
2017-11-03 12:03

업무상 필요해서 구현했던건데... 슬쩍 던져봅니다.

public enum Arithmetic {

	ADDITION("+") {
		@Override
		public double apply(double x1, double x2) {
			return x1 + x2;
		}
	},
	SUBTRACTION("-") {
		@Override
		public double apply(double x1, double x2) {
			return x1 - x2;
		}
	},
	MULTIPLICATION("*") {
		@Override
		public double apply(double x1, double x2) {
			return x1 * x2;
		}
	},
	DIVISION("/") {
		@Override
		public double apply(double x1, double x2) throws HrbException {
			if (x2 == 0) {
				throw new Exception('DIVIDE_BY_ZERO');		//TODO divide by zero 일 경우가 있는지 체크
			}
			return x1 / x2;
		}
	},
	
	;

	private final String textOperator;

	private Arithmetic(String textOperator) {
		this.textOperator = textOperator;
	}

	public abstract double apply(double x1, double x2) throws HrbException;

	public String getTextOperator() {
		return textOperator;
	}

}

2017-11-03 12:32

@권용근 예상 가능한 모든 답변을 구현했네요. 굿..

@kimmunsu 수강생 중에 한명이 네가 구현한 것과 똑같이 구현했네.

Map과 BiFunction을 사용한 구현 코드 공유해 본다.

public class Number {
    private static final Map<String, BiFunction<Number, Number, Number>> operators = new HashMap<>();
    
    static {
        operators.put("+", (a, b) -> new Number(a.no + b.no));
        operators.put("-", (a, b) -> new Number(a.no - b.no));
        operators.put("*", (a, b) -> new Number(a.no * b.no));
        operators.put("/", (a, b) -> new Number(a.no / b.no));
    }

    private int no;

    public Number(int no) {
        this.no = no;
    }

    public Number calculate(String expression, Number number) {
        BiFunction<Number, Number, Number> operator = operators.get(expression);
        if (operator == null) {
          throw new IllegalArgumentException();
        }
        
        return operator.apply(this, number);
    }
}
2017-11-03 13:34

@자바지기 https://stackoverflow.com/questions/2902458/is-it-possible-to-pass-arithmetic-operators-to-a-method-in-java 아마 저도 뭘 보고 했을텐데, 이 링크가 그 힌트인듯 합니다 ㅎㅎㅎㅎㅎ

2017-11-03 17:06

저도 비슷한 작업을 해봤는데요. Enum에 List에 대한 Sort를 처리를 하도록 하는..Comparator를 직접 "(o1, o2) -> o1.getxxx() > o2.getxxx();" 써도 되기는 하는데.. 여튼 만들어보면..

public enum SortType {
    ASC("d-asc", o -> {
       o.sort(Comparator.comparingInt(CObj::getDay));
    },
    DESC("d-desc", o -> {
       o.sort(Comparator.comparingInt(CObj::getDay).reversed());
    };

    private String description;

    private CObjSorter<CObj> cObjSorter;

    SortType(String description, CObjSorter<CObj> cObjSorter) {
        this.description = description;
        this.cObjSorter = cObjSorter;
    }

    public String getDescription() {
        return this.description;
    }

    public void sort(List<CObj> cObjs) {
        cObjSorter.apply(cObjs);
    }
}
// 또는 T
@FunctionalInterface
public interface CObjSorter<CObj> {

    void apply(List<CObj> cObjs);

}

뭐 대충 이런식. 근데 음.. 뭐랄까 enum이 좀 애매해 보이기도하고 이뻐보이지가 않기도 하고.. 해서 다른 방법을 고민중였는데, 공부되네요.~

의견 추가하기