Статьи

Как создать хороший, регулярный API

Люди имеют твердое мнение о том, как разработать хороший API. Следовательно, в сети много страниц и книг, объясняющих, как это сделать. Эта статья будет посвящена конкретному аспекту хороших API: регулярности. Регулярность — это то, что происходит, когда вы следуете « Принципу наименьшего удивления ». Этот принцип действует независимо от того, какой личный вкус и стиль вы хотели бы использовать в своем API, в противном случае. Таким образом, это одна из самых важных функций хорошего API.

При разработке «обычного» API необходимо учитывать следующее.

Правило № 1: Установите сильные условия

Если ваш API будет расти, вы будете снова и снова использовать одни и те же термины. Например, некоторые действия будут иметь несколько разновидностей, что приведет к различным классам / типам / методам, которые отличаются лишь незначительным поведением. Тот факт, что они похожи, должен отражаться в их именах. Имена должны использовать сильные термины. Взять, к примеру, JDBC. Независимо от того, как вы выполняете заявление , вы всегда будете использовать этот термин execute. Например, вы будете вызывать любой из этих методов:

Подобным образом, вы всегда будете использовать термин closeдля освобождения ресурсов, независимо от того, какой ресурс вы выпускаете. Например, вы позвоните:

На самом деле, closeэто настолько сильный и устоявшийся термин в JDK, что он привел к интерфейсам java.io.Closeable(начиная с Java 1.5) и java.lang.AutoCloseable(начиная с Java 1.7), которые обычно устанавливают договор об освобождении ресурсов.

Нарушение правила: наблюдаемое

Это правило нарушается пару раз в JDK. Например, в java.util.Observableклассе. В то время как другие «коллекционные» типы установили условия

  • size()
  • remove()
  • removeAll()

… Этот класс объявляет

В этом контексте нет веских оснований для использования других терминов. То же самое относится к Observer.update(), который на самом деле должен называться notify(), другому термину, установленному в API JDK.

Нарушение правила: весна. Большинство из этого

Весна действительно стала популярной в те дни, когда J2EE был странным, медленным и громоздким. Подумайте об EJB 2.0 … Там могут быть похожие мнения о Spring, которые не по теме для этого поста. Вот как Spring нарушает это конкретное правило. Пара случайных примеров, в которых Spring не может установить строгие термины и вместо этого использует длинные конкатенации бессмысленных, неконкретных слов:

Помимо «ощущения» как ужасного API (для меня), вот еще несколько объективный анализ:

  • В чем разница между а CreatorиFactory
  • В чем разница между а Sourceи а Provider?
  • В чем неощутимая разница между Advisorи Provider?
  • В чем неощутимая разница между а Discovererи а Provider?
  • Это Advisorсвязано с AspectJAdvice?
  • Это ScanningCandidateили CandidateComponent?
  • Что TargetSource? И как бы это отличалось от a SourceTargetесли не a SourceSourceили моего любимого: A SourceSourceTargetProviderSource?

Гари Флеминг прокомментировал мой предыдущий пост в блоге о забавных именах Spring:

Я готов поспорить, что имя класса, сгенерированного цепочкой Маркова (на основе Spring Security), будет неотличимо от реального.

Вернуться к большей серьезности …

Правило № 2: применяйте симметрию к комбинациям терминов

Как только вы установили сильные условия, вы начнете их комбинировать. Когда вы смотрите на коллекции API , в JDK, вы заметите, что они симметричны таким образом , что они установлены сроки add(), remove(), contains(), и all, прежде чем объединять их симметрично:

Теперь Collectionтип является хорошим примером, где может быть приемлемо исключение из этого правила , когда метод не «тянет свой вес» . Это, вероятно, имеет место для retainAll(Collection<?>), который не имеет эквивалентного retain(E)метода. Впрочем, это может быть и регулярным нарушением этого правила.

Нарушение правила: карта

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

Заметьте также, что нет смысла использовать термин Setв именах методов. Подпись метода уже указывает, что у результата есть Setтип. Это было бы более последовательным и симметричным , если эти методы будут уже были названы keys(), values(), entries(). (В дополнение к этому, Setsи Listsеще одна тема, о которой я скоро напишу в блоге, так как я думаю, что эти типы также не имеют собственного веса)

В то же время Mapинтерфейс нарушает это правило, предоставляя

Кроме того, нет необходимости устанавливать термин clear()вместо повторного использования removeAll()без аргументов. Это относится ко всем членам Collection API. На самом деле, clear()метод также нарушает правило № 1. Это не сразу очевидно, если clearчто-то немного отличается от removeудаления элементов коллекции.

Правило № 3: добавьте удобства за счет перегрузки

В большинстве случаев есть только одна веская причина, по которой вы хотите перегружать метод: удобство. Часто вы хотите делать одно и то же в разных контекстах, но создание этого очень специфического типа аргумента метода обременительно. Поэтому для удобства вы предлагаете своим пользователям API другой вариант того же метода с установленным «дружественным» типом аргумента. Это можно наблюдать снова в Collectionтипе. У нас есть:

  • toArray(), что является удобной перегрузкой…
  • toArray(T[])

Другим примером является Arraysслужебный класс. У нас есть:

Перегрузка в основном используется по двум причинам:

  1. Предоставление поведения аргумента «по умолчанию», как в Collection.toArray()
  2. Поддержка нескольких несовместимых, но «похожих» наборов аргументов, как в Arrays.copyOf ()

