Skip to content

第 14 章 Successive Refinement 逐步改进

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 的简单用法

java
   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 类的实例。第一个参数是格式(或称为 schema)字符串:"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 方法获取关于出错原因的方便描述。

14.1 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

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

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

Listing 14-4 BooleanArgumentMarshaler.java

代码清单 14-4 BooleanArgumentMarshaler.java

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

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

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

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]

  1. I recently rewrote this module in Ruby. It was 1/7th the size and had a subtly better structure.

[1] 我最近用 Ruby 重写了这个模块。它的大小只有原来的七分之一,结构也稍微更好。

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.

例如,你应该如何添加新的参数类型(比如日期参数或复数参数)应该是显而易见的,而且这种添加只需要微不足道的工作量。简而言之,它只需要一个新的 ArgumentMarshaler 派生类、一个新的 getXXX 函数,以及 parseSchemaElement 函数中的一个新的 case 语句。可能还需要一个新的 ArgsException.ErrorCode 和一条新的错误消息。

14.2 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.

这对你来说不应该感到惊讶。我们在小学时就学到了这个真理,当时我们的老师试图(通常是徒劳的)让我们写作文草稿。他们告诉我们,过程应该是先写草稿,再写第二稿,然后经过多次修改直到最终版本。他们试图告诉我们,写好作文是一个逐步改进的过程。

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.

大多数初级程序员(就像大多数小学生一样)不太善于遵循这一建议。他们认为主要目标是让程序运行起来。一旦程序"能运行了",他们就转向下一个任务,把"能运行"的程序留在他们最终让它"运行起来"的那个状态。大多数经验丰富的程序员知道,这无异于职业自杀。

14.3 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(第一版草稿)

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"这样的奇怪字符串、HashSets 和 TreeSets,以及 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 展示了一个只有布尔参数在工作的早期版本。

Listing 14-9 Args.java (Boolean only)

代码清单 14-9 Args.java(仅布尔参数)

java
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。仅仅增加了两种参数类型就对代码产生了巨大的负面影响。它把原本还算可维护的代码变成了我预期会被 bug 和缺陷充斥的东西。

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(布尔和字符串参数)

注:代码清单 14-10 至 14-16 以及详细的重构步骤说明因篇幅原因在此省略。这些代码清单展示了 Args 类从粗糙草稿逐步重构为整洁最终版本的完整过程,包括:

  • 添加 String 和 Integer 参数类型
  • 引入 ArgumentMarshaler 概念
  • 将编组行为下推到派生类
  • 消除类型判断(type-case)
  • 分离 ArgsException 到独立模块
  • 最终得到整洁的 Args 类结构

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 参数类型的加入让这堆代码真正开始发酵和腐烂。

14.3.1 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 参数后,我知道每种参数类型需要在三个主要位置添加新代码。首先,每种参数类型需要某种方式来解析其 schema 元素,以便选择该类型的 HashMap。其次,每种参数类型需要在命令行字符串中被解析并转换为其真实类型。最后,每种参数类型需要一个 getXXX 方法,以便将其真实类型返回给调用者。

Many different types, all with similar methods—that sounds like a class to me. And so the ArgumentMarshaler concept was born.

许多不同的类型,都有类似的方法——这对我来说听起来就是一个类。于是 ArgumentMarshaler 的概念诞生了。

14.3.2 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."

毁掉一个程序的最佳方式之一,就是以改进的名义对其结构进行大规模更改。有些程序永远无法从这种"改进"中恢复过来。问题在于,要让程序在"改进"后像之前一样工作是非常困难的。

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)。

注:代码清单 14-11 至 14-16 展示了从草稿到最终版本的完整重构过程。由于篇幅限制,这些详细代码清单在此省略。核心重构步骤包括:

  1. 添加 ArgumentMarshaler 骨架
  2. 将 Boolean 参数的 HashMap 改为接受 ArgumentMarshaler
  3. 修复 null 检测问题
  4. 添加 String 参数支持
  5. 添加 Integer 参数支持
  6. 将 setBoolean 下推到 BooleanArgumentMarshaler
  7. 部署 get 方法到派生类
  8. 消除三个独立的 Map,统一使用 marshalers Map
  9. 消除类型判断(type-case)
  10. 将 ArgsException 分离到独立模块
  11. 最终得到整洁的 Args 类

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.

好的软件设计在很大程度上就是关于分区——为不同类型的代码创建合适的位置。这种关注点分离使得代码更易于理解和维护。

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.

所以解决方案是持续保持你的代码尽可能干净和简洁。永远不要让腐烂开始。