Главная страница
Навигация по странице:

  • …Потом очистить код

  • Создание, анализ ирефакторинг


    Скачать 3.16 Mb.
    НазваниеСоздание, анализ ирефакторинг
    Дата29.09.2022
    Размер3.16 Mb.
    Формат файлаpdf
    Имя файлаChistyj_kod_-_Sozdanie_analiz_i_refaktoring_(2013).pdf
    ТипКнига
    #706087
    страница34 из 49
    1   ...   30   31   32   33   34   35   36   37   ...   49
    Прежде всего — заставить работать
    В классе
    SerialDateTests содержится набор модульных тестов (листинг Б .2, с . 411) . Все тесты проходят . К сожалению, беглое изучение тестов показывает, что тестирование не покрывает часть кода [T1] . Например, поиск показывает, что метод
    MonthCodeToQuarter
    (строка 334) не используется [F4] . Соответственно он не включается в модульные тесты .
    Итак, я запустил Clover, чтобы узнать, какая часть кода реально покрывается модульными тестами . Clover сообщает, что модульные тесты выполняют только
    91 из 185 исполняемых команд
    SerialDate
    (около 50%) [T2] . Карта покрытия на- поминала лоскутное одеяло, а по всему классы были разбросаны большие пятна невыполняемого кода .
    Моей целью было полное понимание и переработка кода этого класса . Я не мог добиться этого без значительного улучшения тестового покрытия, поэтому мне пришлось написать собственный набор абсолютно независимых модульных тестов (листинг Б .4, с . 419) .
    Просматривая код тестов, можно заметить, что многие из них закомментиро- ваны . Эти тесты не проходили в исходном варианте . Однако они представляют поведение, которым, на мой взгляд, должен обладать класс
    SerialDate
    . Соответ- ственно, в ходе переработки
    SerialDate я буду работать над тем, чтобы эти тесты тоже проходили .
    304

    Прежде всего — заставить работать
    305
    Даже с несколькими закомментированными тестами Clover сообщает, что новые модульные тесты покрывают 170 (92%) из 185 исполняемых команд . Неплохо, хотя я думаю, что и этот показатель можно улучшить .
    Возможно, в нескольких первых закомментированных тестах (строки 23–63) я слегка хватил через край . Их прохождение не было формально заложено при проектировании программы, но данное поведение казалось мне абсолютно оче- видным [G2] .
    Я не знаю, зачем создавался метод testWeekdayCodeToString
    , но раз уж он был на- писан, казалось очевидным, что в работе метода не должен учитываться регистр символов . Написать соответствующий тест было элементарно [T3] . Заставить его работать было еще проще; я просто изменил строки 259 и 263, чтобы в них использовалась функция equalsIgnoreCase
    Тесты в строках 32 и 45 остались закомментированными, так как мне было не- ясно, нужно ли поддерживать сокращения вида «tues» и «thurs» .
    Тесты в строках 153 и 154 не проходят . Хотя, естественно, должны про ходить
    [G2] . Проблема (а заодно и тесты в строках 163–213) легко исправляется внесе- нием следующих изменений в функцию stringToMonthCode
    457 if ((result < 1) || (result > 12)) {
    result = -1;
    458 for (int i = 0; i < monthNames.length; i++) {
    459 if (s.equalsIgnoreCase(shortMonthNames[i])) {
    460 result = i + 1;
    461 break;
    462 }
    463 if (s.equalsIgnoreCase(monthNames[i])) {
    464 result = i + 1;
    465 break;
    466 }
    467 }
    468 }
    Закомментированный тест в строке 318 выявляет ошибку в методе get-
    FollowingDayOfWeek
    (строка 672) . 25 декабря 2004 года было субботой . Сле- дующей субботой было 1 января 2005 года . Тем не менее при запуске теста getFollowingDayOfWeek утверждает, что первой субботой, предшествующей 25 де- кабря, было 25 декабря . Разумеется, это неверно [G3],[T1] . Проблема — типичная ошибка граничного условия [T5] — кроется в строке 685 . Строка должна читаться следующим образом:
    685 if (baseDOW >= targetWeekday) {
    Интересно, что проблемы с этой функцией возникали и раньше . Из исто- рии изменений (строка 43) видно, что в функциях getPreviousDayOfWeek
    , getFollowingDayOfWeek и getNearestDayOfWeek
    [T6] «исправлялись ошибки» .
    Модульный тест testGetNearestDayOfWeek
    (строка 329), проверяющий работу метода getNearestDayOfWeek
    (строка 705), изначально был не таким длинным
    305

    306
    Глава 16 . Переработка SerialDate и исчерпывающим, как в окончательной версии . Я включил в него много допол- нительных тестовых сценариев, потому что не все исходные тесты проходили успешно [T6] . Посмотрите, какие тестовые сценарии были закомментирова- ны — закономерность проявляется достаточно очевидно [T7] . Сбой в алгоритме происходит в том случае, если ближайший день находится в будущем . Очевидно, и здесь происходит какая-то ошибка граничного условия [T5] .
    Результаты тестового покрытия кода, полученные от Clover, тоже весьма интерес- ны [T8] . Строка 719 никогда не выполняется! Следовательно, условие if в строке
    718 всегда ложно . С первого взгляда на код понятно, что это действительно так .
    Переменная adjust всегда отрицательна, она не может быть больше либо равна
    4 . Значит, алгоритм попросту неверен .
    Правильный алгоритм выглядит так:
    int delta = targetDOW - base.getDayOfWeek();
    int positiveDelta = delta + 7;
    int adjust = positiveDelta % 7;
    if (adjust > 3)
    adjust -= 7;
    return SerialDate.addDays(adjust, base);
    Наконец, для прохождения тестов в строках 417 и 429 достаточно инициировать исключение
    IllegalArgumentException вместо возвращения строки ошибки в функ- циях weekInMonthToString и relativeToString
    После таких изменений все модульные тесты проходят . Вероятно, класс
    Serial-
    Date теперь действительно работает . Теперь пришло время «довести его до ума» .
    …Потом очистить код
    Мы проанализируем код
    SerialDate от начала до конца, усовершенствуя его в ходе просмотра . Хотя ниже об этом не упоминается, после каждого вносимого изме- нения выполнялись все модульные тесты JCommon, включая мой доработанный модульный тест
    SerialDate
    . Итак, вы можете быть уверены в том, что вносимые изменения не нарушают работы JCommon .
    Начнем со строки 1 . В ней приводятся многословные комментарии с информа- цией о лицензии, авторских правах, авторах и истории изменений . Бесспорно, существуют некоторые юридические формальности, которые необходимо соблю- дать, поэтому авторские права и лицензии должны остаться . С другой стороны, история изменений является пережитком из 1960-х годов . Сегодня у нас имеются системы управления исходным кодом, которые все это делают за нас . Историю следует удалить [C1] .
    Список импорта, начинающийся в строке 61, следует сократить при помощи конструкций java.text.*
    и java.util.*
    [J1] .
    К форматированию HTML в Javadoc (строка 67) я отношусь без восторга . Меня беспокоят исходные файлы, написанные более чем на одном языке . В этом ком-
    306

    …Потом очистить код
    307
    ментарии встречаются четыре языка: Java, английский, Javadoc и HTML [G1] .
    При таком количестве языков трудно поддерживать порядок в коде . Например, аккуратное размещение строк 71 и 72 теряется при генерировании кода Javadoc, да и кому захочется видеть теги

      и

    • в исходном коде? Правильнее было бы просто заключить весь комментарий в теги
      , чтобы форматирование в ис- ходном коде сохранилось в Javadoc
      1
      Строка 86 содержит объявление класса . Почему этот класс называется
      SerialDate
      ?
      Почему в нем присутствует слово «serial» — только потому, что класс объявлен производным от
      Serializable
      ? Это выглядит маловероятно .
      Не буду держать вас в неведении . Я знаю (или по крайней мере полагаю, что знаю), почему было использовано слово «serial» . На это указывают константы
      SERIAL_LOWER_BOUND
      и
      SERIAL_UPPER_BOUND
      в строках 98 и 101 . Еще более очевид- ная подсказка содержится в комментарии, начинающемся в строке 830 . Класс назван
      SerialDate
      , потому что его реализация построена на использовании
      «порядкового номера» (serial number), то есть количества дней с 30 декабря
      1899 года .
      На мой взгляд, у такого решения два недостатка . Во-первых, термин «порядко- вый номер» некорректен . Кому-то это покажется пустяком, но выбранное пред- ставление представляет собой относительное смещение, а не порядковый номер .
      Термин «порядковый номер» скорее относится к маркировке промышленных изделий, а не к датам . Так что, на мой взгляд, название получилось не слишком содержательным [N1] .
      Второй недостаток более важен . Имя
      SerialDate подразумевает определенную реализацию . Однако класс является абстрактным и для него реализацию скорее нужно скрывать! Я считаю, что выбранное имя находится на неверном уровне абстракции [N2] . По моему мнению, класс было бы лучше назвать
      Date
      К сожалению, в библиотеку Java входит слишком много классов с именем
      Date
      ; вероятно, это не лучший вариант . Поскольку класс скорее ориентирован на работу с сутками, я подумывал о том, чтобы назвать его
      Day
      , но и это имя часто используется в других местах . В конечном итоге я решил, что лучшим компро- миссом будет имя
      DayDate
      В дальнейшем обсуждении будет использоваться имя
      DayDate
      . Не забывайте, что в листингах, на которые вы будете смотреть, класс по-прежнему называется
      SerialDate
      Я понимаю, почему
      DayDate наследует от
      Comparable и
      Serializable
      . Но почему он наследует от
      MonthConstants
      ? Класс
      MonthConstants
      (листинг Б .3, с . 417) пред- ставляет собой простой набор статических констант, определяющих месяцы .
      Наследование от классов с константами — старый трюк, который использовался
      Java-программистами, чтобы избежать выражений вида
      MonthConstants.January
      ,
      1
      А еще правильнее было бы считать в Javadoc все комментарии заранее отформатирован- ными, чтобы они одинаково смотрелись в коде и в документации .
      307

    308
    Глава 16 . Переработка SerialDate но это неудачная мысль [J2] .
    MonthConstants следовало бы оформить в виде пере- числения .
    public abstract class DayDate implements Comparable,
    Serializable {
    public static enum Month {
    JANUARY(1),
    FEBRUARY(2),
    MARCH(3),
    APRIL(4),
    MAY(5),
    JUNE(6),
    JULY(7),
    AUGUST(8),
    SEPTEMBER(9),
    OCTOBER(10),
    NOVEMBER(11),
    DECEMBER(12);
    Month(int index) {
    this.index = index;
    }
    public static Month make(int monthIndex) {
    for (Month m : Month.values()) {
    if (m.index == monthIndex)
    return m;
    }
    throw new IllegalArgumentException("Invalid month index " + monthIndex);
    }
    public final int index;
    }
    Преобразование
    MonthConstants в enum инициирует ряд изменений в классе
    DayDate и всех его пользователях . На внесение всех изменений мне потребовалось около часа . Однако теперь любая функция, прежде получавшая int вместо месяца, теперь получает значение из перечисления
    Month
    . Это означает, что мы можем удалить метод isValidMonthCode
    (строка 326), а также все проверки ошибок кодов месяцев — например, monthCodeToQuarter
    (строка 356) [G5] .
    Далее возьмем строку 91, serialVersionUID
    . Переменная используется для управ- ления сериализацией данных . Если изменить ее, то данные
    DayDate
    , записанные старой версией программы, перестанут читаться, а попытки приведут к исклю- чению
    InvalidClassException
    . Если вы не объявите переменную serialVersionUID
    , компилятор автоматически сгенерирует ее за вас, причем значение переменной будет различаться при каждом внесении изменений в модуль . Я знаю, что во всей документации рекомендуется управлять этой переменной вручную, но мне кажется, что автоматическое управление сериализацией надежнее [G4] . В кон- це концов, я предпочитаю отлаживать исключение
    InvalidClassException
    , чем необъяснимое поведение программы в результате того, что я забыл изменить serialVersionUID
    . Итак, я собираюсь удалить эту переменную — по крайней мере пока

    308

    …Потом очистить код
    309
    Комментарий в строке 93 выглядит избыточным . Избыточные комментарии толь- ко распространяют лживую и недостоверную информацию [C2] . Соответственно, я удаляю его вместе со всеми аналогами .
    В комментариях в строках 97 и 100 упоминаются порядковые номера, о которых говорилось ранее [C1] . Комментарии описывают самую раннюю и самую позд- нюю дату, представляемую классом
    DayDate
    . Их можно сделать более понятными
    [N1] .
    public static final int EARLIEST_DATE_ORDINAL = 2; // 1/1/1900
    public static final int LATEST_DATE_ORDINAL = 2958465; // 12/31/9999
    Мне неясно, почему значение
    EARLIEST_DATE_ORDINAL
    равно 2, а не 0 . Комментарий в строке 829 подсказывает, что это как-то связано с представлением дат в Micro- soft Excel . Более подробное объяснение содержится в производном от
    DayDate классе с именем
    SpreadsheetDate
    (листинг Б .5, с . 428) . Комментарий в строке 71 хорошо объясняет суть дела .
    Проблема в том, что такой выбор относится к реализации
    SpreadsheetDate и не имеет ничего общего с
    DayDate
    . Из этого я заключаю, что
    EARLIEST_DATE_ORDINAL
    и
    LATEST_DATE_ORDINAL
    реально не относятся к DayDate и их следует переместить в
    SpreadsheetDate
    [G6] .
    Поиск по коду показывает, что эти переменные используются только в
    Spread- sheetDate
    . Они не используются ни в
    DayDate
    , ни в других классах JCommon . Со- ответственно, я перемещаю их в
    SpreadsheetDate
    Со следующими переменными,
    MINIMUM_YEAR_SUPPORTED
    и
    MAXIMUM_YEAR_SUPPORTED
    (строки 104 и 107), возникает дилемма . Вроде бы понятно, что если
    DayDate явля- ется абстрактным классом, то он не должен содержать информации о минималь- ном или максимальном годе . У меня снова возникло искушение переместить эти переменные в
    SpreadsheetDate
    [G6] . Тем не менее поиск показал, что эти перемен- ные используются еще в одном классе:
    RelativeDayOfWeekRule
    (листинг Б .6, с . 438) .
    В строках 177 и 178 функция getDate проверяет, что в ее аргументе передается действительный год . Дилемма состоит в том, что пользователю абстрактного класса необходима информация о его реализации .
    Наша задача — предоставить эту информацию, не загрязняя самого класса
    DayDate
    . В общем случае мы могли бы получить данные реализации из экземпля- ра производного класса, однако функция getDate не получает экземпляр
    DayDate
    С другой стороны, она возвращает такой экземпляр, а это означает, что она его где-то создает . Из строк 187–205 можно заключить, что экземпляр
    DayDate созда- ется при вызове одной из трех функций: getPreviousDayOfWeek
    , getNearestDayOfWeek или getFollowingDayOfWeek
    . Обратившись к листингу
    DayDate
    , мы видим, что все эти функции (строки 638–724) возвращают дату, созданную функцией addDays
    (строка 571), которая вызывает createInstance
    (строка 808), которая создает
    SpreadsheetDate
    ! [G7] .
    В общем случае базовые классы не должны располагать информацией о своих производных классах . Проблема решается применением паттерна АБСТРАКТ-
    309

    310
    Глава 16 . Переработка SerialDate
    НАЯ ФАБРИКА [GOF] и созданием класса
    DayDateFactory
    . Фабрика создает экземпляры
    DayDate
    , а также предоставляет информацию по поводу реализации — в частности, минимальное и максимальное значение даты .
    public abstract class DayDateFactory {
    private static DayDateFactory factory = new SpreadsheetDateFactory();
    public static void setInstance(DayDateFactory factory) {
    DayDateFactory.factory = factory;
    }
    protected abstract DayDate _makeDate(int ordinal);
    protected abstract DayDate _makeDate(int day, DayDate.Month month, int year);
    protected abstract DayDate _makeDate(int day, int month, int year);
    protected abstract DayDate _makeDate(java.util.Date date);
    protected abstract int _getMinimumYear();
    protected abstract int _getMaximumYear();
    public static DayDate makeDate(int ordinal) {
    return factory._makeDate(ordinal);
    }
    public static DayDate makeDate(int day, DayDate.Month month, int year) {
    return factory._makeDate(day, month, year);
    }
    public static DayDate makeDate(int day, int month, int year) {
    return factory._makeDate(day, month, year);
    }
    public static DayDate makeDate(java.util.Date date) {
    return factory._makeDate(date);
    }
    public static int getMinimumYear() {
    return factory._getMinimumYear();
    }
    public static int getMaximumYear() {
    return factory._getMaximumYear();
    }
    }
    Фабрика заменяет методы createInstance методами makeDate
    , в результа- те чего имена выглядят гораздо лучше [N1] . По умолчанию используется
    SpreadsheetDateFactory
    , но этот класс можно в любой момент заменить другой фа- брикой . Статические методы, делегирующие выполнение операций абстрактным методам, используют комбинацию паттернов СИНГЛЕТ [GOF], ДЕКОРАТОР
    [GOF] и АБСТРАКТНАЯ ФАБРИКА .
    Класс
    SpreadsheetDateFactory выглядит так:
    public class SpreadsheetDateFactory extends DayDateFactory {
    public DayDate _makeDate(int ordinal) {
    return new SpreadsheetDate(ordinal);
    }
    public DayDate _makeDate(int day, DayDate.Month month, int year) {
    return new SpreadsheetDate(day, month, year);
    }
    public DayDate _makeDate(int day, int month, int year) {
    return new SpreadsheetDate(day, month, year);
    }
    310

    …Потом очистить код
    311
    public DayDate _makeDate(Date date) {
    final GregorianCalendar calendar = new GregorianCalendar();
    calendar.setTime(date);
    return new SpreadsheetDate(
    calendar.get(Calendar.DATE),
    DayDate.Month.make(calendar.get(Calendar.MONTH) + 1),
    calendar.get(Calendar.YEAR));
    }
    protected int _getMinimumYear() {
    return SpreadsheetDate.MINIMUM_YEAR_SUPPORTED;
    }
    protected int _getMaximumYear() {
    return SpreadsheetDate.MAXIMUM_YEAR_SUPPORTED;
    }
    }
    Как видите, я уже переместил переменные
    MINIMUM_YEAR_SUPPORTED
    и
    MAXIMUM_YEAR_
    SUPPORTED
    в класс
    SpreadsheetDate
    , в котором им положено находиться [G6] .
    Следующая проблема
    DayDate
    — константы дней, начинающиеся со строки 109 .
    Их следует оформить в виде другого перечисления [J3] . Мы уже видели, как это делается, поэтому я не буду повторяться . При желании посмотрите в итоговом листинге .
    Далее мы видим серию таблиц, начинающуюся с
    LAST_DAY_OF_MONTH
    в строке 140 .
    Моя первая претензия к этим таблицам состоит в том, что описывающие их ком- ментарии избыточны [C3] . Одних имен вполне достаточно, поэтому я собираюсь удалить комментарии .
    Также неясно, почему эта таблица не объявлена приватной [G8], потому что в классе имеется статическая функция lastDayOfMonth
    , предоставляющая те же данные .
    Следующая таблица,
    AGGREGATE_DAYS_TO_END_OF_MONTH
    , выглядит загадочно — она ни разу не используется в JCommon [G9] . Я удалил ее .
    То же произошло с
    LEAP_YEAR_AGGREGATE_DAYS_TO_END_OF_MONTH
    Следующая таблица,
    AGGREGATE_DAYS_TO_END_OF_PRECEDING_MONTH
    , используется только в
    SpreadsheetDate
    (строки 434 и 473) . Так почему бы не переместить ее в
    SpreadsheetDate
    ? Против перемещения говорит тот факт, что таблица не при- вязана ни к какой конкретной реализации [G6] . С другой стороны, никаких реализаций, кроме
    SpreadsheetDate
    , фактически не существует, поэтому таблицу следует переместить ближе к месту ее использования [G10] .
    Для меня решающим обстоятельством является то, что для обеспечения логи- ческой согласованности [G11] таблицу следует объявить приватной и предо- ставить доступ к ней через функцию вида julianDateOfLastDayOfMonth
    . Но по- хоже, такая функция никому не нужна . Более того, если этого потребует новая реализация
    DayDate
    , таблицу можно будет легко вернуть на место . Поэтому я ее переместил .
    Далее следуют три группы констант, которые можно преобразовать в перечис- ления (строки 162–205) .
    311

    1   ...   30   31   32   33   34   35   36   37   ...   49


    написать администратору сайта