클린코드 14장 점진적인 개선

kimjunkyung·2021년 8월 5일
0

클린코드

목록 보기
15/15
post-thumbnail

노션에서 정리한 내용을 벨로그로 옮겼기 때문에 노션으로 보면 조금 더 보기 더 편합니다🤗

이동하기 → junnkk's Notion


14장 → 점진적인 개선을 보여주는 사례 연구

  1. 출발은 좋았으나 확장성 ↓인 모듈 소개
  2. 모듈을 개선하고 정리하는 단계
  • 프로그램을 짜다 보면 명령행 인수의 구문을 분석할 필요가 생김

    편리한 유틸리티가 없다면 main 함수로 넘어오는 문자열을 직접 분석하게 됨

    → 책에서 새로 짠 유틸리티 Args

    : 생성자에 (입력으로 들어온) 문자열과 형식 문자열을 넘겨 Args 인스턴스를 생성 후 Args 인스턴스에다 인수 값을 질의한다.

    • [14-1] 간단한 Args 사용법

      public static void main(String[] args) {
        try {
          Args arg = new Args("l,p#,d*", args);
          boolean logging = arg.getBoolean('l');
          int port = arg.getInt('p');
          String directory = arg.getString('d');
          executeApplication(logging, port, directory);
        } catch (ArgsException e) {
          System.out.print("Argument error: %s\n", e.errorMessage());
        }
      }
      • 두 개의 매개변수로 Args 인스턴스 생성.

        → 첫 번째 매개변수 : 형식 또는 스키마를 지정하는 "l, p#, d*"는 명령행 인수 세 개를 정의

        -l 은 부울 인수, -p 는 정수 인수, -d 는 문자열 인수

        → 두 번째 매개변수 : main으로 넘어온 명령행 인수 배열 자체

      • ArgsException이 발생하지 않을 경우 명령행 인수의 구문을 성공적으로 분석했으며 Args 인스턴스에 질의를 던져도 좋다. 인수 값을 가져오려면 getBoolean, getInteger, getString 등 사용.


Args 구현

  • [14-2] Args.java

    package com.objectmentor.utilities.args;
    
    import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*; 
    import java.util.*;
    
    public class Args {
      private Map<Character, ArgumentMarshaler> marshalers;
      private Set<Character> argsFound;
      private ListIterator<String> currentArgument;
      
      public Args(String schema, String[] args) throws ArgsException { 
        marshalers = new HashMap<Character, ArgumentMarshaler>(); 
        argsFound = new HashSet<Character>();
        
        parseSchema(schema);
        parseArgumentStrings(Arrays.asList(args)); 
      }
      
      private void parseSchema(String schema) throws ArgsException { 
        for (String element : schema.split(","))
          if (element.length() > 0) 
            parseSchemaElement(element.trim());
      }
      
      private void parseSchemaElement(String element) throws ArgsException { 
        char elementId = element.charAt(0);
        String elementTail = element.substring(1); validateSchemaElementId(elementId);
        if (elementTail.length() == 0)
          marshalers.put(elementId, new BooleanArgumentMarshaler());
        else if (elementTail.equals("*")) 
          marshalers.put(elementId, new StringArgumentMarshaler());
        else if (elementTail.equals("#"))
          marshalers.put(elementId, new IntegerArgumentMarshaler());
        else if (elementTail.equals("##")) 
          marshalers.put(elementId, new DoubleArgumentMarshaler());
        else if (elementTail.equals("[*]"))
          marshalers.put(elementId, new StringArrayArgumentMarshaler());
        else
          throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail);
      }
      
      private void validateSchemaElementId(char elementId) throws ArgsException { 
        if (!Character.isLetter(elementId))
          throw new ArgsException(INVALID_ARGUMENT_NAME, elementId, null); 
      }
      
      private void parseArgumentStrings(List<String> argsList) throws ArgsException {
        for (currentArgument = argsList.listIterator(); currentArgument.hasNext();) {
          String argString = currentArgument.next(); 
          if (argString.startsWith("-")) {
            parseArgumentCharacters(argString.substring(1)); 
          } else {
            currentArgument.previous();
            break; 
          }
        } 
      }
      
      private void parseArgumentCharacters(String argChars) throws ArgsException { 
        for (int i = 0; i < argChars.length(); i++)
          parseArgumentCharacter(argChars.charAt(i)); 
      }
      
      private void parseArgumentCharacter(char argChar) throws ArgsException { 
        ArgumentMarshaler m = marshalers.get(argChar);
        if (m == null) {
          throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null); 
        } else {
          argsFound.add(argChar); 
          try {
            m.set(currentArgument); 
          } catch (ArgsException e) {
            e.setErrorArgumentId(argChar);
            throw e; 
          }
        } 
      }
      
      public boolean has(char arg) { 
        return argsFound.contains(arg);
      }
      
      public int nextArgument() {
        return currentArgument.nextIndex();
      }
      
      public boolean getBoolean(char arg) {
        return BooleanArgumentMarshaler.getValue(marshalers.get(arg));
      }
      
      public String getString(char arg) {
        return StringArgumentMarshaler.getValue(marshalers.get(arg));
      }
      
      public int getInt(char arg) {
        return IntegerArgumentMarshaler.getValue(marshalers.get(arg));
      }
      
      public double getDouble(char arg) {
        return DoubleArgumentMarshaler.getValue(marshalers.get(arg));
      }
      
      public String[] getStringArray(char arg) {
        return StringArrayArgumentMarshaler.getValue(marshalers.get(arg));
      } 
    }
    • 참고
      • trim() : 문자열 양 끝의 공백 제거
      • charAt(): 함수는 문자열에서 특정 인덱스에 위치하는 유니코드 단일문자를 반환
      • substring(): string 객체의 시작 인덱스로 부터 종료 인덱스 전까지 문자열의 부분 문자열을 반환
      • boolean hasNext(): 이 리스트 반복자가 해당 리스트를 순방향으로 순회할 때 다음 요소를 가지고 있으면 true를 반환하고, 더 이상 다음 요소를 가지고 있지 않으면 false를 반환
      • E next(): 리스트의 다음 요소를 반환하고, 커서(cursor)의 위치를 순방향으로 이동시킴.
      • E previous(): 리스트의 이전 요소를 반환하고, 커서(cursor)의 위치를 역방향으로 이동시킴.
  • [14-3] ArgumentMarshaler.java (인터페이스)

    public interface ArgumentMarshaler {
      void set(Iterator<String> currentArgument) throws ArgsException;
    }
    • Marshalling

      :한 객체의 메모리에서의 표현방식을 저장 또한 전송에 적합한 다른 데이터 형식으로 변환하는 과정

  • [14-4] BooleanArgumentMarshaler.java

    public class BooleanArgumentMarshaler implements ArgumentMarshaler { 
      private boolean booleanValue = false;
      
      public void set(Iterator<String> currentArgument) throws ArgsException { 
        booleanValue = true;
      }
      
      public static boolean getValue(ArgumentMarshaler am) {
        if (am != null && am instanceof BooleanArgumentMarshaler)
          return ((BooleanArgumentMarshaler) am).booleanValue; 
        else
          return false; 
      }
    }
  • [14-5] StringArgumentMarshaler.java

    import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
    
    public class StringArgumentMarshaler implements ArgumentMarshaler { 
      private String stringValue = "";
      
      public void set(Iterator<String> currentArgument) throws ArgsException { 
        try {
          stringValue = currentArgument.next(); 
        } catch (NoSuchElementException e) {
          throw new ArgsException(MISSING_STRING); 
        }
      }
      
      public static String getValue(ArgumentMarshaler am) {
        if (am != null && am instanceof StringArgumentMarshaler)
          return ((StringArgumentMarshaler) am).stringValue; 
        else
          return ""; 
      }
    }
  • [14-6] IntegerArgumentMarshaler.java

    import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
    
    public class IntegerArgumentMarshaler implements ArgumentMarshaler { 
      private int intValue = 0;
      
      public void set(Iterator<String> currentArgument) throws ArgsException { 
        String parameter = null;
        try {
          parameter = currentArgument.next();
          intValue = Integer.parseInt(parameter);
        } catch (NoSuchElementException e) {
          throw new ArgsException(MISSING_INTEGER);
        } catch (NumberFormatException e) {
          throw new ArgsException(INVALID_INTEGER, parameter); 
        }
      }
      
      public static int getValue(ArgumentMarshaler am) {
        if (am != null && am instanceof IntegerArgumentMarshaler)
          return ((IntegerArgumentMarshaler) am).intValue; 
        else
        return 0; 
      }
    }
  • [14-7] ArgsException.java

    import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
    
    public class ArgsException extends Exception { 
      private char errorArgumentId = '\0'; 
      private String errorParameter = null; 
      private ErrorCode errorCode = OK;
      
      public ArgsException() {}
      
      public ArgsException(String message) {super(message);}
      
      public ArgsException(ErrorCode errorCode) { 
        this.errorCode = errorCode;
      }
      
      public ArgsException(ErrorCode errorCode, String errorParameter) { 
        this.errorCode = errorCode;
        this.errorParameter = errorParameter;
      }
      
      public ArgsException(ErrorCode errorCode, char errorArgumentId, String errorParameter) {
        this.errorCode = errorCode; 
        this.errorParameter = errorParameter; 
        this.errorArgumentId = errorArgumentId;
      }
      
      public char getErrorArgumentId() { 
        return errorArgumentId;
      }
      
      public void setErrorArgumentId(char errorArgumentId) { 
        this.errorArgumentId = errorArgumentId;
      }
      
      public String getErrorParameter() { 
        return errorParameter;
      }
      
      public void setErrorParameter(String errorParameter) { 
        this.errorParameter = errorParameter;
      }
      
      public ErrorCode getErrorCode() { 
        return errorCode;
      }
      
      public void setErrorCode(ErrorCode errorCode) { 
        this.errorCode = errorCode;
      }
      
      public String errorMessage() { 
        switch (errorCode) {
          case OK:
            return "TILT: Should not get here.";
          case UNEXPECTED_ARGUMENT:
            return String.format("Argument -%c unexpected.", errorArgumentId);
          case MISSING_STRING:
            return String.format("Could not find string parameter for -%c.", errorArgumentId);
          case INVALID_INTEGER:
            return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
          case MISSING_INTEGER:
            return String.format("Could not find integer parameter for -%c.", errorArgumentId);
          case INVALID_DOUBLE:
            return String.format("Argument -%c expects a double but was '%s'.", errorArgumentId, errorParameter);
          case MISSING_DOUBLE:
            return String.format("Could not find double parameter for -%c.", errorArgumentId); 
          case INVALID_ARGUMENT_NAME:
            return String.format("'%c' is not a valid argument name.", errorArgumentId);
          case INVALID_ARGUMENT_FORMAT:
            return String.format("'%s' is not a valid argument format.", errorParameter);
        }
        return ""; 
      }
      
      public enum ErrorCode {
        OK, INVALID_ARGUMENT_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, 
        MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, MISSING_DOUBLE, INVALID_DOUBLE
      }
    }

