Освоение API: полное руководство для начинающих

Освоение API: полное руководство для начинающих

11 марта 2023 г.

В Книге по компьютерным наукам я научу вас всем основам информатики, необходимым для того, чтобы стать отличным разработчиком. Я знаю, что вам нужно, потому что я изучал компьютерные науки после окончания учебного лагеря и в итоге получил работу в FAANG.

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

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

Я хочу донести три ключевых момента:

  1. API чрезвычайно полезны и появляются повсюду.
  2. Когда вы пишете код, вы разрабатываете API, осознаете вы это или нет.
  3. Проектировать API сложно!

Первоначальное определение API

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

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

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

Orcs evidently appreciate the power of abstraction

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

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

В идеале API должен быть четко разработан и задокументирован. Любой может прийти, посмотреть документацию и быстро понять, как работает API. Документация HTTP API в Твиттере — хороший пример документирования общедоступных API. Иногда API может быть недокументированным. Это неприятно, потому что это означает, что единственный способ понять API — это метод проб и ошибок. Хуже всего, когда API неявный. Это означает, что автор даже не знал, что пишет API, и не думал о том, как люди будут его использовать. Позже мы увидим, как это может произойти.

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

Закон Хайрама предупреждает нас, что кто-то в конечном итоге будет зависеть от любого наблюдаемого поведения, что делает его частью общедоступного API. При разработке Windows 95 компания Microsoft обнаружила, что некоторые популярные программы зависят от ошибочного поведения в Windows 3.1. Для обеспечения совместимости Microsoft вынуждены эмулировать ошибочное поведение в Windows 95. Хотя Microsoft явно не намеревалась делать ошибки частью Windows API, поведение можно было наблюдать, и поэтому оно стало частью API, когда программы начали его ожидать.

<цитата>

Все это говорит о том, что дизайн API — это сложно!

Теперь давайте рассмотрим несколько примеров.

Операции со структурой данных

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

Вот (чрезвычайно) упрощенный набор методов, доступных в C++ std::unordered_map, форме хэш-карты. emplace — необычный способ вставить элемент.

| метод | сложность | |----|----| | размер () | постоянная | | вставить (ключ, значение) | постоянный в среднем | | найти(ключ) | постоянный в среднем | | стереть(ключ) | в среднем постоянный |

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

Эти инварианты и характеристики производительности сами по себе являются частью API типа данных и в идеале должны быть четко задокументированы.

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

Проектировать API сложно!

Системные вызовы ОС

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

Набор доступных системных вызовов образует API между ядром и кодом пространства пользователя. Это уровень абстракции, который отлично подходит для всех участников.

System calls bridge the gap between user space and the kernel

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

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

(И еще приятнее вызывать функцию fread из стандартной библиотеки C, которая оборачивает специфичные для ОС системные вызовы в согласованный интерфейс с дополнительными функциями. Угадайте, что такое стандартная библиотека C!).

За прошедшие годы люди выявили ограничения в разработке различных системных вызовов. Там, где невозможно изменить системный вызов без нарушения существующего кода, разработчикам приходилось вводить пронумерованные версии (см. в качестве примера clone3). Некрасиво, но изменение существующих API означает нарушение кода.

Дизайн API — это сложно!

Внутренние веб-сервисы

Я считаю, что люди находят API сбивающими с толку из-за неточного языка. Разработчики часто говорят что-то вроде «мы можем получить эти данные из API!». Это вызывает путаницу, поскольку API — это абстрактное понятие, а как именно вы получаете данные из абстрактного понятия? Меня это, конечно, запутало.

Чтобы быть полностью точным, мы должны сказать: «мы можем получить эти данные с сервера, реализующего API». Сервер — это просто программа, работающая на подключенном к сети компьютере, которая отвечает на запросы (HTTP, gRPC и т. д.). API определяет, на какие типы запросов он должен отвечать, как он должен их обрабатывать и что он должен выводить. Авторы серверной программы должны убедиться, что она точно реализует API.

Популярный подход в настоящее время заключается в том, чтобы веб-серверы реализовывали REST API через HTTP. Все это означает, что операции API определяются в терминах HTTP-ресурсов и глаголов. Дополнительные сведения см. в главе о работе в сети.

Хотя API-интерфейсы HTTP REST чрезвычайно распространены, и вы должны быть знакомы с ними, помните, что не все API-интерфейсы являются веб-сервисами. Понятие гораздо шире. Помните также, что сам веб-сервис не является API. Это программа, которая ведет себя в соответствии с API. Разница тонкая, но важная.

Написание кода означает разработку API

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

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

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

Независимо от деталей, весь смысл функции в том, что остальная часть вашей кодовой базы должна знать только объявление функции. Детали его реализации скрыты в определении. Какой тип API вы хотите предложить для остальной части вашей программы?

Вот как вы можете определить обработчик ввода для нажатия клавиши со стрелкой вниз:

function handleUserInput(inputEvent) {
  if (inputEvent.keyCode === 'down') {
    // ... trigger some behaviour
  }
}
handleUserInput(event);

Эта функция имеет очень ограничительный API, который не допускает никакой перенастройки. Что произойдет, если позже вам нужно будет вызвать одно и то же поведение при нажатии «ввод» и «вниз»? Наивный подход состоит в том, чтобы просто добавить в предложение if:

function handleUserInput(inputEvent) {
  if (inputEvent.keyCode === 'down'
      || inputEvent.keyCode === 'enter') {
    // ... trigger some behaviour
  }
}
handleUserInput(event);

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

function handleUserInput(inputEvent, keys) {
  if (keys.find(inputEvent.keyCode)) {
    // ... trigger some behaviour
  }
}
handleUserInput(event, ['down', 'enter']);

Наша функция теперь намного полезнее и ее можно использовать повторно!

Что, если вы хотите разное поведение для разных клавиш? Может возникнуть соблазн передать какой-либо объект конфигурации, сопоставляющий код клавиши с обработчиком. Это может быть уместно в некоторых ситуациях, особенно если handleUserInput включает общую логику, общую для всех кодов клавиш, но часто лучше создавать отдельные функции, чем делать все в одной. Зная, где проходит разделительная линия, берется опыт.

Дизайн API — это сложно!

Закончим примером компонента React:

const Confirmation = (props: ConfirmationProps) => (
  <div className="confirmation">
    <p>{`Greetings, ${props.user.name}! Click below to confirm`}</p>
    <form onSubmit={props.handleSubmit}>
      <button class="confirmation-button" type="submit">Confirm</button>
    </form>
  </div>
)

interface ConfirmationProps {
  user: User;
  handleSubmit: (event: React.FormEvent) => void
}

const StyledComfirmation = styled(Confirmation)`
  .confirmation {
    background-color: #fafafa;
  }
  .confirmation button {
    color: red;
  }
`

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

Вы видите более тонкую проблему? Приведенные здесь правила CSS являются примером неявного API. Автор компонента намеревался предоставить стиль кнопки по умолчанию с возможностью настройки с помощью стиля класса confirmation-button. Это хорошо, потому что это означает, что потребителям нужно знать только два имени класса, чтобы применить свой стиль.

Но из-за специфических правил CSS красный цвет будет иметь приоритет над стилями в кнопка подтверждения. Чтобы преодолеть это, потребителям нужно будет написать более конкретные правила стиля, такие как .confirmation button, что приведет к утечке информации о внутренней реализации компонента в более широкую кодовую базу. Фактический API компонента не соответствует ожиданиям автора.

Дизайн API — это сложно!

Заключение

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

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

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

Проектировать API сложно!


Также опубликовано здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE