第 14 章 逐步改进 (Successive Refinement)
总结 (Summary)
本章是关于代码重构和“逐步改进”的经典案例研究。作者 Robert C. Martin(Uncle Bob)通过一个命令行参数解析器(Args)的开发过程,展示了整洁代码并非一蹴而就,而是通过不断的迭代和精炼产生的。
核心思想包括:
- 先写脏代码,再清理:承认初稿通常是粗糙的,重点在于不要让其保留在粗糙状态。
- 增量式重构:利用测试驱动开发(TDD)保护代码,以极小的步伐修改结构,确保每一步系统都能正常运行。
- 分离关注点:将参数解析逻辑(
ArgumentMarshaler)与错误处理逻辑(ArgsException)从主类中分离,使代码高内聚、低耦合。
小计 (Key Takeaways)
- 重构策略:从单一的大类(God Class)逐步提取接口和实现类(
ArgumentMarshaler),利用多态替代复杂的if-else或switch语句。 - 保持代码运行:在重构过程中,通过单元测试和验收测试(FitNesse)确保现有功能不被破坏。
- 代码腐烂的代价:糟糕的代码会拖慢进度,而随时清理代码则是保持开发效率的关键。
- 最终成果:从一个充斥着成员变量和复杂逻辑的“烂摊子”,变成了一个结构清晰、易于扩展(如添加
Double类型)的模块。
Case Study of a Command-Line Argument Parser
命令行参数解析器案例研究

