책 초반에, 좋은 코드를 짜기 위해서는 일단 막 짜고 나서 다듬어야 한다고 했다.. 초안 코드를 단계적으로 개선해 좋은 코드를 만들어야 한다.
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');
executeAppliocation(logging, port, directory);
} catch (ArgsException e) {
System.out.printf("Argument error: %s\n", e.errorMessage());
}
}
이 장에서 분석할 예제는 위 코드를 기반으로 한다.
프로그램을 짜다 보면 종종 명령행 인수의 구문을 분석할 필요가 생긴다. 마땅한 유틸리티가 없다면 main 함수로 넘어오는 argument(매개변수) args를 직접 분석하게 된다. 직접 구현할 유틸리티를 Args라고 부르자.
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
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));
}
}
책에 있는 코드는 이렇게 생겼다. 쭉 읽어보니 대충 이런 코든데? 하고 느낌이 오는, 상당히 깔끔한 코드다.
하지만 처음부터 이런 코드를 짤 수 는 없다. 위 코드는 아주 더러운 초안 코드를 점진적으로 개선해 나온 것이다.
초기 코드가 어떻게 짜여져 있는지 묘사하자면... Boolean 인수만 분석할 수 있는 초창기 코드는 그럭저럭 괜찮았다. 하지만 위 깔끔한 최종 코드와는 생긴게 달랐다. (정확한 코드는 클린코드 261p) Boolean 인수 전용(?) 함수가 많았던 것이다... 이상태에서 String과 Integer 인수도 분석하도록 추가했더니 상당히 지저분해졌다.
🙋♀️그래서 멈췄다.
계속 밀어붙이면 프로그램은 어떻게든 완성하겠지만, 그랬다간 코드가 훨씬 더 나빠질 것이 분명했다.
그러므로 저자는 기능을 더 이상 추가하지 않고 리팩터링을 시작했다.
인수 유형에 해당하는 HashMap을 선택하기 위해 스키마 요소의 구문 분석하는 부분 추가했다.(최종 코드의 Schema 어쩌구 함수들)
명령행 인수에서 인수 유형을 분석해 진짜 유형으로 변환한다.
getXXX메서드를 구현해 호출자에게 진짜 유형을 반환한다. 인수 유형은 다양하지만 모두가 유사한 메서드를 제공하므로 클래스 하나가 적합하다 판단했다. 그래서 저자는 ArgumentMarshaler
라는 개념을 탄생시킨다.
(참고)
최종 코드를 읽어보면 ArgumentMarshaler
가 인터페이스라는 것을 알 수 있다. 최종 코드에는 이름만 등장하는(?) ArgumentMarshaler
에 대해 간단히 설명하자면... 새로운 인수 유형을 추가하기 위해 필요한데, ArgumentMarshaler
클래스에서 새 클래스를 파생해 getXXX 함수를 추가한 후 parseSchemaElement
함수에 새 case문을 추가하면 된다.
🔎점진적으로 개선하다.
개선이라는 이름 아래 구조를 크게 뒤집다가 프로그램을 망칠 수도 있다. 이렇게 되면 개선 전과 똑같이 프로그램을 돌리기 어려워진다.
그러므로 테스트 주도 개발(TDD) 기법을 사용하는 것이 마땅하다. 시스템이 개선 뒤에도 테스트케이스를 모두 통과한다면 올바르게 동작한다고 봐도 좋을 것이다.
그저 돌아가는 코드만으로는 부족하다. 초안을 짠 뒤, TDD 기법을 사용하여 조금씩 조금씩 개선해 깔끔하고 단순한 코드를 짤 수 있도록 하자.
코드 참고: https://han.gl/TghVb