Узнайте принципы абстракции данных Барбары Лисков, чтобы проектировать стабильные интерфейсы, сокращать число поломок и создавать сопровождаемые системы с понятными и надёжными API.

Барбара Лисков — учёный в области информатики, чьи идеи незримо сформировали то, как современные команды строят ПО, которое не разваливается. Её исследования по абстракции данных, сокрытию информации и позднее — Принципу подстановки Лисков (LSP) — повлияли и на языки программирования, и на повседневное понимание API: определяй понятное поведение, защищай внутренности и делай так, чтобы другим было безопасно полагаться на твой интерфейс.
Надёжный API — это не просто «теоретически корректный» интерфейс. Это интерфейс, который помогает продукту двигаться быстрее:
Эта надёжность — это опыт: для разработчика, вызывающего API, для команды, которая его поддерживает, и для конечных пользователей, зависящих от него косвенно.
Абстракция данных — идея простая: вызывающие стороны должны взаимодействовать с концептом (аккаунт, очередь, подписка) через небольшой набор операций — а не через грязные детали того, как это хранится или вычисляется.
Когда вы скрываете детали представления, вы устраняете целые классы ошибок: никто не может «случайно» опереться на поле в базе, которое не предназначалось для публичного доступа, или мутировать общий стейт так, что система не справится. Не менее важно, что абстракция снижает накладные расходы на координацию: команды не нуждаются в разрешениях на рефакторинг внутренностей, если публичное поведение остаётся неизменным.
К концу статьи у вас будут практические приёмы, чтобы:
Если нужен краткий пересказ позже, перейдите к /blog/a-practical-checklist-for-designing-reliable-apis.
Абстракция данных — простая идея: вы взаимодействуете с чем‑то по тому, что оно делает, а не по тому, как это устроено.
Представьте вендинговый автомат. Вам не нужно знать, как вращаются моторы или как считаются монеты. Вам нужны только управляющие элементы («выбрать товар», «оплатить», «получить товар») и правила («если оплачено достаточно — получаешь товар; если товар продан — возвращается сумма»). Это и есть абстракция.
В ПО интерфейс — это «что оно делает»: имена операций, какие входы принимаются, какие выходы возвращаются и какие ошибки ожидать. Реализация — это «как оно работает»: таблицы в базе, стратегия кеширования, внутренние классы и трюки производительности.
Разделение этих уровней — путь к API, которые остаются стабильными, пока система эволюционирует. Вы можете переписать внутренности, поменять библиотеки или оптимизировать хранение — при этом интерфейс останется тем же для пользователей.
Абстрактный тип данных — это «контейнер + допустимые операции + правила», описанные без привязки к конкретной внутренней структуре.
Пример: Стек (последним пришёл — первым вышел).
push(item): добавить элементpop(): удалить и вернуть последний добавленный элементpeek(): посмотреть верхний элемент, не удаляяКлючевое обещание: pop() возвращает последний push(). Используется массив, связный список или что‑то ещё — это приватно.
Та же идея везде применима:
POST /payments — это интерфейс; проверки на мошенничество, ретраи и записи в базу — реализация.client.upload(file) — интерфейс; чанкинг, компрессия и параллельные запросы — реализация.Проектируя с абстракцией, вы фокусируетесь на контракте, на который опираются пользователи, и покупаете себе свободу менять всё за кулисами без их ломки.
Инвариант — правило, которое всегда должно быть верным внутри абстракции. При проектировании API инварианты — это ограничители, не дающие данным скатиться в невозможные состояния, например: банковский счёт с двумя валютами одновременно или «завершённый» заказ без позиций.
Инвариант — это «форма реальности» для вашего типа:
Cart не может содержать отрицательные количества.UserEmail всегда валиден как адрес электронной почты (не «проверяется позже»).Reservation имеет start < end, и оба времени в одной временной зоне.Если эти утверждения перестают быть верными, система становится непредсказуемой: каждая фича начинает догадываться, что означает «сломанные» данные.
Хорошие API обеспечивают инварианты на границах:
Это улучшает обработку ошибок: вместо расплывчатых сбоев позже («что‑то пошло не так») API может объяснить, какое правило нарушено («end должен быть позже start»).
Вызывающие не должны запоминать внутренние правила типа «этот метод работает только после вызова normalize()». Если инвариант зависит от специального ритуала — это не инвариант, а ловушка.
Спроектируйте интерфейс так, чтобы:
При документировании типа API пропишите:
Хороший API — это не просто набор функций, это обещание. Контракты делают это обещание явным, чтобы вызывающие могли на него опираться, а поддерживающие могли менять реализацию без неожиданностей.
Минимум — документируйте:
Такая ясность делает поведение предсказуемым: вызывающие знают, какие входы безопасны и какие исходы обрабатывать, а тесты проверяют обещание, а не догадываются о намерении.
Без контрактов команды полагаются на память и неформальные нормы: «не передавай null», «вызов иногда ретраится», «в случае ошибки возвращает пустое». Эти правила теряются при онбординге, рефакторах или инцидентах.
Письменный контракт превращает скрытые правила в общий ресурс. Он также служит стабильной целью для ревью кода: обсуждение становится «удовлетворяет ли изменение контракту?» вместо «у меня работает».
Расплывчато: «Создаёт пользователя.»
Лучше: «Создаёт пользователя с уникальным email.
email должен быть валидным адресом; вызывающий должен иметь право users:create.userId; пользователь сохранён и сразу доступен для чтения.409, если email уже существует; возвращает 400 при неверных полях; частичный пользователь не создаётся.»Расплывчато: «Получает элементы быстро.»
Лучше: «Возвращает до limit элементов, отсортированных по createdAt по убыванию.
nextCursor для следующей страницы; курсоры истекают через 15 минут.»Сокрытие информации — практическая сторона абстракции данных: вызывающие должны полагаться на что делает API, а не на как он это делает. Если пользователи не видят внутренности, вы можете менять их без превращения каждого релиза в ломающее изменение.
Хороший интерфейс публикует небольшой набор операций (create, fetch, update, list, validate) и держит представление — таблицы, кеши, очереди, файловые раскладки, границы сервисов — приватным.
Например, «добавить товар в корзину» — это операция. «CartRowId» из вашей базы — деталь реализации. Когда вы экспонируете деталь, вы приглашаете пользователей строить свою логику вокруг неё, что замораживает возможность изменений.
Когда клиенты зависят только от стабильного поведения, вы можете:
…и API остаётся совместимым, потому что контракт не сдвинулся. Это настоящая выгода: стабильность для пользователей и свобода для поддерживающих.
Немного способов, как внутренности случайно просачиваются:
status=3 вместо понятного имени или отдельной операции.Предпочитайте ответы, которые описывают смысл, а не механику:
"userId": "usr_…") вместо номеров строк.Если деталь может измениться, не публикуйте её. Если пользователям она нужна — сделайте её частью интерфейса явно и документированно.
Принцип подстановки Лисков (LSP) в одно предложение: если код работает с интерфейсом, он должен продолжать работать, когда вы подставите любую корректную реализацию этого интерфейса — без специальных случаев.
LSP меньше про наследование и больше про доверие. Публикуя интерфейс, вы даёте обещание о поведении. LSP говорит, что каждая реализация должна держать это обещание, даже если использует совсем другие внутренности.
Вызывающие опираются на то, что говорит API — не на то, что он случайно делает сейчас. Если интерфейс говорит «вы можете вызвать save() с любым валидным документом», то каждая реализация должна принимать такие документы. Если интерфейс говорит «get() возвращает значение или ясный результат «не найдено»», то реализации не должны внезапно бросать новые ошибки или возвращать частичные данные.
Безопасное расширение означает, что вы можете добавлять реализации (или менять провайдеров) без переписывания клиентского кода. Это практический смысл LSP: интерфейсы остаются взаимозаменяемыми.
Два частых способа нарушить обещание:
Уже входы (строже предусловия): новая реализация отклоняет входы, которые интерфейс позволял. Пример: интерфейс принимает любую UTF‑8 строку как id, а одна реализация требует только числовые id.
Слабее выходы (ослабленные постусловия): новая реализация возвращает меньше, чем обещано. Пример: интерфейс обещал отсортированные, уникальные и полные результаты, а реализация возвращает несортированные данные или дубликаты.
Третье, тонкое нарушение — изменение поведения при ошибках: если одна реализация возвращает «не найдено», а другая бросает исключение для той же ситуации, клиенты не смогут безопасно подменять одно на другое.
Чтобы поддерживать «плагины», опишите интерфейс как контракт:
Если реализация действительно нуждается в более строгих правилах, не прячьте это за тем же интерфейсом. Либо (1) определите отдельный интерфейс, либо (2) сделайте ограничение явной возможностью (например, supportsNumericIds()), чтобы клиенты сознательно на это соглашались.
Она популяризировала идеи абстракции данных и сокрытия информации, которые напрямую переводятся в современное проектирование API: публикуйте небольшой, стабильный контракт и держите реализацию гибкой. Практическая выгода очевидна: меньше ломаний, безопасные рефакторы и предсказуемые интеграции.
Надёжный API — это тот, на который клиенты могут полагаться с течением времени:
Надёжность меньше про «никогда не ломаться», и больше про предсказуемые отказы и соблюдение контракта.
Оформите поведение как контракт:
Включите граничные случаи (пустые результаты, дубликаты, порядок), чтобы потребители могли реализовать и протестировать поведение по контракту.
Инвариант — это правило, которое всегда должно соблюдаться внутри абстракции (например, «количество не может быть отрицательным»). API должен обеспечивать инварианты на границах:
Это уменьшает количество последующих ошибок, потому что остальная система не вынуждена обрабатывать невозможные состояния.
Сокрытие информации означает выносить наружу операции и смысл, а не внутреннее представление. Не связывайте потребителей с тем, что вы планируете менять (таблицы, кеши, ключи шардинга, внутренние статусы).
Практические приёмы:
usr_…) вместо номеров строк в базе.Потому что это «замораживает» реализацию. Если клиенты зависят от фильтров, устроенных по‑SQL, ключей JOIN или внутренних идентификаторов, то рефактор схемы превращается в ломание API.
Лучше моделировать интерфейс вокруг доменных концепций: «заказы клиента за период» вместо «сделай where по этой таблице». Храните модель данных скрытой за контрактом.
LSP означает: если код работает с интерфейсом, он должен продолжать работать с любой корректной реализацией этого интерфейса без специальных случаев. В терминах API это правило «не удивляй вызывающего».
Чтобы поддерживать подстановку реализаций, стандартизируйте:
Следите за:
Если реализация действительно требует дополнительных ограничений, публикуйте отдельный интерфейс или явную возможность (capability), чтобы клиенты могли явно согласиться на дополнительные условия.
Держите интерфейсы малые и связные:
options: any и пачек булевых флагов, которые порождают неоднозначные комбинации.Проектируйте ошибки как часть контракта:
Последовательность важнее точного механизма (исключения vs result‑типы), главное — чтобы потребители могли предсказать и обработать исходы.
status=3reserve, release, list, validate).Если есть разные роли или разные скорости изменений, разделите модули/ресурсы. Для эволюции см. /blog/evolving-apis-without-breaking-users.