«1. Обзор
Java — один из столпов мира открытого исходного кода. Почти каждый проект Java использует другие проекты с открытым исходным кодом, поскольку никто не хочет изобретать велосипед. Однако часто случается так, что нам нужна библиотека для ее функциональности, но мы понятия не имеем, как ее использовать. Мы сталкиваемся с такими вещами, как:
-
Что не так со всеми этими классами «*Service»? Как мне это создать, это требует слишком много зависимостей. Что такое «защелка»? О, я собрал это вместе, но теперь оно начинает бросать исключение IllegalStateException. Что я делаю неправильно?
Беда в том, что не все разработчики библиотек думают о своих пользователях. Большинство думает только о функциональности и функциях, но мало кто думает о том, как API будет использоваться на практике, как будет выглядеть и тестироваться пользовательский код.
Эта статья содержит несколько советов о том, как избавить наших пользователей от некоторых проблем — и нет, это не с помощью написания документации. Конечно, на эту тему можно было бы написать целую книгу (и было сделано несколько); это некоторые из ключевых моментов, которые я усвоил, работая над несколькими библиотеками.
Я проиллюстрирую идеи здесь, используя две библиотеки: charles и jcabi-github
2. Границы
Это должно быть очевидно, но часто это не так. Прежде чем приступить к написанию любой строки кода, нам нужно иметь четкий ответ на некоторые вопросы: какие входные данные необходимы? какой первый класс увидит мой пользователь? нам нужны какие-либо реализации от пользователя? каков результат? Как только на эти вопросы будут даны четкие ответы, все станет проще, так как в библиотеке уже есть подкладка, форма.
2.1. Ввод
Возможно, это самая важная тема. Мы должны убедиться, что понятно, что пользователь должен предоставить библиотеке, чтобы она выполняла свою работу. В некоторых случаях это очень тривиальный вопрос: это может быть просто строка, представляющая токен авторизации для API, но это также может быть реализация интерфейса или абстрактный класс.
Очень хорошая практика — брать все зависимости через конструкторы и делать их короткими с несколькими параметрами. Если нам нужен конструктор с более чем тремя-четырьмя параметрами, то код однозначно должен быть рефакторинг. И если методы используются для внедрения обязательных зависимостей, то пользователи, скорее всего, столкнутся с третьим разочарованием, описанным в обзоре.
Кроме того, мы всегда должны предлагать более одного конструктора, давать пользователям альтернативы. Позвольте им работать как со String, так и с Integer или не ограничивайте их FileInputStream, работайте с InputStream, чтобы они могли отправлять, возможно, ByteArrayInputStream при модульном тестировании и т. д.
Например, вот несколько способов, которыми мы можем создать экземпляр Github Точка входа API с использованием jcabi-github:
Github noauth = new RtGithub();
Github basicauth = new RtGithub("username", "password");
Github oauth = new RtGithub("token");
Просто, без суеты, без сомнительных объектов конфигурации для инициализации. И имеет смысл иметь эти три конструктора, потому что вы можете использовать веб-сайт Github, когда вы вышли из системы, вошли в систему или приложение может аутентифицироваться от вашего имени. Естественно, некоторые функции не будут работать, если вы не авторизованы, но вы знаете это с самого начала.
В качестве второго примера, вот как мы будем работать с charles, библиотекой для сканирования веб-сайтов:
WebDriver driver = new FirefoxDriver();
Repository repo = new InMemoryRepository();
String indexPage = "http://www.amihaiemil.com/index.html";
WebCrawl graph = new GraphCrawl(
indexPage, driver, new IgnoredPatterns(), repo
);
graph.crawl();
Я думаю, это также довольно очевидно. Однако при написании этого я понимаю, что в текущей версии есть ошибка: все конструкторы требуют, чтобы пользователь предоставил экземпляр IgnoredPatterns. По умолчанию никакие шаблоны не должны игнорироваться, но пользователь не должен указывать это. Я решил оставить это здесь, чтобы вы увидели встречный пример. Я предполагаю, что вы попытаетесь создать экземпляр WebCrawl и задаетесь вопросом: «Что случилось с этим IgnoredPatterns?!».
Переменная indexPage — это URL-адрес, с которого должно начинаться сканирование, драйвер — это используемый браузер (по умолчанию ничего не может быть так как мы не знаем, какой браузер установлен на работающей машине). Переменная repo будет объяснена ниже в следующем разделе.
«Итак, как вы видите в примерах, постарайтесь, чтобы это было просто, интуитивно понятно и не требует пояснений. Инкапсулируйте логику и зависимости таким образом, чтобы пользователь не ломал голову, глядя на ваши конструкторы.
Если у вас все еще есть сомнения, попробуйте сделать HTTP-запросы к AWS с помощью aws-sdk-java: вам придется иметь дело с так называемым AmazonHttpClient, который где-то использует ClientConfiguration, а затем должен взять ExecutionContext где-то посередине. . Наконец, вы можете выполнить свой запрос и получить ответ, но все еще не знаете, например, что такое ExecutionContext.
2.2. Вывод
Это в основном для библиотек, которые взаимодействуют с внешним миром. Здесь мы должны ответить на вопрос «как будет обрабатываться вывод?». Опять же, довольно забавный вопрос, но здесь легко ошибиться.
Посмотрите еще раз на приведенный выше код. Почему мы должны предоставить реализацию репозитория? Почему метод WebCrawl.crawl() просто не возвращает список элементов WebPage? Очевидно, что обработка просканированных страниц не входит в обязанности библиотеки. Откуда ему вообще знать, что мы хотели бы с ними сделать? Примерно так:
WebCrawl graph = new GraphCrawl(...);
List<WebPage> pages = graph.crawl();
Хуже быть не может. Исключение OutOfMemory может возникнуть из ниоткуда, если просканированный сайт содержит, скажем, 1000 страниц — библиотека загружает их все в память. Есть два решения этой проблемы:
-
Продолжайте возвращать страницы, но реализуйте некоторый механизм пейджинга, в котором пользователь должен будет указать начальный и конечный номера. Или попросите пользователя реализовать интерфейс с помощью метода export(List\u003cWebPage\u003e), который алгоритм будет вызывать каждый раз, когда будет достигнуто максимальное количество страниц
Второй вариант, безусловно, лучший; это делает вещи проще с обеих сторон и более проверяемо. Подумайте, сколько логики пришлось бы реализовать на стороне пользователя, если бы мы пошли по первому пути. Таким образом, указывается репозиторий для страниц (возможно, для отправки их в БД или записи на диск), и после вызова метода crawl() больше ничего делать не нужно.
Кстати, код из раздела «Ввод» выше — это все, что нам нужно написать, чтобы получить содержимое веб-сайта (все еще в памяти, как говорит реализация репозитория, но это наш выбор — мы предоставили эту реализацию, поэтому мы берем на себя риск).
Подводя итог этому разделу: мы никогда не должны полностью отделять нашу работу от работы клиента. Мы всегда должны думать о том, что происходит с продуктом, который мы создаем. Так же, как водитель грузовика должен помогать распаковывать товары, а не просто выбрасывать их по прибытии в пункт назначения.
3. Интерфейсы
Всегда используйте интерфейсы. Пользователь должен взаимодействовать с нашим кодом только через строгие контракты.
Например, в библиотеке jcabi-github класс RtGithub si является единственным, который фактически видит пользователь:
Repo repo = new RtGithub("oauth_token").repos().get(
new Coordinates.Simple("eugenp/tutorials"));
Issue issue = repo.issues()
.create("Example issue", "Created with jcabi-github");
Приведенный выше фрагмент кода создает тикет в репозитории eugenp/tutorials. Используются экземпляры Repo и Issue, но фактические типы никогда не раскрываются. Мы не можем сделать что-то вроде этого:
Repo repo = new RtRepo(...)
Вышеупомянутое невозможно по логической причине: мы не можем напрямую создать задачу в репозитории Github, не так ли? Сначала мы должны войти в систему, затем выполнить поиск в репозитории и только после этого мы можем создать задачу. Конечно, описанный выше сценарий можно было бы допустить, но тогда пользовательский код загрязнился бы большим количеством шаблонного кода: этому RtRepo, вероятно, пришлось бы брать какой-то объект авторизации через свой конструктор, авторизовать клиента и попадать в нужное репо. и т. д.
Интерфейсы также обеспечивают простоту расширения и обратной совместимости. С одной стороны, мы, как разработчики, обязаны соблюдать уже выпущенные контракты, а с другой стороны, пользователь может расширить предлагаемые нами интерфейсы — он может их украсить или написать альтернативные реализации.
Другими словами, абстрагируйте и инкапсулируйте как можно больше. Используя интерфейсы, мы можем сделать это элегантным и не ограничивающим образом — мы применяем архитектурные правила, предоставляя программисту свободу улучшать или изменять предоставляемое поведение.
«Чтобы закончить этот раздел, просто имейте в виду: наша библиотека, наши правила. Мы должны точно знать, как будет выглядеть код клиента и как он собирается его тестировать. Если мы этого не знаем, никто не узнает, и наша библиотека просто будет способствовать созданию кода, который трудно понять и поддерживать.
4. Третьи стороны
Имейте в виду, что хорошая библиотека — это легкая библиотека. Ваш код может решить проблему и быть функциональным, но если jar добавляет 10 МБ к моей сборке, то ясно, что вы давно потеряли чертежи своего проекта. Если вам нужно много зависимостей, вы, вероятно, пытаетесь охватить слишком много функций и должны разбить проект на несколько небольших проектов.
Будьте максимально прозрачными, по возможности не привязывайтесь к реальным реализациям. Лучший пример, который приходит на ум: используйте SLF4J, который является только API для ведения журнала — не используйте log4j напрямую, возможно, пользователь захочет использовать другие средства ведения журнала.
Документируйте библиотеки, которые транзитивно проходят через ваш проект, и убедитесь, что вы не включаете опасные зависимости, такие как xalan или xml-apis (почему они опасны, не является предметом рассмотрения в этой статье).
Суть в том, что ваша сборка должна быть легкой, прозрачной и всегда знать, с чем вы работаете. Это может избавить ваших пользователей от большей суеты, чем вы могли себе представить.
5. Заключение
В статье изложены несколько простых идей, которые могут помочь проекту оставаться на высоте в отношении удобства использования. Библиотека, являясь компонентом, который должен найти свое место в более широком контексте, должна быть мощной по функциональности, но при этом предлагать гладкий и хорошо продуманный интерфейс.
Это простой шаг за черту, который портит дизайн. Участники всегда будут знать, как его использовать, но кто-то новый, кто впервые увидит его, может не знать. Производительность важнее всего, и, следуя этому принципу, пользователи должны иметь возможность начать использовать библиотеку в считанные минуты.