Перейти к основному содержимому

Проектирование удобной для пользователя библиотеки Java

· 8 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

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<WebPage>), который алгоритм будет вызывать каждый раз, когда будет достигнуто максимальное количество страниц.

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

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

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

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

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

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

Repo repo = new RtGithub("oauth_token").repos().get(
new Coordinates.Simple("foreach/tutorials"));
Issue issue = repo.issues()
.create("Example issue", "Created with jcabi-github");

Фрагмент кода выше создает тикет в репозитории foreach/tutorials . Используются экземпляры Repo и Issue, но фактические типы никогда не раскрываются. Мы не можем сделать что-то вроде этого:

Repo repo = new RtRepo(...)

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

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

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

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

4. Третьи лица

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

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

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

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

5. Вывод

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

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