Разработка удобной библиотеки Java

Разработка удобной библиотеки Java

1. обзор

Java является одним из столпов мира с открытым исходным кодом. Почти каждый Java-проект использует другие проекты с открытым исходным кодом, поскольку никто не хочет изобретать велосипед. Тем не менее, часто случается, что нам нужна библиотека для ее функциональности, но мы понятия не имеем, как ее использовать. Мы сталкиваемся с такими вещами, как:

  • Что это со всеми этими классами * Service?

  • Как я это создаю, это требует слишком много зависимостей. Что такое «latch»?

  • Ой, собрал, а теперь начинает выкидыватьIllegalStateException. Что я делаю неправильно?

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

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

Я проиллюстрирую идеи здесь, используя две библиотеки:charles иjcabi-github.

2. границы

Это должно быть очевидно, но во многих случаях это не так. Прежде чем начать писать какую-либо строку кода, нам нужно получить четкий ответ на некоторые вопросы: какие входные данные необходимы? Какой первый класс увидит мой пользователь? нам нужны какие-либо реализации от пользователя? какой вывод? Как только на эти вопросы четко дан ответ, все становится проще, поскольку у библиотеки уже есть подкладка, форма.

2.1. вход

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

Хорошей практикой является прохождение всех зависимостей через конструкторы и их краткость с несколькими параметрами. Если нам нужен конструктор с более чем тремя или четырьмя параметрами, тогда код должен быть явно реорганизован. И если методы используются для введения обязательных зависимостей, то пользователи, скорее всего, в конечном итоге столкнутся с третьим разочарованием, описанным в обзоре.

Также мы всегда должны предлагать более одного конструктора, предлагать пользователям альтернативы. Пусть они работают как сString, так и сInteger или не ограничивают ихFileInputStream, работают сInputStream, чтобы они могли отправить, возможно,ByteArrayInputStream, когда модульное тестирование и т. д.

Например, вот несколько способов создания точки входа API Github с помощью 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 ?!»

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

Итак, как вы видите в примерах, постарайтесь сделать его простым, интуитивно понятным и не требующим пояснений. Инкапсулируйте логику и зависимости таким образом, чтобы пользователь не почесал в затылке, глядя на ваши конструкторы.

Если у вас все еще есть сомнения, попробуйте сделать HTTP-запросы к AWS, используяaws-sdk-java: вам придется иметь дело с так называемым AmazonHttpClient, который где-то использует ClientConfiguration, а затем должен взять ExecutionContext где-то посередине. Наконец, вы можете выполнить ваш запрос и получить ответ, но все равно не знаете, что такое, например, ExecutionContext.

2.2. Выход

Это в основном для библиотек, которые общаются с внешним миром. Здесь мы должны ответить на вопрос «как будет обрабатываться вывод?». Опять же, довольно забавный вопрос, но ошибиться легко.

Посмотрите еще раз на код выше. Почему мы должны предоставлять реализацию репозитория? Почему метод WebCrawl.crawl () не возвращает просто список элементов WebPage? Совершенно очевидно, что работа библиотеки не заключается в том, чтобы обрабатывать просканированные страницы. Откуда ему вообще знать, что мы хотели бы с ними сделать? Что-то вроде этого:

WebCrawl graph = new GraphCrawl(...);
List pages = graph.crawl();

Ничего не может быть хуже. Исключение OutOfMemory может произойти из ниоткуда, если просканированный сайт имеет, скажем, 1000 страниц - библиотека загружает их все в память. Есть два решения этого:

  • Продолжайте возвращать страницы, но реализуйте некоторый механизм подкачки, при котором пользователь должен будет указывать начальный и конечный номера. Or

  • Попросите пользователя реализовать интерфейс с методом с именем export (List ), который алгоритм будет вызывать каждый раз, когда будет достигнуто максимальное количество страниц.

Второй вариант, безусловно, лучший; это упрощает работу с обеих сторон и является более тестируемым. Подумайте, сколько логики пришлось бы реализовать на стороне пользователя, если бы мы выбрали первое. Таким образом, указывается Репозиторий для страниц (возможно, для отправки их в БД или записи на диск), и ничего больше не нужно делать после вызова метода crawl ().

Кстати, код из раздела «Ввод» выше - это все, что нам нужно написать, чтобы получить содержимое веб-сайта (все еще в памяти, как говорит реализация репо, но это наш выбор - мы предоставили эту реализацию так мы рискуем).

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

3. Интерфейсы

Всегда используйте интерфейсы. Пользователь должен взаимодействовать с нашим кодом только через строгие контракты.

Например, в библиотекеjcabi-github класс RtGithub - единственный, который реально видит пользователь:

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. Экземпляры Repo и Issue используются, но фактические типы никогда не раскрываются. Мы не можем сделать что-то вроде этого:

Repo repo = new RtRepo(...)

Вышесказанное невозможно по логической причине: мы не можем напрямую создать проблему в репозитории Github, не так ли? Сначала мы должны войти в систему, затем выполнить поиск в репозитории, и только тогда мы сможем создать проблему. Конечно, описанный выше сценарий может быть разрешен, но тогда код пользователя будет загрязнен большим количеством шаблонного кода: этотRtRepo, вероятно, должен будет принять какой-то объект авторизации через свой конструктор, авторизовать клиента и получить вправо репо и др.

Интерфейсы также обеспечивают простоту расширения и обратной совместимости. С одной стороны, мы, как разработчики, обязаны соблюдать уже выпущенные контракты, а с другой стороны, пользователь может расширять предлагаемые нами интерфейсы - он может их украшать или писать альтернативные реализации.

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

Чтобы закончить этот раздел, просто помните: наша библиотека, наши правила. Мы должны точно знать, как будет выглядеть код клиента и как он будет его тестировать. Если мы этого не знаем, никто не сделает этого, и наша библиотека просто внесет свой вклад в создание кода, который трудно понять и поддерживать.

4. Третьи стороны

Имейте в виду, что хорошая библиотека - это легковесная библиотека. Ваш код может решить проблему и быть работоспособным, но если jar добавит 10 МБ к моей сборке, тогда станет ясно, что вы давно потеряли чертежи своего проекта. Если вам нужно много зависимостей, вы, вероятно, пытаетесь охватить слишком много функций и должны разбить проект на несколько небольших проектов.

Будьте максимально прозрачны, по возможности не привязывайтесь к реальным реализациям. Лучший пример, который приходит на ум: использовать SLF4J, который является только API для ведения журнала - не использовать log4j напрямую, возможно, пользователь захочет использовать другие средства ведения журнала.

Библиотеки документов, которые проходят через ваш проект транзитивно и следят за тем, чтобы вы не включали опасные зависимости, такие какxalan илиxml-apis (почему они опасны, в этой статье не рассматривается).

Суть в том, что ваша сборка должна быть легкой, прозрачной и всегда знать, с чем вы работаете. Это может спасти ваших пользователей больше, чем вы могли себе представить.

5. Заключение

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

Это легкий шаг над линией и вносит беспорядок в дизайн. Авторы всегда будут знать, как его использовать, но кто-то новичок, который первым увидит это, может не знать. Производительность является наиболее важной из всех, и следуя этому принципу, пользователи должны иметь возможность начать использовать библиотеку в считанные минуты.