⇒ 코드가 위에서 아래로 읽힌다. 이름을 붙인 방법, 함수 크기, 코드 형식에 주목하면 전반적으로 잘 짜인 프로그램.

  • 어떻게 짰느냐고

    깨끗한 코드를 짜려면 지저분한 코드를 짠 뒤에 정리해야 한다.


Args: 1차 초안

  • 맨 처음 짰던 Args 클래스 - 돌아가지만 엉망

    • [14-8] Args.java(1차 초안)

      ```clike
      import java.text.ParseException; 
      import java.util.*;
      
      public class Args {
        private String schema;
        private String[] args;
        private boolean valid = true;
        private Set<Character> unexpectedArguments = new TreeSet<Character>(); 
        private Map<Character, Boolean> booleanArgs = new HashMap<Character, Boolean>();
        private Map<Character, String> stringArgs = new HashMap<Character, String>(); 
        private Map<Character, Integer> intArgs = new HashMap<Character, Integer>(); 
        private Set<Character> argsFound = new HashSet<Character>();
        private int currentArgument;
        private char errorArgumentId = '\0';
        private String errorParameter = "TILT";
        private ErrorCode errorCode = ErrorCode.OK;
        
        private enum ErrorCode {
          OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT}
          
        public Args(String schema, String[] args) throws ParseException { 
          this.schema = schema;
          this.args = args;
          valid = parse();
        }
        
        private boolean parse() throws ParseException { 
          if (schema.length() == 0 && args.length == 0)
            return true; 
          parseSchema(); 
          try {
            parseArguments();
          } catch (ArgsException e) {
          }
          return valid;
        }
        
        private boolean parseSchema() throws ParseException { 
          for (String element : schema.split(",")) {
            if (element.length() > 0) {
              String trimmedElement = element.trim(); 
              parseSchemaElement(trimmedElement);
            } 
          }
          return true; 
        }
        
        private void parseSchemaElement(String element) throws ParseException { 
          char elementId = element.charAt(0);
          String elementTail = element.substring(1); 
          validateSchemaElementId(elementId);
          if (isBooleanSchemaElement(elementTail)) 
            parseBooleanSchemaElement(elementId);
          else if (isStringSchemaElement(elementTail)) 
            parseStringSchemaElement(elementId);
          else if (isIntegerSchemaElement(elementTail)) 
            parseIntegerSchemaElement(elementId);
          else
            throw new ParseException(String.format("Argument: %c has invalid format: %s.", 
              elementId, elementTail), 0);
          } 
        }
          
        private void validateSchemaElementId(char elementId) throws ParseException { 
          if (!Character.isLetter(elementId)) {
            throw new ParseException("Bad character:" + elementId + "in Args format: " + schema, 0);
          }
        }
        
        private void parseBooleanSchemaElement(char elementId) { 
          booleanArgs.put(elementId, false);
        }
        
        private void parseIntegerSchemaElement(char elementId) { 
          intArgs.put(elementId, 0);
        }
        
        private void parseStringSchemaElement(char elementId) { 
          stringArgs.put(elementId, "");
        }
        
        private boolean isStringSchemaElement(String elementTail) { 
          return elementTail.equals("*");
        }
        
        private boolean isBooleanSchemaElement(String elementTail) { 
          return elementTail.length() == 0;
        }
        
        private boolean isIntegerSchemaElement(String elementTail) { 
          return elementTail.equals("#");
        }
        
        private boolean parseArguments() throws ArgsException {
          for (currentArgument = 0; currentArgument < args.length; currentArgument++) {
            String arg = args[currentArgument];
            parseArgument(arg); 
          }
          return true; 
        }
        
        private void parseArgument(String arg) throws ArgsException { 
          if (arg.startsWith("-"))
            parseElements(arg); 
        }
        
        private void parseElements(String arg) throws ArgsException { 
          for (int i = 1; i < arg.length(); i++)
            parseElement(arg.charAt(i)); 
        }
        
        private void parseElement(char argChar) throws ArgsException { 
          if (setArgument(argChar))
            argsFound.add(argChar); 
          else 
            unexpectedArguments.add(argChar); 
            errorCode = ErrorCode.UNEXPECTED_ARGUMENT; 
            valid = false;
        }
        
        private boolean setArgument(char argChar) throws ArgsException { 
          if (isBooleanArg(argChar))
            setBooleanArg(argChar, true); 
          else if (isStringArg(argChar))
            setStringArg(argChar); 
          else if (isIntArg(argChar))
            setIntArg(argChar); 
          else
            return false;
          
          return true; 
        }
        
        private boolean isIntArg(char argChar) {
          return intArgs.containsKey(argChar);
        }
        
        private void setIntArg(char argChar) throws ArgsException { 
          currentArgument++;
          String parameter = null;
          try {
            parameter = args[currentArgument];
            intArgs.put(argChar, new Integer(parameter)); 
          } catch (ArrayIndexOutOfBoundsException e) {
            valid = false;
            errorArgumentId = argChar;
            errorCode = ErrorCode.MISSING_INTEGER;
            throw new ArgsException();
          } catch (NumberFormatException e) {
            valid = false;
            errorArgumentId = argChar; 
            errorParameter = parameter;
            errorCode = ErrorCode.INVALID_INTEGER; 
            throw new ArgsException();
          } 
        }
        
        private void setStringArg(char argChar) throws ArgsException { 
          currentArgument++;
          try {
            stringArgs.put(argChar, args[currentArgument]); 
          } catch (ArrayIndexOutOfBoundsException e) {
            valid = false;
            errorArgumentId = argChar;
            errorCode = ErrorCode.MISSING_STRING; 
            throw new ArgsException();
          } 
        }
        
        private boolean isStringArg(char argChar) { 
          return stringArgs.containsKey(argChar);
        }
        
        private void setBooleanArg(char argChar, boolean value) { 
          booleanArgs.put(argChar, value);
        }
        
        private boolean isBooleanArg(char argChar) { 
          return booleanArgs.containsKey(argChar);
        }
        
        public int cardinality() { 
          return argsFound.size();
        }
        
        public String usage() { 
          if (schema.length() > 0)
            return "-[" + schema + "]"; 
          else
            return ""; 
        }
        
        public String errorMessage() throws Exception { 
          switch (errorCode) {
            case OK:
              throw new Exception("TILT: Should not get here.");
            case UNEXPECTED_ARGUMENT:
              return unexpectedArgumentMessage();
            case MISSING_STRING:
              return String.format("Could not find string parameter for -%c.", errorArgumentId);
            case INVALID_INTEGER:
              return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
            case MISSING_INTEGER:
              return String.format("Could not find integer parameter for -%c.", errorArgumentId);
          }
          return ""; 
        }
        
        private String unexpectedArgumentMessage() {
          StringBuffer message = new StringBuffer("Argument(s) -"); 
          for (char c : unexpectedArguments) {
            message.append(c); 
          }
          message.append(" unexpected.");
          
          return message.toString(); 
        }
        
        private boolean falseIfNull(Boolean b) { 
          return b != null && b;
        }
        
        private int zeroIfNull(Integer i) { 
          return i == null ? 0 : i;
        }
        
        private String blankIfNull(String s) { 
          return s == null ? "" : s;
        }
        
        public String getString(char arg) { 
          return blankIfNull(stringArgs.get(arg));
        }
        
        public int getInt(char arg) {
          return zeroIfNull(intArgs.get(arg));
        }
        
        public boolean getBoolean(char arg) { 
          return falseIfNull(booleanArgs.get(arg));
        }
        
        public boolean has(char arg) { 
          return argsFound.contains(arg);
        }
        
        public boolean isValid() { 
          return valid;
        }
        
        private class ArgsException extends Exception {
        } 
      }
      ```

      → 인스턴스 변수 개수 너무 많고 'TILT' 같은 희한한 문자열, HashSets, TreeSets, try-catch-catch문 등 지저분한 코드.

      → 처음부터 지저분한 코드는 아니었고 boolean 인수만 지원할 때는 괜찮았지만 인수 유형 추가할 때마다 지저분해짐.

  • 그래서 멈췄다

    • 추가할 인수 유형이 두 대 이상이 있었지만 코드가 너무 지저분해지므로 멈추고 리팩터링 시작.
    • 새 인수 추가 시 공통적으로 주요 지점 세 곳에 코드 추가해야 했음(HashMap 변경 후 parse, get, set)
      1. 인수 유형에 해당하는 HashMap을 선택하기 위해 스키마 요소의 구문을 분석
      2. 명령행 인수에서 인수 유형을 분석해 진짜 유형으로 변환
      3. get~ 메서드를 구현해 호출자에게 진짜 유형을 반환
    • 인수 유형은 다양하지만 모두 유사한 메서드를 제공하므로 ArgumentMarshaler라는 하나의 클래스 구현
  • 점진적으로 개선하다

    개선이라는 이름으로 구조를 크게 뒤집는 것은 프로그램을 망치는 방법이다.

    ∴ 테스트 주도 개발(TDD) 기법 사용

    • 테스트 주도 기법

      • 언제 어느 때라도 시스템이 돌아가야 한다. 즉, 시스템을 망가뜨리는 변경을 허용하지 않고 같이 변경 후에도 변경 전과 돌아가야 한다.

        → 자동화된 테스트 슈트가 필요.

    • [14-11] Args.java 끝에 추가한 ArgumentMarshaler

      private class ArgumentMarshaler { 
        private boolean booleanValue = false;
      
        public void setBoolean(boolean value) { 
          booleanValue = value;
        }
        
        public boolean getBoolean() {return booleanValue;} 
      }
      
      private class BooleanArgumentMarshaler extends ArgumentMarshaler { }
      private class StringArgumentMarshaler extends ArgumentMarshaler { }
      private class IntegerArgumentMarshaler extends ArgumentMarshaler { }
      • 코드를 최소로 건드리는 가장 단순한 변경 가함.

        • Boolean 인수를 저장하는 HashMap에서 Boolean 인수 유형→ ArgumentMarshaler 유형

          ```clike
          private Map<Character, **ArgumentMarshaler**> booleanArgs = 
          	new HashMap<Character, **ArgumentMarshaler**>();
          ```
          
          ```clike
          ...
          
          private void parseBooleanSchemaElement(char elementId) {
            booleanArgs.put(elementId, **new BooleanArgumentMarshaler()**);
          }
            
          ...
          
          private void setBooleanArg(char argChar, boolean value) {
            booleanArgs.**get**(argChar).**setBoolean**(value);
          }
            
          ...
          
          public boolean getBoolean(char arg) {
            return falseIfNull(booleanArgs.get(arg).**getBoolean**());
          }
          ```
          
          ⇒ 앞에서 말한 세 곳 (parse, get, set)

          but 몇몇 테스트 케이스 실패

          getBoolean 함수에서 falseIfNull 함수는 더이상 NullPointerException을 막아주는 역할을 하지 못함

          → null 점검 위치 바꿔준다. null인지 확인하는 객체는 boolean이 아니라 ArgumentMarshaler

        1. getBoolean 함수에서 falseIfNull 제거

        2. falseIfNull 함수도 삭제(더 이상 쓸모 X)

        3. 함수를 두 행으로 쪼갠 후 ArgumentMarshaler를 argumetMarshaler라는 독자적인 변수에 저장. 이름 너무 길고 유형 이름과 중복이 심하므로 am으로 줄임.

        4. null 점검

          public boolean getBoolean(char arg) { 
              Args.ArgumentMarshaler am = booleanArgs.get(arg);
              return am != null && am.getBoolean(); 
            }

