Книга: Чистый код: создание, анализ и рефакторинг. Библиотека программиста
Назад: 13. Многопоточность
Дальше: 15. Внутреннее строение JUnit

14. Последовательное очищение

Дело о разборе аргументов командной строки

14_01.tif 

В этой главе представлен вполне реальный сценарий последовательного очищения кода. Мы рассмотрим модуль, который внешне смотрелся вполне достойно, но плохо масштабировался. Вы увидите, как происходила переработка и очистка этого модуля.

Многим из нас время от времени приходится заниматься разбором аргументов командной строки. Если под рукой не окажется удобного инструмента, мы просто перебираем элементы массива строк, переданного функции main. Я знал немало хороших инструментов из разных источников, однако ни один из них не делал именно того, что мне было нужно. Разумеется, я решил написать собственную реализацию — назовем ее Args.

Класс Args очень прост в использовании. Вы конструируете экземпляр класса 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());

  }

}

Вы и сами видите, что все действительно просто. Мы создаем экземпляр класса Args с двумя параметрами. Первый параметр задает форматную строку: "l,p#,d*.". Эта строка определяет три аргумента командной строки. Первый аргумент, –l, относится к логическому (булевскому) типу. Второй аргумент, -p, относится к целочисленному типу. Третий аргумент, -d, является строковым. Во втором параметре конструктора Args содержится массив аргументов командной строки, полученный main.

Если конструктор возвращает управление без выдачи исключения ArgsException, значит, разбор входной командной строки прошел успешно, и экземпляр Args готов к приему запросов. Методы getBoolean, getInteger, getString и т.д. используются для получения значений аргументов по именам.

При возникновении проблем (в форматной строке или в самих аргументах командной строки) инициируется исключение ArgsException. Для получения текстового описания проблемы следует вызвать метод errorMessage объекта исключения.

Реализация Args

Реализация класса Args приведена в листинге 14.2. Пожалуйста, очень внимательно прочитайте ее. Я основательно потрудился над стилем и структурой кода и надеюсь, что вы сочтете его достойным образцом для подражания.

Листинг 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();

Листинг 14.2 (продолжение)

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

  }

}

Обратите внимание: код читается сверху вниз, и вам не приходится постоянно переходить туда-сюда или заглядывать вперед. Единственное место, где все же необходимо заглянуть вперед, — это определение ArgumentMarshaler, но и это было сделано намеренно. Внимательно прочитав этот код, вы поймете, что собой представляет интерфейс ArgumentMarshaler и что делают производные классы. Примеры таких классов приведены в листингах 14.3–14.6.

Листинг 14.3. ArgumentMarshaler.java

public interface ArgumentMarshaler {

  void set(Iterator<String> currentArgument) throws ArgsException;

}

Листинг 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;

  }

}

Листинг 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)

Листинг 14.5. (продолжение)

      return ((StringArgumentMarshaler) am).stringValue;

    else

      return "";

  }

}

Листинг 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;

  }

}

Другие классы, производные от ArgumentMarshaler, строятся по тому же шаблону, что и классы для массивов double и String. Здесь они не приводятся для экономии места. Оставляю их вам для самостоятельной работы.

Возможно, вы заметили еще одно обстоятельство: где определяются константы для кодов ошибок? Они находятся в классе ArgsException (листинг 14.7).

Листинг 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);

Листинг 14.7 (продолжение)

      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}

}

Удивительно, какой объем кода понадобился для воплощения всех подробностей этой простой концепции. Одна из причин заключается в том, что мы используем весьма «многословный» язык. Поскольку Java относится к числу языков со статической типизацией, для удовлетворения требований системы типов в нем используется немалый объем кода. На таких языках, как Ruby, Python или Smalltalk, программа получится гораздо короче.

Пожалуйста, перечитайте код еще раз. Обратите особое внимание на выбор имен, размеры функций и форматирование кода. Возможно, опытные программисты найдут отдельные недочеты в стиле или структуре кода. Но я надеюсь, что в целом вы согласитесь с тем, что код хорошо написан, а его структура чиста и логична.

