Статьи

Keeping Things DRY: перегрузка метода

Хороший чистый дизайн приложения требует дисциплины в сохранении  СУХОГО :

Все должно быть сделано один раз.
Необходимость сделать это дважды — совпадение.
Необходимость сделать это три раза — это закономерность.

— неизвестный мудрец

Теперь, если вы следуете правилам Xtreme Programming, вы знаете, что нужно делать, когда сталкиваетесь с шаблоном:

рефактор беспощадно

Потому что мы все знаем, что происходит, когда вы этого не делаете:

НЕ СУХОЙ: метод перегрузки

Одной из наименее сухих вещей, которые вы можете сделать, которая по-прежнему приемлема, является перегрузка методов — в тех языках, которые позволяют это (в отличие от  Ceylon , JavaScript). Будучи внутренним языком, специфичным для предметной области,  jOOQ  API интенсивно использует перегрузки. Рассмотрим тип поля (моделирование столбца базы данных):

public interface Field<T> {
    // [...]

    Condition eq(T value);
    Condition eq(Field<T> field);
    Condition eq(Select<? extends Record1<T>> query);
    Condition eq(QuantifiedSelect<? extends Record1<T>> query);

    Condition in(Collection<?> values);
    Condition in(T... values);
    Condition in(Field<?>... values);
    Condition in(Select<? extends Record1<T>> query);

    // [...]
}

Таким образом, в некоторых случаях отсутствие СУХОСТИ неизбежно, также в определенной степени при реализации вышеуказанного API. Тем не менее, главное правило заключается в том, чтобы всегда иметь как можно меньше реализаций для перегруженных методов. Попробуйте вызвать один метод из другого. Например, эти два метода очень похожи:

Condition eq(T value);
Condition eq(Field<T> field);

Первый метод является частным случаем второго, когда пользователи jOOQ не хотят явно объявлять переменную связывания. Это буквально реализовано так:

@Override
public final Condition eq(T value) {
    return equal(value);
}

@Override
public final Condition equal(T value) {
    return equal(Utils.field(value, this));
}

@Override
public final Condition equal(Field<T> field) {
    return compare(EQUALS, nullSafe(field));
}

@Override
public final Condition compare(Comparator comparator, Field<T> field) {
    switch (comparator) {
        case IS_DISTINCT_FROM:
        case IS_NOT_DISTINCT_FROM:
            return new IsDistinctFrom<T>(this, nullSafe(field), comparator);
        default:
            return new CompareCondition(this, nullSafe(field), comparator);
    }
}

Как вы видете:

  • eq() это просто синоним унаследованного  equal() метода
  • equal(T) это более специализированная, удобная формаequal(Field<T>)
  • equal(Field<T>) это более специализированная, удобная формаcompare(Comparator, Field<T>)
  • compare() наконец, предоставляет доступ к реализации этого API

Все эти методы также являются частью общедоступного API и могут напрямую вызываться потребителем API, поэтому  nullSafe() проверка повторяется в каждом методе.

Почему все проблемы?

Ответ прост.

  • Существует очень небольшая вероятность ошибки копирования-вставки во всем API.
  • … потому что тот же API должен быть предложен для  negtgeltle
  • Независимо от того, какая часть API проходит тестирование на интеграцию, сама реализация, безусловно, проходит определенный тест.
  • Таким образом, чрезвычайно легко предоставить пользователям очень богатый API с множеством удобных методов, поскольку пользователи не хотят помнить, как эти методы более общего назначения (например  compare()) действительно работают.

Последний пункт особенно важен, и из-за рисков, связанных с обратной совместимостью, например, не всегда сопровождается JDK. Чтобы создать  поток Java 8  из  итератора , вы должны пройти через все эти хлопоты, например:

// Aagh, my fingers hurt...
   StreamSupport.stream(iterator.spliterator(), false);
// ^^^^^^^^^^^^^                 ^^^^^^^^^^^    ^^^^^
//       |                            |           |
// Not Stream!                        |           |
//                                    |           |
// Hmm, Spliterator. Sounds like      |           |
// Iterator. But what is it? ---------+           |
//                                                |
// What's this true and false?                    |
// And do I need to care? ------------------------+

Когда, интуитивно, вы хотели бы иметь:

// Not Enterprise enough
iterator.stream();

Другими словами, тонкие детали реализации Java 8 Streams скоро попадут в большой объем клиентского кода, и многие новые служебные функции будут повторять эти вещи снова и снова.

См  . Объяснение Брайана Гетца о переполнении стека  .

С другой стороны делегирования реализаций перегрузки, конечно, труднее (т.е. больше работы) реализовать такой API. Это особенно обременительно, если поставщик API также позволяет пользователям самостоятельно реализовывать API (например, JDBC). Другая проблема заключается в длине стековых трасс, генерируемых такими реализациями. Но мы уже показали в этом блоге, что  глубокие следы стека могут быть признаком хорошего качества .

Теперь ты знаешь почему.

навынос

Еда на вынос проста. Всякий раз, когда вы сталкиваетесь с образцом, рефакторинг. Найдите наиболее распространенный знаменатель, выделите его в реализацию и убедитесь, что эта реализация практически никогда не используется путем делегирования отдельных этапов ответственности от метода к методу.

Следуя этим правилам, вы будете:

  • Меньше ошибок
  • Иметь более удобный API

Удачного рефакторинга!