String 인수

  • String 인수 추가하는 과정 → boolean 인수를 추가하는 과정과 유사(HashMap 변경 후 parse, get, set 수정)

  • 각 인수 유형을 처리하는 코드를 모두 ArgumentMarshaler 클래스에 넣고 나서 ArgumentMarshaler 파생 클래스를 만들어 코드를 분리할 계획

    ∵ 이렇게 하면 프로그램 구조를 조금씩 변경하는 동안에도 시스템의 정상 동작을 유지하기 쉬워짐.

    • String 인수 추가 p272~273, Int 인수 추가 p274~275

👣 모든 논리 ArgumentMarshaler로 옮겼으니 파생 클래스 만들어 기능 분산

  1. setBoolean 함수를 BooleanArgumentMarshaler로 옮긴 후 올바르게 호출되는지 확인

    i) 우선 ArgumentMarshaler 클래스에 추상 메서드 set 생성

    • 코드 참고

      private **abstract** class ArgumentMarshaler { 
          **protected** boolean booleanValue = false;
          private String stringValue;
          private int integerValue;
      
          public void setBoolean(boolean value) { 
            booleanValue = value;
          } 
          
          public boolean getBoolean() {
              return booleanValue;
          } 
      
          public void setString(String s) { 
              stringValue = s;
          }
          
          public String getString() {
              return stringValue == null ? "" : stringValue;
          } 
      
          public void setInteger(int i) { 
              integerValue = i;
          }
          
          public int getInteger() {
              return integerValue;
          } 
      
          **public abstract void set(String s);**
      }

    추상 set 함수는 string 인수를 받아들이나 BooleanArgumentMarshaler에서는 인수 사용 X. 그럼에도 인수를 정의한 이유는 StringArgumentMarshaler과 IntegerArgumentMarshaler에서 필요하기 때문

    ii) BooleanArgumentMarshaler 클래스에 set 메서드 구현

    • 코드 참고

      private class BooleanArgumentMarshaler extends ArgumentMarshaler {
          public void set(String s){
              booleanValue = true;
          }
      }

    iii) setBoolean 호출을 set 호출로 변경

    • 코드 참고

      private void setBooleanArg(char argChar, boolean value) { 
          booleanArgs.get(argChar).set("true");
      }

    iv) ArgumentMarshaler에서 setBoolean 메서드 제거

  2. get 메서드를 BooleanArgumentMarshaler로 옮김.

    (get 옮기는 것은 어렵. ∵ 반환 객체 유형이 Object 여야 하기 때문)→ 여기서는 boolean으로 형변환

    i) 테스트 용으로 아래 코드를 변경 및 추가

    • 코드

      public boolean getBoolean(char arg) { 
          Args.ArgumentMarshaler am = booleanArgs.get(arg);
          return am != null && **(Boolean)am.get();** 
      }
      public Object get() {
      	return null;
      }

    → 컴파일됐으나 테스트 실패

    ii) 테스트 통과 위해 ArgumentMarshaler에서 get을 추상 메서드로 만든 후 BooleanArgumentMarshaler에다가 get 구현

    • 코드 참고

      private abstract class ArgumentMarshaler { 
          protected boolean booleanValue = false;
          
      		...
      
          public **abstract** Object get();
      }
      
      private class BooleanArgumentMarshaler extends ArgumentMarshaler {
          public void set(String s){
              booleanValue = true;
          }
      
          public Object get() {
              return booleanValue;
          }
      }

    iii) ArgumentMarshaler에서 getBoolean 메서드 제거

    iv) protected 변수인 booleanValue를 BooleanArgumentMarshaler로 내려 private 변수로 선언