Скажем, после чтения кода вам должно быть очевидно, как добавить поддержку нового типа аргументов (например, дат или комплексных чисел), и это по­требует относительно небольших усилий с вашей стороны. Для этого достаточно ­создать новый класс, производный от ArgumentMarshaler, новую функцию getXXX и включить новое условие case в функцию parseSchemaElement. Вероятно, также потребуется новое значение ArgsException.ErrorCode и новое сообщение об ошибке.

Как я это сделал?

Позвольте вас успокоить: я не написал эту программу от начала до конца в ее текущем виде. Более того, я не ожидаю, что вы сможете писать чистые и элегантные программы за один проход.  Если мы чему-то и научились за последнюю пару десятилетий, так это тому, что программирование ближе к ремеслу, чем к науке. Чтобы написать чистый код, мы сначала пишем грязный код, а затем очищаем его.

Вряд ли вас это удивит. Мы усвоили эту истину еще в начальной школе, когда учителя заставляли нас (обычно безуспешно) писать планы сочинений. Предполагалось, что мы должны сначала написать первый вариант плана, затем второй, потом еще несколько версий, пока не придем к окончательной версии. Они пытались объяснить нам, что четкое и ясное сочинение появляется в результате последовательного усовершенствования.

Многие начинающие программисты (впрочем, как и большинство школьников, пишущих сочинения) не слишком усердно следуют этому совету. Они считают, что их главная цель — заставить программу работать. Когда программа «заработает», они переходят к следующей задаче, оставляя «работающую» программу в том состоянии, в котором она «заработала». Опытные программисты знают, что с профессиональной точки зрения такой подход равносилен самоубийству.

Args: черновик

В листинге 14.8 приведена более ранняя версия класса Args. Она «работает». И при этом выглядит крайне неряшливо.

Листинг 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}

Листинг 14.8 (продолжение)

  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;

    }

  }

Листинг 14.8 (продолжение)

  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;

  }

Листинг 14.8 (продолжение)

  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 {

  }

}

Надеюсь, при виде этой глыбы кода вам захотелось сказать: «Как хорошо, что она не осталась в таком виде!» Если вы почувствовали нечто подобное, вспомните, что другие люди чувствуют то же самое при виде вашего кода, оставшегося на стадии «черновика».

Вообще говоря, «черновик» — самое мягкое, что можно сказать об этом коде. Очевидно, что перед нами незавершенная работа. От одного количества переменных экземпляров можно прийти в ужас. Загадочные строки вроде "TILT, контейнеры HashSet и TreeSet, конструкции try-catch-catch только увеличивают масштабы этого беспорядочного месива.

Я вовсе не собирался писать беспорядочное месиво. В самом деле, я постарался сохранить более или менее разумную организацию кода. Об этом свидетельствует хотя бы выбор имен функций и переменных, а также наличие у программы примитивной структуры. Но совершенно очевидно, что проблемы вышли из-под моего контроля.

Неразбериха накапливалась постепенно. Ранние версии выглядели вовсе не так отвратительно. Для примера в листинге 14.9 приведена начальная версия, поддерживающая только логические аргументы.

Листинг 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);

    }

  }

Листинг 14.9 (продолжение)

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

  }

}

В этом коде можно найти множество недостатков, однако в целом он не так уж плох. Код компактен и прост, в нем легко разобраться. Тем не менее в этом коде легко прослеживаются зачатки будущего беспорядочного месива. Нетрудно понять, как из него выросла вся последующая неразбериха.

Обратите внимание: в последующей неразберихе добавились всего два новых типа аргументов, String и integer. Добавление всего двух типов аргументов имело огромные отрицательные последствия для кода. Более или менее понятный код превратился в запутанный клубок, наверняка кишащий множеством ошибок и недочетов.

