Статьи

Хранение вещей СУХОЙ: метод перегрузки

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

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

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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. Тем не менее, главное правило заключается в том, чтобы всегда иметь как можно меньше реализаций для перегруженных методов. Попробуйте вызвать один метод из другого. Например, эти два метода очень похожи:

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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 должен быть предложен для ne , gt , ge , lt , le
  • Независимо от того, какая часть API проходит тестирование на интеграцию, сама реализация, безусловно, проходит определенный тест.
  • Таким образом, чрезвычайно легко предоставить пользователям очень богатый API с множеством удобных методов, поскольку пользователи не хотят помнить, как эти методы более общего назначения (например, compare() ) действительно работают.

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

01
02
03
04
05
06
07
08
09
10
11
// 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? ------------------------+

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

1
2
// Not Enterprise enough
iterator.stream();

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

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

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

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

навынос

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

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

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

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