⇒ String, Interger 인수 유형에도 같은 과정 반복

  • interger 인수는 구문을 분석해야 하므로, 즉 parse에서 예외를 던질지도 모르므로 조금 더 복잡.
  • 코드 참고

    import java.text.ParseException; 
    import java.util.*;
    
    public class Args {
      private String schema;
      private String[] args;
      private boolean valid = true;
      private Set<Character> unexpectedArguments = new TreeSet<Character>(); 
      private Map<Character, ArgumentMarshaler> booleanArgs = new HashMap<Character, ArgumentMarshaler>();
      private Map<Character, ArgumentMarshaler> stringArgs = new HashMap<Character, ArgumentMarshaler>(); 
      private Map<Character, ArgumentMarshaler> intArgs = new HashMap<Character, ArgumentMarshaler>(); 
      private Set<Character> argsFound = new HashSet<Character>();
      private int currentArgument;
      private char errorArgumentId = '\0';
      private String errorParameter = "TILT";
      private ErrorCode errorCode = ErrorCode.OK;
      
      private enum ErrorCode {
        OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT}
        
      public Args(String schema, String[] args) throws ParseException { 
        this.schema = schema;
        this.args = args;
        valid = parse();
      }
      
      private boolean parse() throws ParseException { 
        if (schema.length() == 0 && args.length == 0)
          return true; 
        parseSchema(); 
        try {
          parseArguments();
        } catch (ArgsException e) {
        }
        return valid;
      }
      
      private boolean parseSchema() throws ParseException { 
        for (String element : schema.split(",")) {
          if (element.length() > 0) {
            String trimmedElement = element.trim(); 
            parseSchemaElement(trimmedElement);
          } 
        }
        return true; 
      }
      
      private void parseSchemaElement(String element) throws ParseException { 
        char elementId = element.charAt(0);
        String elementTail = element.substring(1); 
        validateSchemaElementId(elementId);
        if (isBooleanSchemaElement(elementTail)) 
          parseBooleanSchemaElement(elementId);
        else if (isStringSchemaElement(elementTail)) 
          parseStringSchemaElement(elementId);
        else if (isIntegerSchemaElement(elementTail)) 
          parseIntegerSchemaElement(elementId);
        else
          throw new ParseException(String.format("Argument: %c has invalid format: %s.", 
            elementId, elementTail), 0);
        } 
      }
        
      private void validateSchemaElementId(char elementId) throws ParseException { 
        if (!Character.isLetter(elementId)) {
          throw new ParseException("Bad character:" + elementId + "in Args format: " + schema, 0);
        }
      }
      
      private void parseBooleanSchemaElement(char elementId) { 
        booleanArgs.put(elementId, new BooleanArgumentMarshaler());
      }
      
      private void parseIntegerSchemaElement(char elementId) { 
        intArgs.put(elementId, new IntegerArgumentMarshaler());
      }
      
      private void parseStringSchemaElement(char elementId) { 
        stringArgs.put(elementId, new StringArgumentMarshaler());
      }
      
      private boolean isStringSchemaElement(String elementTail) { 
        return elementTail.equals("*");
      }
      
      private boolean isBooleanSchemaElement(String elementTail) { 
        return elementTail.length() == 0;
      }
      
      private boolean isIntegerSchemaElement(String elementTail) { 
        return elementTail.equals("#");
      }
      
      private boolean parseArguments() throws ArgsException {
        for (currentArgument = 0; currentArgument < args.length; currentArgument++) {
          String arg = args[currentArgument];
          parseArgument(arg); 
        }
        return true; 
      }
      
      private void parseArgument(String arg) throws ArgsException { 
        if (arg.startsWith("-"))
          parseElements(arg); 
      }
      
      private void parseElements(String arg) throws ArgsException { 
        for (int i = 1; i < arg.length(); i++)
          parseElement(arg.charAt(i)); 
      }
      
      private void parseElement(char argChar) throws ArgsException { 
        if (setArgument(argChar))
          argsFound.add(argChar); 
        else 
          unexpectedArguments.add(argChar); 
          errorCode = ErrorCode.UNEXPECTED_ARGUMENT; 
          valid = false;
      }
      
      private boolean setArgument(char argChar) throws ArgsException { 
        if (isBooleanArg(argChar))
          setBooleanArg(argChar, true); 
        else if (isStringArg(argChar))
          setStringArg(argChar); 
        else if (isIntArg(argChar))
          setIntArg(argChar); 
        else
          return false;
        
        return true; 
      }
      
      private boolean isIntArg(char argChar) {
        return intArgs.containsKey(argChar);
      }
      
      private void setIntArg(char argChar) throws ArgsException { 
        currentArgument++;
        String parameter = null;
        try {
          parameter = args[currentArgument];
          intArgs.get(argChar).set(parameter); 
        } catch (ArrayIndexOutOfBoundsException e) {
          valid = false;
          errorArgumentId = argChar;
          errorCode = ErrorCode.MISSING_INTEGER;
          throw new ArgsException();
        } catch (ArgsException e) {
          valid = false;
          errorArgumentId = argChar; 
          errorParameter = parameter;
          errorCode = ErrorCode.INVALID_INTEGER; 
          throw new e;
        } 
      }
      
      private void setStringArg(char argChar) throws ArgsException { 
        currentArgument++;
        try {
          stringArgs.get(argChar).set(args[currentArgument]); 
        } catch (ArrayIndexOutOfBoundsException e) {
          valid = false;
          errorArgumentId = argChar;
          errorCode = ErrorCode.MISSING_STRING; 
          throw new ArgsException();
        } 
      }
      
      private boolean isStringArg(char argChar) { 
        return stringArgs.containsKey(argChar);
      }
      
      private void setBooleanArg(char argChar, boolean value) { 
        try {
            booleanArgs.get(argChar).set("true");
        } catch (ArgsException e) {
    
        }
      }
      
      private boolean isBooleanArg(char argChar) { 
        return booleanArgs.containsKey(argChar);
      }
      
      public int cardinality() { 
        return argsFound.size();
      }
      
      public String usage() { 
        if (schema.length() > 0)
          return "-[" + schema + "]"; 
        else
          return ""; 
      }
      
      public String errorMessage() throws Exception { 
        switch (errorCode) {
          case OK:
            throw new Exception("TILT: Should not get here.");
          case UNEXPECTED_ARGUMENT:
            return unexpectedArgumentMessage();
          case MISSING_STRING:
            return String.format("Could not find string parameter for -%c.", errorArgumentId);
          case INVALID_INTEGER:
            return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
          case MISSING_INTEGER:
            return String.format("Could not find integer parameter for -%c.", errorArgumentId);
        }
        return ""; 
      }
      
      private String unexpectedArgumentMessage() {
        StringBuffer message = new StringBuffer("Argument(s) -"); 
        for (char c : unexpectedArguments) {
          message.append(c); 
        }
        message.append(" unexpected.");
        
        return message.toString(); 
      }
      
      private boolean falseIfNull(Boolean b) { 
        return b != null && b;
      }
      
      private int zeroIfNull(Integer i) { 
        return i == null ? 0 : i;
      }
      
      private String blankIfNull(String s) { 
        return s == null ? "" : s;
      }
      
      public String getString(char arg) { 
        Args.ArgumentMarshaler am = stringArgs.get(arg);
        return am == null ? "" : (String)am.get(); 
      }
      
      public int getInt(char arg) {
        Args.ArgumentMarshaler am = intArgs.get(arg);
        return am == null ? 0 : (Integer)am.get(); 
      }
      
      public boolean getBoolean(char arg) { 
        Args.ArgumentMarshaler am = booleanArgs.get(arg);
        return am != null && (Boolean)am.get(); 
      }
      
      public boolean has(char arg) { 
        return argsFound.contains(arg);
      }
      
      public boolean isValid() { 
        return valid;
      }
      
      private class ArgsException extends Exception {
      } 
    }
    
    private abstract class ArgumentMarshaler { 
        public abstract void set(String s);
    
        public abstract Object get();
    
    }
    
    private class BooleanArgumentMarshaler extends ArgumentMarshaler {
        private boolean booleanValue = false;
    
        public void set(String s){
            booleanValue = true;
        }
    
        public Object get() {
            return booleanValue;
        }
    }
    
    private class StringArgumentMarshaler extends ArgumentMarshaler {
        private String stringValue = "";
    
        public void set(String s){
            stringValue = s;
        }
    
        public Object get() {
            return stringValue;
        }
    }
    
    private class IntegerArgumentMarshaler extends ArgumentMarshaler {
        private int intValue = 0;
        
        public void set(String s) throws ArgsException {
            try {
                intValue = Integer.parseInt(s);
            } catch (NumberFormatException e) {
                throw new ArgsException();
            }
        }
    
        public Object get() {
            return intValue;
        }
    }
      
      private class BooleanArgumentMarshaler extends ArgumentMarshaler { }
      private class StringArgumentMarshaler extends ArgumentMarshaler { }
      private class IntegerArgumentMarshaler extends ArgumentMarshaler { }