Два новых типа аргументов добавлялись последовательно. Сначала я добавил поддержку 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 =

Листинг 14.10 (продолжение)

    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

Листинг 14.10 (продолжение)

      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;

  }

}

Ситуация явно выходит из-под контроля. Код все еще не ужасен, но путаница очевидно растет. Это уже клубок, хотя и не беспорядочное месиво. А чтобы месиво забродило и стало подниматься, хватило простого добавления целочисленных аргументов.

На этом я остановился

Мне предстояло добавить еще два типа аргументов. Было совершенно очевидно, что с ними все станет намного хуже. Если бы я с упорством бульдозера пошел вперед, скорее всего, мне удалось бы заставить программу работать, но разобраться в получившемся коде не удалось бы уже никому. Если я хотел, чтобы с моим кодом можно было работать, спасать положение нужно было именно сейчас.

Итак, я прекратил добавлять в программу новые возможности и взялся за переработку. После добавления типов String и integer я знал, что для каждого типа аргументов новый код должен добавляться в трех основных местах. Во-первых, для каждого типа аргументов необходимо было обеспечить разбор соответствующего элемента форматной строки, чтобы выбрать объект HashMap для этого типа. Затем аргумент соответствующего типа необходимо было разобрать в командной строке и преобразовать к истинному типу. Наконец, для каждого типа аргументов требовался метод getXXX, возвращающий значение аргумента с его истинным типом.

Много разных типов, обладающих сходными методами… Наводит на мысли о классе. Так родилась концепция ArgumentMarshaler.

О постепенном усовершенствовании

Один из верных способов убить программу — вносить глобальные изменения в ее структуру с целью улучшения. Некоторые программы уже никогда не приходят в себя после таких «усовершенствований». Проблема в том, что код очень трудно заставить работать так же, как он работал до «усовершенствования».

Чтобы этого не произошло, я воспользовался методологией разработки через тестирование (TDD). Одна из центральных доктрин этой методологии гласит, что система должна работать в любой момент в процессе внесения изменений. Иначе говоря, при использовании TDD запрещено вносить в систему изменения, нарушающие работоспособность этой системы. С каждым вносимым изменением система должна работать так же, как она работала прежде.

Для этого был необходим пакет автоматизированных тестов. Запуская их в любой момент времени, я мог бы убедиться в том, что поведение системы осталось неизменным. Я уже создал пакет модульных и приемочных тестов для класса Args, пока работал над начальной версией (она же «беспорядочное месиво»). Модульные тесты были написаны на Java и находились под управлением JUnit. Приемочные тесты были оформлены в виде вики-страниц в FitNesse. Я мог запустить эти тесты в любой момент по своему усмотрению, и если они проходили — можно было не сомневаться в том, что система работает именно так, как положено.

И тогда я занялся внесением множества очень маленьких изменений. Каждое изменение продвигало структуру системы к концепции ArgumentMarshaler, но после каждого изменения система продолжала нормально работать. На первом этапе я добавил заготовку ArgumentMarshaller в конец месива (листинг 14.11).

Листинг 14.11. Класс ArgumentMarshaller, присоединенный к Args.java

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 {

  }

}

Понятно, что добавление класса ничего не нарушит. Поэтому я внес самое простейшее из всех возможных изменений — изменил контейнер HashMap для логических аргументов так, чтобы при конструировании передавался тип ArgumentMarshaler:

private Map<Character, ArgumentMarshaler> booleanArgs =

    new HashMap<Character, ArgumentMarshaler>();

Это нарушило работу нескольких команд, которые я быстро исправил.

...

private void parseBooleanSchemaElement(char elementId) {

  booleanArgs.put(elementId, new BooleanArgumentMarshaler());

}

..

private void setBooleanArg(char argChar, boolean value) {

  booleanArgs.get(argChar).setBoolean(value);

}

...

public boolean getBoolean(char arg) {

  return falseIfNull(booleanArgs.get(arg).getBoolean());

}

