Erhalten Sie Zugang zu diesem und mehr als 300000 Büchern ab EUR 5,99 monatlich.
API представляют собой контракты, которые определяют принципы взаимодействия приложений, сервисов и компонентов. Паттерны проектирования API — это набор лучших практик, спецификаций и стандартов, обеспечивающих простоту и надежность их использования для разработчиков. Книга объединяет и объясняет наиболее важные паттерны, используемые сообществом разработчиков API и экспертами Google. Паттерны проектирования API определяют набор принципов для разработки внутренних и публичных API. Джей Джей Гивакс, будучи специалистом из Google, рассказывает о паттернах, которые обеспечат вашим API согласованность, масштабируемость и гибкость. Вы узнаете, как улучшить дизайн самых распространенных API и как действовать в сложных пограничных случаях. Понятные иллюстрации, актуальные примеры и подробные сценарии позволят тщательно разобраться в каждом паттерне.
Sie lesen das E-Book in den Legimi-Apps auf:
Seitenzahl: 625
Veröffentlichungsjahr: 2024
Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:
Переводчик Д. Брайт
Джей Джей Гивакс
Паттерны проектирования API. — СПб.: Питер, 2023.
ISBN 978-5-4461-1984-4
© ООО Издательство "Питер", 2023
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Посвящается Ка-эль и Луке. Вы потрясающие.
Все началось с электронной ударной установки. Летом 2019 года один мой друг увлек меня игрой на ней, и я погрузился в это занятие с головой. Иногда я действительно играл на барабанах, но бóльшую часть времени все же проводил за написанием кода, который позволял управлять конфигурацией установки с помощью команд MIDI SysEx.
Когда нагрянула пандемия COVID-19, у меня внезапно появились иные приоритеты, направленные на настройку аудиовизуальной связи в нашей местной церкви, где мы перешли в формат удаленного проведения служб и рассматривали возможности возобновления личных встреч. Для этого мне пришлось изучать протоколы VISCA, NDI и OSC (для камер и аудиомикшеров), а также осваивать программно-ориентированную интеграцию с Zoom, VLC, PowerPoint, Stream Deck и др.
Эти проекты не несут в себе больших объемов бизнес-логики. Практически весь код относится к интеграции, что одновременно и раздражает, и дает большие возможности. Раздражение вызывали протоколы, которые либо были плохо задокументированы, либо не предназначались для тех задач, которые я пытался реализовать с их помощью, либо просто не согласовывались друг с другом. Положительная сторона всего этого заключается в том, что, как только вам удастся разобраться с интеграцией, вы сможете очень легко писать полезные приложения.
Несмотря на то что моя работа в последние несколько лет заключалась в основном в локальной интеграции, точно такие же раздражение и ощущение больших возможностей возникают и при работе с веб-API. Каждый опыт выбора нового веб-API представляет собой кривую, состоящую из эмоциональных реакций в виде волнения, замешательства, раздражения, принятия и достигаемого в итоге умиротворения. Как только ты досконально осваиваешь мощный API, возникает чувство, что ты являешься дирижером величественного оркестра, готового играть любую предложенную тобой музыку, — даже если ноты скрипачей согласуются лишь под конец, а для группы медных духовых без каких-либо очевидных причин приходится использовать палочку другого цвета.
Данная книга сама по себе не изменит положение дел. Это всего лишь книга. Но если после прочтения вы будете следовать указаниям, изложенным в ней, то она поможет вам обеспечить пользователям более качественный опыт. Если же эту книгу прочтет множество людей, то вместе мы сможем добиться сдвига в направлении более согласованного и менее раздражающего опыта работы с API.
Важно понять, что сама эта книга в целом гораздо более ценна, чем сумма ее фрагментов. Относительно любого из разбираемых Джей Джеем паттернов любая команда может сделать свой обдуманный выбор (хотя кто-то может упустить некоторые из указанных здесь пограничных случаев). Для конкретной ситуации этот выбор может оказаться даже более удачным, чем предложенные в книге рекомендации, ввиду ограниченных требований контекста. Такой подход позволяет получить множество локальных оптимальных решений, но дает очень фрагментированную общую картину, поскольку в этом случае даже в API одной компании потенциально применяется несколько разрозненных техник.
Помимо согласованности для любой отдельно взятой задачи, книга также предлагает единый подход, охватывающий множество сфер проектирования API. Разработчикам программных интерфейсов редко предоставляется пространство для углубленного анализа этих аспектов, и я считаю, что мне очень повезло работать с Джей Джеем и другими (в частности, с Люком Снирингером (Luke Sneeringer) над обсуждением многих тем книги. Я счастлив, что вклад, внесенный компанией Google в проектирование API, может быть весьма полезным многим разработчикам благодаря этой книге и системе AIP, расположенной по адресу https://aip.dev.
Я абсолютно уверен в ценности этой книги, но она не упростит разработку API. Однако она позволит вам избавиться от ненужных сложностей, которые возникают в ходе проектирования интерфейсов, давая возможность сосредоточиться только на том, что действительно уникально для ваших API. Будьте готовы думать, и думать усиленно, но также знайте, что результатом этих размышлений может стать API, работа с которым доставит лишь удовольствие. Ваши пользователи могут никогда не поблагодарить вас за это открыто. Ведь хорошо спроектированный API зачастую воспринимается как само собой разумеющийся, хоть и является результатом огромных усилий. Но зато вы сможете спать спокойно, зная, что ваши пользователи не будут злиться при работе с API, который пусть и функционирует, но сделан на троечку.
Используйте эту книгу как основу, чтобы построить такой API, который сможет стать фундаментом для других.
Джон Скит (Jon Skeet),Staff Developer Relations Engineer, Google
Многие из нас изучали информатику. Мы анализировали среду выполнения и пространственную сложность алгоритмов с помощью нотации «О большое», знакомились со всевозможными алгоритмами сортировки и изучали различные способы обхода бинарных деревьев. Поэтому, окончив колледж, я планировал, что моя работа будет в первую очередь связана с математикой и точными науками. Но каким же было мое удивление, когда в действительности все оказалось совсем не так.
На деле бо́льшая часть работы была связана с проектированием, структурированием и дизайном, а не математикой и алгоритмами. Мне никогда не приходилось думать о том, какой алгоритм сортировки использовать, поскольку для этого всегда имелась библиотека (обычно что-то вроде array.sort()). Однако мне все же доводилось долго и упорно размышлять над создаваемыми классами, над функциями для них и параметрами, которые должна принимать каждая функция. Все это оказалось намного более сложным, чем я ожидал.
В реальном мире я узнал, что идеально оптимизированный код далеко не столь же ценен, как хорошо спроектированный. И это оказалось вдвойне верным в отношении веб-API, поскольку они, как правило, имеют гораздо более обширную аудиторию и намного больший спектр случаев применения.
Но здесь напрашиваются вопросы: что значит «хорошо спроектированное ПО»? Что такое «хорошо спроектированный API»? Чтобы ответить на эти вопросы, довольно долго мне приходилось полагаться в основном на случайный набор ресурсов. Для одних тем я отыскивал интересные посты, которые раскрывали популярные современные альтернативы. Для других находил отдельные полезные ответы на Stack Overflow, которые подсказывали нужное направление. Однако во многих случаях было довольно мало материалов по рассматриваемой теме, и мне приходилось самостоятельно придумывать ответы, надеясь, что мои коллеги не слишком их осудят.
Спустя многие годы подобных изысканий (и таскания повсюду блокнота с надписью «Пугающие проблемы API») я наконец-то решил, что пришло время записать всю информацию, которую я собрал и использовал в работе. Поначалу все это выглядело как набор правил для Google, которые мы с Люком Снирингером систематизировали и которые в конечном итоге стали AIP.dev. Но эти правила выглядели как свод законов. В них говорилось, что нужно делать, но не пояснялось, почему нужно делать именно так. После долгого анализа и исследований, сопровождавшихся непрестанным повторением в уме этого вопроса, я готов представить вам эти правила, а также пояснить их причины.
Несомненно, было бы замечательно, если бы эта книга стала абсолютным решением для всех проблем в мире проектирования API, но, к сожалению, этого не случится. И причина тому проста: как и в архитектуре, выбор любой модели проектирования обычно опирается на личное мнение. Это означает, что одни из вас могут посчитать мои указания удачными и элегантными и будут использовать их для всех последующих проектов. Другие же могут решить, что эта книга демонстрирует отвратительные и чрезмерно ограниченные модели дизайна, и в связи с этим, наоборот, воспринять их в качестве примера того, как не нужно проектировать API. Поскольку я не могу угодить всем, моей единственной целью было предоставить набор проверенных в деле рекомендаций, подкрепив их логическими обоснованиями, объясняющими, почему они выглядят именно так.
Будете ли вы использовать их в качестве примеров для подражания или избегать — дело ваше. По меньшей мере надеюсь, что рассмотренные в книге темы вызовут много дискуссий и инициируют дальнейшую масштабную работу в этом удивительном, сложном и запутанном мире проектирования API.
Как и бо́льшая часть моей работы, эта книга стала результатом усилий множества людей. В первую очередь хочу поблагодарить свою жену, Ка-эль, за то, что выслушивала мои разглагольствования и жалобы в трудный период окончательной доработки рукописи. Велика вероятность, что не будь постоянной поддержки Ка-эль, я мог бы забросить работу над книгой. Кроме того, аналогичную роль сыграли и многие другие люди, включая Кристен Раньери, Бекки Сусел, Джанетт Кларк, Норриса Кларка, Тадж Кларка, Шерин Чан, Асфию Фазал и Адаму Диалло (Kristen Ranieri, Becky Susel, Janette Clarke, Norris Clarke, Tahj Clarke, Sheryn Chan, Asfia Fazal, Adama Diallo), которым я также очень благодарен.
Команда приверженцев API сыграла важную роль в рецензировании и обсуждении тем книги, а также предоставила ценные рекомендации. В частности, хочу сказать спасибо Эрику Брюэру, Хонгу Чжану, Люку Снирингеру, Джону Скиту, Альфреду Фуллеру, Энджи Лин, Тибо Хоттелье, Гаррету Джонсу, Тиму Берксу, Маку Ахмаду, Карлосу О’Райану, Маршу Гардинеру, Майку Кистлеру, Эрику Уилеру, Максу Россу, Марку Якобсу, Джейсон Вудард, Майклу Рубину, Майло Мартину, Брэду Майерсу, Сэму Маквити, Робу Клевенджеру, Майку Шварцу, Льюису Дейли, Майклу Ричардсу и Брайану Гранту (Eric Brewer, Hong Zhang, Luke Sneeringer, Jon Skeet, Alfred Fuller, Angie Lin, Thibaud Hottelier, Garrett Jones, Tim Burks, Mak Ahmad, Carlos O’Ryan, Marsh Gardiner, Mike Kistler, Eric Wheeler, Max Ross, Marc Jacobs, Jason Woodard, Michael Rubin, Milo Martin, Brad Meyers, Sam McVeety, Rob Clevenger, Mike Schwartz, Lewis Daly, Michael Richards, Brian Grant) за всю их помощь в течение нескольких лет.
Кроме того, многие участвовали в создании книги косвенно, выполняя возложенную на них часть работы, за что я выражаю признательность Рою Филдингу, Банде четырех (Эрих Гамма, Ричард Хелм, Ральф Джонсон и Джон Влиссидес), Санджаю Гемаватту, Урсу Хельцле, Эндрю Файксу, Шону Куинлану и Ларри Гринфилду (Roy Fielding, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Sanjay Ghemawatt, Urs Hoelzle, Andrew Fikes, Sean Quinlan, and Larry Greenfield). Хочу также поблагодарить Стю Фельдмана, Ари Балога, Рича Санци, Йорга Хайлига, Эяль Манора, Юрия Израилевского, Уолта Драммонда, Цезаря Сенгупту и Патрика Тео (Stu Feldman, Ari Balogh, Rich Sanzi, Joerg Heilig, Eyal Manor, Yury Izrailevsky, Walt Drummond, Caesar Sengupta, Patrick Teo) за их поддержку и советы при изучении рабочих тем в Google.
Отдельная благодарность Дэйву Наглу (Dave Nagle) за поддержку в области рекламы, облачных сервисов, API и прочих аспектов, а также за то, что помог мне выйти из зоны комфорта. Выражаю признательность и Марку Чедвику (Mark Chadwick), который более десяти лет назад помог мне преодолеть синдром самозванца в проектировании API. Конструктивная обратная связь Марка и его добрые слова во многом подтолкнули меня погрузиться в изучение этой интересной области компьютерной науки. Отдельное спасибо говорю Марку Хаммонду (Mark Hammond), который научил меня ставить все под сомнение.
Данный проект оказался бы невозможным без команды Manning, в частности Майка Стивенса (Mike Stephens) и Маржаны Байс (Marjan Bace), которые одобрили саму идею книги, а также Кристины Тейлор (Christina Taylor), посвятившей себя моему очередному долгосрочному проекту. Выражаю признательность также Элу Кринкеру (Al Krinker) за детальный обзор всех глав, редактору проекта Дейрдре Хиаму (Deirdre Hiam), литературному редактору Мишель Митчелл (Michele Mitchell), корректору Кери Хэйлз (Keri Hales) и техническому редактору Ивану Мартиновичу (Ivan Martinovic). Спасибо всем сотрудникам Manning, кто помог воплотить этот проект в жизнь.
Благодарю всех рецензентов: Акшата Пола, Энтони Крэмпа, Брайана Дейли, Криса Хенегана, Даниэль Бретой, Дэвида Дж. Бисака, Дениза Вехби, Херардо Лекароса, Жана Лазару, Джона К. Гунвальдсона, Хорхе Эсекьель Бо, Йорта Роденбурга, Люка Купку, Марка Ненадова, Рахул Райа, Ричарда Янга, Роджера Доуэлла, Рубена Вандегинсте, Сатей Кумар Саху, Ставена Смита, Юла Уильямса, Юрия Бодарева и Зохеба Айнапора (Akshat Paul, Anthony Cramp, Brian Daley, Chris Heneghan, Daniel Bretoi, David J. Biesack, Deniz Vehbi, Gerardo Lecaros, Jean Lazarou, John C. Gunvaldson, Jorge Ezequiel Bo, Jort Rodenburg, Luke Kupka, Mark Nenadov, Rahul Rai, Richard Young, Roger Dowell, Ruben Vandeginste, Satej Kumar Sahu, Steven Smith, Yul Williams, Yurii Bodarev, Zoheb Ainapore). Ваши рекомендации помогли улучшить эту книгу.
Я написал эту книгу, стремясь предложить разработчикам безопасные, гибкие и многократно используемые паттерны создания API. Она начинается с рассмотрения ряда общих принципов проектирования, и далее описывается набор паттернов проектирования, которые могут предоставить простые решения для типичных сценариев создания API.
Эта книга для всех, кто уже разрабатывает веб-API или планирует заняться его разработкой, в особенности собираясь сделать его публичным. Будет хорошо, если читатели будут знать о некоторых форматах сериализации (например, JSON, Google Protocol Buffers или Apache Thrift) и распространенных парадигмах хранения данных (например, схемах реляционных баз данных), но эти знания вовсе не обязательны. Будет кстати и наличие опыта работы с HTTP и его методами (например, GET и POST), поскольку именно этот протокол был выбран для использования в примерах книги. Если в процессе разработки API вы столкнетесь со сложностями и подумаете: «С этим уже точно кто-то разобрался», — то эта книга для вас.
Книга разделена на шесть частей. Первые две посвящены более общим темам разработки API, а последующие четыре — самим паттернам проектирования. В части I закладывается фундамент для всего последующего материала, предоставляется ряд определений и оценочных схем для веб-API и паттернов проектирования, которые мы будем применять к этим веб-API в дальнейшем.
• Глава 1 начинается с точного определения понятия API и разъяснения важности этого программного продукта. Здесь же предоставляется своеобразная схема для оценки качества API.
• В главе 2 продолжает рассматриваться тема главы 1, изучаются способы применения паттернов проектирования к API и дается объяснение того, в чем заключается их польза для разработчиков. Здесь также разбирается анатомия паттерна проектирования API и приводится краткий пример того, как использование одного из таких паттернов может помочь получить более качественный API.
В части II продолжается раскрытие материала на основе заложенного фундамента, выделяются несколько общих принципов дизайна, которые нужно учитывать при создании любого API.
• В главе 3 разбираются все компоненты, которым может потребоваться присвоение имен в API, и поясняется, на что необходимо обращать внимание при выборе этих имен. Здесь также демонстрируется, почему именование является критически важным, хотя и кажется чем-то незначительным.
• В главе 4 описываются более крупные API, где могут использоваться несколько взаимосвязанных ресурсов. Рассматривается ряд вопросов, актуальных при выборе ресурсов и их связей. Завершается глава разбором нескольких примеров того, чего следует избегать.
• В главе 5 описано, как правильно использовать в API различные типы данных и предустановленные для них значения. В ней охвачены наиболее распространенные типы, такие как строки и числа, а также более сложные наподобие карт и списков.
В части III мы переходим к началу каталога паттернов проектирования и первыми рассмотрим фундаментальные паттерны, которые должны применяться практически ко всем API.
• В главе 6 представлен подробный разбор способов идентификации ресурсов пользователями API, с погружением в детали идентификаторов, такие как tombstoning (отметка об удалении), набор символов и кодировки, а также использование контрольных сумм для различения между отсутствующими и недействительными ID.
• В главе 7 подробно описывается, как должны работать различные стандартные методы веб-API (Get, List, Create, Update и Delete). В ней также объясняется, почему важно, чтобы каждый стандартный метод вел себя совершенно одинаково во всех ресурсах, а не менялся в зависимости от особенностей каждого из них.
• В главе 8 особое внимание уделяется двум стандартным методам (Get и Update) и показывается, как пользователи могут взаимодействовать не со всем ресурсом сразу, а с отдельными его частями. Кроме того, в ней разъясняется, почему это необходимо и полезно (и для пользователей, и для API), а также то, как сделать поддержку этой функциональности максимально ненавязчивой.
• В главе 9 мы выйдем за рамки стандартных методов и откроем путь к реализации в API любых нужных нам действий с помощью пользовательских методов. Особое внимание уделяется объяснению того, когда такие методы имеют смысл (и когда нет), а также тому, как принимать это решение в собственных API.
• В главе 10 рассматривается уникальный сценарий, в котором методы API могут не быть мгновенными, и поясняется, как поддерживать это удобным для пользователей образом с помощью длительных операций (longrunning operations, LRO). В ней разбираются принцип работы LRO и все методы, которые могут поддерживаться этими операциями, включая паузу, возобновление и отмену длительно выполняющейся задачи.
• В главе 11 читатель познакомится с принципом выполнения повторяющейся работы, в некотором роде напоминающей планирование задач для веб-API. Здесь объясняется, как использовать ресурсы Execution и выполнять их либо по расписанию, либо по требованию.
Часть IV посвящена ресурсам и принципам их взаимосвязи и в некотором роде содержит более обширный разбор материала главы 4.
• В главе 12 показывается, как небольшие изолированные части связанных данных можно отделять в подресурсы-одиночки. В ней детально рассматриваются обстоятельства, при которых это оказывается хорошей идеей (или нет).
• В главе 13 описывается, как ресурсы в веб-API должны хранить ссылки на другие ресурсы с помощью либо ссылочных указателей, либо встроенных значений. В ней также объясняется, как обрабатывать пограничные случаи, такие как каскадирование удалений или обновлений по мере изменения ссылочных данных во времени.
• В главе 14 более широко раскрывается тема связей между ресурсами по типу «один к одному» и объясняется, как использовать ассоциирующие ресурсы для представления связей по схеме «многие ко многим». В ней также описываются способы хранения метаданных об этих связях.
• В главе 15 рассматривается использование сокращенных методов Add и Remove в качестве альтернативы ассоциирующим ресурсам при обработке связей формата «многие ко многим». Помимо этого, в ней разбираются некоторые компромиссы при использовании этих методов и поясняется, почему они не всегда оказываются подходящими.
• Глава 16 посвящена изучению сложного принципа полиморфизма, при котором переменные могут получать целый спектр различных типов. В ней описывается обработка полиморфных полей в ресурсах API, а также объясняется, почему полиморфных методов стоит избегать.
В части V мы выйдем за границы взаимодействия с одним ресурсом API за раз и начнем рассматривать паттерны проектирования, предназначенные для взаимодействия с целыми коллекциями ресурсов.
• В главе 17 объясняется, как ресурсы можно копировать или перемещать в API. В ней разбираются тонкие моменты, такие как обработка внешних данных, метаданные, наследуемые от другого родителя, и то, как правильно рассматривать дочерние ресурсы.
• В главе 18 рассказывается о способах адаптирования стандартных методов (Get, Create, Update и Delete) под одновременное оперирование не с одним ресурсом, а с их коллекцией. В ней также разбирается ряд сложных тем, например, как должны возвращаться результаты и как обрабатывать частичные сбои.
• В главе 19 более широко раскрывается идея пакетного метода удаления, приведенная в главе 17, с примером того, как можно удалять ресурсы, соответствующие заданному фильтру, а не конкретному идентификатору. Помимо этого, в ней рассматриваются способы налаживания согласованности и даются практические рекомендации, помогающие избежать случайного уничтожения данных.
• В главе 20 подробно рассматривается поглощение не относящихся к ресурсам данных, которые сами по себе не допускают прямой адресации. В ней изучается использование анонимных записей, а также темы согласованности и компромиссов в случаях, когда подобный тип поглощения анонимных данных оказывается удачным решением для API.
• В главе 21 объясняется, как обрабатывать просмотр больших наборов данных с помощью разбивки на страницы, используя для перебора этих данных непрозрачные токены. В ней также показывается, как применять пагинацию внутри одиночных крупных ресурсов.
• В главе 22 рассказывается, как применять критерий фильтрации к листингам ресурсов, а также демонстрируется лучший способ представления этих фильтров в API. Все это применяется непосредственно к темам, разобранным в главе 19.
• В главе 23 рассматривается обработка импорта и экспорта ресурсов в API. Кроме того, здесь подробно описаны различия между операциями импорта и экспорта в сравнении с резервным копированием и восстановлением.
В части VI мы сосредоточимся на несколько менее интересной теме реализации безопасности в API. Узнаем, как обеспечивать их защиту от злоумышленников и обезопасить предоставляемые в API методы от собственных ошибок пользователей.
• В главе 24 исследуется тема версионирования и объясняется, что означает совместимость разных версий. Идея совместимости рассматривается через призму разнообразия, и поясняется важность определения политики совместимости, согласующейся по всему API.
• В главе 25 начинается работа по защите пользователей от собственных ошибок, предоставляется паттерн (мягкое удаление), позволяющий удалять ресурсы из представления, не удаляя их полностью из системы.
• В главе 26 описывается защита системы от повторяющихся действий с помощью идентификаторов запросов. В ней разбираются тонкости использования ID запросов, а также алгоритм обеспечения правильной обработки этих ID в крупномасштабных системах.
• В главе 27 рассказывается о проверочных запросах, которые позволяют пользователям получить превью действия в API без фактического выполнения внутренней операции. В ней также демонстрируется обработка более продвинутых моментов, таких как побочные эффекты, возникающие во время запросов реального времени и проверочных запросов.
• В главе 28 вводится понятие ревизии ресурсов, используемой для отслеживания изменений во времени. Помимо этого, в ней разбираются связанные базовые операции, такие как откат к предыдущим ревизиям, и более продвинутые темы наподобие применения ревизий к дочерним ресурсам в иерархии.
• В главе 29 представляется паттерн для информирования пользователей о том, когда запросы API нужно повторить. Кроме того, здесь приводятся руководства по различным кодам HTTP-ответов и инструкции, поясняющие, когда их можно безопасно повторять.
• В главе 30 рассматриваются тема аутентификации отдельных запросов и различные критерии безопасности, которые необходимо учитывать при аутентификации пользователей в API. В ней приводится спецификация для цифровой подписи запросов API, которая соответствует практическим рекомендациям по безопасности, позволяющим убеждаться в достоверности источника и целостности запросов API, а также в том, что они не будут отвергнуты в дальнейшем.
В книге приводится множество примеров исходного кода, как в пронумерованных листингах, так и во встроенных в текст. В обоих случаях код оформлен моношириннымшрифтом, позволяющим отделить его от обычного текста. Иногда элементы кода дополнительно выделяются жирнымшрифтом, чтобы обозначить те фрагменты, которые изменились в течение проделанных шагов, например, когда в существующую строку добавляется новый функционал.
Во многих случаях начальный исходный код подвергался переформатированию. То есть мы добавляли разрывы строк и перерабатывали отступы, чтобы оптимально задействовать все доступное пространство страниц книги. Кроме того, во многих случаях, когда код листинга описывался в тексте, комментарии из этого листинга удалялись. Многие листинги сопровождаются аннотациями, которые поясняют наиболее важные детали.
После долгих обсуждений с первыми читателями и командой рецензентов я по ряду причин решил использовать в качестве стандартного языка TypeScript (TS). Он будет понятен как тем, кто знаком с динамическими языками (такими как JavaScript или Python), так и тем, кто работает со статическими (например, Java или C++). Это может кому-то не понравиться, и не все читатели смогут с ходу начать писать собственный TS-код, однако фрагменты, приводимые на этом языке, можно рассматривать просто как псевдокод, который должен быть понятен большинству разработчиков.
Когда дело доходит до определения API с помощью TS, нужно реализовывать два вида компонентов: ресурсы и методы. Что касается первых, то примитивы TS (например, интерфейсы) оказываются довольно выразительными при определении схем ресурсов API, позволяя почти всегда вписывать определения API буквально в несколько строк. В связи с этим все ресурсы API определяются в виде интерфейсов TS, что дает дополнительный бонус в виде более наглядных представлений JSON.
Что же касается методов API, то с ними все немного сложнее. В этом случае я предпочел задействовать для представления целого API абстрактные классы TS, следуя соглашению, обычно используемому при реализации RPC с помощью Google Protocol Buffers. Это дает возможность определять только методы API, не беспокоясь о внутренних реализациях.
При рассмотрении входов и выходов методов API я решил снова ориентироваться на Protocol Buffers, размышляя в контексте интерфейсов запросов и ответов. Это означает, что в большинстве случаев будут встречаться интерфейсы, представляющие эти входы и выходы и названные по имени метода API с добавлением суффикса -Request или -Response (например, CreateChatRoomRequest для метода CreateChatRooom).
Наконец, поскольку эта книга во многом опирается на принципы RESTful, необходимо было обеспечить способ отображения этих RPC в URL (и HTTP-метод). Для этого я предпочел использовать декораторы TS в качестве аннотаций различных методов API, выделив по одному для каждого отдельного HTTP-метода (например, @get, @post, @delete). Чтобы указать путь URL, в который должен отображаться метод API, каждый декоратор принимает шаблонную строку, которая также поддерживает подстановку переменных в интерфейсе запроса. К примеру, @get("/{id=chatRooms/*}") будет заполнять поле ID запроса. В этом случае звездочка обозначает заполнитель (placeholder) для любого значения, за исключением символа слеша.
Как бы ни было замечательно иметь возможность опираться на спецификации OpenAPI для всех этих паттернов проектирования, есть ряд проблем, которые могут создать сложности для читателей. Во-первых, спецификации OpenAPI предназначены для использования в первую очередь компьютерами (например, генераторами кода, рендерами документации и т.д.). А поскольку цель этой книги — донести сложные темы проектирования API до других разработчиков API, то OpenAPI оказывается не лучшим способом из доступных.
Во-вторых, несмотря на все волшебство проекта OpenAPI, его использование будет довольно громоздким независимо от того, в каком формате мы станем представлять API: в YAML или JSON. К сожалению, хоть эти сложные темы и можно выразить с помощью OpenAPI, такой подход окажется не самым лаконичным решением и приведет к большому количеству лишнего содержимого, не имеющего особой ценности.
Ну и последнее: при оценке использования OpenAPI, Protocol Buffers и TypeScript первые читатели и рецензенты дали четкую обратную связь, что вариант с TS для данного конкретного случая подходит лучше всего. Имейте в виду, что я вовсе не навязываю никому определять API с помощью TS. Просто для данного проекта этот язык подошел наилучшим образом.
Дополнительную информацию по проектированию API можно найти на ресурсе https://aip.dev, где детально разбирается множество сопредельных тем.
Джей Джей Гивакс трудится инженером ПО в Google, специализируясь на платежных системах, облачной инфраструктуре и проектировании API. Он также является автором книги Google Cloud Platform in Action и сооснователем AIP.dev, запущенного в Google общеотраслевого ресурса, который посвящен сотрудничеству в сфере разработки стандартов построения API. Живет в Сингапуре со своей женой Ка-эль и сыном Лукой.
На обложке представлена иллюстрация под названием Marchand d’Estampes à Vienne, или «Торговец полиграфией в Вене». Она взята из коллекции костюмов различных стран, составленной Жаком Грассе де Сен-Совером (1757–1810 годы) и опубликованной им в 1797 году под названием Costumes de Différents Pays. Каждая иллюстрация была старательно отрисована и раскрашена от руки. Богатое разнообразие коллекции Грассе де Сен-Совера напоминает нам, насколько культурно разнообразными были города и регионы планеты буквально 200 лет назад. Будучи изолированными друг от друга, люди говорили на разных диалектах и языках. В поселениях по одеянию человека можно было легко определить, где он живет, чем занимается и какое положение в обществе занимает.
С тех пор манеры одеваться очень изменились. Теперь сложно различить даже жителей разных континентов, не говоря уже о гражданах разных городов, регионов и стран. Возможно, мы променяли культурное разнообразие на более разнообразную личную жизнь — и уж точно на более разнообразную и динамичную технологическую.
Во времена, когда сложно отличить одну компьютерную книгу от другой, издательство Manning чествует изобретательность и новаторство компьютерного бизнеса с помощью обложек, отражающих богатое разнообразие региональной жизни двухсотлетней давности, о котором нам напоминают картины Грассе де Сен-Совера.
Ваши замечания, предложения, вопросы отправляйте по адресу [email protected] (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
Проектирование API — непростая задача. Будь она простой, потребности в этой книге не возникло бы. Однако прежде чем переходить к изучению инструментов и паттернов, способствующих более гибкой и удобной разработке API, нам нужно утвердить основополагающую терминологию и сформировать представление о том, чего ожидать от книги. В следующих двух главах мы разберем кое-какой ознакомительный материал, который послужит платформой, облегчающей изучение материала всей оставшейся книги.
Главу 1 начнем с подробного определения значения API. Более того, мы разберемся, как должен выглядеть хороший API и как отличить его от плохого. Затем в главе 2 мы более внимательно рассмотрим понятие паттерна проектирования и анатомию паттернов, представленных на страницах книги, чтобы научиться использовать их для построения стабильно хороших API.
В этой главе
• Что такое интерфейсы.
• Что такое API.
• Что такое ориентация на ресурсы.
• Что делает API «хорошим».
Раз вы выбрали эту книгу, то, скорее всего, уже в целом знакомы с API. Кроме того, вы наверняка уже знаете, что API означает «программный интерфейс приложения», так что в первой главе мы сосредоточимся на тщательном разъяснении значений этих базовых понятий и их значимости. Начнем с более подробного разбора идеи API.
API определяет способ взаимодействия компьютерных систем. А поскольку в изолированном режиме функционирует очень небольшое количество систем, то можно не удивляться тому, что API буквально повсюду. Их можно найти в библиотеках, которые мы используем из языковых диспетчеров пакетов (например, в библиотеке шифрования, предоставляющей метод наподобие functionencrypt(input:string):string), а также в коде, который пишем сами, даже если он и не предназначается для использования другими. Но существует особый тип API, который создается для раскрытия по сети и используется удаленно многими людьми. Именно такие программные интерфейсы, зачастую называемые веб-API, и рассматриваются в этой книге.
Веб-API привлекают внимание благодаря нескольким аспектам, но самым интересным из них, вероятно, является то, что создатели таких API имеют практически полный контроль над ними, при том, что конечные пользователи, наоборот, в контроле ограничены. Используя библиотеку, мы работаем с ее локальными копиями, в связи с чем создатели API могут в любое время делать все, что захотят, без риска навредить пользователям. Веб-API отличаются тем, что никаких копий не используется. В результате при внесении в веб-API изменений последние сказываются на пользователях вне зависимости от того, просили они о них или нет.
Представьте, к примеру, вызов веб-API, который позволяет вам зашифровать данные. Если работающая над ним команда решит начать использовать для шифрования другой алгоритм, то в этом случае у вас не будет выбора. При вызове соответствующего метода шифрования ваши данные будут зашифрованы последним заданным алгоритмом. В более экстремальном примере авторы API могут решить полностью закрыть его и игнорировать ваши запросы. В этот момент ваше приложение внезапно перестанет работать, и вы ничего не сможете с этим поделать. Оба описанных сценария показаны на рис. 1.1.
Рис. 1.1. Возможные сценарии при работе с веб-API со стороны потребителя
Тем не менее эти особенности, оказывающиеся для потребителей недостатками, для разработчиков API обычно выступают главными преимуществами, так как позволяют сохранять полный контроль над своим детищем. К примеру, если в API шифрования используется новый суперсекретный алгоритм, то создатели этого интерфейса вряд ли захотят просто так раскрыть его код миру в форме библиотеки. Вместо этого они скорее предпочтут использовать веб-API, который позволит им предоставить пользователям функциональность этого суперсекретного алгоритма, не выдавая свою ценную интеллектуальную собственность. В иных случаях системе может требоваться невероятная вычислительная мощность, что приведет к очень длительному выполнению, если развернуть ее в качестве библиотеки и запускать на домашнем ПК или ноутбуке. В похожих случаях, например при работе со многими API машинного обучения, создание веб-API позволяет предоставить потребителям мощную функциональность, скрыв вычислительные требования. Схематично это представлено на рис. 1.2.
Рис. 1.2. Пример веб-API, скрывающего необходимую вычислительную мощность
Теперь, когда мы понимаем, что такое API (и, в частности, веб-API), возникает вопрос: в чем их значимость?
Программное обеспечение нередко создается исключительно для использования человеком, и в этом нет ничего плохого. Однако за последние несколько лет мы видим все больший уклон в сторону автоматизации, когда компьютерные программы создаются для того, чтобы делать то же, что и человек, но быстрее и эффективнее. К сожалению, именно на этом рубеже ПО «только для людей» становится проблемой.
Разрабатывая нечто, предназначенное исключительно для использования человеком, и подразумевая взаимодействие с системой с помощью мыши и клавиатуры, мы склонны смешивать строение системы и визуальные аспекты с сырыми данными и функциональными аспектами. Проблема в том, что может быть сложно объяснить компьютеру, как взаимодействовать с графическим интерфейсом. Причем она усугубляется тем, что изменение визуальных аспектов программы также может потребовать переобучения компьютера взаимодействию с этим новым графическим интерфейсом. Те изменения, которые кажутся чисто косметическими для нас, оказываются совершенно нераспознаваемыми для компьютера. Иными словами, для компьютера нет такого понятия, как «чисто косметически».
API — это интерфейсы, которые предназначены специально для компьютеров и имеют важные свойства, позволяющие компьютерам легко их использовать. К примеру, у этих интерфейсов отсутствуют визуальные составляющие, значит, не нужно беспокоиться о внешних изменениях. К тому же эти интерфейсы обычно развиваются только «совместимым» образом (подробнее см. в главе 24), значит, нет необходимости переучивать компьютер чему-либо в связи с изменениями. Если кратко, то API предоставляют способ говорить на языке, который нужен компьютерам, чтобы осуществлять безопасное и стабильное взаимодействие.
Но все это не ограничивается простой автоматизацией. API также открывают возможности для композиции, которая позволяет нам рассматривать функциональность как кубики лего, соединяя детали все новыми и новыми способами, чтобы получить нечто, в разы превосходящее сумму своих частей. Более того, эти получаемые композиции API также можно рассматривать как строительные блоки, формируя из них еще более сложные и невероятные проекты.
Но здесь возникает важный вопрос: как сделать так, чтобы создаваемые нами API сочетались подобно кубикам лего? Начнем с рассмотрения используемой для этого ресурсно-ориентированной стратегии.
Многие из существующих сегодня веб-API действуют подобно слугам: вы просите их выполнить что-либо, и они это делают. К примеру, если мы хотим узнать прогноз погоды в своем городе, то можем приказать веб-API predictWeather(postalCode=10011). Такой вид передачи приказа другому удаленному компьютеру с помощью вызова предварительно настроенной подпрограммы или метода обычно называется выполнением «удаленного вызова процедуры» (remote procedure call, RPC), так как, по существу, мы вызываем библиотечную функцию (или процедуру) для выполнения на другом компьютере, находящемся в другом (удаленном) месте. Важнейший аспект подобных API — акцент на выполняемых действиях. То есть мы думаем о вычислении погоды (predictWeather(postalCode=...)), либо шифровании данных (encrypt(data=...)), либо отправке электронной почты (sendEmail(to=...)), во всех случаях делая акцент на «выполнении» чего-либо.
Тогда почему не все API ориентированы на RPC? Одна из основных причин связана с идеей «сохранения состояния» (statefulness), когда вызовы API могут быть либо «с сохранением состояния» (stateful), либо «без сохранения состояния» (stateless). Вызов API рассматривается как stateless, когда его можно совершить независимо от всех других запросов API без дополнительного контекста или данных. Например, вызов веб-API для прогнозирования погоды задействует только один независимый ввод данных (почтовый код), в связи с чем считается stateless. С другой стороны, веб-API, который сохраняет выбранные пользователем города и предоставляет для них прогнозы погоды, не имеет входных данных в среде выполнения, но требует, чтобы у пользователя уже были сохранены интересующие его города. В результате такой вид запроса API, подразумевающего предварительные запросы или использование ранее сохраненных данных, будет считаться stateful. На деле оказывается, что RPC-ориентированные API отлично подходят для stateless-функциональности, но при этом гораздо менее эффективны, когда мы вводим stateful-методы API.
Примечание
Если вы знакомы с концепцией REST, то сейчас будет кстати сказать, что этот раздел не посвящен конкретно REST и RESTful API, а является более общим для всех API, которые выделяют «ресурсы» (как большинство RESTful API). Другими словами, хоть здесь и будет много пересечений с темой REST, данный раздел все же охватывает более общий материал.
Чтобы понять, почему это так, мы рассмотрим пример stateful-API для бронирования билетов на самолет. В табл. 1.1 отражен список RPC для взаимодействия с планированием авиаперелетов, включая такие действия, как планирование новых броней, просмотр существующих и отмена полета.
Таблица 1.1. Перечень методов для примера API бронирования авиаперелетов
Метод
Описание
ScheduleFlight()
Планирует новый перелет
GetFlightDetails()
Показывает информацию о конкретном перелете
ShowAllBookings()
Показывает все забронированные на данный момент перелеты
CancelReservation()
Отменяет резервирование перелета
RescheduleFlight()
Переназначает существующий перелет на другую дату или время
UpgradeTrip()
Переводит из эконом-класса в первый класс
Каждый из этих RPC отлично говорит сам за себя, но здесь никак не обойтись без запоминания этих методов API, многие из которых очень похожи между собой. Так, иногда метод говорит о «перелете» (тот же RescheduleFlight()), а в других случаях оперирует словом «резервирование» (например, CancelReservation()). Кроме того, нужно помнить, какие из множества синонимичных форм этих действий использовались. К примеру, следует фиксировать, с помощью какого из методов мы просматриваем все свои брони: ShowFlights(), ShowAllFlights(), ListFlights() или ListAllFlights() (в данном случае это ShowAllFlights()). Но как можно решить такую проблему? С помощью стандартизации.
Ориентированность на ресурсы призвана помочь решить эту проблему в двух направлениях, предоставляя стандартный набор строительных блоков для использования при проектировании API. Во-первых, ресурсно-ориентированные API опираются на идею «ресурсов», являющихся ключевыми единицами, которые мы сохраняем и с которыми взаимодействуем, стандартизируя все, что реализует API. Во-вторых, вместо того чтобы использовать для любого нужного действия произвольных имен RPC, ресурсно-ориентированные API ограничивают действия до небольшого стандартного набора (описанного в табл. 1.2), который применяется к каждому ресурсу, формируя полезные действия в API. Если посмотреть немного под другим углом, то ресурсно-ориентированные API — это просто особый тип API, основанных на RPC, в которых каждый RPC следует понятному и стандартизированному паттерну: <StandardMethod><Resource>().
Таблица 1.2. Перечень стандартных методов и их значения
RPC
Описание
Create<Resource>()
Создает новый <Resource>
Get<Resource>()
Показывает информацию о конкретном <Resource>
List<Resources>()
Показывает список всех существующих <Resources>
Delete<Resource>()
Удаляет существующий <Resource>
Update<Resource>()
Обновляет существующий <Resource> на месте
Если пойти дальше по этому пути особых, ограниченных RPC, то получится, что вместо разнообразия всевозможных методов RPC, показанных в табл. 1.1, мы можем придумать один ресурс (например, FlightReservation) и получить равнозначную функциональность с набором стандартных методов, показанных в табл. 1.3.
Таблица 1.3. Перечень стандартных методов, применяемых к ресурсу перелетов
Метод
Ресурс
Методы
Create
CreateFlightReservation()
Get
GetFlightReservation()
List
×
FlightReservation
ListFlightReservations()
Delete
DeleteFlightReservation()
Update
UpdateFlightReservation()
Очевидно, что стандартизация оказывается более организованной, но значит ли это, что все ресурсно-ориентированные API лучше, чем RPC-ориентированные? Вообще-то нет. В некоторых сценариях RPC-ориентированные API окажутся более эффективными (особенно в случае stateless-методов API). Однако во многих других случаях пользователям будет намного проще понять, изучить и запомнить именно ресурсно-ориентированные API. Причина в том, что предоставляемая ими стандартизация упрощает совмещение того, что вы уже знаете (например, набора стандартных методов), с тем, что вы можете легко усвоить (например, названием нового ресурса), позволяя сразу начать взаимодействовать с API. Говоря в цифрах, если вы знаете, скажем, пять стандартных методов, то благодаря надежному паттерну освоение одного нового ресурса в действительности будет приравнено к освоению пяти новых RPC.
Конечно же, не все API одинаковы, и было бы несколько опрометчиво определять их сложность в формате размера списка того, что «нужно изучить». С другой стороны, здесь работает один важный принцип: сила паттернов. На деле изучать совмещаемые части и объединять их в более сложные компоненты, которые следуют установленному образцу, оказывается легче, чем изучать предварительно собранные компоненты, которые каждый раз соответствуют особой структуре. Поскольку ресурсно-ориентированные API зиждутся на проверенных временем паттернах проектирования, зачастую их проще выучить, а значит, они оказываются «лучше», чем их RPC-ориентированные аналоги. Но вслед за этим возникает важный вопрос: что в данном случае значит «лучше»? Как нам понять, является ли API «хорошим»? Что здесь вообще значит «хороший»?
Прежде чем рассматривать различные аспекты, делающие API «хорошим», нужно разобраться, зачем мы вообще используем программный интерфейс. Иначе говоря, какова цель его создания? Обычно она сводится к двум простым причинам:
1) у нас есть востребованная определенными пользователями функциональность;
2) эти пользователи хотят использовать данную функциональность программно.
К примеру, у нас может быть система, которая прекрасно справляется с переводом текстов. В мире наверняка много людей, заинтересованных в такой возможности, но одного этого недостаточно. В конце концов, мы можем запустить вместо API мобильное приложение, предоставляющее эту прекрасную систему перевода. Чтобы получить API, люди, заинтересованные в этой функциональности, должны также захотеть написать программу, позволяющую использовать ее. Учитывая два этих критерия, какие качества будут желательными для нашего API?
Начнем с самого важного: вне зависимости от того, как будет выглядеть окончательный интерфейс, система в целом должна быть функциональной. Иначе говоря, она должна делать то, что пользователям действительно нужно. Если она предназначена для перевода текстов с одного языка на другой, то должна уметь это делать. Кроме того, к большинству систем может предъявляться множество нефункциональных требований. Например, если наша система переводит текст, то к ней могут предъявляться нефункциональные требования, связанные с такими аспектами, как задержка (например, процесс перевода должен занимать несколько миллисекунд, а не дней) или точность (например, переводы не должны оказываться ложными и вводить в заблуждение). Вот эти два аспекта могут составлять функциональную сторону системы.
Помимо функциональной эффективности, система также должна иметь интерфейс, который бы позволял пользователям выражать свои намерения ясно и просто. Другими словами, если система переводит текст, то ее API должен быть создан так, чтобы делать это можно было простым и понятным способом. В данном случае это может быть RPC под названием TranslateText(). Конечно, это может звучать вполне очевидно, но в действительности нередко оказывается сложнее, чем кажется.
К примерам подобной скрытой сложности относится случай, когда API уже поддерживает некую функциональность, но по недосмотру мы не замечаем, что она нужна пользователям, и не реализуем выразительного способа доступа к ней. В результате люди вынуждены использовать ее обходными путями, совершая ряд непривычных и не всегда очевидных действий. К примеру, если API предоставляет возможность переводить текст, то пользователь может косвенно задействовать его исключительно для определения языка, даже если переводить ничего не хочет. Нетрудно представить, что прямой RPC DetectLanguage() оказался бы для пользователей куда удобнее, чем совершение множества вызовов API в попытке угадать язык.
Листинг 1.1. Функциональность, позволяющая определять язык с помощью только метода API TranslateText
Как показывает этот пример, API, которые поддерживают определенную функциональность, но не упрощают пользователям доступ к ней, оказываются не самыми удачными. При этом выразительные API предоставляют пользователям возможность ясно выражать не только то, что они хотят сделать (например, перевести текст), но и каким именно образом (например, в течение 150 миллисекунд, с точностью 95 %).
Одним из важнейших факторов, связанных с удобством использования любой системы, является ее простота. И хотя логично предположить, что достичь простоты можно за счет сокращения компонентов (RPC, ресурсов и т.п.) в API, к сожалению, это не так. К примеру, API может опираться на один метод ExecuteAcrion(), обрабатывающий всю функциональность. Тем не менее это ничего не упрощает, а только перемещает сложность из одного места (множества разных RPC) в другое (много настроек в одном RPC). Как же тогда выглядит простой API?
Вместо того чтобы пытаться излишне сокращать количество RPC, API должен стремиться раскрывать нужную пользователям функциональность наиболее доступным способом, делая API максимально простым, но не более. Представьте, что в API перевода нужно добавить возможность определять язык входного текста. Можно сделать это путем возвращения определенного исходного текста в ответ на перевод. Однако это все равно запутывает функциональность, скрывая ее внутри метода, предназначенного для другой цели. Вместо этого будет разумнее создать новый метод специально для поставленной задачи, например DetectLanguage(). (Обратите внимание, что мы также можем включать распознание языка при переводе содержимого, но это служит уже совсем для другой цели.)
Еще один универсальный подход к обеспечению простоты основан на старом правиле «стандартного случая» (делать обработку стандартных случаев быстрой), но вместо этого ориентируется на простоту использования, оставляя пространство для пограничных случаев. Такая измененная формулировка подразумевает, что нужно «реализовать типичный случай превосходно, а продвинутый сделать возможным». Это означает, что при каждом добавлении чего-либо, усложняющего API в угоду продвинутому пользователю, лучше все-таки скрывать это усложнение от типичного пользователя, который заинтересован лишь в стандартном случае. При таком подходе более частые сценарии получатся простыми и легкими в использовании, но при этом сохранятся и продвинутые возможности для тех, кому они нужны.
В качестве примера представим, что наш API перевода включает принцип использования модели машинного обучения, которая понадобится при переводе текста, когда вместо того, чтобы указать целевой язык, мы выбираем основанную на нем модель и используем ее как «механизм перевода». Несмотря на то что такая функциональность предоставляет гораздо больше гибкости и контроля для пользователей, она окажется и намного сложнее. Этот новый типичный случай показан на рис. 1.3.
Рис. 1.3. Перевод текста после выбора модели
Как видите, в итоге в обмен на поддержку более продвинутой функциональности мы существенно усложнили процесс перевода. Чтобы увидеть это более наглядно, сравните код из листинга 1.2 с простотой вызова TranslateText("Helloworld","es").
Листинг 1.2. Перевод текста после выбора модели
Как сделать такой API максимально простым, одновременно сделав типичный случай превосходным, а продвинутый — возможным? Поскольку типичный случай подразумевает пользователей, которых не интересует конкретная модель, можно спроектировать API так, чтобы он принимал либо targetLanguage, либо modelId. Тогда продвинутый случай будет по-прежнему рабочим (код из листинга 1.2 продолжит функционировать), но типичный случай будет выглядеть намного проще, опираясь просто на параметр targetLanguage (и ожидая, что параметр modelId останется неопределенным) (листинг 1.3).
Листинг 1.3. Перевод текста на целевой язык (типичный случай)
function translateText(inputText: string,
targetLanguage: string,
modelId?: string): string {
return TranslateApi.TranslateText({
text: inputText,
targetLanguage: targetLanguage,
modelId: modelId,
});
}
Теперь, когда у нас есть представление о том, насколько для «хорошего» API важна простота, рассмотрим заключительный критерий: предсказуемость.
Если в жизни сюрпризы иногда оказываются забавными, то в API им точно места нет, хоть в определении интерфейса, хоть во внутреннем поведении. Это утверждение можно сравнить с поговоркой об инвестировании: «Если вы очень им увлечены, значит, действуете неправильно». Так что же мы подразумеваем под API «без сюрпризов»?
API без сюрпризов опираются на повторяющиеся паттерны, применяемые и к внешнему определению API, и к его поведению. Например, если API перевода текста содержит метод TranslateText(), который в качестве параметра получает входное содержимое в поле text, то при добавлении метода DetectLanguage() входное содержимое должно также называться text (а не inputText, content или textContent). Хоть это и выглядит очевидным сейчас, помните, что многие API построены несколькими командами и выбор имен для полей при наличии множества параметров зачастую оказывается произвольным. В итоге, когда два отдельных человека отвечают за два таких отдельных поля, возникает большая вероятность, что они сделают разный выбор. И когда это происходит, мы получаем несогласованный API, то есть API с сюрпризом.
Несмотря на то что такая несогласованность может казаться незначительной, в действительности подобные проблемы оказываются намного более важными, чем выглядят на первый взгляд. Причина в том, что пользователи API довольно редко изучают каждую деталь, тщательно вчитываясь в документацию. Вместо этого они просматривают ровно столько, сколько нужно, чтобы добиться требуемого результата. То есть если человек узнает, что поле называется text в одном текстовом сообщении, он наверняка предположит, что оно так же называется и в другом. При этом человек будет строить аналогичные догадки, основанные на известных ему фактах, и в отношении остального. Если этот процесс потерпит неудачу (например, потому что в другом сообщении поле было названо inputText), то работа встанет и пользователю придется выяснять, почему его предположение не оправдалось.
Очевидный вывод здесь в том, что API, которые опираются на повторяющиеся, предсказуемые паттерны (например, согласованное именование полей), проще и быстрее освоить, а значит, они лучше. Аналогичные преимущества дают и более сложные паттерны, такие как стандартные действия, которые мы видели при изучении ресурсно-ориентированных API. Это дает нам представление об основной цели книги: научить использовать известные, четко определенные, понятные и (надеюсь) простые паттерны для создания предсказуемых, доступных, следовательно, и более качественных API. Теперь, когда мы разобрались с понятием API и выяснили, что делает их хорошими, пора начать рассматривать высокоуровневые паттерны, которым мы можем следовать при разработке наших API.
• Интерфейсы представляют собой контракты, определяющие, как две системы должны взаимодействовать друг с другом.
• API — это особые типы интерфейсов, которые определяют правила взаимодействия двух компьютерных систем. Они могут выражаться в разных формах, таких как скачиваемые библиотеки и веб-API.
• Веб-API отличаются тем, что предоставляют функциональность по сети, скрывая конкретную реализацию или вычислительные требования, необходимые для ее выполнения.
• Ресурсно-ориентированные API — это способ проектирования программных интерфейсов, позволяющий снизить сложность за счет использования стандартного набора действий, называемых методами, среди ограниченного набора компонентов, называемых ресурсами.
• Однозначно определить, что значит «хороший» API, сложно, но обычно они отличаются функциональностью, выразительностью, простотой и предсказуемостью.
В этой главе
• Что такое паттерн проектирования API.
• Почему паттерны проектирования API важны.
• Анатомия и структура паттерна проектирования API.
• Проектирование API с помощью паттерна и без него.
Получив представление о том, что делает API «хорошими», мы можем начать изучать способы использования различных паттернов при их создании. Сначала мы выясним, что собой представляют эти паттерны, почему они важны и как будут описываться в последующих главах. В завершение мы рассмотрим пример API и разберемся, как использование предварительно созданных паттернов может сэкономить много времени и избавить нас от проблем в будущем.
Прежде чем заняться изучением этих паттернов, нам нужно заложить кое-какую основу, начав с простого вопроса: что такое паттерн проектирования? Если мы утвердим, что проектирование ПО относится к структуре или схеме некоего кода, написанного для решения задачи, то паттерн проектирования ПО будет означать возможность его повторяющегося применения к множеству схожих задач по созданию ПО с внесением лишь минимальных правок, подстраивающих его под частные сценарии. Это означает, что данный паттерн не является какой-то заранее собранной библиотекой, с помощью которой мы решаем отдельные задачи, а выступает скорее в роли схемы для решения схожих по структуре задач.
Если это звучит слишком абстрактно, то в качестве более осязаемой аналогии представим, что хотим построить на заднем дворе сарай. Для этого можно выбрать один из множества технологических подходов, начиная от техник строительства, которые использовали несколько сотен лет назад, и заканчивая современными решениями, доступными благодаря таким компаниям, как Lowe и Home Depot. Вариантов оказывается и впрямь очень много, но вот четыре основных.
• Купить готовый сарай и привезти его во двор.
• Купить комплект для сборки сарая (чертежи и материалы) и собрать его самостоятельно.
• Купить набор чертежей сараев, скорректировать проект под свои нужды, потом также построить его самостоятельно.
• Спроектировать и построить сарай с нуля.
Если представить эти варианты через призму ПО, то они будут охватывать диапазон от использования готового программного пакета до создания системы, полностью настраиваемой под нашу задачу. В табл. 2.1 мы видим, что по ходу списка варианты становятся все сложнее, но при этом от варианта к варианту растет гибкость. Другими словами, самый простой вариант является наименее гибким, а самый сложный, наоборот, обеспечивает максимальную подвижность в решениях.
Таблица 2.1. Сравнение способов постройки сарая со способами создания ПО
Вариант
Сложность
Гибкость
Программный эквивалент
Купить готовый
Проще некуда
Отсутствует
Использовать готовый программный пакет
Собрать из набора
Легко
Очень низкая
Создать путем кастомизации существующего ПО
Построить по чертежам
Нужно постараться
Средняя
Создать на основе проектной документации
Построить с нуля
Трудно
Максимальная
Написать полностью заказное ПО
Инженеры ПО чаще всего выбирают вариант «построить с нуля». Иногда это необходимо, особенно в случаях, когда стоящие перед нами задачи новы. В прочих ситуациях этот выбор выигрывает в соотношении «затраты — выгоды», поскольку задача различается ровно в той степени, чтобы не позволить нам опереться на более простые варианты. И все же бывают случаи, когда мы знаем библиотеку, которая способна решить нашу задачу полноценно (или достаточно близко к этому), и мы предпочитаем опереться на кого-то, кто уже реализовал это решение. Оказывается, промежуточные варианты (индивидуальная настройка существующего ПО или его создание на основе проектной документации) выбираются гораздо реже, хотя, вероятно, могут использоваться чаще, давая отличные результаты. Вот здесь и вступают в игру паттерны проектирования.
На высоком уровне они предназначены для варианта «постройки на основе чертежей», но в отношении ПО. Аналогично тому, как в чертежах сарая указываются размеры, обозначается расположение окон и дверей, а также перечисляются материалы для крыши, паттерны проектирования тоже несут в себе набор спецификаций и деталей для кода, который мы будем писать. В сфере ПО это обычно означает формирование высокоуровневой схемы кода, а также описание нюансов ее использования для решения конкретной задачи. Тем не менее редко бывает так, что паттерн проектирования создается для автономного использования. Чаще всего они фокусируются на конкретных компонентах, а не целых системах. Другими словами, чертежи дают информацию об одном аспекте (например, форме крыши) или компоненте (например, дизайне окна), а не обо всем сарае. На первый взгляд это может показаться недостатком, но так бывает, только если целью является реально постройка сарая. Если же вы создаете лишь нечто подобное ему, то наличие чертежей для каждого отдельного компонента будет означать возможность сопоставлять их все в нужных вам соотношениях, выбирая форму крыши А и дизайн окна B. Это относится и к нашему обсуждению паттернов проектирования, поскольку каждый из них фокусируется на одном компоненте или типе задачи системы, помогая вам создавать ровно то, что вы хотите, собирая множество готовых частей воедино.
К примеру, если вы хотите добавить в систему логирование отладки, то наверняка предпочтете только один способ логирования сообщений. Реализовать это можно по-разному (например, используя одну общую глобальную переменную), но существует специальный паттерн проектирования, предназначенный для решения именно этой программной задачи. Данный паттерн описан в фундаментальной работе «Паттерны проектирования» (Design Patterns) (Гамма и др., 1994 год) и называется «Одиночка» (или «Синглтон»). Он гарантирует создание только одного экземпляра класса. Этот «чертеж» вызывает класс с закрытым конструктором и одним статическим методом getInstance(), который всегда возвращает один экземпляр этого класса (он создает этот один экземпляр, только если тот еще не существует). Причем данный паттерн вовсе не является завершенным (какой смысл иметь класс-одиночку, который ничего не делает?). Тем не менее это четко определенный и протестированный паттерн, которому можно следовать, когда требуется решить эту небольшую отдельную задачу — всегда иметь только один экземпляр класса.
Теперь, когда мы знаем в целом, чем являются паттерны проектирования ПО, нужно задать вопрос: что такое паттерны проектирования API? Если опереться на определение API, приведенное в главе 1, то они представляют собой просто паттерны проектирования программного обеспечения, примененные к API, а не ко всему ПО. Это означает, что паттерны разработки API, подобно стандартным паттернам, — просто чертежи, используемые для создания и структурирования API. Поскольку в данном случае фокус на интерфейсе, а не реализации, то в большинстве случаев паттерн проектирования API будет сосредоточен исключительно на интерфейсе без обязательного построения реализации. Несмотря на то что большинство паттернов создания API зачастую будут пассивны в отношении внутренней реализации этих интерфейсов, иногда они диктуют конкретные аспекты поведения API. К примеру, такой паттерн может указывать, что определенный RPC согласуется под конец, подразумевая, что возвращаемые от этого RPC данные могут быть слегка устаревшими (например, он может производить считывание из кэша, а не из общей системы хранения).
Чуть позже мы более детально разберем дальнейшее документирование паттернов API, но сначала кратко рассмотрим, почему нам вообще нужно беспокоиться о паттернах проектирования API.
Как мы уже узнали, паттерны разработки API оказываются полезны при построении этих интерфейсов точно так же, как чертежи при построении сарая: они выступают в качестве готовых строительных блоков, которые можно использовать в наших проектах. При этом мы не разобрались, зачем они вообще нам нужны. Неужели не настолько умны, чтобы самостоятельно создавать хорошие API? Разве нам не лучше известна сфера реализации проекта и технические задачи? Конечно, зачастую это так, но в действительности некоторые из техник, с помощью которых мы создаем хорошо спроектированное ПО, при построении API не слишком эффективны. Если говорить конкретнее, то итеративный подход, рекомендуемый в процессе гибкой разработки, сложно применить при проектировании API. Чтобы понять, почему это так, нужно взглянуть на два аспекта программных систем. Сначала нам необходимо изучить гибкость (или ригидность) различных интерфейсов в общем и затем уже понять, как аудитория этого интерфейса влияет на нашу возможность вносить изменения и экспериментировать со всем дизайном. Начнем с рассмотрения гибкости.
Как мы видели в главе 1, API — это особый вид интерфейса, который создается в первую очередь для взаимодействия компьютерных систем. Несмотря на то что наличие программного доступа к системе очень ценно, он существенно повышает хрупкость в том плане, что изменения интерфейса могут легко вызвать сбои у его пользователей. К примеру, изменение названия поля в API вызовет сбой во всем коде, написанном пользователями до этого изменения. С позиции сервера API этот старый код будет обращаться к чему-то, используя имя, которого уже не существует. Данный сценарий не столь актуален для других видов интерфейсов, таких как графические интерфейсы пользователя (graphical user-interfaces, GUI), которые используются в основном людьми, а не компьютерами, в результате чего оказываются намного более устойчивыми к изменениям. Это означает, что, даже если изменение может оказаться раздражающим или эстетически некрасивым, оно вряд ли приведет к катастрофическому сбою, который бы сделал использование интерфейса в принципе невозможным. Например, изменение цвета области или кнопки на веб-странице может сделать макет некрасивым и неудобным, но мы все равно сможем разобраться, как работает интерфейс и как с его помощью достичь нужного нам результата.
Нередко это свойство интерфейсов называют гибкостью, подразумевая, что те из них, где пользователи могут свободно приспосабливаться к изменениям, являются гибкими, а те, где даже небольшие корректировки (например, переименование поля) приводят к полному сбою — ригидными. Это различие важно, поскольку возможность вносить множество изменений в значительной степени определяется гибкостью интерфейса. Более того, мы можем видеть, что ригидные интерфейсы, как это бывает и в других программных проектах, существенно усложняют итеративное продвижение к идеальному дизайну. Это означает, что мы зачастую оказываемся в тупике со всеми решениями дизайна, как хорошими, так и плохими. Здесь можно подумать, что тогда из-за ригидности API никогда не удастся использовать итеративный процесс разработки. Но благодаря еще одному важному свойству интерфейсов — видимости — это не всегда оказывается так.
Как правило, большинство интерфейсов можно разделить на две категории: те, которые пользователи могут просматривать и с которыми могут взаимодействовать (в ПО обычно зовутся фронтендом), и недоступные им (бэкенд). К примеру, открыв браузер, мы можем легко наблюдать графический интерфейс Facebook. Однако у нас нет возможности видеть, как Facebook хранит наш профиль в этой соцсети и другие данные. Если использовать для этого свойства видимости более формальные выражения, можно говорить, что фронтенд (часть, доступная пользователям для просмотра и взаимодействия) обычно считается публичным, а бэкенд (видимый только небольшой внутренней группе ресурса) — закрытым. Это различие важно тем, что отчасти определяет нашу возможность вносить изменения в разные виды интерфейсов, в частности в ригидные, такие как API.
Если мы вносим изменение в публичный интерфейс, то оно станет видимо для всего мира и потенциально окажет на него влияние. Ввиду столь обширной аудитории бездумное изменение может вызвать злость и раздражение у пользователей. При том, что этот факт однозначно касается ригидных интерфейсов, он относится и к гибким. К примеру, во времена зарождения Facebook большинство значительных функциональных или дизайн-изменений вызывали у студентов колледжей гнев, длящийся до нескольких недель. Но что, если интерфейс не публичный? Насколько сложно вносить изменения в бэкенд-интерфейсы, видимые только членам внутренней группы людей? В этом сценарии количество подверженных изменениям людей существенно меньше и, возможно, даже ограничено участниками одной команды или офиса, так что здесь мы более свободны. И это прекрасно, поскольку означает, что у нас должна быть возможность быстро продвигаться к идеальному дизайну, попутно применяя принципы гибкой разработки.
Так в чем же особенность API? Оказывается, проектируя множество API (которые ригидны по определению) и делясь ими с миром, мы получаем худший сценарий для обоих вариантов. Это означает, что вносить изменения будет даже сложнее, чем в случае любой другой комбинации этих двух свойств (обобщенно представлено в табл. 2.2).
Таблица 2.2. Сложность изменения различных интерфейсов
Гибкость
Аудитория
Пример интерфейса
Сложность изменения
Гибкий
Закрытый
Консоль внутреннего мониторинга
Очень просто
Гибкий
Публичный
Facebook.com
Нужно постараться
Ригидный
Закрытый
API внутреннего хранилища фотографий
Трудно
Ригидный
Публичный
Публичный Facebook API
Очень трудно
Проще говоря, этот «вдвойне худший» сценарий (и ригидный, и сложный для изменения) делает многократно используемые и проверенные паттерны проектирования еще более важными при построении API, чем при создании других видов ПО. В то время как в большинстве программных проектов код чаще всего оказывается закрыт и недоступен для просмотра, дизайн-решения при создании API всегда оказываются видны всем пользователям сервиса. И поскольку это серьезно ограничивает нашу возможность вносить постепенные доработки, то использование существующих, прошедших проверку временем паттернов оказывается очень ценным фактором, позволяющим наладить все с первого раза, а не в конечном итоге, как это бывает в большинстве ПО.
Теперь, когда мы изучили ряд причин для использования паттернов проектирования, пора углубиться в их суть, разобрав все их компоненты.