Люди имеют твердое мнение о том, как разработать хороший 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 не может установить строгие термины и вместо этого использует длинные конкатенации бессмысленных, неконкретных слов:
AbstractBeanFactoryBasedTargetSourceCreator
AbstractInterceptorDrivenBeanDefinitionDecorator
AbstractRefreshablePortletApplicationContext
AspectJAdviceParameterNameDiscoverer
BeanFactoryTransactionAttributeSourceAdvisor
ClassPathScanningCandidateComponentProvider
- … это может продолжаться бесконечно, мое любимое существо …
- J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource . Обратите внимание, я писал о краткости раньше …
Помимо «ощущения» как ужасного API (для меня), вот еще несколько объективный анализ:
- В чем разница между а
Creator
иFactory
- В чем разница между а
Source
и аProvider
? - В чем неощутимая разница между
Advisor
иProvider
? - В чем неощутимая разница между а
Discoverer
и аProvider
? - Это
Advisor
связано сAspectJAdvice
? - Это
ScanningCandidate
илиCandidateComponent
? - Что
TargetSource
? И как бы это отличалось от aSourceTarget
если не aSourceSource
или моего любимого: ASourceSourceTargetProviderSource
?
Гари Флеминг прокомментировал мой предыдущий пост в блоге о забавных именах Spring:
Я готов поспорить, что имя класса, сгенерированного цепочкой Маркова (на основе Spring Security), будет неотличимо от реального.
Вернуться к большей серьезности …
Правило № 2: применяйте симметрию к комбинациям терминов
Как только вы установили сильные условия, вы начнете их комбинировать. Когда вы смотрите на коллекции API , в JDK, вы заметите, что они симметричны таким образом , что они установлены сроки add()
, remove()
, contains()
, и all
, прежде чем объединять их симметрично:
add(E)
addAll(Collection<? extends E>)
remove(Object)
removeAll(Collection<?>)
contains(Object)
containsAll(Collection<?>)
Теперь Collection
тип является хорошим примером, где может быть приемлемо исключение из этого правила , когда метод не «тянет свой вес» . Это, вероятно, имеет место для retainAll(Collection<?>)
, который не имеет эквивалентного retain(E)
метода. Впрочем, это может быть и регулярным нарушением этого правила.
Нарушение правила: карта
Это правило постоянно нарушается, в основном из-за того, что некоторые методы не увеличивают свой вес (что в конечном итоге является вопросом вкуса). С методами защиты Java 8 больше не будет оправданий для того, чтобы не добавлять реализации по умолчанию для полезных служебных методов, которые должны были быть в некоторых типах. Например: Map
. Это нарушает это правило пару раз:
- Это
keySet()
такжеcontainsKey(Object)
- Это
values()
такжеcontainsValue(Object)
- Имеет
entrySet()
но нетcontainsEntry(K, V)
Заметьте также, что нет смысла использовать термин Set
в именах методов. Подпись метода уже указывает, что у результата есть Set
тип. Это было бы более последовательным и симметричным , если эти методы будут уже были названы keys()
, values()
, entries()
. (В дополнение к этому, Sets
и Lists
еще одна тема, о которой я скоро напишу в блоге, так как я думаю, что эти типы также не имеют собственного веса)
В то же время Map
интерфейс нарушает это правило, предоставляя
put(K, V)
а такжеputAll(Map)
remove(Object)
, но нетremoveAll(Collection<?>)
Кроме того, нет необходимости устанавливать термин clear()
вместо повторного использования removeAll()
без аргументов. Это относится ко всем членам Collection API. На самом деле, clear()
метод также нарушает правило № 1. Это не сразу очевидно, если clear
что-то немного отличается от remove
удаления элементов коллекции.
Правило № 3: добавьте удобства за счет перегрузки
В большинстве случаев есть только одна веская причина, по которой вы хотите перегружать метод: удобство. Часто вы хотите делать одно и то же в разных контекстах, но создание этого очень специфического типа аргумента метода обременительно. Поэтому для удобства вы предлагаете своим пользователям API другой вариант того же метода с установленным «дружественным» типом аргумента. Это можно наблюдать снова в Collection
типе. У нас есть:
toArray()
, что является удобной перегрузкой…toArray(T[])
Другим примером является Arrays
служебный класс. У нас есть:
copyOf(T[], int)
, что является несовместимой перегрузкой …copyOf(boolean[], int)
, и из…copyOf(int[], int)
- … и все остальные
Перегрузка в основном используется по двум причинам:
- Предоставление поведения аргумента «по умолчанию», как в
Collection.toArray()
- Поддержка нескольких несовместимых, но «похожих» наборов аргументов, как в 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
служебного класса:
copyOf(T[], int)
, что является несовместимой перегрузкой …copyOf(boolean[], int)
copyOf(int[], int)
- … и все остальные
Но вы быстро заметите, что все методы в этом классе будут использовать массив в первую очередь. Несколько примеров:
binarySearch(Object[], Object)
copyOfRange(T[], int, int)
fill(Object[], Object)
sort(T[], Comparator<? super T>)
Нарушение правила: массивы
Этот же класс также «тонко» нарушает это правило, поскольку он помещает необязательные аргументы между другими аргументами при перегрузке методов. Например, он объявляет
Когда последний должен был быть 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
:
EntityManager.find()
методы возвращаются,null
если ни одна сущность не может быть найденаQuery.getSingleResult()
выдает,NoResultException
если ни одна сущность не может быть найдена
Как NoResultException
и RuntimeException
этот недостаток, он сильно нарушает принцип наименьшего удивления , так как вы можете не знать об этой разнице до времени выполнения!
Если вы настаиваете на исключении NoResultExceptions, сделайте их проверенными исключениями, поскольку клиентский код ДОЛЖЕН их обработать
Заключение и дальнейшее чтение
… вернее, дальнейшее наблюдение. Взгляните на презентацию Джоша Блоха по дизайну API. Он согласен с большинством моих требований, около 0:30:30
Другой полезный пример такой веб-страницы — «Контрольный список разработки Java API» от The Amiable API :
http://theamiableapi.com/2012/01/16/java-api-design-checklist/