👣 알고리즘 처음에 나오는 (인수 유형마다 따로 만든) 맵 세 개를 없앰 → 전체 시스템이 더 일반적으로 변함.

  1. 시스템이 깨지기 때문에 맵을 그냥 없애지는 못하므로 ArgumentMarshaler로 맵을 만들어 원래 맵을 교체하고 관련 메서드 변경

    → Boolean, String, Integer 차례로 변경

  2. isBooleanArgs, isStringArgs, isIntArgs 변경 후 marshaler.get 호출 코드 모두 삭제

  3. is~Args 메서드도 인라인 코드로 만듦.

    • 코드 참고

      private boolean setArgument(char argChar) throws ArgsException { 
              ArgumentMarshaler m = marshalers.get(argChar);
              if (m instanceof BooleanArgumentMarshaler)
                  setBooleanArg(argChar);
              else if (m instanceof StringArgumentMarshaler)
                  setStringArg(argChar);
              else if (m instanceof IntegerArgumentMarshaler)
                  setIntArg(argChar);
              else
                  return false;
          
              return false;
          }
  4. set 함수에서 기존 HashMap을 marshaler HashMap으로 교체

    → Boolean, String, Integer 차례로 변경

  5. getBoolean 함수 변경 후 boolean 맵 제거 → String, Integer 차례로 변경 및 맵 제거

    public boolean getBoolean(char arg) { 
            Args.ArgumentMarshaler am = marshalers.get(arg); 
            boolean b = false;
            try {
              b = am != null && (Boolean) am.get(); 
            } catch (ClassCastException e) {
              b = false; 
            }
            return b; 
          }
  6. 거의 사용하지 않는 parse 메서드 세 개를 인라인 코드로 변경 (p288 참고)

  • [14-12] Args.java (첫 번째 리팩터링을 끝낸 버전)

    import java.text.ParseException; 
    import java.util.*;
    
    public class Args {
      private String schema;
      private String[] args;
      private boolean valid = true;
      private Set<Character> unexpectedArguments = new TreeSet<Character>(); 
      private Map<Character, Marshaler> marshalers = new HashMap<Character, Marshaler>();
      private Set<Character> argsFound = new HashSet<Character>();
      private int currentArgument;
      private char errorArgumentId = '\0';
      private String errorParameter = "TILT";
      private ErrorCode errorCode = ErrorCode.OK;
      
      private enum ErrorCode {
        OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT}
        
      public Args(String schema, String[] args) throws ParseException { 
        this.schema = schema;
        this.args = args;
        valid = parse();
      }
      
      private boolean parse() throws ParseException { 
        if (schema.length() == 0 && args.length == 0)
          return true; 
        parseSchema(); 
        try {
          parseArguments();
        } catch (ArgsException e) {
        }
        return valid;
      }
      
      private boolean parseSchema() throws ParseException { 
        for (String element : schema.split(",")) {
          if (element.length() > 0) {
            String trimmedElement = element.trim(); 
            parseSchemaElement(trimmedElement);
          } 
        }
        return true; 
      }
      
      private void parseSchemaElement(String element) throws ParseException { 
        char elementId = element.charAt(0);
        String elementTail = element.substring(1); 
        validateSchemaElementId(elementId);
        if (isBooleanSchemaElement(elementTail)) 
            marshalers.put(elementId, new BooleanArgumentMarshaler());
        else if (isStringSchemaElement(elementTail)) 
            marshalers.put(elementId, new StringArgumentMarshaler());
        else if (isIntegerSchemaElement(elementTail)) 
            marshalers.put(elementId, new IntegerArgumentMarshaler());
        else
            throw new ParseException(String.format("Argument: %c has invalid format: %s.", elementId, elementTail), 0);
        } 
      }
        
      private void validateSchemaElementId(char elementId) throws ParseException { 
        if (!Character.isLetter(elementId)) {
          throw new ParseException("Bad character:" + elementId + "in Args format: " + schema, 0);
        }
      }
      
      private boolean isStringSchemaElement(String elementTail) { 
        return elementTail.equals("*");
      }
      
      private boolean isBooleanSchemaElement(String elementTail) { 
        return elementTail.length() == 0;
      }
      
      private boolean isIntegerSchemaElement(String elementTail) { 
        return elementTail.equals("#");
      }
      
      private boolean parseArguments() throws ArgsException {
        for (currentArgument = 0; currentArgument < args.length; currentArgument++) {
          String arg = args[currentArgument];
          parseArgument(arg); 
        }
        return true; 
      }
      
      private void parseArgument(String arg) throws ArgsException { 
        if (arg.startsWith("-"))
          parseElements(arg); 
      }
      
      private void parseElements(String arg) throws ArgsException { 
        for (int i = 1; i < arg.length(); i++)
          parseElement(arg.charAt(i)); 
      }
      
      private void parseElement(char argChar) throws ArgsException { 
        if (setArgument(argChar))
          argsFound.add(argChar); 
        else 
          unexpectedArguments.add(argChar); 
          errorCode = ErrorCode.UNEXPECTED_ARGUMENT; 
          valid = false;
      }
      
      private boolean setArgument(char argChar) throws ArgsException { 
        ArgumentMarshaler m = marshalers.get(argChar);
        try {
            if (m instanceof BooleanArgumentMarshaler)
                setBooleanArg(argChar);
            else if (m instanceof StringArgumentMarshaler)
                setStringArg(argChar);
            else if (m instanceof IntegerArgumentMarshaler)
                setIntArg(argChar);
            else
                return false;
        } catch (ArgsException e) {
            valid = false;
            errorArgumentId = argChar;
            throw e;
        }
        return true; 
      }
      
      private void setIntArg(ArgumentMarshaler m) throws ArgsException { 
        currentArgument++;
        String parameter = null;
        try {
          parameter = args[currentArgument];
          m.set(parameter); 
        } catch (ArrayIndexOutOfBoundsException e) {
          valid = false;
          errorArgumentId = argChar;
          errorCode = ErrorCode.MISSING_INTEGER;
          throw new ArgsException();
        } catch (ArgsException e) {
          errorParameter = parameter;
          errorCode = ErrorCode.INVALID_INTEGER; 
          throw e;
        } 
      }
      
      private void setStringArg(ArgumentMarshaler m) throws ArgsException { 
        currentArgument++;
        try {
          m.set(args[currentArgument]); 
        } catch (ArrayIndexOutOfBoundsException e) {
          errorCode = ErrorCode.MISSING_STRING; 
          throw new ArgsException();
        } 
      }
      
      private void setBooleanArg(ArgumentMarshaler m) { 
        try {
            m.set("true");
        } catch (ArgsException e) {
        }
      }
      
      public int cardinality() { 
        return argsFound.size();
      }
      
      public String usage() { 
        if (schema.length() > 0)
          return "-[" + schema + "]"; 
        else
          return ""; 
      }
      
      public String errorMessage() throws Exception { 
        switch (errorCode) {
          case OK:
            throw new Exception("TILT: Should not get here.");
          case UNEXPECTED_ARGUMENT:
            return unexpectedArgumentMessage();
          case MISSING_STRING:
            return String.format("Could not find string parameter for -%c.", errorArgumentId);
          case INVALID_INTEGER:
            return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
          case MISSING_INTEGER:
            return String.format("Could not find integer parameter for -%c.", errorArgumentId);
        }
        return ""; 
      }
      
      private String unexpectedArgumentMessage() {
        StringBuffer message = new StringBuffer("Argument(s) -"); 
        for (char c : unexpectedArguments) {
          message.append(c); 
        }
        message.append(" unexpected.");
        
        return message.toString(); 
      }
      
      public boolean getBoolean(char arg) { 
        Args.ArgumentMarshaler am = marshalers.get(arg); 
        boolean b = false;
        try {
          b = am != null && (Boolean) am.get(); 
        } catch (ClassCastException e) {
          b = false; 
        }
        return b; 
      }
    
      public String getString(char arg) { 
        Args.ArgumentMarshaler am = marshalers.get(arg); 
        try {
           return am == null ? "" : (String)am.get();
          } catch (ClassCastException e) {
            return "";
          }
      }
      
      public int getInt(char arg) {
        Args.ArgumentMarshaler am = marshalers.get(arg); 
        try {
            return am == null ? 0 : (Integer)am.get();
          } catch (ClassCastException e) {
            return 0;
          }
      }
      
      public boolean has(char arg) { 
        return argsFound.contains(arg);
      }
      
      public boolean isValid() { 
        return valid;
      }
      
      private class ArgsException extends Exception {
      } 
    }
    
    private abstract class ArgumentMarshaler { 
        public abstract void set(String s);
        public abstract Object get();
    }
    
    private class BooleanArgumentMarshaler extends ArgumentMarshaler {
        private boolean booleanValue = false;
    
        public void set(String s){
            booleanValue = true;
        }
    
        public Object get() {
            return booleanValue;
        }
    }
    
    private class StringArgumentMarshaler extends ArgumentMarshaler {
        private String stringValue = "";
    
        public void set(String s){
            stringValue = s;
        }
    
        public Object get() {
            return stringValue;
        }
    }
    
    private class IntegerArgumentMarshaler extends ArgumentMarshaler {
        private int intValue = 0;
        
        public void set(String s) throws ArgsException {
            try {
                intValue = Integer.parseInt(s);
            } catch (NumberFormatException e) {
                throw new ArgsException();
            }
        }
    
        public Object get() {
            return intValue;
        }
    }

