티스토리 뷰
인터넷을 하다가 https://www.jetbrains.com/help/idea/replace-conditional-logic-with-strategy-pattern.html#5bed2 에서 아래와 같은 코드를 발견했다.
public class IfElseDemo {
public double calculateInsurance(double income) {
if (income <= 10000) {
return income*0.365;
} else if (income <= 30000) {
return (income-10000)*0.2+35600;
} else if (income <= 60000) {
return (income-30000)*0.1+76500;
} else {
return (income-60000)*0.02+105600;
}
}
}
해당 사이트에서는 if else가 좋지 않은 코드를 만들기 때문에 어떻게 깔끔하게 고칠 수 있는지 보여주려고 했다.
if else가 나쁜 것이라고 단정짓기는 어렵지만, 좀 더 OOP스럽게 고칠 수 있다는 점에는 동의한다. 저 사이트에서 제공하는 솔루션을 보았다.
class IfElseDemo {
private InsuranceStrategy strategy;
public double calculateInsurance(double income) {
if (income <= 10000) {
strategy = new InsuranceStrategyLow();
return strategy.calculateInsuranceVeryHigh(income);
} else if (income <= 30000) {
strategy = new InsuranceStrategyMedium();
return strategy.calculateInsuranceVeryHigh(income);
} else if (income <= 60000) {
strategy = new InsuranceStrategyHigh();
return strategy.calculateInsuranceVeryHigh(income);
} else {
strategy = new InsuranceStrategyVeryHigh();
return strategy.calculateInsuranceVeryHigh(income);
}
}
}
난 잠시 생각했다. "뭔가 이상해". if else를 없앤다고 했는데, 제일 핵심적인 if else는 그래도 남아있다. 오히려 괜히 클래스만 더 만드는 수고를 했다. "이건 아니다" 라는 생각을 하면서 더 나은 방법에 대해서 생각해 보았다.
흔히 if else나 switch문은 별도의 클래스로 만들 수가 있다. 이 때의 장점은 OOP의 상속의 장점을 이용할 수가 있기 때문에 타입에 안전한 코드를 만들 수 있다.
먼저 인터페이스를 만들자.
interface IInsuranceCalculation {
boolean accept();
double calculate();
}
그리고 각각의 조건을 클래스로 만든다.
class LowIncome implements IInsuranceCalculation {
private double income;
public LowIncome(double income) {
this.income = income;
}
@Override
public boolean accept() {
return income <= 10000;
}
@Override
public double calculate() {
return income * 0.365;
}
}
class MediumIncome implements IInsuranceCalculation {
private double income;
public MediumIncome(double income) {
this.income = income;
}
@Override
public boolean accept() {
return income <= 30000;
}
@Override
public double calculate() {
return (income - 10000) * 0.2 + 35600;
}
}
class HighIncome implements IInsuranceCalculation {
private double income;
public HighIncome(double income) {
this.income = income;
}
@Override
public boolean accept() {
return income <= 60000;
}
@Override
public double calculate() {
return (income - 30000) * 0.1 + 76500;
}
}
class VeryHighIncome implements IInsuranceCalculation {
private double income;
public VeryHighIncome(double income) {
this.income = income;
}
@Override
public boolean accept() {
return income > 60000;
}
@Override
public double calculate() {
return (income - 60000) * 0.02 + 105600;
}
}
물론 위의 고정된 숫자들은 상수로 만들면 더 좋을 것이고 IInsuranceCalculation
는 Interface
대신 Abstract class
를 쓸 수 있을 것이다.
그리고 클라이언트가 코드가 사용할 수 있는 중계 클래스가 필요하다. 실제 보험료 계산 클래스가 어떻게 선택되는지 클라이언트 쪽이 알게 할 필요가 없다.(Encapsulation)
class InsuranceCalculator {
private List<IInsuranceCalculation> incomes = new ArrayList<>();
public InsuranceCalculator(List<IInsuranceCalculation> incomes) {
this.incomes = incomes;
}
public double calculate(double income) {
for (IInsuranceCalculation calculation : incomes) {
if (calculation.accept()) {
return calculation.calculate();
}
}
throw new UnsupportedOperationException("Cannot find suitable calculation");
}
}
InsuranceCalculator
에서 인컴 클래스들을 직접 생성할 수도 있겠지만, 외부에서 injection하는 방법도 있겠다. OOP에서는 한가지 목적에만 충실할 수록 좋은 코드를 만들 수 있는 확률이 더 올라가다는 원칙이 있다.(Single Responsibility Principle). 나중에 SpecialIncome
처럼 인컴 클래스가 더 늘어나게 되면 InsuranceCalculator
를 손댈 필요가 없다.
이제 IfElseDemo
클래스를 수정할 차례다.
public class IfElseDemo {
private InsuranceCalculator calculator;
public IfElseDemo(InsuranceCalculator calculator) {
this.calculator = calculator;
}
public double calculateInsurance(double income) {
return calculator.calculate(income);
}
}
이게 인컴 조건에 대한 변경사항이 오면 해당 인컴 클래스만 찾아서 고치면 된다. 또한 클래스 이름으로 인컴상태가 구분이 되므로 쉽게 해당 클래스를 찾을 수 있다.
마지막으로 테스트를 돌려본다. 기존 기능이 여전히 잘 동작하는지 검증하는 건 아주 중요하니까.
public class IfElseDemoTest {
@Test
public void low() {
assertEquals(1825, insuranceFor(5000), 0.01);
}
@Test
public void medium() {
assertEquals(38600, insuranceFor(25000), 0.01);
}
@Test
public void high() {
assertEquals(78500, insuranceFor(50000), 0.01);
}
@Test
public void veryHigh() {
assertEquals(106400, insuranceFor(100_000), 0.01);
}
private double insuranceFor(double income) {
return new IfElseDemo(
new InsuranceCalculator(
List.of(
new LowIncome(income),
new MediumIncome(income),
new HighIncome(income),
new VeryHighIncome(income)
)
)
).calculateInsurance(income);
}
}
테스가 모두 잘 동작한다. 굿잡~
'Object Oriented Programming > Refactoring' 카테고리의 다른 글
Erase Calcluations (0) | 2020.10.26 |
---|