Изменения вносятся в тех местах, о которых я упоминал ранее: методы parse, set и get для типа аргумента. К сожалению, при всей незначительности изменений некоторые тесты стали завершаться неудачей. Внимательно присмотревшись к getBoolean, вы увидите, что если при вызове метода с 'y' аргумента y не существует, вызов booleanArgs.get(‘y’) вернет null, а функция выдаст исключение NullPointerException. Функция falseIfNull защищала от подобных ситуаций, но в результате внесенных изменений она перестала работать.

Стратегия постепенных изменений требовала, чтобы я немедленно наладил ра­боту программы, прежде чем вносить какие-либо дополнительные изменения. Действительно, проблема решалась просто: нужно было добавить проверку null. Но на этот раз проверять нужно было не логическое значение, а ArgumentMarshaller.

Сначала я убрал вызов falseIfNull из getBoolean. Функция falseIfNull стала бесполезной, поэтому я убрал и саму функцию. Тесты все равно не проходили, поэтому я был уверен, что новых ошибок от этого уже не прибавится.

public boolean getBoolean(char arg) {

    return booleanArgs.get(arg).getBoolean();

  }

Затем я разбил функцию getBoolean надвое и разместил ArgumentMarshaller в собственной переменной с именем argumentMarshaller. Длинное имя мне не понравилось; во-первых, оно было избыточным, а во-вторых, загромождало функцию. Соответственно я сократил его до am [N5].

public boolean getBoolean(char arg) {

  Args.ArgumentMarshaler am = booleanArgs.get(arg);

  return am.getBoolean();

}

Наконец, я добавил логику проверки null:

public boolean getBoolean(char arg) {

  Args.ArgumentMarshaler am = booleanArgs.get(arg);

  return am != null && am.getBoolean();

}

Аргументы String

Добавление поддержки String было очень похоже на добавление поддержки Boolean. Мне предстояло изменить HashMap и заставить работать функции parse, set и get. Полагаю, следующий код понятен без пояснений — если не считать того, что я разместил всю реализацию компоновки аргументов в базовом клссе ArgumentMarshaller, вместо того чтобы распределять ее по производным классам.

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;

  }

}

И снова изменения вносились последовательно и только так, чтобы тесты по крайней мере хотя бы запускались (даже если и не проходили). Если работоспособность теста была нарушена, я сначала добивался того, чтобы он работал, и только потом переходил к следующему изменению.

Вероятно, вы уже поняли, что я собираюсь сделать. Собрав все текущее поведение компоновки аргументов в базовом классе ArgumentMarshaler, я намерен перемещать его вниз в производные классы. Это позволит мне сохранить работоспособность программы в ходе постепенного изменения ее структуры.

Очевидным следующим шагом стало перемещение функциональности аргумента 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;

  }

}

Переместив всю логику компоновки аргументов в 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);

}

Затем метод set был реализован в BooleanArgumentMarshaller.

private class BooleanArgumentMarshaler extends ArgumentMarshaler {

  public void set(String s) {

    booleanValue = true;

  }

}

Наконец, вызов setBoolean был заменен вызовом set.

private void setBooleanArg(char argChar, boolean value) {

  booleanArgs.get(argChar).set("true");

}

Все тесты прошли успешно. Так как изменения привели к перемещению set в BooleanArgumentMarshaler, я удалил метод setBoolean из базового класса ArgumentMarshaler.

Обратите внимание: абстрактная функция set получает аргумент String, но реализация в классе BooleanArgumentMarshaller его не использует. Я добавил этот аргумент, потому что знал, что он будет использоваться классами StringArgumentMarshaller и IntegerArgumentMarshaller.

На следующем шаге я решил разместить метод get в BooleanArgumentMarshaler. Подобные размещения get всегда выглядят уродливо, потому что фактически возвращается тип Object, который в данном случае приходится преобразовывать в Boolean.

public boolean getBoolean(char arg) {

  Args.ArgumentMarshaler am = booleanArgs.get(arg);

  return am != null && (Boolean)am.get();

}

