Эволюция 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 .