티스토리 뷰

반응형


| 14. 점진적인 개선


Sources and Reference


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

* (일단 프로그램이 돌아간다고 다음 업무로 넘어가지 말고) 깔끔한 작품을 내놓으려면 단계적으로 개선해야 한다.

- 테스트 주도 개발(Test-Driven Development, TDD) 기법을 사용해보자.


1. 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다

2. 적절한 장소를 만들어 코드만 불리해도 설계가 좋아진다.

3. 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 더 쉬워진다.



|| Args 사용법


-

1
2
3
4
5
6
7
8
9
10
11
12
// 간단한 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());
  }
}
cs

- 형식 문자열이나 명령행 인수 자체에 문제가 있다면 ArgsException이 발생

- 구체적인 오류를 알아내려면 예외가 제공하는 errorMessage 메서드를 사용



|| Args.java


-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import java.util.*;
 
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);
    }
}
cs

1. 인수 유형에 해당하는 HashMap을 선택하기 위해 스키마 요소의 구문을 분석 (line 31~46)

2. 명령행 인수에서 인수 유형을 분석해 진짜 유형으로 변환 (line 55~60)

3. getXXX 메서드를 구현해 호출자에게 진짜 유형을 반환 (line 104~141)


- 인수 유형은 다향하지만 모두 유사한 메서드를 제공하므로 ArgumentMarshaler Interface 생성



|| ArgsException.java


-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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, MISSING_BOOLEAN, INVALID_BOOLEAN, INVALID_DOUBLE}
    }
cs

- ArgsException을 독자적인 모듈로 만들면 Args 모듈에서 잡다한 오류 지원 코드를 옮겨올 수 있다.

ArgsException 모듈은 잡다한 오류 지원 코드가 들어갈 합당하고 당연한 장소이다.



|| ArgumentMarshaler.java


-

1
2
3
4
5
6
import java.util.Iterator;
 
public interface ArgumentMarshaler {
    void set(Iterator<String> currentArgument) throws ArgsException;
    Object get();
}
cs



|| BooleanArgumentMarshaler.java


-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Iterator;
import java.util.NoSuchElementException;
 
import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.INVALID_BOOLEAN;
import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.MISSING_BOOLEAN;
 
public class BooleanArgumentMarshaler implements ArgumentMarshaler {
  private boolean booleanValue = false;
 
  public void set(Iterator<String> currentArgument) throws ArgsException {
    String parameter = null;
    try {
      parameter = currentArgument.next();
      booleanValue = Boolean.parseBoolean(parameter);
    } catch (NoSuchElementException e) {
      throw new ArgsException(MISSING_BOOLEAN);
    } catch (NumberFormatException e) {
      throw new ArgsException(INVALID_BOOLEAN, parameter);
    }
  }
 
  public Object get() {
    return booleanValue;
  }
}
cs



|| DoubleArgumentMarshaler.java


-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.Iterator;
import java.util.NoSuchElementException;
 
import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.INVALID_DOUBLE;
import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.MISSING_DOUBLE;
 
public class DoubleArgumentMarshaler implements ArgumentMarshaler {
 
  private double doubleValue = 0;
 
  public void set(Iterator<String> currentArgument) throws ArgsException {
    String parameter = null;
    try {
      parameter = currentArgument.next();
      doubleValue = Double.parseDouble(parameter);
    } catch (NoSuchElementException e) {
      throw new ArgsException(MISSING_DOUBLE);
    } catch (NumberFormatException e) {
      throw new ArgsException(INVALID_DOUBLE, parameter);
    }
  }
 
  public Object get() {
    return doubleValue;
  }
}
cs



|| IntegerArgumentMarshaler.java


-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Iterator;
import java.util.NoSuchElementException;
 
import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.INVALID_INTEGER;
import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.MISSING_INTEGER;
 
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 Object get() {
    return intValue;
  }
}
cs



|| StringArgumentMarshaler.java


-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Iterator;
import java.util.NoSuchElementException;
 
import static clean.code.chapter14.refactored.second.ArgsException.ErrorCode.MISSING_STRING;
 
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 Object get() {
    return stringValue;
  }
}
cs



|| Test Case



||| ArgsTest


-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import junit.framework.TestCase;
 
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());
    }
  }
}
cs



||| ArgsExceptionTest


-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import junit.framework.TestCase;
 
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());
  }
}
cs

-





출처 : 클린 코드 (Robert C. Martin)



반응형
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday