2번째 미션 - 로또, TDD
로또 미션 이전에 자동차 경주 5단계까지 마무리했는데 스프링이나 DB 공부나 과제 미션 클리어하는데 집중한다고 제대로 블로그를 작성하지 못했다.. 지금부터라도 제대로 작성해보자! 이번 첫 단계인 문자열 사칙연산 계산기는 좋은 피드백도 많이 받아서 기분이 좋다ㅎㅎ
문자열 사칙연산 계산기
Operand 클래스
public class Operand {
private static final Pattern NUMBER_PATTERN = Pattern.compile("^[0-9]*$");
private static final String ILLEGAL_NUMBER_MESSAGE = "양수를 입력해주세요.";
private final String operand; // 양수만 입력가능하다면 변환후 int 로 관리하는것도 좋을것 같습니다.
public Operand(String operand) {
this.operand = validatedOperand(operand);
}
public int number() {
return Integer.parseInt(operand);
}
private String validatedOperand(String operand) {
if (!isOperand(operand)) {
throw new IllegalArgumentException(ILLEGAL_NUMBER_MESSAGE);
}
return operand;
}
private boolean isOperand(String operand) {
Matcher operandMatcher = NUMBER_PATTERN.matcher(operand);
return operandMatcher.find();
}
}
Operand의 인스턴스 변수 number를 String으로 관리한다는게 조금 이상하지 않나 생각했는데 역시 int로 관리하는게 좋다는 피드백을 받았다.
Operator 클래스
// 연산자에대한 enum 선언 및 캐싱처리 👍
public enum Operator {
ADDITION("+", (operand1, operand2) -> operand1 + operand2),
SUBTRACTION("-", (operand1, operand2) -> operand1 - operand2),
MULTIPLICATION("*", (operand1, operand2) -> operand1 * operand2),
DIVISION("/", (operand1, operand2) -> {
validateDivision(operand2);
return operand1 / operand2;
});
private static final String ILLEGAL_OPERATOR_MESSAGE = "연산자 기호를 입력해주세요.";
private static final String ILLEGAL_DIVISION_MESSAGE = "0으로 나눌 수 없습니다. 다른 숫자를 입력해주세요.";
private static final Map<String, Operator> BY_OPERATOR = new HashMap<>();
static {
for (Operator operator : values()) {
BY_OPERATOR.put(operator.operator, operator);
}
}
private final String operator;
private final BinaryOperator<Integer> calculation;
Operator(final String operator, final BinaryOperator<Integer> calculation) {
this.operator = operator;
this.calculation = calculation;
}
public int applyCalculation(int operand1, int operand2) {
return calculation.apply(operand1, operand2);
}
public static Operator valueOfOperator(String operator) {
validateContainsKey(operator);
return BY_OPERATOR.get(operator);
}
private static void validateDivision(int operand2) {
if (operand2 == 0) {
throw new IllegalArgumentException(ILLEGAL_DIVISION_MESSAGE);
}
}
private static void validateContainsKey(String operator) {
if (!BY_OPERATOR.containsKey(operator)) {
throw new IllegalArgumentException(ILLEGAL_OPERATOR_MESSAGE);
}
}
}
enum 구현과 어떻게 하면 캡슐화할 수 있을지 정말 머리 꽁꽁 싸맸던 부분인데
연산자에 대한 enum 선언과 캐싱 처리에 대해 따봉 받았는데 이것 자체만으로 너무 보상받는 기분이다ㅎㅎ
Operators 클래스
public class Operators {
private final Queue<Operator> operators; // 일급 컬렉션 사용 👍
public Operators(Queue<Operator> operators) {
this.operators = operators;
}
public static Operators of(List<String> operators) {
Queue<Operator> operatorList = new LinkedList<>();
for (String operator : operators) {
operatorList.add(Operator.valueOfOperator(operator));
}
return new Operators(operatorList);
}
public Operator next() { // getter를 작성하기 보다는 스트림에서 하나씩 꺼내 쓰도록 해당 메서드를 작성했다.
return operators.poll();
}
}
Calculator 클래스
public class Calculator {
private static final int ZERO_CONDITION = 0;
private static final int ODD_CONDITION = 1;
private static final int EVEN_CONDITION = 2;
private static final String ILLEGAL_OPERATOR_MESSAGE = "올바른 사칙 연산을 입력해주세요.";
private static final String INCALCULABLE_MESSAGE = "계산이 가능한 수식을 입력해주세요.";
private static final Pattern OPERATORS_SYMBOL = Pattern.compile("^(\\+|-|\\*|\\/)$");
private final Operators operators;
private final Operands operands;
public Calculator(List<String> operators, List<String> operands) {
validateOperation(operators, operands);
this.operators = Operators.of(operators);
this.operands = Operands.of(operands);
}
// ...
// peek 메서드는 javadoc을 확인해보면 "This method exists mainly to support debugging"이라 설명한다.
// 이는 stream의 중간 연산자로서 로그 출력 등의 디버깅을 위함이기 때문에 목적에 맞춰서 사용하는 것이 좋다는 피드백을 받았다.
private static List<String> validateOperators(List<String> operators) {
return operators.stream()
.peek(Calculator::validateOperator)
.collect(Collectors.toList());
}
}
CalculatorTest
public class CalculatorTest {
@Test
@DisplayName("홀수 인덱스에는 operand, 짝수 인덱스에 operator가 주어진 경우 계산하여 반환")
public void calculate_정상_계산() {
Calculator calculator = Calculator.createCalculator(List.of("1", "+", "6", "-", "2"));
assertThat(calculator.calculate()).isEqualTo(5);
}
@Test
@DisplayName("홀수 인덱스에 숫자가 아닌 입력값이 들어간 경우 IllegalArgumentException throw")
public void create_Calculator_숫자_아닌_값() {
assertThatThrownBy(() -> Calculator.createCalculator(List.of("1", "-", "d")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("양수를 입력해주세요.");
}
@Test
@DisplayName("짝수 인덱스에 사칙연산 기호가 아닌 값이 들어간 경우 IllegalArgumentException throw")
public void create_Calculator_사칙연산_아닌_값() {
assertThatThrownBy(() -> Calculator.createCalculator(List.of("1", "^", "3")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("올바른 사칙 연산을 입력해주세요.");
}
@Test
@DisplayName("계산이 불가능한 수식이 입력된 경우 IllegalArgumentException throw")
public void create_Calculator_계산_불가() {
assertThatThrownBy(() -> Calculator.createCalculator(List.of("1", "+")))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("계산이 가능한 수식을 입력해주세요.");
}
@Test
@DisplayName("0으로 나누려고 하는 경우 IllegalArgumentException throw")
public void calculate_0_나누기() {
Calculator calculator = Calculator.createCalculator(List.of("1", "/", "0", "-", "2"));
assertThatThrownBy(() -> calculator.calculate())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("0으로 나눌 수 없습니다. 다른 숫자를 입력해주세요.");
}
}
테스트코드 작성 굉장히 꼼꼼하게 잘했다는 피드백을 받고 그대로 올려봤다 😊
문자열 사칙연산 계산기 리팩토링
Operand 클래스 리팩토링
public class Operand {
private static final Pattern NUMBER_PATTERN = Pattern.compile("^[0-9]*$");
private static final String ILLEGAL_NUMBER_MESSAGE = "양수를 입력해주세요.";
private final int operand;
public Operand(String operand) {
this.operand = validatedOperand(operand);
}
public int operand() {
return operand;
}
private int validatedOperand(String operand) {
if (!isOperand(operand)) {
throw new IllegalArgumentException(ILLEGAL_NUMBER_MESSAGE);
}
return Integer.parseInt(operand);
}
private boolean isOperand(String operand) {
Matcher operandMatcher = NUMBER_PATTERN.matcher(operand);
return operandMatcher.find();
}
}
객체를 사용할 때 검증된 형태로 사용하는 것이 좋고, 정규식을 통해 0을 포함한 양수라는 것을 검증했기 때문에 사용하는 부분에서 별도 검증이 필요없다.
Calculator 클래스 리팩토링
private static List<String> validateOperators(List<String> operators) {
return operators.stream()
.map(Calculator::validateOperator)
.collect(Collectors.toList());
}
peek 메서드는 stream의 중간 연산자로서 로그 출력 등의 디버깅을 위해 사용하기 때문에 다음과 같이 map 메서드로 Operator 객체들이 "+", "-", "*", "-"인지 확인한 후 반환하도록 리팩토링했다.
'교육 및 책 > TDD, 클린 코드 with Java' 카테고리의 다른 글
TDD, 클린 코드 - 로또 (0) | 2023.05.14 |
---|---|
TDD, 클린 코드 - 로또(자동) (0) | 2023.04.23 |
TDD, 클린 코드 - 자동차 경주 (0) | 2023.04.13 |
TDD, 클린 코드 - 문자열 덧셈 계산기 (0) | 2023.04.08 |
TDD, 클린 코드 학습 테스트 (0) | 2023.04.05 |