→ 구조만 나아졌을 뿐 첫머리에 나오는 변수와 setArgument에 있는 유형을 일일이 확인하는 보기 싫은 코드 그대로. set 함수와 오류 처리 코드도 수정해야 함.

👣 setArgument 함수에서 유형을 일일이 확인하는 대신 Argument.Marshaler.set만 호출하게끔 수정

즉, setIntArg, serStringArg, setBooleanArg를 해당 ArgumentMarshaler의 파생 클래스로 내려줘야 함

  1. args 배열을 list로 변환 후 Iterator를 set 함수로 전달

    (∵ setIntArg에서 args, currentArgument 라는 두 개의 인스턴스 변수를 사용하는데 인수는 하나만 넘기는 게 낫기 때문에)

    • 코드 참고

      import java.text.ParseException; 
      import java.util.*;
      
      public class Args {
        private String schema;
        private boolean valid = true;
        private Set<Character> unexpectedArguments = new TreeSet<Character>(); 
        private Map<Character, Marshaler> marshalers = new HashMap<Character, Marshaler>();
        private Set<Character> argsFound = new HashSet<Character>();
        private Iterator<String> currentArgument;
        private char errorArgumentId = '\0';
        private String errorParameter = "TILT";
        private ErrorCode errorCode = ErrorCode.OK;
        private List<String> argsList;
      
        private enum ErrorCode {
          OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT
          }
          
        public Args(String schema, String[] args) throws ParseException { 
          this.schema = schema;
          argsList = Array.asList(args);
          valid = parse();
        }
        
        private boolean parse() throws ParseException { 
          if (schema.length() == 0 && argsList.size() == 0)
            return true; 
          parseSchema(); 
          try {
            parseArguments();
          } catch (ArgsException e) {
          }
          return valid;
        }
        
        private boolean parseSchema() throws ParseException { 
          for (String element : schema.split(",")) {
            if (element.length() > 0) {
              String trimmedElement = element.trim(); 
              parseSchemaElement(trimmedElement);
            } 
          }
          return true; 
        }
        
        private void parseSchemaElement(String element) throws ParseException { 
          char elementId = element.charAt(0);
          String elementTail = element.substring(1); 
          validateSchemaElementId(elementId);
          if (isBooleanSchemaElement(elementTail)) 
              marshalers.put(elementId, new BooleanArgumentMarshaler());
          else if (isStringSchemaElement(elementTail)) 
              marshalers.put(elementId, new StringArgumentMarshaler());
          else if (isIntegerSchemaElement(elementTail)) 
              marshalers.put(elementId, new IntegerArgumentMarshaler());
          else
              throw new ParseException(String.format("Argument: %c has invalid format: %s.", elementId, elementTail), 0);
          } 
        }
          
        private void validateSchemaElementId(char elementId) throws ParseException { 
          if (!Character.isLetter(elementId)) {
            throw new ParseException("Bad character:" + elementId + "in Args format: " + schema, 0);
          }
        }
        
        private boolean isStringSchemaElement(String elementTail) { 
          return elementTail.equals("*");
        }
        
        private boolean isBooleanSchemaElement(String elementTail) { 
          return elementTail.length() == 0;
        }
        
        private boolean isIntegerSchemaElement(String elementTail) { 
          return elementTail.equals("#");
        }
        
        private boolean parseArguments() throws ArgsException {
          for (currentArgument = argsList.iterator() ; currentArgument.hasNext();) {
            String arg = currentArgument.next();
            parseArgument(arg); 
          }
          return true; 
        }
        
        private void parseArgument(String arg) throws ArgsException { 
          if (arg.startsWith("-"))
            parseElements(arg); 
        }
        
        private void parseElements(String arg) throws ArgsException { 
          for (int i = 1; i < arg.length(); i++)
            parseElement(arg.charAt(i)); 
        }
        
        private void parseElement(char argChar) throws ArgsException { 
          if (setArgument(argChar))
            argsFound.add(argChar); 
          else 
            unexpectedArguments.add(argChar); 
            errorCode = ErrorCode.UNEXPECTED_ARGUMENT; 
            valid = false;
        }
        
        private boolean setArgument(char argChar) throws ArgsException { 
          ArgumentMarshaler m = marshalers.get(argChar);
          try {
              if (m instanceof BooleanArgumentMarshaler)
                  setBooleanArg(argChar);
              else if (m instanceof StringArgumentMarshaler)
                  setStringArg(argChar);
              else if (m instanceof IntegerArgumentMarshaler)
                  setIntArg(argChar);
              else
                  return false;
          } catch (ArgsException e) {
              valid = false;
              errorArgumentId = argChar;
              throw e;
          }
          return true; 
        }
        
        private void setIntArg(ArgumentMarshaler m) throws ArgsException { 
          currentArgument++;
          String parameter = null;
          try {
            parameter = currentArgument.next();
            m.set(parameter); 
          } catch (NoSuchElementException e) {
            valid = false;
            errorArgumentId = argChar;
            errorCode = ErrorCode.MISSING_INTEGER;
            throw new ArgsException();
          } catch (ArgsException e) {
            errorParameter = parameter;
            errorCode = ErrorCode.INVALID_INTEGER; 
            throw e;
          } 
        }
        
        private void setStringArg(ArgumentMarshaler m) throws ArgsException { 
          currentArgument++;
          try {
            m.set(currentArgument.next()); 
          } catch (NoSuchElementException e) {
            errorCode = ErrorCode.MISSING_STRING; 
            throw new ArgsException();
          } 
        }
        
        private void setBooleanArg(ArgumentMarshaler m) { 
          try {
              m.set("true");
          } catch (ArgsException e) {
          }
        }
        
        public int cardinality() { 
          return argsFound.size();
        }
        
        public String usage() { 
          if (schema.length() > 0)
            return "-[" + schema + "]"; 
          else
            return ""; 
        }
        
        public String errorMessage() throws Exception { 
          switch (errorCode) {
            case OK:
              throw new Exception("TILT: Should not get here.");
            case UNEXPECTED_ARGUMENT:
              return unexpectedArgumentMessage();
            case MISSING_STRING:
              return String.format("Could not find string parameter for -%c.", errorArgumentId);
            case INVALID_INTEGER:
              return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
            case MISSING_INTEGER:
              return String.format("Could not find integer parameter for -%c.", errorArgumentId);
          }
          return ""; 
        }
        
        private String unexpectedArgumentMessage() {
          StringBuffer message = new StringBuffer("Argument(s) -"); 
          for (char c : unexpectedArguments) {
            message.append(c); 
          }
          message.append(" unexpected.");
          
          return message.toString(); 
        }
        
        public boolean getBoolean(char arg) { 
          Args.ArgumentMarshaler am = marshalers.get(arg); 
          boolean b = false;
          try {
            b = am != null && (Boolean) am.get(); 
          } catch (ClassCastException e) {
            b = false; 
          }
          return b; 
        }
      
        public String getString(char arg) { 
          Args.ArgumentMarshaler am = marshalers.get(arg); 
          try {
             return am == null ? "" : (String)am.get();
            } catch (ClassCastException e) {
              return "";
            }
        }
        
        public int getInt(char arg) {
          Args.ArgumentMarshaler am = marshalers.get(arg); 
          try {
              return am == null ? 0 : (Integer)am.get();
            } catch (ClassCastException e) {
              return 0;
            }
        }
        
        public boolean has(char arg) { 
          return argsFound.contains(arg);
        }
        
        public boolean isValid() { 
          return valid;
        }
        
        private class ArgsException extends Exception {
        } 
      }
      
      private abstract class ArgumentMarshaler { 
          public abstract void set(String s);
          public abstract Object get();
      }
      
      private class BooleanArgumentMarshaler extends ArgumentMarshaler {
          private boolean booleanValue = false;
      
          public void set(String s){
              booleanValue = true;
          }
      
          public Object get() {
              return booleanValue;
          }
      }
      
      private class StringArgumentMarshaler extends ArgumentMarshaler {
          private String stringValue = "";
      
          public void set(String s){
              stringValue = s;
          }
      
          public Object get() {
              return stringValue;
          }
      }
      
      private class IntegerArgumentMarshaler extends ArgumentMarshaler {
          private int intValue = 0;
          
          public void set(String s) throws ArgsException {
              try {
                  intValue = Integer.parseInt(s);
              } catch (NumberFormatException e) {
                  throw new ArgsException();
              }
          }
      
          public Object get() {
              return intValue;
          }
      }
  2. set 함수를 적절한 파생 클래스로 내려도 이상 없으므로 우선 setArgument 클래스 변경

    • 코드 참고

      private boolean setArgument(char argChar) throws ArgsException { 
          ArgumentMarshaler m = marshalers.get(argChar);
          if (m == null){
              return false;
          }
          try {
              if (m instanceof BooleanArgumentMarshaler)
                  setBooleanArg(argChar);
              else if (m instanceof StringArgumentMarshaler)
                  setStringArg(argChar);
              else if (m instanceof IntegerArgumentMarshaler)
                  setIntArg(argChar);
          } catch (ArgsException e) {
              valid = false;
              errorArgumentId = argChar;
              throw e;
          }
          return true; 
        }
  3. set 함수 옮기기 (setBooleanArg 함수부터)

    • 코드 참고

      private boolean setArgument(char argChar) throws ArgsException { 
          ArgumentMarshaler m = marshalers.get(argChar);
          if (m == null){
              return false;
          }
          try {
              if (m instanceof BooleanArgumentMarshaler)
                  setBooleanArg(m, currentArgument);
              else if (m instanceof StringArgumentMarshaler)
                  setStringArg(argChar);
              else if (m instanceof IntegerArgumentMarshaler)
                  setIntArg(argChar);
          } catch (ArgsException e) {
              valid = false;
              errorArgumentId = argChar;
              throw e;
          }
          return true; 
        }
      
      ...
      
      private void setBooleanArg(ArgumentMarshaler m, Iterator<String> currentArgument) throws ArgsException { 
              m.set("true");
      }

    → 리팩터링을 하다 보면 코드를 여기 저기 옮기는 경우가 많음

    setBooleanArg는 iterator가 필요 없음에도 인수로 넘긴 이유는 SetStringArg, setIntArg에서 필요하기 때문

  4. ArgumentMarshaler에 새로운 추상 메서드 추가 후 각 파생 메서드에 set 메서드 추가하고 setBooleanArg 제거(set 함수는 BooleanArgumentMarshaler에 속한다)

    → String, Integer 인수에도 동일하게

  5. 인수 유형을 일일이 확인하던 코드 제거

    • 코드 참고

      private boolean setArgument(char argChar) throws ArgsException { 
          ArgumentMarshaler m = marshalers.get(argChar);
          if (m == null)
            return false; 
          try {
            m.set(currentArgument);
            return true;
          } catch (ArgsException e) {
            e.setErrorArgumentId(argChar);
            throw e; 
          }
        }
  6. IntegerArgumentMarshaler에서 허술한 코드 수정 및 ArgumentMarshaler 인터페이스로 변환