Просто для того, чтобы программа компилировалась, я добавил в ArgumentMarshaler функцию get.

private abstract class ArgumentMarshaler {

   ...

  public Object get() {

    return null;

  }

}

Программа компилировалась, а тесты, разумеется, не проходили. Чтобы тесты снова заработали, достаточно объявить метод 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;

    }

  }

Итак, тесты снова проходят успешно. Теперь оба метода get и set размещаются в BooleanArgumentMarshaler! Это позволило мне удалить старую функцию getBoolean из ArgumentMarshaler, переместить защищенную переменную booleanValue в BooleanArgumentMarshaler и объявить ее приватной.

Аналогичные изменения были внесены для типа String. Я реализовал методы 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;

    }

  }

}

Осталось лишь повторить этот процесс для integer. На этот раз задача немного усложняется: целые числа необходимо разбирать, а в ходе разбора возможны исключения. Но внешний вид кода улучшается тем, что вся концепция 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;

    }

  }

Конечно, тесты по-прежнему проходили. Далее я избавился от трех разновидностей Map в начале алгоритма, отчего система стала намного более универсальной. Впрочем, я не мог их просто удалить, поскольку это нарушило бы работу системы. Вместо этого я добавил новый объект Map для ArgumentMarshaler, а затем последовательно изменял методы, чтобы они использовали этот объект вместо трех исходных.

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

  }

Разумеется, тесты проходили успешно. Далее я привел метод isBooleanArg:

private boolean isBooleanArg(char argChar) {

  return booleanArgs.containsKey(argChar);

}

к следующему виду:

private boolean isBooleanArg(char argChar) {

  ArgumentMarshaler m = marshalers.get(argChar);

  return m instanceof BooleanArgumentMarshaler;

}

Тесты по-прежнему проходят. Я внес аналогичные изменения в 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;

}

Тесты проходят. Я удалил все повторяющиеся вызовы 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;

}

Причин для существования трех методов 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;

}

На следующем шаге я перешел на использование ассоциативного массива marshalers в функциях set, отказываясь от использования трех старых контейнеров. Преобразование началось с 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”); // было: booleanArgs.get(argChar).set(«true»);

  } catch (ArgsException e) {

  }

}

Тесты проходили успешно, и я сделал то же самое для типов String и Integer. Это позволило мне интегрировать часть некрасивого кода обработки исключений в функцию 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();

  }

}

Я вплотную подошел к удалению трех старых объектов Map. Прежде всего было необходимо привести функцию getBoolean:

public boolean getBoolean(char arg) {

  Args.ArgumentMarshaler am = booleanArgs.get(arg);

  return am != null && (Boolean) am.get();

}

к следующему виду:

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;

}

Возможно, последнее изменение вас удивило. Почему я вдруг решил обрабатывать ClassCastException? Дело в том, что наряду с набором модульных тестов у меня был отдельный набор приемочных тестов, написанных для FitNesse. Оказалось, что тесты FitNesse проверяли, что при вызове getBoolean для аргумента с типом, отличным от Boolean, возвращается false.  Модульные тесты этого не делали. До этого момента я запускал только модульные тесты.

Последнее изменение позволило исключить еще одну точку использования объекта Map для типа Boolean:

private void parseBooleanSchemaElement(char elementId) {

  ArgumentMarshaler m = new BooleanArgumentMarshaler();

  booleanArgs.put(elementId, m);

  marshalers.put(elementId, m);

}

Теперь объект Map для типа Boolean можно было удалить:

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>();

...

Далее я проделал аналогичную процедуру для аргументов String и Integer и немного подчистил код:

  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 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>();

...

Затем я подставил в parseSchemaElement код трех методов 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);

  }

}

Давайте взглянем на общую картину. В листинге 14.12 представлена текущая форма класса Args.

Листинг 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("#");

  }

Листинг 14.12 (продолжение)

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

