[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をベースにコードを記述しているので,テストを容易にするために自然に機能を小さいセルに分割する.しかし,テストコードを重視しすぎるため,実際に使用する方法ではなく,テストに用いる方法が実現された.時間とテストがかかりやすく、ビジネスロジックに使用できるコードの作成が困難になりました.