Skip to content
Open
44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
## [NEXTSTEP 플레이그라운드의 미션 진행 과정](https://github.com/next-step/nextstep-docs/blob/master/playground/README.md)
# 문자열 계산기 2번째 구현

---
## 학습 효과를 높이기 위해 추천하는 미션 진행 방법
## step2. 기능 요구 맟 구현 순서

---
1. 피드백 강의 전까지 미션 진행
> 피드백 강의 전까지 혼자 힘으로 미션 진행. 미션을 진행하면서 하나의 작업이 끝날 때 마다 add, commit
> 예를 들어 다음 숫자 야구 게임의 경우 0, 1, 2단계까지 구현을 완료한 후 push
1. 연산자(Operator)
- [X] 연산자 기호에 해당하는 연산 담당 객체를 찾아 반환한다.
- [X] 연산자 객체에 두 정수가 전달되면 계산 결과를 반환한다.

![mission baseball](https://raw.githubusercontent.com/next-step/nextstep-docs/master/playground/images/mission_baseball.png)
2. 계산기(Calculator)
- [X] 여러개의 연산자 객체와 정수를 받아 전체 계산 결과를 반환한다.

---
2. 피드백 앞 단계까지 미션 구현을 완료한 후 피드백 강의를 학습한다.
3. 계산 내용 준비(PrepareCalculationContents)
- [X] 문자열 타입의 수식을 받아, 계산기가 사용할 수 있는 형태로 데이터를 가공하여 반환한다.
- [X] 계산기 객체를 반환한다.

4. 계산기 프로그램 화면(CalculatorDisplay)
- [X] 안내에 따라 문자열 수식을 입력하면, 계산 결과를 반환한다. 실제 계산은 내부적으로 다른 객체의 도움을 받는다.

5. 프로그램 실행 장치(Main)
- [] 계산기 프로그램을 실행한다

---
3. Git 브랜치를 master 또는 main으로 변경한 후 피드백을 반영하기 위한 새로운 브랜치를 생성한 후 처음부터 다시 미션 구현을 도전한다.

```
git branch -a // 모든 로컬 브랜치 확인
git checkout master // 기본 브랜치가 master인 경우
git checkout main // 기본 브랜치가 main인 경우
## 야구게임에서 받은 피드백 메모

- 적절한 객체를 도출하여 객체지향적 구현을 고민해 볼 것
- 핵심 로직과 UI 로직 분리하여 구현
- indent depth를 1로 맞출 것(메서드 분리 활용)
- 객체는 상태와 기능을 모두 가질 것
- 파일 저장시 마지막줄 개행 옵션 체크
- 의미없는 공백 두지 말 것
- 지역변수 초기화 시 가급적 inline 사용
- 불필요한 주석 달지 말 것

git checkout -b 브랜치이름
ex) git checkout -b apply-feedback
```
---
Empty file removed src/main/java/.gitkeep
Empty file.
30 changes: 30 additions & 0 deletions src/main/java/casepractice/calculator/Operator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package casepractice.calculator;

import java.util.Arrays;
import java.util.function.IntBinaryOperator;

public enum Operator {
SUM("+", Integer::sum),
SUBTRACTION("-", (a, b) -> a - b),
MULTIPLICATION("*", (a, b) -> a * b),
DIVISION("/", (a, b) -> a / b);

private final String symbol;
private final IntBinaryOperator calculation;

Operator(String symbol, IntBinaryOperator calculation) {
this.symbol = symbol;
this.calculation = calculation;
}

static Operator from(String symbol) {
return Arrays.stream(Operator.values())
.filter(operator -> symbol.equals(operator.symbol))
.findFirst()
.orElseThrow(IllegalArgumentException::new);
}

public int calculate(int a, int b) {
return calculation.applyAsInt(a, b);
}
}
50 changes: 50 additions & 0 deletions src/main/java/step1/Calculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package step1;

import java.util.List;

public class Calculator {

private CalculatorResource calculatorResource;

public Calculator(String stringCalculatorSentence) {
this.calculatorResource = new CalculatorResource(stringCalculatorSentence);
}
Comment on lines +7 to +11

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

계산기가 하나의 계산기 자원을 상태로 가지도록 구현하셨네요 !
만약 계산기가 또 다른 자원에 대한 계산을 해야하는 상황이라면 어떻게 해야할까요?

현재 상태에서는 하나의 계산기 객체가 하나의 자원만을 계산할 수 있는데요.
여러 개의 자원에 대해 계산할 수 있도록 개선해보면 좋을 것 같습니다 :)


public int calculate() {

List<Integer> numberList = calculatorResource.getNumberArray();
List<String> operatorList = calculatorResource.getOperatorArray();

int operatorListIndex = 0;
int result = 0;

for (int i = 1; i < numberList.size(); i++) {

result = calcualteInit(numberList, result, i);

result = getResult(numberList, operatorList, operatorListIndex, result, i);
operatorListIndex++;
}
return result;
}

private int calcualteInit(List<Integer> numberList, int result, int i) {
if (i == 1) {
result = (numberList.get(i - 1));
}
return result;
}

private int getResult(List<Integer> numberList, List<String> operatorList, int operatorListIndex, int result, int i) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드의 파라미터의 개수를 2개 이하로 리팩터링해보는건 어떨까요?
클린 코드 책에서는 메서드의 파라미터 개수는 0~2개가 적절하며 4개 이상은 무조건적으로 피해야한다고 이야기합니다.
파라미터의 개수가 많을 수록 하나의 메서드에서 하는 일이 많아질 수 있고 코드를 읽기 어려워지기 때문입니다 :)

