[TDD]文字列計算機の実装
📌 機能
4つの演算子と数値からなる入力値を受信し、演算子の優先度を考慮せずに順番に計算する文字列計算機.
📌 To do list
計算機の操作手順に基づいて,必要な機能を記述した.
受信
アルファベットが数字でない場合、異常が発生します.
最後の文字が数字でない場合、例外
非四則演算子
入力値
2つの配列による繰返し順序演算
出力
📌 enum Operator
何度も再構築した後、最も時間をかけて体現します.
最初に、StringCalcクラスではMapマッピング演算子と演算子を使用して実装されます.しかしStringCalcクラスの責任は増加し,コード量は増加し,可読性は低下した.最後に単一責任の原則に反して再構築されたと考えられる.演算子と演算子をメンバーフィールドとするOperatorクラスを作成し、静的フィールドをOperatorオブジェクトとするCalcOperatorクラスを作成します.その結果,StringCalcクラスの責任は減少したものの,CalcOperatorクラスとOperatorクラスの結合度が向上し,CalcOperatorクラスの役割が不明確になったため,再構築を行った.そこで,このような列挙でOperatorを実現した.
@DisplayName("열거타입 연산기능수행 테스트")
@ParameterizedTest
@EnumSource(Operator.class)
void calc(Operator operator){
if(operator == Operator.PLUS)
assertThat(operator.op.calc(1,2)).isEqualTo(3);
if(operator == Operator.MINUS)
assertThat(operator.op.calc(1,2)).isEqualTo(-1);
if(operator == Operator.MULTIPLY)
assertThat(operator.op.calc(1,2)).isEqualTo(2);
if(operator == Operator.DIVIDE)
assertThat(operator.op.calc(10,2)).isEqualTo(5);
}
public enum Operator{
PLUS("+",(a,b)->a+b),
MINUS("-",(a,b)->a-b),
MULTIPLY("*",(a,b)->a*b),
DIVIDE("/",(a,b)->a/b);
String symbol;
Calculable op;
Operator(String symbol, Calculable op){
this.symbol = symbol;
this.op = op;
}
@FunctionalInterface
interface Calculable{
public int calc(int a, int b);
}
}
ジェネレータを使用して四則演算子を初期化します.演算子の最も重要な演算機能は,関数インタフェースのオーバーライドによって実現される.📍 Operator-演算子オブジェクトと演算子のマッピング
@DisplayName("문자와 Operator맵핑 메소드 테스트")
@Test
void of(){
assertThat(Operator.of("+")).isEqualTo(Operator.PLUS);
assertThat(Operator.of("-")).isEqualTo(Operator.MINUS);
assertThat(Operator.of("/")).isEqualTo(Operator.DIVIDE);
assertThat(Operator.of("*")).isEqualTo(Operator.MULTIPLY);
}
public static Operator of(String symbol){
return Arrays.stream(values())
.filter(op -> op.symbol.equals(symbol))
.findFirst()
.get();
}
📌 class StringCalc
まずユニットテストを行い,次に実施し,テストを容易にするために自然に機能を1つずつ区分し,最後にこのクラスを実現した.
📍 StringCalc-モノトーンモード
public class StringCalcTest {
StringCalc calc;
@BeforeEach
void setUp(){ calc = StringCalc.getInstance(); }
@DisplayName("싱글톤패턴 테스트")
@Test
void isSingleton(){
assertThat(calc).isEqualTo(StringCalc.getInstance());
}
}
public class StringCalc {
private static final StringCalc stringCalc = new StringCalc();
private StringCalc(){}
public static StringCalc getInstance(){ return stringCalc; }
}
当初、StringCalcクラスはステータスフィールド値がなく、utilクラスとロールが似ていると考えられ、ジェネレータをプライベート化し、メソッドを静的化した.再構築段階では,計算機の機能は継承によって拡張できると考えられる.したがって、各オブジェクトに個別に格納する必要のない状態フィールド値はないので、1つのオブジェクトのみを使用するために、モノトーンモードが採用されています.📍 StringCalc-入力値の計算
@DisplayName("계산기능 테스트")
@ParameterizedTest
@CsvSource(value={"0 +0:0","1+ 2 - 3/2*2:0","100+1 0 0 -100*5/2:250","10-10+1000/2*5:2500"},delimiter=':')
void calc(String s, int expected){
assertThat(calc.calc(s)).isEqualTo(expected);
}
public int calc(String str){
int result = 0;
str = CalcUtil.removeSpace(str);
ValidationUtil.checkFirstIdx(str);
ValidationUtil.checkLastIdx(str);
ValidationUtil.checkOp("[\\+|\\*|/|-]",str);
List<String> nos = CalcUtil.removeOps("[\\+|\\*|/|-]",str);
List<String> ops = CalcUtil.removeNos(str);
result = Integer.parseInt(nos.get(0));
for(int i = 1; i < nos.size(); i++){
result = Operator.of(ops.get(i-1)).op.calc(result,Integer.parseInt(nos.get(i)));
}
return result;
}
計算機を実現するために必要なutilメソッドとOperatorクラスの演算とマッピングメソッドを用いた.📌 class CalcUtil
utilクラスをオブジェクト化するか静的メソッドとして使用するかを考慮して、静的メソッドを使用することにします.これは,現在使用するutil機能が継承によって拡張する余地がないと考えているためである.
📍 CalcUtil-作成者
public class CalcUtil {
private CalcUtil() throws InstantiationException{
throw new InstantiationException("CalcUtil객체를 생성할 수 없습니다.");
}
}
CalcUtilクラスのジェネレータをprivateに設定します.これを異常処理することにより,CalcUtilクラス内部でのジェネレータの使用を禁止する.📍 CalcUtil-入力値を演算子と数値リストに設定
@DisplayName("입력값중 숫자만 뽑아서 배열생성")
@Test
void removeOp(){
String regex = "[\\+|\\*|/|-]";
assertThat(CalcUtil.removeOps(regex,"1+1")).isEqualTo(Arrays.asList(new String[]{"1","1"}));
assertThat(CalcUtil.removeOps(regex,"11+111")).isEqualTo(Arrays.asList(new String[]{"11","111"}));
assertThat(CalcUtil.removeOps(regex,"1+1-1/1*1")).isEqualTo(Arrays.asList(new String[]{"1","1","1","1","1"}));
}
@DisplayName("입력값중 연산자만 뽑아서 배열생성")
@Test
void removeNos(){
assertThat(CalcUtil.removeNos("1+1")).isEqualTo(Arrays.asList(new String[]{"+"}));
assertThat(CalcUtil.removeNos("1-1+1")).isEqualTo(Arrays.asList(new String[]{"-","+"}));
assertThat(CalcUtil.removeNos("1+1-1/1*1")).isEqualTo(Arrays.asList(new String[]{"+","-","/","*"}));
}
public static List<String> removeNos(String s) {
return Arrays.stream(s.split("[0-9]"))
.filter(op->!op.equals(""))
.collect(Collectors.toList());
}
public static List<String> removeOps(String regex, String s) {
return Arrays.asList(s.split(regex));
}
入力値の最初の文字が数値であるため、removeNos内部splitでは配列の先頭に空の文字列が返され、除外されます.📍 CalcUtil-I/Oとスペースの消去
@DisplayName("공백제거 테스트")
@Test
void removeSpace(){
assertThat(CalcUtil.removeSpace(" 1 1 1+ 2")).isEqualTo("111+2");
}
public static String input(Scanner sc){
System.out.println("다음줄에 값을 입력하세요.");
return sc.nextLine();
}
public static void output(int result){
System.out.println("결과값: " + result);
}
public static String removeSpace(String s) {
return s.replaceAll(" ","");
}
📌 class ValidationUtil
@DisplayName("입력값 첫번째가 숫자일 경우")
@Test
void checkFirstIdx(){
ValidationUtil.checkFirstIdx("1+1");
}
@DisplayName("입력값 첫번째가 숫자가 아닐 경우")
@ParameterizedTest
@ValueSource(strings = {"+11","-1","-","*9"})
void checkFirstIdx(String s){
assertThatIllegalArgumentException().isThrownBy(()->{
ValidationUtil.checkFirstIdx(s);
}).withMessageMatching("첫입력자는 숫자여야합니다.");
}
@DisplayName("입력값 마지막이 숫자일 경우")
@Test
void checkLastIdx(){
ValidationUtil.checkLastIdx("1+1");
}
@DisplayName("입력값 마지막이 숫자가 아닐 경우")
@ParameterizedTest
@ValueSource(strings={"11+","1-","1-1-"})
void checkLastIdx(String s){
assertThatIllegalArgumentException().isThrownBy(()->{
ValidationUtil.checkLastIdx(s);
}).withMessageMatching("마지막 입력자는 숫자여야합니다.");
}
@DisplayName("입력값중 사칙연산자이외의 연산자가 없는 경우")
@Test
void checkOp(){
ValidationUtil.checkOp("[\\+|\\*|/|-]","1+1");
}
@DisplayName("사칙연산자이외의 연산자가 있을 경우 테스트")
@ParameterizedTest
@ValueSource(strings = {"1%1","2!2","2+3=5"})
void checkOp(String s){
assertThatIllegalArgumentException().isThrownBy(()->{
ValidationUtil.checkOp("[\\+|\\*|/|-]",s);
}).withMessageMatching("연산자가 올바르지 않습니다.");
}
public class ValidationUtil {
private ValidationUtil() throws InstantiationException { throw new InstantiationException("ValidationUtil");}
public static void checkFirstIdx(String s) {
if(!(s.charAt(0) >= '0' && s.charAt(0) <='9'))
throw new IllegalArgumentException("첫입력자는 숫자여야합니다.");
}
public static void checkLastIdx(String s) {
int lastIdx = s.length() - 1;
if(!(s.charAt(lastIdx) >= '0' && s.charAt(lastIdx) <= '9'))
throw new IllegalArgumentException("마지막 입력자는 숫자여야합니다.");
}
public static void checkOp(String regex, String s) {
s = s.replaceAll("[0-9]","");
s = s.replaceAll(regex,"");
if(!s.equals(""))
throw new IllegalArgumentException("연산자가 올바르지 않습니다.");
}
}
📌 StringCalcの実行
public class Application {
private static final Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
CalcUtil.output(StringCalc.getInstance().calc(CalcUtil.input(sc)));
}
📌 に感銘を与える
TDDをベースにコードを記述しているので,テストを容易にするために自然に機能を小さいセルに分割する.しかし,テストコードを重視しすぎるため,実際に使用する方法ではなく,テストに用いる方法が実現された.時間とテストがかかりやすく、ビジネスロジックに使用できるコードの作成が困難になりました.
Reference
この問題について([TDD]文字列計算機の実装), 我々は、より多くの情報をここで見つけました https://velog.io/@0_0_yoon/TDD-문자열-계산기-구현テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol