14장. 점진적인 개선
결론
그저 돌아만가는 코드만으로는 부족하다
나쁜 코드가 프로젝트에 악영향을 끼친다
코드는 개선할 수 있다 하지만 나중에 개선하기 어렵다
처음부터 깨끗하게 유지하는 것이 효율적이다
점진적인 개선
명령행 인수의 구문을 분석하는 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.printf("Argument error: %s\n", e.errorMessage());
}
}
예시
-l -p Integer -d String
Args 클래스가 하는 일
스키마를 분석
명령행 인수들의 값을 관리
최종
```java
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 { // "l,p#,d*"
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);
}
```
```java
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;
}
}
}
```
1차 초안
private Map<Character, Boolean> booleanArgs = new HashMap<Character, Boolean>();
private boolean parseSchema() {
for (String element : schema.split(",")) {
parseSchemaElement(element);
}
return true;
}
private void parseSchemaElement(String element) {
if (element.length() == 1)
parseBooleanSchemaElement(element);
}
private void parseBooleanSchemaElement(char element) {
char c = element.charAt(0);
if (Character.isLetter(c)) {
booleanArgs.put(c, false);
}
}
boolean 인수들만 가능하고 map에 저장
private boolean parseArguments() {
for (String arg : args)
parseArgument(arg);
return true;
}
private void parseArgument(String arg) {
if (arg.startsWith("-"))
parseElements(arg);
}
private void parseElements(String arg) {
for (int i = 1; i < arg.length(); i++)
parseElement(arg.charAt(i));
}
private void parseElement(char argChar) {
if (isBoolean(argChar)) {
numberOfArguments++;
setBooleanArg(argChar, true);
} else
unexpectedArguments.add(argChar);
}
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.put(argChar, value);
}
private boolean isBooleanArg(char argChar) {
return booleanArgs.containsKey(argChar);
}
public boolean getBoolean(char arg) {
return falseIfNull(booleanArgs.get(arg));
}
명령행 인수 문자열 배열에서 -
로 시작하는 문자열이 어떤 인수 유형인지를 map에서 key로 가지고 있는지로 확인하고 value를 갱신하여 저장
Integer와 String 추가
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 void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (elementTail.length() == 0)
booleanArgs.put(elementId, false);
else if (elementTail.equals("*"))
stringArgs.put(elementId, "");
else if (elementTail.equals("#"))
intArgs.put(elementId, 0);
else
throw new ParseException(String.format("Argument: %c has invalid format: %s.",
elementId, elementTail), 0);
}
스키마 분석은 스키마를 타입별로 Map 을 여러 개 만들어 관리
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 isStringArg(char argChar) {
return stringArgs.containsKey(argChar);
}
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();
}
}
public String getString(char arg) {
return blankIfNull(stringArgs.get(arg));
}
타입이 늘어남에 따라 setArgument가 변하고 is, set, get 메소드가 추가로 필요
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 { }
인수 유형은 여러 가지이지만 모두 유사한 메소드를 제공하므로 클래스로 추출
private Map<Character, ArgumentMarshaler> booleanArgs = new HashMap<Character, ArgumentMarshaler>();
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) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && am.getBoolean();
}
ArgumentMarshaler 클래스를 도입하면서 실패하는 테스트를 위한 코드 수정
String과 Integer도 ArgumentMarshaler에게 로직을 옮기고 파생 클래스를 만들어 기능을 분산한다
private abstract class ArgumentMarshaler {
public abstract void set(String s) throws ArgsException;
public abstract Object get();
}
private class BooleanArgumentMarshaler extends ArgumentMarshaler { ... }
private class StringArgumentMarshaler extends ArgumentMarshaler { ... }
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private boolean intValue = 0;
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException;
}
}
public boolean get() {return intValue;}
}
첫 번째 리팩터링 이후
이제 ArgumentMarshaler를 활용해 인수 유형 마다 존재하고 있던 Map들을 교체하고 관련 메소드를 변경한다
private Map<Character, ArgumentMarshaler> marshalers = new HashMap<Character, ArgumentMarshaler>();
private void parseSchemaElement(String element) throws ParseException {
char elementId = element.charAt(0);
String elementTail = element.substring(1);
validateSchemaElementId(elementId);
if (elementTail.length() == 0)
marshalers.put(elementId, new BooleanArgumentMarshalers());
else if (elementTail.equals("*"))
marshalers.put(elementId, new StringArgumentMarshalers());
else if (elementTail.equals("#"))
marshalers.put(elementId, new IntegerArgumentMarshalers());
else
throw new ParseException(String.format("Argument: %c has invalid format: %s.",
elementId, elementTail), 0);
}
}
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(argChar, true);
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) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
public int getInt(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
boolean b = false;
try {
return am == null ? 0 : (Integer) am.get();
} catch (Exception e) {
return 0;
}
}
setArgument에서의 유형을 일일이 확이하는 부분을 없애고 싶다
ArgumentMarshaler.set만 호출해도 충분하게 만들고 싶다
그러면 setXXXArg도 각 ArgumentMarshaler의 파생클래스로 내리자
파라미터인 args(String[])와 currentArgument(int) 대신 args를 리스트로 바꾸고 iterator만 전달하자
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) {
valid = false;
errorArgumentId = argChar;
throw e;
}
}
private interface ArgumentMarshaler {
void set(Iterator<String> currentArgument) throws ArgsException;
Object get();
}
private 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) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw new ArgsException();
}
}
public int get() {
return intValue;
}
}
이젠 새로운 인수 유형 추가하기도 쉬워졌다
parseSchemaElement에서 새로운 인수유형에 대한 판별 코드를 추가하고
새로운 인수 유형에 대한 ArgumentMarshaler 의 구현 클래스를 만들고
getXXX와 새로운 오류 처리 코드만 추가하면 된다
Args 클래스에서 ArgsException과 모든 ArgumentMarshaler 클래스를 빼서 각자 파일로 옮긴다
분할도 이해하기 쉬워지고 유지보수가 쉬워지므로 소프트웨서 설계의 품질을 높인다
Last updated