if (operatorList.get(operatorListIndex).equals("+")) {
result = result + (numberList.get(i));
} else if (operatorList.get(operatorListIndex).equals("-")) {
result = result - (numberList.get(i));
} else if (operatorList.get(operatorListIndex).equals("*")) {
result = result * (numberList.get(i));
} else if (operatorList.get(operatorListIndex).equals("/")) {
result = result / (numberList.get(i));
}
return result;
Comment on lines +39 to +48

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java의 Enum 클래스를 사용하면 상태와 행위를 한곳에서 관리할 수 있는데요!

링크를 보시고 참고하셔서 enum 클래스를 사용해보면 좋을 것 같습니다 :)

https://techblog.woowahan.com/2527/

}
}
64 changes: 64 additions & 0 deletions src/main/java/step1/CalculatorResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package step1;

import java.util.ArrayList;
import java.util.List;

public class CalculatorResource {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CalculatorResource 클래스 네이밍은 추상화가 많이 되어 있는 클래스 명입니다.
클래스 내부를 보지 않으면 어떤 상태를 가지고 있는지 유추하기 힘듭니다. 조금 더 구체적인 클래스 명을 만들어보면 좋을 것 같아요 :)


private String sentence;
private String[] sentenceArray;
Comment on lines +8 to +9

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체가 생성된 이후에 두 변수는 사용되지 않고 있네요! 🤔

그렇다면 계산기 자원클래스에서 sentence와 sentenceArray를 상태로 가지고 있을 필요가 있을까요?

private List<Integer> numberArray;
private List<String> operatorArray;

public List<Integer> getNumberArray() {
return numberArray;
}

public List<String> getOperatorArray() {
return operatorArray;
}

public CalculatorResource(String sentence) {
this.sentence = sentence;
splitSentence();
creatNumberList();
creatOperatorList();
}

private void splitSentence() {
this.sentenceArray = this.sentence.split(" ");
}

public void creatNumberList() {
this.numberArray = new ArrayList<>();

for (String s : sentenceArray) {
char numberCheck = s.charAt(0);
validateNumber(numberArray, s, numberCheck);
}
this.numberArray = numberArray;
}

private void validateNumber(List<Integer> numberArray, String s, char numberCheck) {
if (numberCheck > 48 && numberCheck < 58) {
this.numberArray.add(Integer.parseInt(s));
}
}

