Java/TDD

[TDD] 문자열 계산기 && 2차 리팩토링

Gamii 2024. 6. 17. 08:10
728x90

 

1.  리팩토링 반영 List

1) 객체 지향 관점에서 최대한 클래스로 묶을 있는 부분은 묶자.

  • 문자열 split Class(Formula Class), 사칙연산 Class(Operator Class), 연산 실행 Class(Calculator) 

 

2) 다양한 Test 코드 만들기(예외 처리)

  • 분리한 Class 별로 Test
  • 인수가 null이거나 공백일 경우 (예 - "2 +  + 3 * 4")
  • 사칙연산 기호가 아닐 경우 (예 - "2 & 3 * 2")
  • 0으로 나눌 경우 (예 - "3 / 0 + 2")

 

 

 

 

 

2.  3차 실습 코드(caclulator_v2)

 

package calculator_v3;


import java.util.regex.Pattern;

public class Calculator {
    private Operator currentOperator;
    private static final String regExp = "[0-9]+";

    public int calculateFormula(String[] formulaArr){
        //초기화
        int result = 0;
        currentOperator = Operator.PLUS;

        for(String formulaPart : formulaArr){
            int addValue = calculatePartial(result, formulaPart);
            if(addValue != 0) {
                result = addValue;
            }
        }

        return result;
    }

    private int calculatePartial(int result, String formulaPart){
        if(Pattern.matches(regExp, formulaPart)){
            //숫자일 경우
            return currentOperator.operate(result, Integer.parseInt(formulaPart));
        }

        //문자 설정
        currentOperator = Operator.findSymbol(formulaPart);
        return 0;
    }
}

 

 

package calculator_v3;

public class Formula {
    private String formula;
    private String splitStr;

    public Formula(String formula, String splitStr){
        this.formula = formula;
        this.splitStr = splitStr;
    }

    public String[] splitFormula(){
        if(splitStr != null){
            return formula.split(splitStr);
        }

        throw new IllegalArgumentException();
    }
}

 

더보기

Enum Class) 상수 정의 및 초기화

각 상수(PLUS, MINUS, MULTIPLY, DIVIDE)는 enum 생성자를 호출하여 초기화된다. 상수 정의 부분에서 생성자에 전달되는 인수는 각각의 필드와 연관되어 있다.

예)

  • PLUS("+", (num1, num2) -> num1 + num2):
    • symbol 필드는 +로 초기화된다.
    • operation 필드는 (num1, num2) -> num1 + num2라는 람다 표현식으로 초기화된다.
package calculator_v3;

import java.util.Collections;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public enum Operator {
    PLUS("+", (num1, num2) -> num1 + num2),
    MINUS("-", (num1, num2) -> num1 - num2),
    MULTIPLY("*", (num1, num2) -> num1 * num2),
    DIVIDE("/", (num1, num2) -> {
        if(num2 == 0) throw new IllegalArgumentException();
        return num1 / num2;
    });

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

    private static final Map<String, Operator> operatorMap
            = Collections.unmodifiableMap(Stream.of(values()).collect(
                            Collectors.toMap(Operator::getSymbol, operator -> operator)));

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

    public String getSymbol(){
        return symbol;
    }

    public int operate(int num1, int num2){
        return operation.apply(num1, num2);
    }

    public static Operator findSymbol(String symbol){
        Operator operator = operatorMap.get(symbol);

        if(operator == null){
            throw new IllegalArgumentException();
        }

        return operator;
    }
}

 

 

package study;

import calculator_v3.Calculator;
import calculator_v3.Formula;
import calculator_v3.Operator;
import org.assertj.core.util.TriFunction;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;


import java.util.Arrays;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.Assertions.*;

public class Calculator_v3 {

    public Operator currentOperator;

    public Calculator calculator = new Calculator();

    @DisplayName("Formula Class 테스트")
    @Test
    void formulaTest(){
        String[] formulaArr =  new Formula("2 + 3 * 4 / 2", " ").splitFormula();

        int result = calculator.calculateFormula(formulaArr);

        assertThat(10).isEqualTo(result);
    }

    @DisplayName("Formula 공백 test - IllegalArgumentException")
    @Test
    void formulaTest2(){
        assertThatThrownBy(() -> {
            String[] formulaArr =  new Formula("2 +   * 4 / 2", " ").splitFormula();
            int result = calculator.calculateFormula(formulaArr);
        }).isInstanceOf(IllegalArgumentException.class).hasToString("공백 예외!!");
    }

    @DisplayName("Formula 사칙연산이 아닌 기호 test - IllegalArgumentException")
    @Test
    void formulaTest3(){
        assertThatThrownBy(() -> {
            String[] formulaArr =  new Formula("2 & 4  * 4 / 2", " ").splitFormula();
            int result = calculator.calculateFormula(formulaArr);
        }).isInstanceOf(IllegalArgumentException.class).hasToString("사칙 연산 예외!!");
    }

    @DisplayName("Formula 0으로 나눌 때 test - IllegalArgumentException")
    @Test
    void formulaTest4(){
        assertThatThrownBy(() -> {
            String[] formulaArr =  new Formula("2 / 0  * 4 / 2", " ").splitFormula();
            int result = calculator.calculateFormula(formulaArr);
        }).isInstanceOf(IllegalArgumentException.class).hasToString("0으로 나눌 때 예외!!");
    }

    @DisplayName("Operator Class 테스트")
    @Test
    void operatorTest1(){
        assertThat(10).isEqualTo(Operator.PLUS.operate(5,5));
    }
}

 

 

 

 

 

 

 

 

[느낀점]

1. 최대한 Class를 작게 나눠서 그 Class 객체의 책임과 역할에 맞게 코드를 작성하자.

    처음 코드 작성했을 때는 한 Class에 많은 역할이 들어가 있었다. 더 작게 분리해서 리팩토링을 해보니 확실히 Class의 역할이 눈에 잘 보이고 기능을 찾기 수월했다.

 

 

 

2. Enum Class와 가까워 졌다.

    여기서는 필드를 명시만 하고 끝내는 게 아니라 해당 필드에 관련된 메서드도 만들어서 사용하므로써 꽉 찬(?) 사칙연산 Class가 되었다. Enum에 관련된 메서드는 Enum Class에 명시해서 사용할 수 있도록 만들어보자. 

 

 

 

 

 

 

 

 

[참고]

 

Enum 조회 성능 높여보기 - HashMap을 이용해서 빠르게 조회해보자

Enum 조회 성능 높여보기 자바의 Enum 에서 매핑을 할 때 find 메서드를 정의해서 알맞는 enum 을 찾는 로직을 많이 구현해본적이 있을 것 같아요. 다음과 같은 코드가 익숙하실텐테요. 저도 이런식으

pjh3749.tistory.com

 

 

2차 피드백

##### '객체지향' 관점을 견지하고 구현하는 것이 중요하다 ```{.no-highlight} 평소에 코드를 짤 때에 항상 클래스로 처리가 가능한 것이 있으면 되도록 클래스로 …

wikidocs.net

 

'Java > TDD' 카테고리의 다른 글

[TDD] 문자열 계산기 && 1차 리팩토링  (0) 2024.05.28