Чистый код. Создание, анализ и рефакторинг — страница 37 из 94

    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, программа получится гораздо короче[68].

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

Скажем, после чтения кода вам должно быть очевидно, как добавить поддержку нового типа аргументов (например, дат или комплексных чисел), и это потребует относительно небольших усилий с вашей стороны. Для этого достаточно создать новый класс, производный от 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 unexpectedArguments = new TreeSet();

  private Map booleanArgs =

    new HashMap();

  private Map stringArgs = new HashMap();

  private Map intArgs = new HashMap();

  private Set argsFound = new HashSet();

  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 unexpectedArguments = new TreeSet();

  private Map booleanArgs =

    new HashMap();

  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 unexpectedArguments = new TreeSet();

  private Map booleanArgs =

    new HashMap();

  private Map stringArgs =


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

    new HashMap();

  private Set argsFound = new HashSet();

  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 booleanArgs =

    new HashMap();

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

...

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