public void creatOperatorList() {
this.operatorArray = new ArrayList<>();

for (String s : sentenceArray) {
char operatorCheck = s.charAt(0);
validateOperator(operatorArray, s, operatorCheck);
}
}

private void validateOperator(List<String> operatorArray, String s, char operatorCheck) {
if (operatorCheck < 48 || operatorCheck > 58) {
this.operatorArray.add(s);
}
}


}
11 changes: 11 additions & 0 deletions src/main/java/step1/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package step1;

public class Main {

public static void main(String[] args) {
String stringCalculatorSentence = "1 * 2 - 3 + 4 * 5";
Calculator calculator = new Calculator(stringCalculatorSentence);
Print print = new Print(calculator.calculate());
print.printResult();
}
}
14 changes: 14 additions & 0 deletions src/main/java/step1/Print.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package step1;

public class Print {

private int calculateResult;

public Print(int calculateResult) {
this.calculateResult = calculateResult;
}
Comment on lines +5 to +9

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Print가 하나의 계산결과를 상태로 가지도록 구현하셨네요!
만약 Print를 통해 또 다른 계산 결과를 출력해야한다면 어떻게 될까요? Print라는 클래스의 책임에 맞는 역할을 하고 있다고 볼 수 있을까요?


public void printResult() {
System.out.println("calculateResult = " + calculateResult);
}
}
6 changes: 6 additions & 0 deletions src/main/java/step2/CalculatePreparator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package step2;

public interface CalculatePreparator {
Calculator create(String stringExpression);
}

56 changes: 56 additions & 0 deletions src/main/java/step2/Calculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package step2;

import java.util.Collections;
import java.util.List;

public class Calculator {

private static final int NUMBERS_MIN_SIZE = 2;
private static final int OPERATORS_MIN_SIZE = 1;
private static final int NUMBERS_AND_OPERATORS_INTERVAL = 1;

private final List<Integer> numbers; // 숫자만 모아놓은 List
private final List<Operator> operators; // 연산자만 모아놓은 List

// 계산기 인스턴스는 생성자를 이용해 계산의 재료가 되는 숫자와 연산자 모음을 필수로 전달 받는다.
public Calculator(List<Integer> numbers, List<Operator> operators) {
validateMinSize(numbers, operators); // 연산에 필요한 최소 조건을 검증
validateFormat(numbers, operators); // 연산자의 갯수가 숫자의 갯수보다 1개 작은지 검증
this.numbers = Collections.unmodifiableList(numbers); // Read-Only 처리하여 필드로 넘긴다.
this.operators = Collections.unmodifiableList(operators); // Read-Only 처리하여 필드로 넘긴다.
}

// 계산을 수행할 수 있는 정수와 연산자의 최소 조건(갯수) 검증
private void validateMinSize(List<Integer> numbers, List<Operator> operators) {
if (numbers.size() < NUMBERS_MIN_SIZE) {
// 계산의 대상이 되는 숫자가 2개보다 작을 경우 예외 발생
throw new IllegalArgumentException(String.format("방정식의 숫자의 수는 %d개 이상이어야 합니다.", NUMBERS_MIN_SIZE));
}

if (operators.size() < OPERATORS_MIN_SIZE) {
// 계산에 사용되는 연산자가 없을 경우 예외 발생
throw new IllegalArgumentException(String.format("방정식의 연산자의 수는 %d개 이상이어야 합니다.", OPERATORS_MIN_SIZE));
}
}

// 숫자와 연산자의 갯수 차이를 통해 정상적인 계산이 가능한지 검증
private void validateFormat(List<Integer> numbers, List<Operator> operators) {
int interval = numbers.size() - operators.size();
if (interval != NUMBERS_AND_OPERATORS_INTERVAL) {
// (숫자 - 연산자)의 수가 1이 아닐 경우 예외 발생
throw new IllegalArgumentException("방정식의 올바른 형식이 아닙니다.");
}
}

// Calculator 클래스가 상태로 가지고 있는 숫자와 연산자의 통합 계산을 수행하고, 결과를 반환한다.
public int calculate() {
int result = numbers.get(0); // 첫번째 숫자를 result에 넣어 계산 초기값을 준비한다
for (int i = 0; i < operators.size(); i++) { // 연산자의 갯수만큼 연산을 반복한다
Operator operator = operators.get(i);// 순차적으로 연산자 enum 객체를 찾는다
Integer number = numbers.get(i + 1); // 연산의 대상이 되는 숫자를 number에 셋팅한다
result = operator.calculate(result, number); // 두 수를 계산하여 중간 계산 결과를 result에 담는다
}
return result;
}
}