This chapter is a case study in successive refinement. You will see a module that started well but did not scale. Then you will see how the module was refactored and cleaned. 本章是一个关于逐步改进的案例研究。你将看到一个起初设计良好但未能顺利扩展的模块。随后,你将看到该模块是如何被重构和清理的。
Most of us have had to parse command-line arguments from time to time. If we don’t have a convenient utility, then we simply walk the array of strings that is passed into the main function. There are several good utilities available from various sources, but none of them do exactly what I want. So, of course, I decided to write my own. I call it: Args. 我们大多数人都曾时不时地需要解析命令行参数。如果没有顺手的工具,我们就只能简单地遍历传递给 main 函数的字符串数组。虽然有很多来源提供了不错的工具,但没有一个能完全满足我的需求。所以,理所当然地,我决定自己写一个。我称之为:Args。
Args is very simple to use. You simply construct the Args class with the input arguments and a format string, and then query the Args instance for the values of the arguments. Consider the following simple example: Args 用起来非常简单。你只需用输入参数和格式字符串构造 Args 类,然后查询 Args 实例以获取参数值。请看下面这个简单的例子:
Listing 14-1 Simple use of 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.printf(“Argument error: %s\n”, e.errorMessage());
}
}You can see how simple this is. We just create an instance of the Args class with two parameters. The first parameter is the format, or schema, string: “l,p#,d*.” It defines three command-line arguments. The first, -l, is a boolean argument. The second, -p, is an integer argument. The third, -d, is a string argument. The second parameter to the Args constructor is simply the array of command-line argument passed into main. 你可以看到这有多简单。我们只需用两个参数创建一个 Args 类的实例。第一个参数是格式(或模式)字符串:“l,p#,d*”。它定义了三个命令行参数。第一个,-l,是布尔参数。第二个,-p,是整数参数。第三个,-d,是字符串参数。Args 构造函数的第二个参数就是传入 main 函数的命令行参数数组。
If the constructor returns without throwing an ArgsException, then the incoming command-line was parsed, and the Args instance is ready to be queried. Methods like getBoolean, getInteger, and getString allow us to access the values of the arguments by their names. 如果构造函数返回时没有抛出 ArgsException,说明传入的命令行已被解析,Args 实例已准备好被查询。像 getBoolean、getInteger 和 getString 这样的方法允许我们通过参数名称来访问其值。
If there is a problem, either in the format string or in the command-line arguments themselves, an ArgsException will be thrown. A convenient description of what went wrong can be retrieved from the errorMessage method of the exception. 如果格式字符串或命令行参数本身有问题,就会抛出 ArgsException。可以通过异常的 errorMessage 方法获取关于错误原因的简便描述。
ARGS IMPLEMENTATION ARGS 的实现
Listing 14-2 is the implementation of the Args class. Please read it very carefully. I worked hard on the style and structure and hope it is worth emulating. 代码清单 14-2 是 Args 类的实现。请非常仔细地阅读。我在其风格和结构上下了很大功夫,希望它值得模仿。
Listing 14-2 Args.java 代码清单 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));
}
}Notice that you can read this code from the top to the bottom without a lot of jumping around or looking ahead. The one thing you may have had to look ahead for is the definition of ArgumentMarshaler, which I left out intentionally. Having read this code carefully, you should understand what the ArgumentMarshaler interface is and what its derivatives do. I’ll show a few of them to you now (Listing 14-3 through Listing 14-6). 注意,你可以从上到下阅读这段代码,而不需要来回跳转或提前查看。你唯一可能需要提前查看的是 ArgumentMarshaler 的定义,我是故意略去的。仔细阅读这段代码后,你应该能理解 ArgumentMarshaler 接口是什么以及其派生类是做什么的。我现在向你展示其中的几个(代码清单 14-3 到 14-6)。
Listing 14-3 ArgumentMarshaler.java 代码清单 14-3 ArgumentMarshaler.java
public interface ArgumentMarshaler {
void set(Iterator<String> currentArgument) throws ArgsException;
}Listing 14-4 BooleanArgumentMarshaler.java 代码清单 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;
}
}Listing 14-5 StringArgumentMarshaler.java 代码清单 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 ””;
}
}The other ArgumentMarshaler derivatives simply replicate this pattern for doubles and String arrays and would serve to clutter this chapter. I’ll leave them to you as an exercise. 其他的 ArgumentMarshaler 派生类只是简单地为 double 和 String 数组复制了这个模式,如果列出来会让本章显得杂乱。我把它们留给你作为练习。
One other bit of information might be troubling you: the definition of the error code constants. They are in the ArgsException class (Listing 14-7). 还有一点信息可能会让你感到困惑:错误代码常量的定义。它们在 ArgsException 类中(代码清单 14-7)。
Listing 14-6 IntegerArgumentMarshaler.java 代码清单 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;
}
}Listing 14-7 ArgsException.java 代码清单 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}
}It’s remarkable how much code is required to flesh out the details of this simple concept. One of the reasons for this is that we are using a particularly wordy language. Java, being a statically typed language, requires a lot of words in order to satisfy the type system. In a language like Ruby, Python, or Smalltalk, this program is much smaller.1 值得注意的是,为了充实这个简单概念的细节,竟然需要这么多代码。原因之一是我们使用了一种特别啰嗦的语言。Java 作为一种静态类型语言,需要很多词语来满足类型系统的要求。在 Ruby、Python 或 Smalltalk 这样的语言中,这个程序会小得多。1
- I recently rewrote this module in Ruby. It was 1/7th the size and had a subtly better structure.
- 我最近用 Ruby 重写了这个模块。它的大小只有原来的 1/7,而且结构稍微好一些。
Please read the code over one more time. Pay special attention to the way things are named, the size of the functions, and the formatting of the code. If you are an experienced programmer, you may have some quibbles here and there with various parts of the style or structure. Overall, however, I hope you conclude that this program is nicely written and has a clean structure. 请再读一遍代码。特别注意命名方式、函数的大小以及代码的格式。如果你是一位经验丰富的程序员,你可能对风格或结构的某些部分有些微词。但总的来说,我希望你能得出结论:这个程序写得很好,结构也很清晰。
For example, it should be obvious how you would add a new argument type, such as a date argument or a complex number argument, and that such an addition would require a trivial amount of effort. In short, it would simply require a new derivative of Argument-Marshaler, a new getXXX function, and a new case statement in the parseSchemaElement function. There would also probably be a new ArgsException.ErrorCode and a new error message. 例如,显而易见,如果你想添加一个新的参数类型,比如日期参数或复数参数,你会知道该怎么做,而且这只需极少的工作量。简而言之,它只需要一个新的 Argument-Marshaler 派生类、一个新的 getXXX 函数,以及在 parseSchemaElement 函数中添加一个新的 case 语句。可能还需要一个新的 ArgsException.ErrorCode 和一个新的错误消息。
How Did I Do This? 我是怎么做到的?
Let me set your mind at rest. I did not simply write this program from beginning to end in its current form. More importantly, I am not expecting you to be able to write clean and elegant programs in one pass. If we have learned anything over the last couple of decades, it is that programming is a craft more than it is a science. To write clean code, you must first write dirty code and then clean it. 让我先让你放心。我并不是从头到尾直接把这个程序写成现在这个样子的。更重要的是,我不指望你能一次性写出整洁优雅的程序。如果说在过去的几十年里我们学到了什么,那就是编程更像是一门手艺而不是科学。要写出整洁的代码,你必须先写出脏代码,然后再清理它。
This should not be a surprise to you. We learned this truth in grade school when our teachers tried (usually in vain) to get us to write rough drafts of our compositions. The process, they told us, was that we should write a rough draft, then a second draft, then several subsequent drafts until we had our final version. Writing clean compositions, they tried to tell us, is a matter of successive refinement. 这应该不会让你感到惊讶。我们在小学时就学到了这个真理,当时老师试图(通常是徒劳地)让我们写作文的草稿。他们告诉我们,过程应该是先写一个初稿,然后是第二稿,接着是后续的几稿,直到我们得到最终版本。他们试图告诉我们,写出整洁的作文是一个逐步改进(Successive Refinement)的过程。
Most freshman programmers (like most grade-schoolers) don’t follow this advice particularly well. They believe that the primary goal is to get the program working. Once it’s “working,” they move on to the next task, leaving the “working” program in whatever state they finally got it to “work.” Most seasoned programmers know that this is professional suicide. 大多数编程新手(像大多数小学生一样)并没有很好地遵循这个建议。他们认为主要目标是让程序运行起来。一旦它“能用了”,他们就转向下一个任务,把那个“能用了”的程序留在它最终“能用”时的状态。大多数经验丰富的程序员都知道,这无异于职业自杀。
ARGS: THE ROUGH DRAFT ARGS:初稿
Listing 14-8 shows an earlier version of the Args class. It “works.” And it’s messy. 代码清单 14-8 展示了 Args 类的早期版本。它“能用”。而且它很乱。
Listing 14-8 Args.java (first draft) 代码清单 14-8 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, 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 {
}
}I hope your initial reaction to this mass of code is “I’m certainly glad he didn’t leave it like that!” If you feel like this, then remember that’s how other people are going to feel about code that you leave in rough-draft form. 我希望你对这堆代码的最初反应是:“真庆幸他没把它留成那样!”如果你有这种感觉,那么请记住,这也是别人看到你留在初稿状态的代码时的感受。
Actually “rough draft” is probably the kindest thing you can say about this code. It’s clearly a work in progress. The sheer number of instance variables is daunting. The odd strings like “TILT,” the HashSets and TreeSets, and the try-catch-catch blocks all add up to a festering pile. 实际上,“初稿”可能是你能对这段代码给出的最客气的评价了。很明显这是一个未完成的工作。实例变量的数量令人望而生畏。像“TILT”这样奇怪的字符串,HashSet 和 TreeSet,以及 try-catch-catch 代码块,所有这些加在一起,构成了一个正在溃烂的垃圾堆。
I had not wanted to write a festering pile. Indeed, I was trying to keep things reasonably well organized. You can probably tell that from my choice of function and variable names and the fact that there is a crude structure to the program. But, clearly, I had let the problem get away from me. 我并不想写一个溃烂的垃圾堆。确实,我当时正试图保持事物的合理组织。你可能从我对函数和变量名的选择,以及程序中存在粗略结构这一事实看出来。但是,很明显,我让问题失控了。
The mess built gradually. Earlier versions had not been nearly so nasty. For example, Listing 14-9 shows an earlier version in which only Boolean arguments were working. 这种混乱是逐渐积累的。早期的版本并没有这么糟糕。例如,代码清单 14-9 展示了一个只有 Boolean 参数起作用的早期版本。
Listing 14-9 Args.java (Boolean only) 代码清单 14-9 Args.java(仅 Boolean)
package com.objectmentor.utilities.getopts;
import java.util.*;
public class Args {
private String schema;
private String[] args;
private boolean valid;
private Set<Character> unexpectedArguments = new TreeSet<Character>();
private Map<Character, Boolean> booleanArgs =
new HashMap<Character, Boolean>();
private int numberOfArguments = 0;
public Args(String schema, String[] args) {
this.schema = schema;
this.args = args;
valid = parse();
}
public boolean isValid() {
return valid;
}
private boolean parse() {
if (schema.length() == 0 && args.length == 0)
return true;
parseSchema();
parseArguments();
return unexpectedArguments.size() == 0;
}
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(String element) {
char c = element.charAt(0);
if (Character.isLetter(c)) {
booleanArgs.put(c, false);
}
}
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 isBoolean(char argChar) {
return booleanArgs.containsKey(argChar);
}
public int cardinality() {
return numberOfArguments;
}
public String usage() {
if (schema.length() > 0)
return ”-[“+schema+”]”; else
return ””;
}
public String errorMessage() {
if (unexpectedArguments.size() > 0) {
return unexpectedArgumentMessage();
} else
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) {
return booleanArgs.get(arg);
}
}Although you can find plenty to complain about in this code, it’s really not that bad. It’s compact and simple and easy to understand. However, within this code it is easy to see the seeds of the later festering pile. It’s quite clear how this grew into the latter mess. 虽然你可以在这段代码中找到很多抱怨的地方,但它实际上并没有那么糟糕。它紧凑、简单且易于理解。然而,在这段代码中很容易看到后来那个溃烂垃圾堆的种子。很明显它是如何演变成后来的混乱局面的。
Notice that the latter mess has only two more argument types than this: String and integer. The addition of just two more argument types had a massively negative impact on the code. It converted it from something that would have been reasonably maintainable into something that I would expect to become riddled with bugs and warts. 注意,后来的混乱版本只比这个多了两种参数类型:String 和 integer。仅仅增加这两种参数类型就对代码产生了巨大的负面影响。它将代码从一种本该合理维护的状态,变成了一种我预计会充满漏洞和缺陷的状态。
I added the two argument types incrementally. First, I added the String argument, which yielded this: 我是增量式地添加这两种参数类型的。首先,我添加了 String 参数,结果如下:
Listing 14-10 Args.java (Boolean and String) 代码清单 14-10 Args.java(Boolean 和 String)
package com.objectmentor.utilities.getopts;
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 Set<Character> argsFound = new HashSet<Character>();
private int currentArgument;
private char errorArgument = '\0';
enum ErrorCode {
OK, MISSING_STRING}
private ErrorCode errorCode = ErrorCode.OK;
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();
parseArguments();
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);
}
private void validateSchemaElementId(char elementId) throws ParseException {
if (!Character.isLetter(elementId)) {
throw new ParseException(
“Bad character:” + elementId + “in Args format: ” + schema, 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 void parseBooleanSchemaElement(char elementId) {
booleanArgs.put(elementId, false);
}
private boolean parseArguments() {
for (currentArgument = 0; currentArgument < args.length; currentArgument++)
{
String arg = args[currentArgument];
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 (setArgument(argChar))
argsFound.add(argChar);
else {
unexpectedArguments.add(argChar);
valid = false;
}
}
private boolean setArgument(char argChar) {
boolean set = true;
if (isBoolean(argChar))
setBooleanArg(argChar, true);
else if (isString(argChar))
setStringArg(argChar, “ ”);
else
set = false;
return set;
}
private void setStringArg(char argChar, String s) {
currentArgument++;
try {
stringArgs.put(argChar, args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgument = argChar;
errorCode = ErrorCode.MISSING_STRING;
}
}
private boolean isString(char argChar) {
return stringArgs.containsKey(argChar);
}
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.put(argChar, value);
}
private boolean isBoolean(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 {
if (unexpectedArguments.size() > 0) {
return unexpectedArgumentMessage();
} else
switch (errorCode) {
case MISSING_STRING:
return String.format(“Could not find string parameter for -%c.”, errorArgument);
case OK:
throw new Exception(“TILT: Should not get here.”);
}
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) {
return falseIfNull(booleanArgs.get(arg));
}
private boolean falseIfNull(Boolean b) {
return b == null ? false : b;
}
public String getString(char arg) {
return blankIfNull(stringArgs.get(arg));
}
private String blankIfNull(String s) {
return s == null ? “ ” : s;
}
public boolean has(char arg) {
return argsFound.contains(arg);
}
public boolean isValid() {
return valid;
}
}You can see that this is starting to get out of hand. It’s still not horrible, but the mess is certainly starting to grow. It’s a pile, but it’s not festering quite yet. It took the addition of the integer argument type to get this pile really fermenting and festering. 你可以看到这开始有点失控了。它还算不上糟糕透顶,但混乱肯定开始滋生了。它是个垃圾堆,但还没到溃烂的地步。正是加入了 integer 参数类型,才让这个垃圾堆真正开始发酵和溃烂。
So I Stopped 于是我停下来了
I had at least two more argument types to add, and I could tell that they would make things much worse. If I bulldozed my way forward, I could probably get them to work, but I’d leave behind a mess that was too large to fix. If the structure of this code was ever going to be maintainable, now was the time to fix it. 我至少还有两种参数类型要添加,我能预感到它们会让情况变得更糟。如果我硬着头皮继续做,我或许能让它们跑起来,但我会留下一个大到无法修复的烂摊子。如果希望这段代码的结构还能保持可维护性,现在就是修复它的时机。
So I stopped adding features and started refactoring. Having just added the String and integer arguments, I knew that each argument type required new code in three major places. First, each argument type required some way to parse its schema element in order to select the HashMap for that type. Next, each argument type needed to be parsed in the command-line strings and converted to its true type. Finally, each argument type needed a getXXX method so that it could be returned to the caller as its true type. 所以我停止添加新功能,开始重构。刚添加完 String 和 integer 参数,我知道每种参数类型都需要在三个主要地方添加新代码。首先,每种参数类型都需要某种方式来解析其模式元素,以便为该类型选择 HashMap。其次,每种参数类型都需要在命令行字符串中被解析并转换为其真实类型。最后,每种参数类型都需要一个 getXXX 方法,以便将其作为真实类型返回给调用者。
Many different types, all with similar methods—that sounds like a class to me. And so the ArgumentMarshaler concept was born. 许多不同的类型,都有相似的方法——这听起来像是一个类。于是 ArgumentMarshaler 的概念诞生了。
On Incrementalism 关于渐进主义
One of the best ways to ruin a program is to make massive changes to its structure in the name of improvement. Some programs never recover from such “improvements.” The problem is that it’s very hard to get the program working the same way it worked before the “improvement.” 毁掉一个程序的最好方法之一,就是打着改进的旗号对其结构进行大规模的修改。有些程序再也没能从这种“改进”中恢复过来。问题在于,很难让程序恢复到“改进”之前那样正常工作。
ARGS: THE ROUGH DRAFT ARGS:初稿
To avoid this, I use the discipline of Test-Driven Development (TDD). One of the central doctrines of this approach is to keep the system running at all times. In other words, using TDD, I am not allowed to make a change to the system that breaks that system. Every change I make must keep the system working as it worked before. 为了避免这种情况,我使用了测试驱动开发(TDD)的原则。这种方法的核心教条之一是让系统始终保持运行状态。换句话说,使用 TDD 时,我不允许对系统进行任何会破坏系统的修改。我所做的每一个修改都必须保持系统像以前一样正常工作。
To achieve this, I need a suite of automated tests that I can run on a whim and that verifies that the behavior of the system is unchanged. For the Args class I had created a suite of unit and acceptance tests while I was building the festering pile. The unit tests were written in Java and administered by JUnit. The acceptance tests were written as wiki pages in FitNesse. I could run these tests any time I wanted, and if they passed, I was confident that the system was working as I specified. 为了实现这一点,我需要一套可以随时运行的自动化测试,用来验证系统的行为没有改变。对于 Args 类,我在构建那个“溃烂的垃圾堆”时就已经创建了一套单元测试和验收测试。单元测试是用 Java 写的,由 JUnit 管理。验收测试是作为 wiki 页面写在 FitNesse 中的。我可以随时运行这些测试,如果它们通过了,我就确信系统正如我指定的那样工作。
So I proceeded to make a large number of very tiny changes. Each change moved the structure of the system toward the ArgumentMarshaler concept. And yet each change kept the system working. The first change I made was to add the skeleton of the ArgumentMarshaller to the end of the festering pile (Listing 14-11). 于是我开始进行大量的微小修改。每一个修改都将系统的结构向 ArgumentMarshaler 的概念推进。而且每一个修改都保持了系统正常运行。我做的第一个修改是将 ArgumentMarshaller 的骨架添加到那个垃圾堆的末尾(代码清单 14-11)。
Listing 14-11 ArgumentMarshaller appended to Args.java 代码清单 14-11 添加到 Args.java 的 ArgumentMarshaller
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 {
}
}Clearly, this wasn’t going to break anything. So then I made the simplest modification I could, one that would break as little as possible. I changed the HashMap for the Boolean arguments to take an ArgumentMarshaler. 很明显,这不会破坏任何东西。所以接下来我做了我能做的最简单的修改,也是破坏性最小的修改。我修改了 Boolean 参数的 HashMap,使其接受 ArgumentMarshaler。
private Map<Character, ArgumentMarshaler> booleanArgs =
new HashMap<Character, ArgumentMarshaler>();
This broke a few statements, which I quickly fixed.
这破坏了几条语句,我很快就修复了。
```java
…
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());
}Notice how these changes are in exactly the areas that I mentioned before: the parse, set, and get for the argument type. Unfortunately, small as this change was, some of the tests started failing. If you look carefully at getBoolean, you’ll see that if you call it with 'y,' but there is no y argument, then booleanArgs.get('y') will return null, and the function will throw a NullPointerException. The falseIfNull function had been used to protect against this, but the change I made caused that function to become irrelevant. 注意这些变化正是发生在我之前提到的区域:参数类型的解析(parse)、设置(set)和获取(get)。不幸的是,尽管这个变化很小,但一些测试开始失败了。如果你仔细看 getBoolean,你会发现如果你用 'y' 调用它,但没有 y 参数,那么 booleanArgs.get('y') 将返回 null,函数将抛出 NullPointerException。falseIfNull 函数原本是用来防止这种情况的,但我做的修改导致该函数变得无关紧要了。
Incrementalism demanded that I get this working quickly before making any other changes. Indeed, the fix was not too difficult. I just had to move the check for null. It was no longer the boolean being null that I needed to check; it was the ArgumentMarshaller. 渐进主义要求我在做任何其他修改之前迅速让它恢复工作。确实,修复并不难。我只需要移动 null 检查。我需要检查的不再是 boolean 是否为 null,而是 ArgumentMarshaller。
First, I removed the falseIfNull call in the getBoolean function. It was useless now, so I also eliminated the function itself. The tests still failed in the same way, so I was confident that I hadn’t introduced any new errors. 首先,我移除了 getBoolean 函数中的 falseIfNull 调用。它现在没用了,所以我把这个函数也删除了。测试依然以同样的方式失败,所以我确信我没有引入任何新错误。
public boolean getBoolean(char arg) {
return booleanArgs.get(arg).getBoolean();
}Next, I split the function into two lines and put the ArgumentMarshaller into its own variable named argumentMarshaller. I didn’t care for the long variable name; it was badly redundant and cluttered up the function. So I shortened it to am [N5]. 接下来,我将函数分成两行,并将 ArgumentMarshaller 放入名为 argumentMarshaller 的变量中。我不喜欢这个长变量名;它非常冗余且让函数显得杂乱。所以我把它缩短为 am [N5]。
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am.getBoolean();
}And then I put in the null detection logic. 然后我加入了 null 检测逻辑。
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && am.getBoolean();
}STRING ARGUMENTS STRING 参数
Addin_g String arguments was very similar to adding boolean arguments. I had to change the HashMap and get the parse, set, and get functions working. There shouldn’t be any surprises in what follows except, perhaps, that I seem to be putting all the marshalling implementation in the ArgumentMarshaller base class instead of distributing it to the derivatives. 添加 String 参数与添加 boolean 参数非常相似。我必须修改 HashMap 并让 parse、set 和 get 函数工作。接下来的内容应该没有什么意外,除了也许你会发现我似乎把所有的编组实现都放在了 ArgumentMarshaler 基类中,而不是分发到派生类中。
private Map<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
…
private void parseStringSchemaElement(char elementId) {
stringArgs.put(elementId, new StringArgumentMarshaler());
}
…
private void setStringArg(char argChar) throws ArgsException {
currentArgument++;
try {
stringArgs.get(argChar).setString(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
…
public String getString(char arg) {
Args.ArgumentMarshaler am = stringArgs.get(arg);
return am == null ? “ ” : am.getString();
}
…
private class ArgumentMarshaler {
private boolean booleanValue = false;
private String stringValue;
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;
}
}Again, these changes were made one at a time and in such a way that the tests kept running, if not passing. When a test broke, I made sure to get it passing again before continuing with the next change. 同样,这些修改是一次一个地进行的,并且尽量保持测试运行,即便不能全部通过。当一个测试中断时,我会确保在进行下一个修改之前让它重新通过。
By now you should be able to see my intent. Once I get all the current marshalling behavior into the ArgumentMarshaler base class, I’m going to start pushing that behavior down into the derivatives. This will allow me to keep everything running while I gradually change the shape of this program. 到现在你应该能看出我的意图了。一旦我把所有的编组行为都放入 ArgumentMarshaler 基类中,我就开始把这些行为下推到派生类中。这将允许我在逐步改变程序结构的同时保持一切运行正常。
The obvious next step was to move the int argument functionality into the ArgumentMarshaler. Again, there weren’t any surprises. 显而易见的下一步是将 int 参数的功能移动到 ArgumentMarshaler 中。同样,没有什么意外。
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
…
private void parseIntegerSchemaElement(char elementId) {
intArgs.put(elementId, new IntegerArgumentMarshaler());
}
…
private void setIntArg(char argChar) throws ArgsException {
currentArgument++;
String parameter = null;
try {
parameter = args[currentArgument];
intArgs.get(argChar).setInteger(Integer.parseInt(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();
}
}
…
public int getInt(char arg) {
Args.ArgumentMarshaler am = intArgs.get(arg);
return am == null ? 0 : am.getInteger();
}
…
private class ArgumentMarshaler {
private 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;
}
}With all the marshalling moved to the ArgumentMarshaler, I started pushing functionality into the derivatives. The first step was to move the setBoolean function into the BooleanArgumentMarshaller and make sure it got called correctly. So I created an abstract set method. 随着所有的编组逻辑都移到了 ArgumentMarshaler,我开始将功能推送到派生类中。第一步是将 setBoolean 函数移动到 BooleanArgumentMarshaller 中,并确保它被正确调用。所以我创建了一个抽象的 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);
}Then I implemented the set method in BooleanArgumentMarshaller. 然后我在 BooleanArgumentMarshaller 中实现了 set 方法。
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
public void set(String s) {
booleanValue = true;
}
}And finally I replaced the call to setBoolean with a call to set. 最后,我用调用 set 替换了对 setBoolean 的调用。
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.get(argChar) .set(“true”);
}The tests all still passed. Because this change caused set to be deployed to the Boolean-ArgumentMarshaler, I removed the setBoolean method from the ArgumentMarshaler base class. 所有的测试仍然通过。因为这个修改使得 set 被部署到了 BooleanArgumentMarshaler,所以我从 ArgumentMarshaler 基类中移除了 setBoolean 方法。
Notice that the abstract set function takes a String argument, but the implementation in the BooleanArgumentMarshaller does not use it. I put that argument in there because I knew that the StringArgumentMarshaller and IntegerArgumentMarshaller would use it. 注意,抽象 set 函数接受一个 String 参数,但 BooleanArgumentMarshaller 中的实现并没有使用它。我把它放在那是因我知道 StringArgumentMarshaller 和 IntegerArgumentMarshaller 会用到它。
Next, I wanted to deploy the get method into BooleanArgumentMarshaler. Deploying get functions is always ugly because the return type has to be Object, and in this case needs to be cast to a Boolean. 接下来,我想将 get 方法部署到 BooleanArgumentMarshaler 中。部署 get 函数总是很丑陋,因为返回类型必须是 Object,而在这种情况下需要强制转换为 Boolean。
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && (Boolean)am.get();
}Just to get this to compile, I added the get function to the ArgumentMarshaler. 为了能编译通过,我向 ArgumentMarshaler 添加了 get 函数。
private abstract class ArgumentMarshaler {
…
public Object get() {
return null;
}
}This compiled and obviously failed the tests. Getting the tests working again was simply a matter of making get abstract and implementing it in BooleanAgumentMarshaler. 这能编译通过,但显然测试失败了。让测试重新工作只需要将 get 设为抽象并在 BooleanAgumentMarshaler 中实现它。
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;
}
}Once again the tests passed. So both get and set deploy to the BooleanArgumentMarshaler! This allowed me to remove the old getBoolean function from ArgumentMarshaler, move the protected booleanValue variable down to BooleanArgumentMarshaler, and make it private. 测试再次通过。所以 get 和 set 都部署到了 BooleanArgumentMarshaler!这允许我从 ArgumentMarshaler 中移除旧的 getBoolean 函数,将受保护的 booleanValue 变量向下移动到 BooleanArgumentMarshaler,并将其设为私有。
I did the same pattern of changes for Strings. I deployed both set and get, deleted the unused functions, and moved the variables. 我对 Strings 做了同样的修改模式。我部署了 set 和 get,删除了未使用的函数,并移动了变量。
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();
}
}
…
public String getString(char arg) {
Args.ArgumentMarshaler am = stringArgs.get(arg);
return am == null ? “ ” : (String) am.get();
}
…
private abstract class ArgumentMarshaler {
private int integerValue;
public void setInteger(int i) {
integerValue = i;
}
public int getInteger() {
return integerValue;
}
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 {
public void set(String s) {
}
public Object get() {
return null;
}
}
}Finally, I repeated the process for integers. This was just a little more complicated because integers needed to be parsed, and the parse operation can throw an exception. But the result is better because the whole concept of NumberFormatException got buried in the IntegerArgumentMarshaler. 最后,我对 integers 重复了这个过程。这稍微复杂一点,因为整数需要解析,而解析操作可能会抛出异常。但结果更好了,因为整个 NumberFormatException 的概念都被埋在了 IntegerArgumentMarshaler 中。
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 e;
}
}
…
private void setBooleanArg(char argChar) {
try {
booleanArgs.get(argChar).set(“true”);
} catch (ArgsException e) {
}
}
…
public int getInt(char arg) {
Args.ArgumentMarshaler am = intArgs.get(arg);
return am == null ? 0 : (Integer) am.get();
}
…
private abstract class ArgumentMarshaler {
public abstract void set(String s) throws ArgsException;
public abstract Object get();
}
…
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;
}
}Of course, the tests continued to pass. Next, I got rid of the three different maps up at the top of the algorithm. This made the whole system much more generic. However, I couldn’t get rid of them just by deleting them because that would break the system. Instead, I added a new Map for the ArgumentMarshaler and then one by one changed the methods to use it instead of the three original maps. 当然,测试继续通过。接下来,我清理了算法顶部的三个不同的 map。这使整个系统更加通用。然而,我不能仅仅通过删除来摆脱它们,因为那会破坏系统。相反,我为 ArgumentMarshaler 添加了一个新的 Map,然后逐个修改方法来使用它,而不是原来的三个 map。
public class Args {
…
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 Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
…
private void parseBooleanSchemaElement(char elementId) {
ArgumentMarshaler m = new BooleanArgumentMarshaler();
booleanArgs.put(elementId, m);
marshalers.put(elementId, m);
}
private void parseIntegerSchemaElement(char elementId) {
ArgumentMarshaler m = new IntegerArgumentMarshaler();
intArgs.put(elementId, m);
marshalers.put(elementId, m);
}
private void parseStringSchemaElement(char elementId) {
ArgumentMarshaler m = new StringArgumentMarshaler();
stringArgs.put(elementId, m);
marshalers.put(elementId, m);
}Of course the tests all still passed. Next, I changed isBooleanArg from this: 测试当然依然通过。接下来,我将 isBooleanArg 从这样:
private boolean isBooleanArg(char argChar) {
return booleanArgs.containsKey(argChar);
}to this: 改成这样:
private boolean isBooleanArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof BooleanArgumentMarshaler;
}The tests still passed. So I made the same change to isIntArg and isStringArg. 测试依然通过。所以我对 isIntArg 和 isStringArg 做了同样的修改。
private boolean isIntArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof IntegerArgumentMarshaler;
}
private boolean isStringArg(char argChar) {
ArgumentMarshaler m = marshalers.get(argChar);
return m instanceof StringArgumentMarshaler;
}The tests still passed. So I eliminated all the duplicate calls to marshalers.get as follows: 测试依然通过。所以我消除了所有重复的 marshalers.get 调用,如下所示:
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (isBooleanArg(m))
setBooleanArg(argChar);
else if (isStringArg(m))
setStringArg(argChar);
else if (isIntArg(m))
setIntArg(argChar);
else
return false;
return true;
}
private boolean isIntArg(ArgumentMarshaler m) {
return m instanceof IntegerArgumentMarshaler;
}
private boolean isStringArg(ArgumentMarshaler m) {
return m instanceof StringArgumentMarshaler;
}
private boolean isBooleanArg(ArgumentMarshaler m) {
return m instanceof BooleanArgumentMarshaler;
}This left no good reason for the three isxxxArg methods. So I inlined them: 这使得三个 isxxxArg 方法没有存在的理由了。所以我将它们内联:
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 true;
}Next, I started using the marshalers map in the set functions, breaking the use of the other three maps. I started with the booleans. 接下来,我开始在 set 函数中使用 marshalers map,打破对其他三个 map 的使用。我从 boolean 开始。
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(argChar);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(argChar);
else
return false;
return true;
}
…
private void setBooleanArg(ArgumentMarshaler m) {
try {
m.set(“true”); // was: booleanArgs.get(argChar).set(“true”);
} catch (ArgsException e) {
}
}The tests still passed, so I did the same with Strings and Integers. This allowed me to integrate some of the ugly exception management code into the setArgument function. 测试依然通过,所以我对 Strings 和 Integers 做了同样的事。这允许我将一些丑陋的异常管理代码集成到 setArgument 函数中。
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
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;
}
}
private void setStringArg(ArgumentMarshaler m) throws ArgsException {
currentArgument++;
try {
m.set(args[currentArgument]);
} catch (ArrayIndexOutOfBoundsException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}I was close to being able to remove the three old maps. First, I needed to change the getBoolean function from this: 我快要可以移除那三个旧 map 了。首先,我需要将 getBoolean 函数从这样:
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg);
return am != null && (Boolean) am.get();
}to this: 改成这样:
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;
}This last change might have been a surprise. Why did I suddenly decide to deal with the ClassCastException? The reason is that I have a set of unit tests and a separate set of acceptance tests written in FitNesse. It turns out that the FitNesse tests made sure that if you called getBoolean on a nonboolean argument, you got a false. The unit tests did not. Up to this point I had only been running the unit tests.2 这最后一个修改可能会让人惊讶。为什么我突然决定处理 ClassCastException?原因是这我有一套单元测试和一套用 FitNesse 写的验收测试。事实证明,FitNesse 测试确保了如果你在非 boolean 参数上调用 getBoolean,你会得到 false。单元测试则没有。到目前为止,我只运行了单元测试。2
- To prevent further surprises of this kind, I added a new unit test that invoked all the FitNesse tests.
- 为了防止再发生这类意外,我添加了一个新的单元测试来调用所有的 FitNesse 测试。
This last change allowed me to pull out another use of the boolean map: 这最后一个修改允许我抽出 boolean map 的另一个用途:
private void parseBooleanSchemaElement(char elementId) {
ArgumentMarshaler m = new BooleanArgumentMarshaler();
booleanArgs.put(elementId, m);
marshalers.put(elementId, m);
}And now we can delete the boolean map. 现在我们可以删除 boolean map 了。
public class Args {
…
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 Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
…Next, I migrated the String and Integer arguments in the same manner and did a little cleanup with the booleans. 接下来,我以同样的方式迁移了 String 和 Integer 参数,并对 booleans 做了一些清理。
private void parseBooleanSchemaElement(char elementId) {
marshalers.put(elementId, new BooleanArgumentMarshaler());
}
private void parseIntegerSchemaElement(char elementId) {
marshalers.put(elementId, new IntegerArgumentMarshaler());
}
private void parseStringSchemaElement(char elementId) {
marshalers.put(elementId, new StringArgumentMarshaler());
}
…
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 (Exception e) {
return 0;
}
}
…
public class Args {
…
private Map<Character, ArgumentMarshaler> stringArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> intArgs =
new HashMap<Character, ArgumentMarshaler>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
…Next, I inlined the three parse methods because they didn’t do much anymore: 接下来,我内联了三个 parse 方法,因为它们已经没做什么事了:
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);
}
}Okay, so now let’s look at the whole picture again. Listing 14-12 shows the current form of the Args class. 好了,现在让我们再看一看全貌。代码清单 14-12 展示了 Args 类当前的形式。
Listing 14-12 Args.java (After first refactoring) 代码清单 14-12 Args.java(第一次重构后)
package com.objectmentor.utilities.getopts;
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> marshalers =
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))
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(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
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;
}
}
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 (Exception 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) throws ArgsException;
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;
}
}
}After all that work, this is a bit disappointing. The structure is a bit better, but we still have all those variables up at the top; there’s still a horrible type-case in setArgument; and all those set functions are really ugly. Not to mention all the error processing. We still have a lot of work ahead of us. 做了这么多工作后,结果有点令人失望。结构稍微好了一点,但顶部仍然有那么多变量;setArgument 中仍然有一个糟糕的类型检查(type-case);所有的 set 函数都很丑陋。更不用说所有的错误处理了。我们还有很多工作要做。
I’d really like to get rid of that type-case up in setArgument [G23]. What I’d like in setArgument is a single call to ArgumentMarshaler.set. This means I need to push setIntArg, setStringArg, and setBooleanArg down into the appropriate ArgumentMarshaler derivatives. But there is a problem. 我真的很想去掉 setArgument 中的那个类型检查 [G23]。我希望在 setArgument 中只调用一次 ArgumentMarshaler.set。这意味着我需要将 setIntArg、setStringArg 和 setBooleanArg 下推到相应的 ArgumentMarshaler 派生类中。但是有一个问题。
If you look closely at setIntArg, you’ll notice that it uses two instance variables: args and currentArg. To move setIntArg down into BooleanArgumentMarshaler, I’ll have to pass both args and currentArgs as function arguments. That’s dirty [F1]. I’d rather pass one argument instead of two. Fortunately, there is a simple solution. We can convert the args array into a list and pass an Iterator down to the set functions. The following took me ten steps, passing all the tests after each. But I’ll just show you the result. You should be able to figure out what most of the tiny little steps were. 如果你仔细观察 setIntArg,你会发现它使用了两个实例变量:args 和 currentArg。要将 setIntArg 下推到 BooleanArgumentMarshaler,我必须将 args 和 currentArgs 作为函数参数传递。这很脏 [F1]。我宁愿传一个参数而不是两个。幸运的是,有一个简单的解决方案。我们可以将 args 数组转换为列表,并将一个 Iterator 传递给 set 函数。接下来的过程我花了十步完成,每一步都通过了所有测试。但我只向你展示结果。你应该能推断出大部分微小的步骤是什么。
public class Args {
private String schema;
private String[] args;
private boolean valid = true;
private Set<Character> unexpectedArguments = new TreeSet<Character>();
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
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 = Arrays.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 parseArguments() throws ArgsException {
for (currentArgument = argsList.iterator(); currentArgument.hasNext();) {
String arg = currentArgument.next();
parseArgument(arg);
}
return true;
}
---
private void setIntArg(ArgumentMarshaler m) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
m.set(parameter);
} catch (NoSuchElementException e) {
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 {
try {
m.set(currentArgument.next());
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}These were simple changes that kept all the tests passing. Now we can start moving the set functions down into the appropriate derivatives. First, I need to make the following change in setArgument: 这都是些简单的修改,所有测试都保持通过。现在我们可以开始将 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(m);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
else
return false;
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}This change is important because we want to completely eliminate the if-else chain. Therefore, we needed to get the error condition out of it. 这个修改很重要,因为我们想彻底消除 if-else 链。因此,我们需要将错误条件从中移出。
Now we can start to move the set functions. The setBooleanArg function is trivial, so we’ll prepare that one first. Our goal is to change the setBooleanArg function to simply forward to the BooleanArgumentMarshaler. 现在我们可以开始移动 set 函数了。setBooleanArg 函数很简单,所以我们先准备这个。我们的目标是将 setBooleanArg 函数改为简单地转发给 BooleanArgumentMarshaler。
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(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
---
private void setBooleanArg(ArgumentMarshaler m,
Iterator<String> currentArgument)
throws ArgsException {
try {
m.set(”true”);
catch (ArgsException e) {
}
}Didn’t we just put that exception processing in? Putting things in so you can take them out again is pretty common in refactoring. The smallness of the steps and the need to keep the tests running means that you move things around a lot. Refactoring is a lot like solving a Rubik’s cube. There are lots of little steps required to achieve a large goal. Each step enables the next. 我们不是刚把那个异常处理放进去吗?为了以后能取出来而先把东西放进去,在重构中很常见。步骤的微小以及保持测试运行的需求意味着你要频繁地移动代码。重构很像解魔方。为了实现一个大目标需要很多小步骤。每一步都为下一步创造条件。
Why did we pass that iterator when setBooleanArg certainly doesn’t need it? Because setIntArg and setStringArg will! And because I want to deploy all three of these functions through an abstract method in ArgumentMarshaller, I need to pass it to setBooleanArg. 既然 setBooleanArg 肯定不需要 iterator,为什么我们还要传递它呢?因为 setIntArg 和 setStringArg 会用到!而且因为我想通过 ArgumentMarshaller 中的一个抽象方法来部署这三个函数,所以我必须把它传给 setBooleanArg。
So now setBooleanArg is useless. If there were a set function in ArgumentMarshaler, we could call it directly. So it’s time to make that function! The first step is to add the new abstract method to ArgumentMarshaler. 所以现在 setBooleanArg 没用了。如果 ArgumentMarshaler 中有一个 set 函数,我们就可以直接调用它。所以是时候创建那个函数了!第一步是向 ArgumentMarshaler 添加新的抽象方法。
private abstract class ArgumentMarshaler {
public abstract void set(Iterator<String> currentArgument)
throws ArgsException;
public abstract void set(String s) throws ArgsException;
public abstract Object get();
}Of course this breaks all the derivatives. So let’s implement the new method in each. 当然这破坏了所有派生类。所以让我们在每个类中实现这个新方法。
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
private boolean booleanValue = false;
public void set(Iterator<String> currentArgument) throws ArgsException {
booleanValue = true;
}
public void set(String s) {
booleanValue = true;
}
public Object get() {
return booleanValue;
}
}
private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = ””;
public void set(Iterator<String> currentArgument) throws ArgsException {
}
public void set(String s) {
stringValue = s;
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
}
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
} public Object get() {
return intValue;
}
}And now we can eliminate setBooleanArg! 现在我们可以消除 setBooleanArg 了!
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof StringArgumentMarshaler)
setStringArg(m);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(m);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}The tests all pass, and the set function is deploying to BooleanArgumentMarshaler! Now we can do the same for Strings and Integers. 所有测试都通过,set 函数已部署到 BooleanArgumentMarshaler!现在我们可以对 Strings 和 Integers 做同样的事。
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null)
return false;
try {
if (m instanceof BooleanArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof StringArgumentMarshaler)
m.set(currentArgument);
else if (m instanceof IntegerArgumentMarshaler)
m.set(currentArgument);
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
---
private class StringArgumentMarshaler extends ArgumentMarshaler {
private String stringValue = ””;
public void set(Iterator<String> currentArgument) throws ArgsException {
try {
stringValue = currentArgument.next();
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
public void set(String s) {
}
public Object get() {
return stringValue;
}
}
private class IntegerArgumentMarshaler extends ArgumentMarshaler {
private int intValue = 0;
public void set(Iterator<String> currentArgument) throws ArgsException {
String parameter = null;
try {
parameter = currentArgument.next();
set(parameter);
} catch (NoSuchElementException e) {
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
public void set(String s) throws ArgsException {
try {
intValue = Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}And so the coup de grace: The type-case can be removed! Touche! 于是致命一击来了:类型检查可以被移除了!漂亮!
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;
}
}Now we can get rid of some crufty functions in IntegerArgumentMarshaler and clean it up a bit. 现在我们可以去掉 IntegerArgumentMarshaler 中一些糟糕的函数并稍微清理一下。
private class IntegerArgumentMarshaler extends 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 Object get() {
return intValue;
}
}We can also turn ArgumentMarshaler into an interface. 我们也可以把 ArgumentMarshaler 变成一个接口。
private interface ArgumentMarshaler {
void set(Iterator<String> currentArgument) throws ArgsException;
Object get();
}So now let’s see how easy it is to add a new argument type to our structure. It should require very few changes, and those changes should be isolated. First, we begin by adding a new test case to check that the double argument works correctly. 现在让我们看看向我们的结构添加一个新的参数类型有多容易。它应该只需要很少的修改,而且这些修改应该是隔离的。首先,我们添加一个新的测试用例来检查 double 参数是否工作正常。
public void testSimpleDoublePresent() throws Exception {
Args args = new Args(”x##”, new String[] {”-x”,”42.3”});
assertTrue(args.isValid());
assertEquals(1, args.cardinality());
assertTrue(args.has(’x’));
assertEquals(42.3, args.getDouble(’x’), .001);
}Now we clean up the schema parsing code and add the ## detection for the double argument type. 现在我们清理模式解析代码,并为 double 参数类型添加 ## 检测。
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 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 ParseException(String.format(
”Argument: %c has invalid format: %s.”, elementId, elementTail), 0);
}Next, we write the DoubleArgumentMarshaler class. 接下来,我们编写 DoubleArgumentMarshaler 类。
private 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) {
errorCode = ErrorCode.MISSING_DOUBLE;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_DOUBLE;
throw new ArgsException();
}
}
public Object get() {
return doubleValue;
}
}This forces us to add a new ErrorCode. 这迫使我们添加一个新的 ErrorCode。
private enum ErrorCode {
OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT,
MISSING_DOUBLE, INVALID_DOUBLE}And we need a getDouble function. 我们需要一个 getDouble 函数。
public double getDouble(char arg) {
Args.ArgumentMarshaler am = marshalers.get(arg);
try {
return am == null ? 0 : (Double) am.get();
} catch (Exception e) {
return 0.0;
}
}And all the tests pass! That was pretty painless. So now let’s make sure all the error processing works correctly. The next test case checks that an error is declared if an unparseable string is fed to a ## argument. 所有测试都通过了!这真是毫不费力。现在让我们确保所有的错误处理都工作正常。下一个测试用例检查如果将无法解析的字符串传给 ## 参数,是否会声明错误。
public void testInvalidDouble() throws Exception {
Args args = new Args(”x##”, new String[] {”-x”,”Forty two”});
assertFalse(args.isValid());
assertEquals(0, args.cardinality());
assertFalse(args.has(’x’));
assertEquals(0, args.getInt(’x’));
assertEquals(”Argument -x expects a double but was ‘Forty two’.”,
args.errorMessage());
}
---
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);
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””;
}And the tests pass. The next test makes sure we detect a missing double argument properly. 测试通过。下一个测试确保我们能正确检测到缺失的 double 参数。
public void testMissingDouble() throws Exception {
Args args = new Args(”x##”, new String[]{”-x”});
assertFalse(args.isValid());
assertEquals(0, args.cardinality());
assertFalse(args.has(’x’));
assertEquals(0.0, args.getDouble(’x’), 0.01);
assertEquals(”Could not find double parameter for -x.”,
args.errorMessage());
}This passes as expected. We wrote it simply for completeness. 这如预期般通过。我们写这个只是为了完整性。
The exception code is pretty ugly and doesn’t really belong in the Args class. We are also throwing out ParseException, which doesn’t really belong to us. So let’s merge all the exceptions into a single ArgsException class and move it into its own module. 异常代码非常丑陋,其实并不属于 Args 类。我们还抛出了 ParseException,这也不真正属于我们。所以让我们把所有的异常合并到一个 ArgsException 类中,并将其移入自己的模块。
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 enum ErrorCode {
OK, MISSING_STRING, MISSING_INTEGER,
INVALID_INTEGER, UNEXPECTED_ARGUMENT,
MISSING_DOUBLE, INVALID_DOUBLE}
}
---
public class Args {
…
private char errorArgumentId = ’\0’;
private String errorParameter = ”TILT”;
private ArgsException.ErrorCode errorCode = ArgsException.ErrorCode.OK;
private List<String> argsList;
public Args(String schema, String[] args) throws ArgsException {
this.schema = schema;
argsList = Arrays.asList(args);
valid = parse();
}
private boolean parse() throws ArgsException {
if (schema.length() == 0 && argsList.size() == 0)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}
private boolean parseSchema() throws ArgsException {
…
}
private void parseSchemaElement(String element) throws ArgsException {
…
else
throw new ArgsException(
String.format(”Argument: %c has invalid format: %s.”,
elementId,elementTail));
}
private void validateSchemaElementId(char elementId) throws ArgsException {
if (!Character.isLetter(elementId)) {
throw new ArgsException(
”Bad character:” + elementId + ”in Args format: ” + schema);
}
}
…
private void parseElement(char argChar) throws ArgsException {
if (setArgument(argChar))
argsFound.add(argChar);
else {
unexpectedArguments.add(argChar);
errorCode = ArgsException.ErrorCode.UNEXPECTED_ARGUMENT;
valid = false;
}
}
…
private class StringArgumentMarshaler implements ArgumentMarshaler {
private String stringValue = ””;
public void set(Iterator<String> currentArgument) throws ArgsException {
try {
stringValue = currentArgument.next();
} catch (NoSuchElementException e) {
errorCode = ArgsException.ErrorCode.MISSING_STRING;
throw new ArgsException();
}
}
public Object get() {
return stringValue;
}
}
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 = ArgsException.ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ArgsException.ErrorCode.INVALID_INTEGER;
throw new ArgsException();
}
}
public Object get() {
return intValue;
}
}
private 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) {
errorCode = ArgsException.ErrorCode.MISSING_DOUBLE;
throw new ArgsException();
} catch (NumberFormatException e) {
errorParameter = parameter;
errorCode = ArgsException.ErrorCode.INVALID_DOUBLE;
throw new ArgsException();
}
}
public Object get() {
return doubleValue;
}
}
}This is nice. Now the only exception thrown by Args is ArgsException. Moving ArgsException into its own module means that we can move a lot of the miscellaneous error support code into that module and out of the Args module. It provides a natural and obvious place to put all that code and will really help us clean up the Args module going forward. 这很好。现在 Args 抛出的唯一异常就是 ArgsException。将 ArgsException 移入它自己的模块意味着我们可以将大量杂乱的错误支持代码移入该模块,从而移出 Args 模块。这为放置所有这些代码提供了一个自然且明显的地方,并且将真正有助于我们在未来清理 Args 模块。
So now we have completely separated the exception and error code from the Args module. (See Listing 14-13 through Listing 14-16.) This was achieved through a series of about 30 tiny steps, keeping the tests passing between each step. 现在我们已经完全将异常和错误代码从 Args 模块中分离出来了。(见代码清单 14-13 到 14-16。)这是通过一系列大约 30 个微小的步骤完成的,每一步之间都保持测试通过。
Listing 14-13 ArgsTest.java 代码清单 14-13 ArgsTest.java
package com.objectmentor.utilities.args;
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”});
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”});
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());
}
}
}Listing 14-14 ArgsExceptionTest.java 代码清单 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());
}
}Listing 14-15 ArgsException.java 代码清单 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}
}Listing 14-16 Args.java 代码清单 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);
}
}The majority of the changes to the Args class were deletions. A lot of code just got moved out of Args and put into ArgsException. Nice. We also moved all the ArgumentMarshaller s into their own files. Nicer! Args 类的大部分修改都是删除。大量代码只是从 Args 移出,放入了 ArgsException 中。不错。我们也把所有的 ArgumentMarshaller 移到了它们自己的文件中。更好!
Much of good software design is simply about partitioning—creating appropriate places to put different kinds of code. This separation of concerns makes the code much simpler to understand and maintain. 优秀的软件设计在很大程度上只是关于划分——为不同类型的代码创建合适的位置。这种关注点分离使代码更容易理解和维护。
Of special interest is the errorMessage method of ArgsException. Clearly it was a violation of the SRP to put the error message formatting into Args. Args should be about the processing of arguments, not about the format of the error messages. However, does it really make sense to put the error message formatting code into ArgsException? 特别值得关注的是 ArgsException 的 errorMessage 方法。显然,将错误消息格式化放入 Args 违反了单一职责原则(SRP)。Args 应该是关于参数处理的,而不是关于错误消息格式的。然而,将错误消息格式化代码放入 ArgsException 真的合理吗?
Frankly, it’s a compromise. Users who don’t like the error messages supplied by ArgsException will have to write their own. But the convenience of having canned error messages already prepared for you is not insignificant. 坦白说,这是一个折衷方案。不喜欢 ArgsException 提供的错误消息的用户将不得不自己编写。但是,拥有预先准备好的现成错误消息,其便利性是不容忽视的。
By now it should be clear that we are within striking distance of the final solution that appeared at the start of this chapter. I’ll leave the final transformations to you as an exercise. 到现在为止,我们显然已经非常接近本章开头出现的最终解决方案了。我把最后的转换留给你作为练习。
CONCLUSION 结论
It is not enough for code to work. Code that works is often badly broken. Programmers who satisfy themselves with merely working code are behaving unprofessionally. They may fear that they don’t have time to improve the structure and design of their code, but I disagree. Nothing has a more profound and long-term degrading effect upon a development project than bad code. Bad schedules can be redone, bad requirements can be redefined. Bad team dynamics can be repaired. But bad code rots and ferments, becoming an inexorable weight that drags the team down. Time and time again I have seen teams grind to a crawl because, in their haste, they created a malignant morass of code that forever thereafter dominated their destiny. 代码能运行是不够的。能运行的代码往往是严重破损的。满足于代码仅仅能运行的程序员,其行为是不专业的。他们可能担心没有时间改进代码的结构和设计,但我不同意。没有什么比糟糕的代码对开发项目有更深远、更长期的破坏性影响了。糟糕的进度可以重做,糟糕的需求可以重定义。糟糕的团队动态可以修复。但糟糕的代码会腐烂发酵,成为拖累团队的无情重负。我不止一次看到团队陷入爬行般的缓慢进度,只因在匆忙中制造了恶性的代码泥潭,从此主宰了他们的命运。
Of course bad code can be cleaned up. But it’s very expensive. As code rots, the modules insinuate themselves into each other, creating lots of hidden and tangled dependencies. Finding and breaking old dependencies is a long and arduous task. On the other hand, keeping code clean is relatively easy. If you made a mess in a module in the morning, it is easy to clean it up in the afternoon. Better yet, if you made a mess five minutes ago, it’s very easy to clean it up right now. 当然,糟糕的代码是可以清理的。但这非常昂贵。随着代码腐烂,模块之间相互渗透,产生大量隐藏和纠缠的依赖关系。寻找并打破旧的依赖是一项漫长而艰巨的任务。另一方面,保持代码整洁相对容易。如果你早上在一个模块里弄乱了,下午很容易就能清理干净。更好的是,如果你五分钟前弄乱了,现在清理起来非常容易。
So the solution is to continuously keep your code as clean and simple as it can be. Never let the rot get started. 所以解决方案是持续保持代码尽可能整洁简单。永远不要让腐烂开始。