Листинг 14.12 (продолжение)

      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;

    }

  }

}

Вроде бы проделана большая работа, а результат не впечатляет. Структура кода немного улучшилась, но в начале листинга по-прежнему объявляются многочисленные переменные; в setArgument осталась кошмарная конструкция проверки типа; функции set выглядят просто ужасно. Я уже не говорю об обработке ошибок… Нам еще предстоит большая работа.

Прежде всего хотелось бы избавиться от конструкции выбора в setArgument [G23]. В идеале она должна быть заменена единственным вызовом ArgumentMarshaler.set. Это означает, что код setIntArg, setStringArg и setBooleanArg должен быть перемещен в соответствующие классы, производные от ArgumentMarshaler. Однако при этом возникает одна проблема.

Внимательно присмотревшись к функции setIntArg, можно заметить, что в ней используются две переменные экземпляров: args и currentArg. Чтобы переместить setIntArg в BooleanArgumentMarshaler, мне придется передать args и currentArgs в аргументах при вызове. Решение получается «грязным» [F1]. Я бы предпочел передать один аргумент вместо двух. К счастью, у проблемы существует простое решение: мы можем преобразовать массив args в list и передать 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();

    }

  }

Все изменения были простыми и не нарушали работы тестов. Теперь можно заняться перемещением функций в соответствующие производные классы. Начнем с внесения изменений в 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;

}

Это изменение важно, потому что мы хотим полностью устранить цепочку if-else. Для этого из нее необходимо вывести состояние ошибки.

Теперь можно переходить к перемещению функций 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) {

  }

}

Но ведь мы только что перенесли обработку исключения в функцию? Ситуация с включением того, что вы намереваетесь вскоре исключить, весьма часто встречается при переработке кода. Малый размер шагов и необходимость прохождения тестов означает, что вам придется часто перемещать туда-сюда фрагменты кода. Переработка кода напоминает кубик Рубика: чтобы добиться большой цели, необходимо выполнить множество мелких операций. Каждая операция делает возможной следующую.

Зачем передавать итератор, если setBooleanArg он не нужен? Потому что он нужен setIntArg и setStringArg! И если я хочу организовать доступ ко всем трем функциям через абстрактный метод в ArgumentMarshaller, мне не обойтись без его передачи setBooleanArg.

Итак, функция 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();

}

Конечно, это нарушает работу всех производных классов, поэтому мы добавим реализацию нового метода в каждый из них.

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;

  }

}

А теперь 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;

}

Все тесты проходят, а функция set размещается в BooleanArgumentMarshaler! Теперь можно сделать то же самое для String and Integer.

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;

  }

}

А теперь завершающий штрих: убираем цепочку if-else!

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;

  }

}

Избавляемся от лишних функций в 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;

    }

  }

  }

ArgumentMarshaler преобразуется в интерфейс:

private interface ArgumentMarshaler {

  void set(Iterator<String> currentArgument) throws ArgsException;

  Object get();

}

А теперь посмотрите, как легко добавлять новые типы аргументов в эту структуру. Количество изменений минимально, а все изменения логически изолированы. Начнем с добавления нового тестового сценария, проверяющего правильность работы аргументов 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);

}

Чистим код разбора форматной строки и добавляем обнаружение ## для аргументов типа 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);

}

Затем пишется класс 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;

  }

}

Для нового типа добавляются новые коды ошибок:

private enum ErrorCode {

  OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT,

  MISSING_DOUBLE, INVALID_DOUBLE}

А еще понадобится функция 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;

  }

}

И все тесты успешно проходят! Добавление нового типа прошло в целом безболезненно. Теперь давайте убедимся в том, что обработка ошибок работает правильно. Следующий тестовый сценарий проверяет, что при передаче неразбираемой строки с аргументом ## выдается соответствующая ошибка:

  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 "";

  }

Тесты успешно проходят. Следующий тест проверяет, что ошибка с отсутствующим аргументом 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());

}

