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

          s.equalsIgnoreCase(weekDayNames[day.index])) {

        return day;

      }

    }

    throw new IllegalArgumentException(

      String.format("%s is not a valid weekday string", s));

  }


  public String toString() {

    return dateSymbols.getWeekdays()[index];

  }

}

В программе две функции getMonths (строки 288–316); первая функция вызывает вторую. Вторая функция не вызывается никем, кроме первой функцией. Я свернул две функции в одну, что привело к значительному упрощению кода [G9],[G12],[F4]. В завершение я переименовал итоговую функцию, присвоив ей более содержательное имя [N1].

public static String[] getMonthNames() {

  return dateFormatSymbols.getMonths();

}

Функция isValidMonthCode (строки 326–346) потеряла актуальность после введения перечисления Month, поэтому я ее удалил [G9].

Функция monthCodeToQuarter (строки 356–375) отдает ФУНКЦИОНАЛЬНОЙ ЗАВИСТЬЮ [Refactoring]; вероятно, ее логичнее включить в перечисление Month в виде метода с именем quarter. Я выполнил замену.

public int quarter() {

  return 1 + (index-1)/3;

}

В результате перечисление Month стало достаточно большим для выделения в отдельный класс. Я убрал его из DayDate по образцу перечисления Day [G11],[G13].

Следующие два метода называются monthCodeToString (строки 377–426). И снова мы видим, как один метод вызывает своего «двойника» с передачей флага. Обычно передавать флаг в аргументе не рекомендуется, особенно если он просто выбирает формат вывода [G15]. Я переименовал, упростил и реструктурировал эти функции и переместил их в перечисление Month [N1],[N3],[C3],[G14].

public String toString() {

  return dateFormatSymbols.getMonths()[index - 1];

}

public String toShortString() {

  return dateFormatSymbols.getShortMonths()[index - 1];

}

Далее в листинге идет метод stringToMonthCode (строки 428–472). Я переименовал его, переместил в перечисление Month и упростил [N1],[N3],[C3],[G14],[G12].

public static Month parse(String s) {

  s = s.trim();

  for (Month m : Month.values())

    if (m.matches(s))

      return m;


  try {

    return make(Integer.parseInt(s));

  }

  catch (NumberFormatException e) {}

  throw new IllegalArgumentException("Invalid month " + s);

}


private boolean matches(String s) {

  return s.equalsIgnoreCase(toString()) ||

         s.equalsIgnoreCase(toShortString());

}

Метод isLeapYear (строки 495–517) можно сделать более выразительным [G16].

public static boolean isLeapYear(int year) {

  boolean fourth = year % 4 == 0;

  boolean hundredth = year % 100 == 0;

  boolean fourHundredth = year % 400 == 0;

  return fourth && (!hundredth || fourHundredth);

}

Следующая функция, leapYearCount (строки 519–536), не принадлежит DayDate. Она не вызывается никем, кроме двух методов SpreadsheetDate. Я переместил ее в производный класс [G6].

Функция lastDayOfMonth (строки 538–560) использует массив LAST_DAY_OF_MONTH. Этот массив принадлежит перечислению Month [G17], поэтому функция была перемещена. Заодно я упростил ее код и сделал его более выразительным [G16].

public static int lastDayOfMonth(Month month, int year) {

  if (month == Month.FEBRUARY && isLeapYear(year))

    return month.lastDay() + 1;

  else

    return month.lastDay();

}

Начинается самое интересное. Далее в листинге идет функция addDays (строки 562–576). Прежде всего, поскольку эта функция работает с переменными DayDate, она не должна быть статической [G18]. Соответственно, я преобразовал ее в метод экземпляра. Также она вызывает функцию toSerial, которую правильнее называть toOrdinal [N1]. Наконец, метод можно несколько упростить.

public DayDate addDays(int days) {

  return DayDateFactory.makeDate(toOrdinal() + days);

}

Сказанное относится и к функции addMonths (строки 578–602). Она должна быть оформлена в виде метода экземпляра [G18]. Алгоритм относительно сложен, поэтому я воспользовался ПОЯСНИТЕЛЬНЫМИ ВРЕМЕННЫМИ ПЕРЕМЕННЫМИ [Beck97] [G19], чтобы сделать его смысл более прозрачным. Заодно метод getYYY был переименован в getYear [N1].

public DayDate addMonths(int months) {

  int thisMonthAsOrdinal = 12 * getYear() + getMonth().index - 1;

  int resultMonthAsOrdinal = thisMonthAsOrdinal + months;

  int resultYear = resultMonthAsOrdinal / 12;

  Month resultMonth = Month.make(resultMonthAsOrdinal % 12 + 1);

  int lastDayOfResultMonth = lastDayOfMonth(resultMonth, resultYear);

  int resultDay = Math.min(getDayOfMonth(), lastDayOfResultMonth);

  return DayDateFactory.makeDate(resultDay, resultMonth, resultYear);

}

Функция addYears (строки 604–626) преобразуется по тем же принципам, что и ее аналоги.

public DayDate plusYears(int years) {

  int resultYear = getYear() + years;

  int lastDayOfMonthInResultYear = lastDayOfMonth(getMonth(), resultYear);

  int resultDay = Math.min(getDayOfMonth(), lastDayOfMonthInResultYear);

  return DayDateFactory.makeDate(resultDay, getMonth(), resultYear);

}

Преобразование статических методов в методы экземпляров вызвало у меня некоторое беспокойство. Поймет ли читатель при виде выражения date.addDays(5), что объект date не изменяется, а вместо этого возвращается новый экземпляр DayDate? Или он ошибочно решит, что к объекту date прибавляются пять дней? Казалось бы, проблема не столь серьезна, но конструкции вроде следующей могут оказаться очень коварными [G20].

DayDate date = DateFactory.makeDate(5, Month.DECEMBER, 1952);

date.addDays(7); // Смещение date на одну неделю.

Скорее всего, читатель кода предположит, что вызов addDays изменяет объект date. Значит, нам понадобится имя, разрушающее эту двусмысленность [N4]. Я переименовал методы в plusDays и plusMonths. Мне кажется, что предназначение данного метода отлично отражается конструкциями вида

DayDate date = oldDate.plusDays(5);

С другой стороны, следующая конструкция читается недостаточно бегло, чтобы читатель сразу предположил, что изменяется объект date:

date.plusDays(5);

Алгоритмы становятся все интереснее. Функция getPreviousDayOfWeek (строки 628–660) работает, но выглядит слишком сложно. После некоторых размышлений относительно того, что же в действительности происходит в этой функции [G21], мне удалось упростить ее и воспользоваться ПОЯСНИТЕЛЬНЫМИ ВРЕМЕННЫМИ ПЕРЕМЕННЫМИ [G19], чтобы сделать код более понятным. Я также преобразовал статический метод в метод экземпляра [G18] и избавился от дублирующего метода экземпляра [G5] (строки 997–1008).

public DayDate getPreviousDayOfWeek(Day targetDayOfWeek) {

  int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;

  if (offsetToTarget >= 0)

    offsetToTarget -= 7;

  return plusDays(offsetToTarget);

}

Абсолютно такой же анализ с тем же результатом был проведен для метода getFollowingDayOfWeek (строки 662–693).

public DayDate getFollowingDayOfWeek(Day targetDayOfWeek) {

  int offsetToTarget = targetDayOfWeek.index - getDayOfWeek().index;

  if (offsetToTarget <= 0)

    offsetToTarget += 7;