Другие языки включили эти понятия в свой языковой синтаксис. Многие языки (например, PL / SQL) формально поддерживают именованные аргументы по умолчанию. Некоторые языки (например, JavaScript) даже не заботятся о том, сколько на самом деле аргументов. И еще один новый язык JVM под названием Ceylon избавился от перегрузки , объединив поддержку именованных аргументов по умолчанию с типами объединения. Так как Ceylon является языком статической типизации, это, вероятно, самый мощный подход для повышения удобства вашего API.

Нарушение правила: TreeSet

Трудно найти хороший пример случая, когда это правило нарушается в JDK. Но есть одно: TreeSetи TreeMap. Их конструкторы перегружены несколько раз. Давайте посмотрим на эти два конструктора:

Последнее «умно» добавляет некоторое удобство к первому в том, что оно извлекает хорошо известное Comparatorиз аргумента, SortedSetчтобы сохранить порядок. Это поведение сильно отличается от совместимого (!) Первого конструктора, который не выполняет instanceofпроверку набора аргументов. Т.е. эти два вызова конструктора приводят к разному поведению:

SortedSet<Object> original = // [...]

// Preserves ordering:
new TreeSet<Object>(original);

// Resets ordering:
new TreeSet<Object>((Collection<Object>) original);

Эти конструкторы нарушают правило в том, что они производят совершенно другое поведение. Они не просто удобство.

Правило № 4: последовательный порядок аргументов

Убедитесь, что вы последовательно упорядочиваете аргументы своих методов. Это очевидная вещь для перегруженных методов, поскольку вы можете сразу увидеть, как лучше всегда ставить массив первым, а затем int в предыдущем примере из Arraysслужебного класса:

Но вы быстро заметите, что все методы в этом классе будут использовать массив в первую очередь. Несколько примеров:

Нарушение правила: массивы

Этот же класс также «тонко» нарушает это правило, поскольку он помещает необязательные аргументы между другими аргументами при перегрузке методов. Например, он объявляет

Когда последний должен был быть fill(Object[], Object, int, int). Это «тонкое» нарушение правила, так как вы также можете утверждать, что те методы, Arraysкоторые ограничивают массив аргументов диапазоном, всегда помещают массив и аргумент диапазона вместе. Таким образом, fill()метод снова будет следовать правилу, так как он обеспечивает такой же порядок аргументов, как copyOfRange(), например:

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

Нарушение правила: строка

Другой случай нарушения правил — это Stringкласс:

Проблемы здесь:

  • Трудно сразу понять разницу между этими двумя методами, так как необязательный booleanаргумент вставляется в начало списка аргументов.
  • Трудно сразу понять назначение каждого аргумента int, так как в одном методе много аргументов

Правило № 5: Установите типы возвращаемых значений

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

  • Методы, возвращающие один объект, должны возвращаться, nullкогда объект не был найден
  • Методы возвращающие несколько объектов не должен возвращать пустой List, Set, Map, массив и т.д. , когда не было найдено ни одного объекта (никогда null)
  • Методы должны генерировать исключения только в случае… ну, исключения

При таком наборе правил не рекомендуется использовать 1-2 метода, которые:

  • … Бросить, ObjectNotFoundExceptionsкогда объект не был найден
  • … Вернуть nullвместо пустогоLists

Нарушение правила: Файл

Файл является примером класса JDK, который нарушает многие правила. Среди них правило регулярных типов возврата. Его File.list()Javadoc гласит:

Массив строк с именами файлов и каталогов в каталоге, обозначенном этим абстрактным путем. Массив будет пустым, если каталог пуст. Возвращает ноль, если это абстрактное имя пути не обозначает каталог или если произошла ошибка ввода-вывода.

Итак, правильный способ перебора имен файлов (если вы занимаетесь защитным программированием):

String[] files = file.list();

// You should never forget this null check!
if (files != null) {
    for (String file : files) {
        // Do things with your file
    }
}

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

default:

кейс). В этом случае они, вероятно, предпочли подход «провалиться раньше».

Дело в том, что Fileуже есть достаточные средства проверки, fileдействительно ли каталог ( File.isDirectory()). И он должен бросить, IOExceptionесли что-то пошло не так, вместо того, чтобы вернуться null. Это очень сильное нарушение этого правила, вызывающее много боли на сайте вызова… Следовательно:

НИКОГДА не возвращайте ноль при возврате массивов или коллекций!

Нарушение правила: JPA

Примером того, как JPA нарушает это правило, является способ извлечения сущностей из EntityManagerили из Query:

Как NoResultExceptionи RuntimeExceptionэтот недостаток, он сильно нарушает принцип наименьшего удивления , так как вы можете не знать об этой разнице до времени выполнения!

Если вы настаиваете на исключении NoResultExceptions, сделайте их проверенными исключениями, поскольку клиентский код ДОЛЖЕН их обработать

Заключение и дальнейшее чтение

… вернее, дальнейшее наблюдение. Взгляните на презентацию Джоша Блоха по дизайну API. Он согласен с большинством моих требований, около 0:30:30

Другой полезный пример такой веб-страницы — «Контрольный список разработки Java API» от The Amiable API :

http://theamiableapi.com/2012/01/16/java-api-design-checklist/