Создание, анализ ирефакторинг
Скачать 3.16 Mb.
|
283 } 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 private Set private Iterator private List 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("##")) продолжение 283 284 Глава 14 . Последовательное очищение листинг 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; } } 284 Аргументы String 285 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); } } 285 286 Глава 14 . Последовательное очищение Основные изменения в классе Args свелись к удалениям . Большая часть кода ушла из Args в ArgsException . Хорошо . Мы также переместили все разновидности ArgumentMarshaller в отдельные файлы . Еще лучше! Одним из важнейших аспектов хорошей программной архитектуры является логическое разбиение кода — создание подходящих мест для размещения разных кодовых блоков . Разделение ответственности заметно упрощает понимание и со- провождение кода . Обратите внимание на метод errorMessage класса ArgsException . Очевидно, раз- мещение форматирования сообщения об ошибках нарушает принцип единой ответственности . Класс Args должен заниматься обработкой аргументов, а не форматом сообщений об ошибках . Но насколько логично размещать код форма- тирования сообщений в ArgsException ? Откровенно говоря, это компромиссное решение . Пользователям, которым не нравится, что сообщения об ошибках поставляет класс ArgsException , придется написать собственную реализацию . К этому моменту мы уже вплотную подошли к окончательному решению, приве- денному в начале этой главы . Завершающие преобразования остаются читателю для самостоятельных упражнений . Заключение Заставить код работать недостаточно . Работоспособный код часто несовершенен . Программисты, которые заставляют свой код работать и на этом считают свою задачу выполненной, ведут себя непрофессионально . Возможно, они опасаются, что у них не хватит времени для совершенствования структуры и архитектуры кода, но я не могу с этим согласиться . Ничто не оказывает настолько всесторон- него и длительного отрицательного влияния на судьбу программного проекта, как плохой код . Плохой график можно переделать, плохие требования можно переопределить . Плохую динамику рабочей группы еще можно исправить . Пло- хой код загнивает и разбухает, превращаясь в беспощадный груз, который тянет группу ко дну . Сколько раз я видел, как работа заходит в тупик по одной причине: в спешке вместо добротного кода создавалась какая-то безобразная мешанина, которая после этого обрекала группу на бесконечные мучения . Конечно, плохой код можно вычистить . Но это обходится очень дорого . В про- цессе загнивания кода модули постепенно проникают друг в друга, образуется множество скрытых и запутанных зависимостей . Поиск и разрыв старых зависи- мостей — длительная, тяжелая работа . С другой стороны, поддерживать чистоту в коде относительно несложно . Если утром вы устроили беспорядок в модуле, то его будет легко вычистить днем . Или еще лучше, если вы устроили беспорядок пять минут назад, то его будет очень легко вычистить прямо сейчас . Итак, постоянно следите за тем, чтобы ваш код оставался как можно более про- стым и чистым . Не допускайте, чтобы он начал загнивать . 286 Внутреннее строение JUnit JUnit — одна из самых известных инфраструктур для языка Java . Как и положено нормальной инфраструктуре, она концептуально проста, точна в определениях и элегантна в реализации . Но как выглядит ее код? В этой главе мы покритикуем пример, взятый из инфраструктуры JUnit . 15 287 288 Глава 15 . Внутреннее строение JUnit Инфраструктура JUnit У JUnit много авторов, но все началось с совместного перелета Кента Бека и Эрика Гамма в Атланту . Кент хотел освоить Java, а Эрик собирался заняться изучением тестовой инфраструктуры Кента для языка Smalltalk . «А что может быть более естественным для двух „технарей“, запертых в тесном пространстве, чем достать портативные компьютеры и взяться за программирование? 1 » За три часа «высотной работы» были написаны основы JUnit . Модуль, который мы рассмотрим в этой главе, предназначен для выявления ошибок сравнения строк . Он называется ComparisonCompactor . Получив две раз- личающиеся строки (например, ABCDE и ABXDE ), он выдает сводку различий между ними, генерируя строку вида <...B[X]D...> Я мог бы объяснить и подробнее, но тестовые сценарии сделают это лучше . Про- смотрите листинг 15 .1 и вы отлично поймете требования этого модуля . А заодно критически проанализируйте структуру тестов . Нельзя ли упростить их, сделать более наглядными? листинг 15 .1 . ComparisonCompactorTest.java package junit.tests.framework; import junit.framework.ComparisonCompactor; import junit.framework.TestCase; public class ComparisonCompactorTest extends TestCase { public void testMessage() { String failure= new ComparisonCompactor(0, "b", "c").compact("a"); assertTrue("a expected:<[b]> but was:<[c]>".equals(failure)); } public void testStartSame() { String failure= new ComparisonCompactor(1, "ba", "bc").compact(null); assertEquals(«expected: but was:», failure); } public void testEndSame() { String failure= new ComparisonCompactor(1, "ab", "cb").compact(null); assertEquals("expected:<[a]b> but was:<[c]b>", failure); } public void testSame() { String failure= new ComparisonCompactor(1, "ab", "ab").compact(null); assertEquals("expected: } public void testNoContextStartAndEndSame() { 1 JUnit Pocket Guide, Kent Beck, O’Reilly, 2004, c . 43 . 288 Инфраструктура JUnit 289 String failure= new ComparisonCompactor(0, "abc", "adc").compact(null); assertEquals("expected:<...[b]...> but was:<...[d]...>", failure); } public void testStartAndEndContext() { String failure= new ComparisonCompactor(1, "abc", "adc").compact(null); assertEquals("expected: but was:", failure); } public void testStartAndEndContextWithEllipses() { String failure= new ComparisonCompactor(1, "abcde", "abfde").compact(null); assertEquals("expected:<...b[c]d...> but was:<...b[f]d...>", failure); } public void testComparisonErrorStartSameComplete() { String failure= new ComparisonCompactor(2, "ab", "abc").compact(null); assertEquals("expected: } public void testComparisonErrorEndSameComplete() { String failure= new ComparisonCompactor(0, "bc", "abc").compact(null); assertEquals("expected:<[]...> but was:<[a]...>", failure); } public void testComparisonErrorEndSameCompleteContext() { String failure= new ComparisonCompactor(2, "bc", "abc").compact(null); assertEquals("expected:<[]bc> but was:<[a]bc>", failure); } public void testComparisonErrorOverlapingMatches() { String failure= new ComparisonCompactor(0, "abc", "abbc").compact(null); assertEquals("expected:<...[]...> but was:<...[b]...>", failure); } public void testComparisonErrorOverlapingMatchesContext() { String failure= new ComparisonCompactor(2, "abc", "abbc").compact(null); assertEquals("expected: } public void testComparisonErrorOverlapingMatches2() { String failure= new ComparisonCompactor(0, "abcdde", "abcde").compact(null); assertEquals("expected:<...[d]...> but was:<...[]...>", failure); } public void testComparisonErrorOverlapingMatches2Context() { String failure= new ComparisonCompactor(2, "abcdde", "abcde").compact(null); assertEquals("expected:<...cd[d]e> but was:<...cd[]e>", failure); } продолжение 289 290 Глава 15 . Внутреннее строение JUnit листинг 15 .1 (продолжение) public void testComparisonErrorWithActualNull() { String failure= new ComparisonCompactor(0, "a", null).compact(null); assertEquals("expected: but was: } public void testComparisonErrorWithActualNullContext() { String failure= new ComparisonCompactor(2, "a", null).compact(null); assertEquals("expected: but was: } public void testComparisonErrorWithExpectedNull() { String failure= new ComparisonCompactor(0, null, "a").compact(null); assertEquals("expected: } public void testComparisonErrorWithExpectedNullContext() { String failure= new ComparisonCompactor(2, null, "a").compact(null); assertEquals("expected: } public void testBug609972() { String failure= new ComparisonCompactor(10, "S&P500", "0").compact(null); assertEquals("expected:<[S&P50]0> but was:<[]0>", failure); } } Я провел для ComparisonCompactor анализ покрытия кода на основе этих тестов . В ходе тестирования обеспечивалось 100%-ное покрытие: была выполнена каж- дая строка кода, каждая команда if и цикл for . Я удостоверился в том, что код работает правильно, а также преисполнился уважения к мастерству его авторов . Код ComparisonCompactor приведен в листинге 15 .2 . Не жалейте времени и как следует разберитесь в нем . Вероятно, вы согласитесь с тем, что код достаточно выразителен, обладает логичным разбиением и простой структурой . А когда вы закончите, мы вместе начнем придираться к мелочам . листинг 15 .2 . ComparisonCompactor.java (исходный код) package junit.framework; public class ComparisonCompactor { private static final String ELLIPSIS = "..."; private static final String DELTA_END = "]"; private static final String DELTA_START = "["; private int fContextLength; private String fExpected; private String fActual; private int fPrefix; private int fSuffix; 290 Инфраструктура JUnit 291 public ComparisonCompactor(int contextLength, String expected, String actual) { fContextLength = contextLength; fExpected = expected; fActual = actual; } public String compact(String message) { if (fExpected == null || fActual == null || areStringsEqual()) return Assert.format(message, fExpected, fActual); findCommonPrefix(); findCommonSuffix(); String expected = compactString(fExpected); String actual = compactString(fActual); return Assert.format(message, expected, actual); } private String compactString(String source) { String result = DELTA_START + source.substring(fPrefix, source.length() - fSuffix + 1) + DELTA_END; if (fPrefix > 0) result = computeCommonPrefix() + result; if (fSuffix > 0) result = result + computeCommonSuffix(); return result; } private void findCommonPrefix() { fPrefix = 0; int end = Math.min(fExpected.length(), fActual.length()); for (; fPrefix < end; fPrefix++) { if (fExpected.charAt(fPrefix) != fActual.charAt(fPrefix)) break; } } private void findCommonSuffix() { int expectedSuffix = fExpected.length() - 1; int actualSuffix = fActual.length() - 1; for (; actualSuffix >= fPrefix && expectedSuffix >= fPrefix; actualSuffix--, expectedSuffix--) { if (fExpected.charAt(expectedSuffix) != fActual.charAt(actualSuffix)) break; } fSuffix = fExpected.length() - expectedSuffix; } private String computeCommonPrefix() { продолжение 291 292 Глава 15 . Внутреннее строение JUnit листинг 15 .2 (продолжение) return (fPrefix > fContextLength ? ELLIPSIS : "") + fExpected.substring(Math.max(0, fPrefix - fContextLength), fPrefix); } private String computeCommonSuffix() { int end = Math.min(fExpected.length() - fSuffix + 1 + fContextLength, fExpected.length()); return fExpected.substring(fExpected.length() - fSuffix + 1, end) + (fExpected.length() - fSuffix + 1 < fExpected.length() - fContextLength ? ELLIPSIS : ""); } private boolean areStringsEqual() { return fExpected.equals(fActual); } } Вероятно, вы найдете в этом модуле некоторые недочеты . В нем встречают- ся длинные выражения, какие-то малопонятные +1 и т . д . Но в целом модуль весьма хорош . В конце концов, он мог бы выглядеть и так, как показано в ли- стинге 15 .3 . листинг 15 .3 . ComparisonCompator.java (переработанная версия) package junit.framework; public class ComparisonCompactor { private int ctxt; private String s1; private String s2; private int pfx; private int sfx; public ComparisonCompactor(int ctxt, String s1, String s2) { this.ctxt = ctxt; this.s1 = s1; this.s2 = s2; } public String compact(String msg) { if (s1 == null || s2 == null || s1.equals(s2)) return Assert.format(msg, s1, s2); pfx = 0; for (; pfx < Math.min(s1.length(), s2.length()); pfx++) { if (s1.charAt(pfx) != s2.charAt(pfx)) break; } int sfx1 = s1.length() - 1; int sfx2 = s2.length() - 1; 292 Инфраструктура JUnit 293 for (; sfx2 >= pfx && sfx1 >= pfx; sfx2--, sfx1--) { if (s1.charAt(sfx1) != s2.charAt(sfx2)) break; } sfx = s1.length() - sfx1; String cmp1 = compactString(s1); String cmp2 = compactString(s2); return Assert.format(msg, cmp1, cmp2); } private String compactString(String s) { String result = "[" + s.substring(pfx, s.length() - sfx + 1) + "]"; if (pfx > 0) result = (pfx > ctxt ? "..." : "") + s1.substring(Math.max(0, pfx - ctxt), pfx) + result; if (sfx > 0) { int end = Math.min(s1.length() - sfx + 1 + ctxt, s1.length()); result = result + (s1.substring(s1.length() - sfx + 1, end) + (s1.length() - sfx + 1 < s1.length() - ctxt ? "..." : "")); } return result; } } Авторы оставили эту модуль в очень хорошей форме . И все же «правило бой- скаута 1 » гласит: все нужно оставлять чище, чем было до вашего прихода . Итак, как же улучшить исходный код в листинге 15 .2? Первое, что мне решительно не понравилось, — префикс f у имен переменных классов [N6] . В современных средах разработки подобное кодирование области видимости излишне . Давайте уберем все префиксы: private int contextLength; private String expected; private String actual; private int prefix; private int suffix; Также бросается в глаза неинкапсулированная условная команда в начале функ- ции compact [G28] . public String compact(String message) { |