Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
листинг 14 .3 . ArgumentMarshaler.java public interface ArgumentMarshaler { void set(Iterator } листинг 14 .4 . BooleanArgumentMarshaler.java public class BooleanArgumentMarshaler implements ArgumentMarshaler { private boolean booleanValue = false; public void set(Iterator 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 try { stringValue = currentArgument.next(); } catch (NoSuchElementException e) { throw new ArgsException(MISSING_STRING); } } public static String getValue(ArgumentMarshaler am) { if (am != null && am instanceof StringArgumentMarshaler) продолжение 229 230 Глава 14 . Последовательное очищение листинг 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 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);} 230 Реализация Args 231 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); продолжение 231 232 Глава 14 . Последовательное очищение листинг 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, программа получится гораздо короче 1 Пожалуйста, перечитайте код еще раз . Обратите особое внимание на выбор имен, размеры функций и форматирование кода . Возможно, опытные програм- мисты найдут отдельные недочеты в стиле или структуре кода . Но я надеюсь, что в целом вы согласитесь с тем, что код хорошо написан, а его структура чиста и логична . Скажем, после чтения кода вам должно быть очевидно, как добавить поддерж- ку нового типа аргументов (например, дат или комплексных чисел), и это по- требует относительно небольших усилий с вашей стороны . Для этого доста- точно создать новый класс, производный от ArgumentMarshaler , новую функцию getXXX и включить новое условие case в функцию parseSchemaElement . Вероятно, также потребуется новое значение ArgsException.ErrorCode и новое сообщение об ошибке . 1 Недавно я переписал этот модуль на Ruby . Код занимает в 7 раз меньше места и имеет более качественную структуру . 232 Args: черновик 233 Как я это сделал? Позвольте вас успокоить: я не написал эту программу от начала до конца в ее те- кущем виде . Более того, я не ожидаю, что вы сможете писать чистые и элегантные программы за один проход . Если мы чему-то и научились за последнюю пару десятилетий, так это тому, что программирование ближе к ремеслу, чем к науке . Чтобы написать чистый код, мы сначала пишем грязный код, а затем очищаем его . Вряд ли вас это удивит . Мы усвоили эту истину еще в начальной школе, когда учителя заставляли нас (обычно безуспешно) писать планы сочинений . Пред- полагалось, что мы должны сначала написать первый вариант плана, затем вто- рой, потом еще несколько версий, пока не придем к окончательной версии . Они пытались объяснить нам, что четкое и ясное сочинение появляется в результате последовательного усовершенствования . Многие начинающие программисты (впрочем, как и большинство школьников, пишущих сочинения) не слишком усердно следуют этому совету . Они считают, что их главная цель — заставить программу работать . Когда программа «зарабо- тает», они переходят к следующей задаче, оставляя «работающую» программу в том состоянии, в котором она «заработала» . Опытные программисты знают, что с профессиональной точки зрения такой подход равносилен самоубийству . 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 private Map private Map private Map private Set 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} продолжение 233 234 Глава 14 . Последовательное очищение листинг 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); } } 234 Args: черновик 235 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; } } продолжение 235 236 Глава 14 . Последовательное очищение листинг 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); } 236 Args: черновик 237 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; } продолжение 237 238 Глава 14 . Последовательное очищение листинг 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 только увеличивают масштабы этого беспорядочного месива . Я вовсе не собирался писать беспорядочное месиво . В самом деле, я постарался сохранить более или менее разумную организацию кода . Об этом свидетельствует хотя бы выбор имен функций и переменных, а также наличие у программы при- митивной структуры . Но совершенно очевидно, что проблемы вышли из-под моего контроля . 238 Args: черновик 239 Неразбериха накапливалась постепенно . Ранние версии выглядели вовсе не так отвратительно . Для примера в листинге 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 private Map 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); } } продолжение 239 |