36 changes: 36 additions & 0 deletions src/main/java/step2/CalculatorDisplay.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package step2;

import java.util.Scanner;

public class CalculatorDisplay {

private final CalculatePreparator calculatePreparator;


// 계산기 재료를 준비해주는 인터페이스를 의존 관계로 받는다.
// 구현체를 변경하는 방식으로 계산기 기능을 다르게 사용할 수 있다.
public CalculatorDisplay(CalculatePreparator calculatePreparator) {
this.calculatePreparator = calculatePreparator;

}

// 양식에 맞는 문자열 수식을 넣으면 전체 계산 결과를 반환한다
private int calculate(String userInputExpression) {
Calculator calculator = calculatePreparator.create(userInputExpression); // 계산 준비 객체에서 계산기 객체를 만든다.
return calculator.calculate(); // 계산기 객체가 계산 결과를 반환한다.
}

// 테스트 코드 실행용 메서드(scanner 사용 제한)
public int calculateForTest(String userInputExpression) {
Calculator calculator = calculatePreparator.create(userInputExpression); // 계산 준비 객체에서 계산기 객체를 만든다.
return calculator.calculate(); // 계산기 객체가 계산 결과를 반환한다.
}

public void displayConsole() {
Scanner scanner = new Scanner(System.in);
System.out.println("문자열 계산식을 입력하면 계산 결과를 반환합니다.");
int calculateResult = calculate(scanner.nextLine());
System.out.println("calculateResult = " + calculateResult);
}
}

11 changes: 11 additions & 0 deletions src/main/java/step2/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package step2;

public class Main {

// 프로그램 실행
public static void main(String[] args) {
CalculatorDisplay calculatorDisplay = new CalculatorDisplay(new StringCalculatePreparator());
calculatorDisplay.displayConsole();
}
}

37 changes: 37 additions & 0 deletions src/main/java/step2/Operator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package step2;

import java.util.function.BiFunction;

import static java.util.Arrays.stream;

public enum Operator {

SUM("+", (a, b) -> a + b),
SUBTRACTION("-", (a, b) -> a - b),
MULTIPLICATION("*", (a, b) -> a * b),
DIVISION("/", (a, b) -> a / b);

// 4개의 enum 객체는 operatorSymbol에 따라 서로 다른 연산 로직을 수행한다.
private final String operatorSymbol;
private final BiFunction<Integer, Integer, Integer> biFunction; // 두 개의 Integer 타입을 받아 Integer 타입을 반환

// 4개의 enum 객체 생성자
Operator(String operatorSymbol, BiFunction<Integer, Integer, Integer> biFunction) {
this.operatorSymbol = operatorSymbol;
this.biFunction = biFunction;
}

// enum 객체가 가지고 있는 BiFunction 인터페이스 구현 로직에 따라 계산 결과를 반환한다.
public int calculate(int a, int b) {
return this.biFunction.apply(a, b);
}

// 문자 형태의 연산자를 operatorSymbol로 가지고 있는 enum 객체를 찾아 반환한다.
static Operator matchOperator(String symbol) {
return stream(Operator.values()) // Operator의 모든 값을 순환한다.
.filter(operator -> symbol.equals(operator.operatorSymbol))// 파라미터로 넘어온 symbol과 enum 객체의 operatorSymbol이 일치하면 통과
.findFirst() // enum 객체를 찾으면 반환한다.
.orElseThrow(IllegalArgumentException::new); // 일치하는 객체가 없다면 예외 발생
}
}

Loading