Статьи

Эволюция защитного API с интерфейсами Java

Эволюция API — это нечто совершенно нетривиальное. То, с чем сталкиваются лишь немногие. Большинство из нас работают над внутренними, проприетарными API каждый день. Современные IDE поставляются с потрясающими инструментами для выделения, переименования, подтягивания, понижения, косвенного, делегирования, вывода, обобщения артефактов нашего кода. Эти инструменты делают рефакторинг наших внутренних API-интерфейсов простым делом. Но некоторые из нас работают над публичными API, где правила резко меняются. Публичные API, если все сделано правильно, являются версионными. Каждое изменение — совместимое или несовместимое — должно быть опубликовано в новой версии API. Большинство людей согласятся с тем, что развитие API следует проводить в основных и второстепенных выпусках, аналогично тому, что указано в семантическом версионировании Вкратце: Несовместимые изменения API публикуются в основных выпусках (1.0, 2.0, 3.0), тогда как совместимые изменения / улучшения API публикуются в небольших выпусках (1.0, 1.1, 1.2).

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

Эволюция интерфейса API

Теперь устаревание — это хороший инструмент для указания того, что вы собираетесь удалить тип или элемент из вашего API. Что если вы собираетесь добавить метод или тип в иерархию типов интерфейса? Это означает, что весь клиентский код, реализующий ваш интерфейс, сломается — по крайней мере, до тех пор, пока методы защитника Java 8 еще не введены. Есть несколько методов, чтобы обойти эту проблему:

1. Не волнуйтесь об этом

Да, это тоже вариант. Ваш API общедоступен, но, возможно, не так широко используется. Посмотрим правде в глаза: не все из нас работают над базами кодов JDK / Eclipse / Apache / etc. Если вы дружелюбны, вы, по крайней мере, будете ждать основного выпуска, чтобы представить новые методы. Но вы можете нарушить правила семантического управления версиями, если вам действительно нужно — если вы можете справиться с последствиями получения толпы злых пользователей.

Тем не менее, обратите внимание, что другие платформы не так обратно совместимы, как юниверс Java (часто из-за особенностей языка или сложности языка). Например, благодаря различным способам Scala объявлять вещи как неявные , ваш API не всегда может быть идеальным.

2. Сделайте это способом Java

Путь «Java» — вообще не развивать интерфейсы. Большинство типов API в JDK всегда были такими, какими они являются сегодня. Конечно, это заставляет API чувствовать себя «динозавром» и добавляет много избыточности между различными похожими типами, такими как StringBuffer и StringBuilder , или Hashtable и HashMap .

Обратите внимание, что некоторые части Java не придерживаются «Java». В частности, это относится к JDBC API, который развивается в соответствии с правилами раздела # 1: «Не заботьтесь об этом».

3. Сделай это способом Eclipse

Внутренности Eclipse содержат огромные API. При разработке для Eclipse / в Eclipse существует множество рекомендаций по развитию ваших собственных API (т. Е. Общедоступных частей вашего плагина). Одним из примеров того, как ребята из Eclipse расширяют интерфейсы, является тип IAnnotationHover . По контракту Javadoc он позволяет реализациям также реализовывать IAnnotationHoverExtension и IAnnotationHoverExtension2 . Очевидно, что в долгосрочной перспективе такой развитый API довольно сложно поддерживать, тестировать и документировать, и, в конечном счете, его сложно использовать! (рассмотрим ICompletionProposal и его 6 (!) типов расширений)

4. Ждите Java 8

В Java 8 вы сможете использовать методы защитника . Это означает, что вы можете предоставить разумную реализацию по умолчанию для ваших новых методов интерфейса, как можно увидеть в Java 1.8 — java.util.Iterator (выдержка):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public interface Iterator<E> {
 
    // These methods are kept the same:
    boolean hasNext();
    E next();
 
    // This method is now made 'optional' (finally!)
    public default void remove() {
        throw new UnsupportedOperationException('remove');
    }
 
    // This method has been added compatibly in Java 1.8
    default void forEach(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        while (hasNext())
            consumer.accept(next());
    }
}

Конечно, вы не всегда хотите предоставлять реализацию по умолчанию. Часто ваш интерфейс — это контракт, который должен быть полностью реализован с помощью клиентского кода.

5. Предоставить публичные реализации по умолчанию

Во многих случаях целесообразно сообщить клиентскому коду, что они могут реализовать интерфейс на свой страх и риск (из-за эволюции API), и вместо этого им лучше расширить поставляемую реализацию или реализацию по умолчанию. Хорошим примером для этого является java.util.List , который может быть проблематичным для правильной реализации. Для простых, не критичных к производительности пользовательских списков большинство пользователей, скорее всего, решат расширить java.util.AbstractList . Единственными оставшимися методами, которые нужно реализовать, являются get (int) и size (). Поведение всех других методов может быть получено из этих двух:

01
02
03
04
05
06
07
08
09
10
11
class EmptyList<E> extends AbstractList<E> {
    @Override
    public E get(int index) {
        throw new IndexOutOfBoundsException('No elements here');
    }
 
    @Override
    public int size() {
        return 0;
    }
}

Хорошее соглашение, которому нужно следовать — назвать реализацию по умолчанию AbstractXXX, если оно абстрактное, или DefaultXXX, если оно конкретное.

6. Сделайте ваш API очень сложным для реализации

Теперь это не очень хорошая техника, а просто вероятный факт. Если ваш API очень сложно реализовать (у вас есть сотни методов в интерфейсе), то пользователи, вероятно, не собираются это делать. Примечание: возможно . Никогда не стоит недооценивать сумасшедшего пользователя. Примером этого является тип org.jooq.Field в jOOQ , который представляет поле / столбец базы данных. Фактически этот тип является частью языка, специфичного для внутреннего домена jOOQ, и предлагает всевозможные операции и функции, которые могут выполняться над столбцом базы данных. Конечно, наличие такого большого количества методов — исключение и — если вы не проектируете DSL — это, вероятно, признак плохого общего дизайна.

7. Добавить компилятор и трюки IDE

И последнее, но не менее важное: есть несколько хитрых приемов, которые вы можете применить к своему API, чтобы помочь людям понять, что они должны делать, чтобы правильно реализовать API на основе интерфейса. Вот жесткий пример, который бросает вызов замыслу разработчика API прямо в лицо. Рассмотрим этот фрагмент API org.hamcrest.Matcher :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public interface Matcher<T> extends SelfDescribing {
 
    // This is what a Matcher really does.
    boolean matches(Object item);
    void describeMismatch(Object item, Description mismatchDescription);
 
    // Now check out this method here:
 
    /**
     * This method simply acts a friendly reminder not to implement
     * Matcher directly and instead extend BaseMatcher. It's easy to
     * ignore JavaDoc, but a bit harder to ignore compile errors .
     *
     * @see Matcher for reasons why.
     * @see BaseMatcher
     * @deprecated to make
     */
    @Deprecated
    void _dont_implement_Matcher___instead_extend_BaseMatcher_();
}

«Дружеское напоминание» , давай.

Другие способы

Я уверен, что существуют десятки других способов развить API на основе интерфейса. Мне любопытно услышать ваши мысли!

Справка: Эволюция защитного API с интерфейсами Java от нашего партнера по JCG Лукаса Эдера из блога JAVA, SQL и AND JOOQ .