Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
листинг 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 ""; } 240 Args: черновик 241 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 private Map private Map продолжение 241 242 Глава 14 . Последовательное очищение листинг 14 .10 (продолжение) new HashMap private Set 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); } } 242 Args: черновик 243 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 продолжение 243 244 Глава 14 . Последовательное очищение листинг 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 ""; } 244 Args: черновик 245 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 я знал, что для каждого типа ар- гументов новый код должен добавляться в трех основных местах . Во-первых, для 245 246 Глава 14 . Последовательное очищение каждого типа аргументов необходимо было обеспечить разбор соответствующего элемента форматной строки, чтобы выбрать объект 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 { 246 Args: черновик 247 } private class StringArgumentMarshaler extends ArgumentMarshaler { } private class IntegerArgumentMarshaler extends ArgumentMarshaler { } } Понятно, что добавление класса ничего не нарушит . Поэтому я внес самое простейшее из всех возможных изменений — изменил контейнер HashMap для логических аргументов так, чтобы при конструировании передавался тип Argu- mentMarshaler : private Map Это нарушило работу нескольких команд, которые я быстро исправил . 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 . Длинное имя мне не понра- 247 248 Глава 14 . Последовательное очищение вилось; во-первых, оно было избыточным, а во-вторых, загромождало функцию . Соответственно я сократил его до 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 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; } 248 Аргументы String 249 public boolean getBoolean() { return booleanValue; } public void setString(String s) { stringValue = s; } public String getString() { return stringValue == null ? "" : stringValue; } } И снова изменения вносились последовательно и только так, чтобы тесты по крайней мере хотя бы запускались (даже если и не проходили) . Если работо- способность теста была нарушена, я сначала добивался того, чтобы он работал, и только потом переходил к следующему изменению . Вероятно, вы уже поняли, что я собираюсь сделать . Собрав все текущее поведе- ние компоновки аргументов в базовом классе ArgumentMarshaler , я намерен пере- мещать его вниз в производные классы . Это позволит мне сохранить работоспо- собность программы в ходе постепенного изменения ее структуры . Очевидным следующим шагом стало перемещение функциональности аргумента int в ArgumentMarshaler . И снова все обошлось без сюрпризов: private Map 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) { 249 250 Глава 14 . Последовательное очищение 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; } |