👣 Double 인수 유형 추가해보기

  1. test case부터 작성 (p 304)

  2. 스키마 구문 분석 코드 정리하고 ## 감지 코드 추가 (##은 double 인수 유형) (p 304)

  3. DoubleArgumentMarshaler 클래스 작성 (p 305)

  4. 새로운 오류 코드와 getDouble 함수 필요 (p 305)

  5. 오류 처리 코드가 제대로 도는지 확인

    1. 구문 분석이 불가능한 문자열을 ##인수에 전달하여 확인(p 306)
    2. double 인수를 빠뜨려 확인(p 307)

    ⇒ 통과하지만 흉한 예외 코드

  6. 예외를 하나로 모아 ArgsException 클래스를 만든 후 독자 모듈로 이동(p 307~310)

    ∵ 예외 코드는 Args 클래스에 속하지 않을 뿐더러 ParseException을 던지지만 ParseException는 Args 클래스에 속하지 않음.

    변경 후 ⇒ Args 클래스가 던지는 예외는 ArgsException 뿐. ArgsException을 독자적인 모듈로 만들면 Args 모듈에서 잡다한 오류 지원 코드를 옮겨올 수 있음 → Args 모듈이 깨끗해져서 차후 확장 쉬워짐.

👣 Args 모듈에서 예외/오류 처리 코드 분리

  • TEST CASE

    • [14-13] ArgsTest.java

      public class ArgsTest extends TestCase {
        public void testCreateWithNoSchemaOrArguments() throws Exception {
          Args args = new Args("", new String[0]);
          assertEquals(0, args.cardinality());
        }
       
        public void testWithNoSchemaButWithOneArgument() throws Exception {
          try {
            new Args("", new String[]{"-x"});
            fail();
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT,
                         e.getErrorCode());
            assertEquals('x', e.getErrorArgumentId());
          }
        }
       
        public void testWithNoSchemaButWithMultipleArguments() throws Exception {
          try {
            new Args("", new String[]{"-x", "-y"});
            fail();
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT,
                         e.getErrorCode());
            assertEquals('x', e.getErrorArgumentId());
          }
       
        }
       
        public void testNonLetterSchema() throws Exception {
          try {
            new Args("*", new String[]{});
            fail("Args constructor should have thrown exception");
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME,
                         e.getErrorCode());
            assertEquals('*', e.getErrorArgumentId());
          }
        }
       
        public void testInvalidArgumentFormat() throws Exception {
          try {
            new Args("f~", new String[]{});
            fail("Args constructor should have throws exception");
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.INVALID_FORMAT, e.getErrorCode());
            assertEquals('f', e.getErrorArgumentId());
          }
        }
       
        public void testSimpleBooleanPresent() throws Exception {
          Args args = new Args("x", new String[]{"-x", "true"});
          assertEquals(1, args.cardinality());
          assertEquals(true, args.getBoolean('x'));
        }
       
        public void testSimpleStringPresent() throws Exception {
          Args args = new Args("x*", new String[]{"-x", "param"});
          assertEquals(1, args.cardinality());
          assertTrue(args.has('x'));
          assertEquals("param", args.getString('x'));
        }
       
        public void testMissingStringArgument() throws Exception {
          try {
            new Args("x*", new String[]{"-x"});
            fail();
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.MISSING_STRING, e.getErrorCode());
            assertEquals('x', e.getErrorArgumentId());
          }
        }
       
        public void testSpacesInFormat() throws Exception {
          Args args = new Args("x, y", new String[]{"-xy", "true", "false"});
          assertEquals(2, args.cardinality());
          assertTrue(args.has('x'));
          assertTrue(args.has('y'));
        }
       
        public void testSimpleIntPresent() throws Exception {
          Args args = new Args("x#", new String[]{"-x", "42"});
          assertEquals(1, args.cardinality());
          assertTrue(args.has('x'));
          assertEquals(42, args.getInt('x'));
        }
       
        public void testInvalidInteger() throws Exception {
          try {
            new Args("x#", new String[]{"-x", "Forty two"});
            fail();
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.INVALID_INTEGER, e.getErrorCode());
            assertEquals('x', e.getErrorArgumentId());
            assertEquals("Forty two", e.getErrorParameter());
          }
       
        }
       
        public void testMissingInteger() throws Exception {
          try {
            new Args("x#", new String[]{"-x"});
            fail();
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.MISSING_INTEGER, e.getErrorCode());
            assertEquals('x', e.getErrorArgumentId());
          }
        }
       
        public void testSimpleDoublePresent() throws Exception {
          Args args = new Args("x##", new String[]{"-x", "42.3"});
          assertEquals(1, args.cardinality());
          assertTrue(args.has('x'));
          assertEquals(42.3, args.getDouble('x'), .001);
        }
       
        public void testInvalidDouble() throws Exception {
          try {
            new Args("x##", new String[]{"-x", "Forty two"});
            fail();
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.INVALID_DOUBLE, e.getErrorCode());
            assertEquals('x', e.getErrorArgumentId());
            assertEquals("Forty two", e.getErrorParameter());
          }
        }
       
        public void testMissingDouble() throws Exception {
          try {
            new Args("x##", new String[]{"-x"});
            fail();
          } catch (ArgsException e) {
            assertEquals(ArgsException.ErrorCode.MISSING_DOUBLE, e.getErrorCode());
            assertEquals('x', e.getErrorArgumentId());
          }
        }
      }
    • [14-14] ArgsExceptionTest.java

      public class ArgsExceptionTest extends TestCase {
        public void testUnexpectedMessage() throws Exception {
          ArgsException e =
            new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT,
                              'x', null);
          assertEquals("Argument -x unexpected.", e.errorMessage());
        }
       
        public void testMissingStringMessage() throws Exception {
          ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_STRING,
                                              'x', null);
          assertEquals("Could not find string parameter for -x.", e.errorMessage());
        }
       
        public void testInvalidIntegerMessage() throws Exception {
          ArgsException e =
            new ArgsException(ArgsException.ErrorCode.INVALID_INTEGER,
                              'x', "Forty two");
          assertEquals("Argument -x expects an integer but was 'Forty two'.",
                       e.errorMessage());
        }
       
        public void testMissingIntegerMessage() throws Exception {
          ArgsException e =
            new ArgsException(ArgsException.ErrorCode.MISSING_INTEGER, 'x', null);
          assertEquals("Could not find integer parameter for -x.", e.errorMessage());
        }
       
        public void testInvalidDoubleMessage() throws Exception {
          ArgsException e = new ArgsException(ArgsException.ErrorCode.INVALID_DOUBLE,
                                              'x', "Forty two");
          assertEquals("Argument -x expects a double but was 'Forty two'.",
                       e.errorMessage());
        }
       
        public void testMissingDoubleMessage() throws Exception {
          ArgsException e = new ArgsException(ArgsException.ErrorCode.MISSING_DOUBLE,
                                              'x', null);
          assertEquals("Could not find double parameter for -x.", e.errorMessage());
        }
      }
  • Final Code

    • [14-15] ArgsException.java

      public class ArgsException extends Exception { 
        private char errorArgumentId = '\0'; 
        private String errorParameter = "TILT"; 
        private ErrorCode errorCode = ErrorCode.OK;
        
        public ArgsException() {}
        
        public ArgsException(String message) {super(message);}
        
        public ArgsException(ErrorCode errorCode) { 
          this.errorCode = errorCode;
        }
        
        public ArgsException(ErrorCode errorCode, String errorParameter) { 
          this.errorCode = errorCode;
          this.errorParameter = errorParameter;
        }
        
        public ArgsException(ErrorCode errorCode, char errorArgumentId, String errorParameter) {
          this.errorCode = errorCode; 
          this.errorParameter = errorParameter; 
          this.errorArgumentId = errorArgumentId;
        }
        
        public char getErrorArgumentId() { 
          return errorArgumentId;
        }
        
        public void setErrorArgumentId(char errorArgumentId) { 
          this.errorArgumentId = errorArgumentId;
        }
        
        public String getErrorParameter() { 
          return errorParameter;
        }
        
        public void setErrorParameter(String errorParameter) {  
          this.errorParameter = errorParameter;
        }
        
        public ErrorCode getErrorCode() { 
          return errorCode;
        }
        
        public void setErrorCode(ErrorCode errorCode) { 
          this.errorCode = errorCode;
        }
        
        public String errorMessage() throws Exception { 
          switch (errorCode) {
            case OK:
              throw new Exception("TILT: Should not get here.");
            case UNEXPECTED_ARGUMENT:
              return String.format("Argument -%c unexpected.", errorArgumentId);
            case MISSING_STRING:
              return String.format("Could not find string parameter for -%c.", errorArgumentId);
            case INVALID_INTEGER:
              return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
            case MISSING_INTEGER:
              return String.format("Could not find integer parameter for -%c.", errorArgumentId);
            case INVALID_DOUBLE:
              return String.format("Argument -%c expects a double but was '%s'.", errorArgumentId, errorParameter);
            case MISSING_DOUBLE:
              return String.format("Could not find double parameter for -%c.", errorArgumentId);
          }
          return ""; 
        }
        
        public enum ErrorCode {
          OK, INVALID_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, MISSING_STRING,
          MISSING_INTEGER, INVALID_INTEGER,
          MISSING_DOUBLE, INVALID_DOUBLE
        }
      }
    • [14-16] Args.java

      public class Args {
        private String schema;
        private Map<Character, ArgumentMarshaler> marshalers = new HashMap<Character, ArgumentMarshaler>();
        private Set<Character> argsFound = new HashSet<Character>(); 
        private Iterator<String> currentArgument;
        private List<String> argsList;
        
        public Args(String schema, String[] args) throws ArgsException { 
          this.schema = schema;
          argsList = Arrays.asList(args);
          parse();
        }
        
        private void parse() throws ArgsException { 
          parseSchema();
          parseArguments();
        }
        
        private boolean parseSchema() throws ArgsException {
          for (String element : schema.split(",")) { 
            if (element.length() > 0) {
              parseSchemaElement(element.trim()); 
            }
          }
          return true; 
        }
        
        private void parseSchemaElement(String element) throws ArgsException { 
          char elementId = element.charAt(0);
          String elementTail = element.substring(1); 
          validateSchemaElementId(elementId);
          if (elementTail.length() == 0)
            marshalers.put(elementId, new BooleanArgumentMarshaler());
          else if (elementTail.equals("*")) 
            marshalers.put(elementId, new StringArgumentMarshaler());
          else if (elementTail.equals("#"))
            marshalers.put(elementId, new IntegerArgumentMarshaler());
          else if (elementTail.equals("##")) 
            marshalers.put(elementId, new DoubleArgumentMarshaler());
          else
            throw new ArgsException(ArgsException.ErrorCode.INVALID_FORMAT, elementId, elementTail);
            
        private void validateSchemaElementId(char elementId) throws ArgsException { 
          if (!Character.isLetter(elementId)) {
            throw new ArgsException(ArgsException.ErrorCode.INVALID_ARGUMENT_NAME, elementId, null);
          } 
        }
        
        private void parseArguments() throws ArgsException {
          for (currentArgument = argsList.iterator(); currentArgument.hasNext();) {
            String arg = currentArgument.next();
            parseArgument(arg); 
          }
        }
        
        private void parseArgument(String arg) throws ArgsException { 
          if (arg.startsWith("-"))
            parseElements(arg); 
        }
        
        private void parseElements(String arg) throws ArgsException { 
          for (int i = 1; i < arg.length(); i++)
            parseElement(arg.charAt(i)); 
        }
        
        private void parseElement(char argChar) throws ArgsException { 
          if (setArgument(argChar))
            argsFound.add(argChar); 
          else 
            throw new ArgsException(ArgsException.ErrorCode.UNEXPECTED_ARGUMENT, argChar, null);
        } 
        
        private boolean setArgument(char argChar) throws ArgsException { 
          ArgumentMarshaler m = marshalers.get(argChar);
          if (m == null)
            return false; 
          try {
            m.set(currentArgument);
            return true;
          } catch (ArgsException e) {
            e.setErrorArgumentId(argChar);
            throw e; 
          }
        }
        
        public int cardinality() { 
          return argsFound.size();
        }
        
        public String usage() { 
          if (schema.length() > 0)
            return "-[" + schema + "]"; 
          else
            return ""; 
        }
        
        public boolean getBoolean(char arg) { 
          ArgumentMarshaler am = marshalers.get(arg); 
          boolean b = false;
          try {
            b = am != null && (Boolean) am.get(); 
          } catch (ClassCastException e) {
            b = false; 
          }
          return b; 
        }
        
        public String getString(char arg) { 
          ArgumentMarshaler am = marshalers.get(arg); 
          try {
            return am == null ? "" : (String) am.get(); 
          } catch (ClassCastException e) {
            return ""; 
          }
        }
        
        public int getInt(char arg) { 
          ArgumentMarshaler am = marshalers.get(arg); 
          try {
            return am == null ? 0 : (Integer) am.get(); 
          } catch (Exception e) {
            return 0; 
          }
        }
        
        public double getDouble(char arg) { 
          ArgumentMarshaler am = marshalers.get(arg); 
          try {
            return am == null ? 0 : (Double) am.get(); 
          } catch (Exception e) {
            return 0.0; 
          }
        }
        
        public boolean has(char arg) { 
          return argsFound.contains(arg);
        } 
      }

⇒ Args 클래스에서는 주로 코드만 삭제 → ArgsException으로 이동,

ArgumentMarshaler클래스도 각자 파일로 이동

⇒ 소프트웨어 설계는 분할만 잘해도 품질 ↑, 관심사를 분리하면 코드를 이해하고 보수하기 쉬워짐.

  • 눈여겨볼 코드 : ArgsException의 errorMessage 메서드

→ 원래 Args 클래스에 속했을 때는 SRP 위반

∵ Args 클래스는 인수 처리 클래스인데 오류 메세지 형식까지 책임졌기 때문


결론

코드는 언제나 최대한 깔끔하고 단순하게 정리하자.

profile
#Backend #Developer

0개의 댓글