Как и ожидалось, все проходит успешно. Этот тест был написан просто для полноты картины.

Код исключения некрасив, и в классе 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;

    }

  }

}

Хорошо — теперь Args выдает единственное исключение ArgsException. Выделение ArgsException в отдельный модуль приведет к тому, что большой объем вспомогательного кода обработки ошибок переместится из модуля Args в этот модуль. Это наиболее естественное и очевидное место для размещения этого кода, вдобавок перемещение поможет очистить перерабатываемый модуль Args.

Итак, нам удалось полностью отделить код исключений и ошибок от модуля Args (листинги 14.13–14.16). Для решения этой задачи понадобилось примерно 30 промежуточных шагов, и после каждого шага проверялось прохождение всех тестов.

Листинг 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());

    }

  }

Листинг 14.13 (продолжение)

  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());

    }

  }

}

Листинг 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());

  }

}

Листинг 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) {

Листинг 14.15 (продолжение)

    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}

}

Листинг 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("##"))

Листинг 14.16 (продолжение)

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

  }

}

Основные изменения в классе Args свелись к удалениям. Большая часть кода ушла из Args в ArgsException. Хорошо. Мы также переместили все разновидности ArgumentMarshaller в отдельные файлы. Еще лучше!

Одним из важнейших аспектов хорошей программной архитектуры является логическое разбиение кода — создание подходящих мест для размещения разных кодовых блоков. Разделение ответственности заметно упрощает понимание и сопровождение кода.

Обратите внимание на метод errorMessage класса ArgsException. Очевидно, размещение форматирования сообщения об ошибках нарушает принцип единой ответственности. Класс Args должен заниматься обработкой аргументов, а не форматом сообщений об ошибках. Но насколько логично размещать код форматирования сообщений в ArgsException?

Откровенно говоря, это компромиссное решение. Пользователям, которым не нравится, что сообщения об ошибках поставляет класс ArgsException, придется написать собственную реализацию.

К этому моменту мы уже вплотную подошли к окончательному решению, приведенному в начале этой главы. Завершающие преобразования остаются читателю для самостоятельных упражнений.

Заключение

Заставить код работать недостаточно. Работоспособный код часто несовершенен. Программисты, которые заставляют свой код работать и на этом считают свою задачу выполненной, ведут себя непрофессионально. Возможно, они опасаются, что у них не хватит времени для совершенствования структуры и архитектуры кода, но я не могу с этим согласиться. Ничто не оказывает настолько всестороннего и длительного отрицательного влияния на судьбу программного проекта, как плохой код. Плохой график можно переделать, плохие требования можно переопределить. Плохую динамику рабочей группы еще можно исправить. Плохой код загнивает и разбухает, превращаясь в беспощадный груз, который тянет группу ко дну. Сколько раз я видел, как работа заходит в тупик по одной причине: в спешке вместо добротного кода создавалась какая-то безобразная мешанина, которая после этого обрекала группу на бесконечные мучения.

Конечно, плохой код можно вычистить. Но это обходится очень дорого. В процессе загнивания кода модули постепенно проникают друг в друга, образуется множество скрытых и запутанных зависимостей. Поиск и разрыв старых зависимостей — длительная, тяжелая работа. С другой стороны, поддерживать чистоту в коде относительно несложно. Если утром вы устроили беспорядок в модуле, то его будет легко вычистить днем. Или еще лучше, если вы устроили беспорядок пять минут назад, то его будет очень легко вычистить прямо сейчас.

Итак, постоянно следите за тем, чтобы ваш код оставался как можно более простым и чистым. Не допускайте, чтобы он начал загнивать.

Недавно я переписал этот модуль на Ruby. Код занимает в 7 раз меньше места и имеет более качественную структуру.

Чтобы предотвратить подобные сюрпризы в будущем, я добавил новый модульный тест, который запускал все тесты FitNesse.

Назад: 13. Многопоточность
Дальше: 15. Внутреннее строение JUnit