Вот простой шаблон, который вы можете использовать, чтобы сделать ваши API расширяемыми, даже третьими лицами, не жертвуя своей способностью поддерживать обратную совместимость.
Очень часто , чтобы создать библиотеку , которая имеет две «стороны» — в сторону API и стороны SPI . API — это то, что приложения вызывают для использования библиотеки. SPI (Service Provider Interface) — это то, как обеспечивается функциональность — например, доступ к различным видам ресурсов.
Одним из примеров этого является JavaMail: для чтения / записи сообщений электронной почты вы вызываете API JavaMail . Когда вы запрашиваете почтовое хранилище для, скажем, почтового сервера IMAP, библиотека JavaMail ищет всех провайдеров, зарегистрированных (внедренных) в пути к классам, и пытается найти того, который поддерживает этот протокол. Обработчик протокола записывается в SPI JavaMail . Если он найдет его, вы сможете получать сообщения с IMAP-серверов, используя его. Но ваш клиентский код только когда-либо вызывает JavaMail API — ему не нужно ничего знать о поставщике услуг IMAP.
Есть одна очень большая проблема с тем, как это обычно делается: классы API действительно должны быть final
почти во всех случаях. Классы SPI должны быть абстрактными классами, если проблемный домен не очень четко определен, и в этом случае интерфейсы имеют смысл (вы можете использовать любой из них, но в нечетко определенном проблемном домене вы можете со временем создать вещи с ужасные имена вроде LayoutManager2
).
Я не буду вдаваться в подробности о том, почему это так здесь (мой друг Jarda делает это в своей новой книге, и мы обсуждаем это в нашей книге « Программирование с помощью богатых клиентов» ). В сокращенной форме причины:
- Вы можете доказательно совместимо добавлять методы в финальный класс. И если класс является окончательным, этот факт имеет коммуникационную ценность — он сообщает пользователю этого класса, что это не то, что им может понадобиться реализовать, где интерфейс будет более запутанным.
- Вы можете совместимо удалять методы из интерфейса SPI или абстрактного класса в обратном направлении, если ваша библиотека — это единственная вещь, которая когда-либо будет вызывать SPI напрямую — это ваша библиотека. В более старых реализациях метод по-прежнему будет существовать, просто он никогда не будет вызываться (в модульной среде, такой как модульная система NetBeans, OSGi или, предположительно, JSR-277, вы могли бы реализовать это, поместив API и SPI в отдельные файлы JAR, поэтому клиент даже не видит классы SPI).
Незначительным преимуществом использования абстрактных классов является то, что вы можете
полусовместимо добавлять неабстрактные методы в абстрактный класс позже. Но помните, что вы рискуете, что у кого-то будет подкласс с тем же именем метода и аргументами и несовместимым типом возврата (JDK фактически сделал это нам однажды в NetBeans, добавив
Exception.getCause()
в JDK 1.3). Таким образом, добавление методов к общедоступному неконечному классу в API
является несовместимым изменением.
Учитывая эти ограничения, что произойдет, если вы смешаете API и SPI в одном классе (что и делают JavaMail и большинство стандартов Java)? Ну, вы не можете добавлять методы совместимо, потому что это может сломать подклассы. И вы не можете удалить их совместимо, потому что клиенты могут звонить им. Вы застряли. Вы не можете совместимо добавлять или удалять что-либо из существующих классов.
Как я уже писал в другом месте, это безумие, когда поставщик сервера приложений должен реализовывать интерфейсы и классы, которые его клиенты вызывают напрямую — именно по этой причине. Это было бы намного чище, и позволяло бы API-интерфейсам Java развиваться намного быстрее, если бы API и SPI были полностью разделены.
Но часть призыва к производителям, в лучшую или в худшую сторону, реализовать эти спецификации, заключается в том, что они могут расширять их нестандартными способами, которые привязывают разработчиков, использующих эти расширения, к их конкретной реализации. Такое поведение не полностью сводится к тому, чтобы быть злым и ограничивать людей. Существует реальная основа для инноваций на вершине стандарта — так развиваются стандарты, и некоторым людям потребуется функциональность, которую стандарт еще не поддерживает.
Введите шаблон возможностей. Шаблон возможностей очень и очень прост. Это выглядит так:
public <T> getCapability (Class<T> type);
Это оно! Это невероятно просто! У этого есть одно предостережение:
любой вызов getCapability()
должен сопровождаться нулевой проверкой . Но это намного чище, чем ловить
UnsupportedOperationException
с, или
if (foo.isAbleToDoX()) foo.doX()
или
if (foo instanceof DoerOfX) ((DoerOfX) foo).doX()
. Нулевая проверка хороша, проста и чиста в сравнении. Это позволяет системе типов Java работать
на вас, вместо того, чтобы бороться с ней.
Теперь, что вы можете с этим сделать? Вот пример. В моем предыдущем блоге я представил альтернативный дизайн, как вы могли бы сделать что-то вроде SwingWorker
. Он содержит класс с именем TaskStatus
, который абстрагирует данные о состоянии задачи от самого объекта, выполняющего задачу. Это простой интерфейс с сеттерами, которые позволяют фоновому потоку информировать другой объект (предположительно, пользовательский интерфейс) о ходе выполнения задачи.
В свете того, что мы только что обсудили, TaskStatus
действительно должен быть последний класс. Итак, давайте немного перепишем это, чтобы выглядеть так. Мы будем использовать зеркальный класс для SPI.
public final class TaskStatus {
private final StatusImpl impl;
TaskStatus (StatusImpl impl) {
this.impl = impl;
}
public void setTitle (String title) {
impl.setTitle (title);
}
public void setProgress (String msg, long progress, long min, long max) {
//We could do argument sanity checks here and make life
//simpler for anyone implementing StatusImpl
impl.setProgress (msg, progress, min, max);
}
public void setProgress (String msg) {
//...you get the idea
//...
}
public abstract class StatusImpl {
public abstract void setTitle (String title);
public abstract void setProgress (String msg, long progress, long min, long max);
public abstract void setProgress (String msg); //indeterminate mode
public abstract void done();
public abstract void failed (Exception e);
}
Итак, у нас есть API, который обрабатывает отображение основного состояния. Но люди собираются изобретать новые аспекты отображения статуса. Мы не можем спасти мир и решить все проблемы со статусом задачи, даже если они о них не подумают — и мы не должны пытаться это делать. Мы не хотим настраивать вещи так, чтобы мы могли реализовать все, что когда-либо захочет мир. К счастью, так не должно быть.
Поскольку мы разработали наш API так, чтобы его можно было совместимо добавлять, мы позволяем остальному миру придумывать вещи, которые им нужны для отображения статуса задачи, и те, которые нужны многим, могут быть добавлены в наш API в будущее. Шаблон возможностей позволяет нам сделать это. Мы добавляем два метода к нашим классам API и SPI:
public abstract class StatusImpl {
//...
public <T> T getCapability (Class<T> type);
}
public final class TaskStatus {
//...
public <T> T getCapability (Class<T> type) {
return impl.getCapability (type);
}
}
Давайте применим это на практике. Кто-то может захотеть показать, сколько времени осталось до выполнения задачи. Наш API не справляется с этим. Через образец возможностей мы можем добавить это. Мы (или кто-либо другой StatusImpl
) можем создать следующий интерфейс:
public interface StatusTime {
public void setTimeRemaining (long milliseconds);
}
Задача, которая хочет предоставить эту информацию пользовательскому интерфейсу, если пользовательский интерфейс поддерживает ее, просто делает это:
public T runInBackground (TaskStatus status) {
StatusTime time = status.getCapability (StatusTime.class);
for (...) {
//do some slow work...
if (time != null) {
long remaining = //estimate the time remaining
time.setTimeRemaining (remaining);
}
}
}
Более того, наш Task
API прямо сейчас не привязан конкретно к Swing или AWT — его можно использовать для всего, что должно следовать шаблону вычисления чего-либо в фоновом потоке, а затем выполнять работу над другим. Почему бы не оставить его привязанным к инструментарию пользовательского интерфейса? Все, что нам нужно сделать, — это сделать код, который фактически обрабатывает подключаемый модуль потоков (я расскажу о том, как вы это делаете, просто используя путь к классам Java для внедрения зависимостей в моем следующем блоге). Затем результат можно использовать также с SWT или Thinlet, или даже в приложении на стороне сервера. Вместо этого у SwingWorker
нас есть AnythingWorker
!
Но мы знаем, что нам нужен пользовательский интерфейс — и мы знаем, что нацеливаемся на Swing прямо сейчас. Как мы можем на самом деле сохранить этот код полностью свободным от кода пользовательского интерфейса и в то же время сделать его полезным?
Образец возможностей снова приходит нам на помощь — очень очень просто. Реальное приложение, использующее этот пользовательский интерфейс, просто извлекает фабрику по умолчанию для StatusImpls (вам нужна такая вещь, если вы хотите запускать несколько одновременных фоновых задач и показывать состояние для каждой — мой следующий блог объяснит, как это можно внедрить, просто вставив JAR в путь к классу) и делает что-то вроде:
Component statusUi = theFactory.getCapability (Component.class);
if (statusUi != null) {
statusBar.add (statusUi);
}
(или если мы хотим разрешить только одну фоновую задачу за раз, мы можем забыть о фабрике и поместить код извлечения компонентов непосредственно в нашу реализацию
StatusImpl
).
Если вы знакомы с API-интерфейсом поиска NetBeans , шаблон возможностей на самом деле является его упрощением (за исключением результатов на основе коллекции и прослушивания изменений).
Дело в том, что шаблон возможностей позволяет вам иметь API, который полностью состоит из хороших, перспективных, эволюционируемых final
классов, но API расширяем, даже если он является окончательным . Результатом является то, что API может развиваться быстрее, с меньшим количеством беспокойств о нарушении чьего-либо существующего кода. Что сокращает время цикла для улучшения существующих библиотек, и все наше программное обеспечение развивается и улучшается быстрее, что хорошо для всех.
Это также помогает избежать попыток «спасти мир» — с учетом расширяемости можно создать полезный API-интерфейс без необходимости обрабатывать все возможные вещи, которые кто-либо может захотеть сделать в этой проблемной области. Попытка спасти мир — вот что ведет к незаметным и незавершенным проектам. В этом уроке я обсуждаю принцип « не пытайся сохранить мир» на практическом примере.
Дизайн зеркального класса кажется немного мазохистским? Я думаю, что это указывает на слабость в ограниченных правилах языка Java. Определенно было бы лучше иметь возможность на уровне методов сделать некоторые методы видимыми для некоторых типов клиентов, а другие методы — для других типов клиентов. Но, несмотря на это, еще более мазохистски оказаться «нарисованным в углу» [1] и неспособным исправлять ошибки или добавлять функции, не нарушая чей-то код. Вот так у вас получаются десятилетние нефиксированные ошибки.
[1] нарисованный в углу — английская идиома, означающая оставить себя без вариантов — вы рисовали пол комнаты таким образом, что оказались в неокрашенном углу комнаты и не могли уйти угол, пока краска не высохнет.