Значение принципа разделения интерфейсов в API-дизайне
Почему дизайн API требует принципа разделения интерфейса
Современные программные системы живут или умирают от своих API. Независимо от того, создаете ли вы RESTful-сервис, конечную точку GraphQL или набор SDK для внутреннего потребления, решения, которые вы принимаете в своем дизайне интерфейса, каскадируются в каждом клиенте, который их касается. Одним из наиболее эффективных способов сохранить ваш API чистым, поддерживающим и удобным для разработчиков, является применение принципа разделения интерфейса (ISP) [[FLT: 1]].
ISP является четвертым из пяти принципов объектно-ориентированного дизайна, первоначально введенных Робертом С. Мартином в конце 1990-х годов. Хотя этот принцип был разработан для классов и интерфейсов на таких языках, как Java или C++, его руководство непосредственно переносимо и, возможно, даже более важно для проектирования API. По сути, ISP говорит: Ни один клиент не должен зависеть от методов, которые он не использует.
В терминах API это означает разработку узких, сфокусированных конечных точек и контрактов, а не монолитных интерфейсов «все в одном». Таким образом, вы уменьшаете связь, улучшаете ясность и позволяете каждому клиенту взаимодействовать только с теми частями API, которые важны для него. В этой статье подробно рассматривается, что означает ISP для разработчиков API, как эффективно его реализовать и почему избегание соблазна толстых интерфейсов принесет долгосрочные дивиденды.
Понимание принципа разделения интерфейса
Происхождение и основная идея
Принцип разделения интерфейсов возник из наблюдения, что большие «жирные» интерфейсы, как правило, накапливают обязанности с течением времени. Единый интерфейс, который обрабатывает чтение, запись, обновление, удаление, аутентификацию, регистрацию и аудит, заставляет каждого потребителя знать и потенциально внедрять каждый из этих методов, даже если им нужны только операции чтения.
ISP выступает за разделение таких раздутых интерфейсов на более мелкие, специфичные для конкретных ролей контракты. Вместо одного интерфейса «DataManager» у вас могут быть «DataReader», «DataWriter», «DataDeleter» и «Auditor». Клиенты тогда зависят только от интерфейсов, которые соответствуют их точным потребностям. Это уменьшает эффект ряби изменений и делает систему более легкой для понимания и развития.
ISP в контексте API дизайна
При проектировании API, подумайте о «интерфейсе» как о контракте между вашим сервисом и его потребителями — будь то фронтенд-приложения, другие микросервисы или сторонние разработчики. Ресурс REST API с десятками конечных точек или схема GraphQL с одним массивным типом мутации, может стать «жирным интерфейсом». Клиенты вынуждены обрабатывать документацию (а иногда и импортировать SDK) для операций, которые они никогда не называют.
ISP помогает вам спросить: «Могу ли я разбить это на более мелкие, независимые контракты?» Ответ часто приводит к более чистой версии, более легкому тестированию и лучшей масштабируемости. Например, API с открытым доступом может предоставить легкий оптимизированный для чтения интерфейс для мобильных клиентов, предлагая более функциональный интерфейс записи для внутренних инструментов администратора.
Ключевые преимущества применения ISP в вашем API
Улучшенный опыт разработчиков (DX)
Узкие интерфейсы проще изучать и использовать. Разработчики, новые для вашего API, могут быстро находить конечные точки или операции, относящиеся к их задаче, не продвигаясь через нерелевантную функциональность. Это снижает когнитивную нагрузку и ускоряет интеграцию. Например, платежный шлюз, который предоставляет отдельные интерфейсы для авторизации, захвата, возврата и пустоты, гораздо более интуитивно понятен, чем одна конечная точка «/транзакции», которая требует сложных полезных нагрузок для различения операций.
Усиление гибкости и эволюционируемости
Когда интерфейсы малы и сфокусированы, изменения в одной части системы оказывают минимальное влияние на другие. Если вам нужно добавить новую возможность в интерфейс чтения — скажем, парагинацию или параметры фильтрации — интерфейс записи остается нетронутым. Аналогично, если конкретной конечной точке нужны ломающие изменения, вы можете обесценить или версию только этого небольшого контракта, а не всего API.
Улучшение устойчивости и тестируемости
Меньшие интерфейсы легче издеваться, заглушать и тестировать изолированно. Для бэкэнд-команд это означает, что можно проводить единичные тесты каждого контракта с конечной точкой без вращения всего стека приложений. Для команд на стороне клиента узкие контракты уменьшают площадь поверхности для интеграционного тестирования. Результатом являются более быстрые петли обратной связи и меньшее количество дефектов.
Сокращение сцепления и раздувание зависимости
Жировые интерфейсы создают неявные зависимости. Мобильное приложение, которому нужно только читать профили пользователей, не должно зависеть от библиотеки или транспортного уровня, который включает в себя возможности записи и удаления. ISP уменьшает эту связь, делая более безопасным развитие как API, так и его потребителей независимо. В микросервисных архитектурах этот принцип имеет решающее значение для поддержания автономии обслуживания.
Внедрение ISP в API дизайн: практические стратегии
1.Определить роли клиентов
Первый шаг - понять, кто ваши клиенты API и какие операции они фактически выполняют.
- Пользователи, только для чтения (например, мобильные приложения, отображающие данные)
- Потребители, использующие только текст (например, процессоры для партии, импортирующие записи)
- Административные потребители (например, панели инструментов, которые нуждаются в возможности удаления и аудита)
- Сторонние разработчики , которым может потребоваться только подмножество функций
Картографируйте каждую роль в соответствии с конкретными операциями, которые она требует. Это выявляет естественные границы для сегрегации.
2. использовать отдельные конечные точки или ресурсы
В REST создаются выделенные конечные точки для различных обязанностей. Вместо одного ресурса / api / orders, обрабатывающего все, рассмотрите возможность разделения:
- «GET /api / Orders» — список заказов
- «POST /api / Orders» — создать заказ (написать)
- «GET /api/orders/{id}/status» — статус проверки (читай, специализированный)
- PATCH /api/orders/{id}/cancel - отменять заказ (запись, объем)
Каждая конечная точка становится мини-интерфейсом со своей семантикой. Это прямое применение ИСП на уровне ресурсов.
3. Наследование (не наследование) для интерфейсов
При разработке внутренних контрактов API (например, в SDK или на уровне сервиса) предпочтение отдается небольшим интерфейсам, которые могут быть составлены. Например, в TypeScript или Java, определяют:
interface OrderReader {
getOrder(id: string): Promise<Order>;
listOrders(filter: OrderFilter): Promise<Order[]>;
}
interface OrderWriter {
createOrder(data: CreateOrderInput): Promise<Order>;
updateOrder(id: string, data: UpdateOrderInput): Promise<Order>;
}
// A composite interface for admin use
interface OrderAdmin extends OrderReader, OrderWriter {
deleteOrder(id: string): Promise<void>;
}
Этот шаблон гарантирует, что клиенты зависят только от того, что им нужно. Сервисы могут реализовывать только соответствующие интерфейсы, избегая неиспользованных заглушки методов.
4. Отдельные модели чтения и письма (CQRS)
Для сложных доменов рассмотрите возможность принятия Сегрегации ответственности командных запросов (CQRS) . CQRS - это стиль архитектуры, который естественным образом обеспечивает соблюдение требований ISP, отделяя модели чтения (запросы) от моделей записи (команд). Ваш API предоставляет различные конечные точки или каналы для запросов и команд. Это мощный способ гарантировать, что клиенты никогда не зависят от методов, которые они не используют.
5. Используйте гранулированные разрешения с ролевым доступом
ISP также применяется к безопасности. Вместо одного монолитного ключа API, предоставляющего все возможности, выдает расширенные токены или ключи API, которые ограничивают доступ к конкретным интерфейсам. Например, у публичного клиента может быть разрешение на вызов «GET / продукты», в то время как внутренняя система также может вызывать «POST / продукты». Это обеспечивает соблюдение ISP на уровне авторизации и предотвращает ненужное воздействие.
Реальные примеры ISP в действии
RESTful API: GitHub, Twilio, Stripe
Основные поставщики API являются отличными примерами ISP. API GitHub имеет выделенные конечные точки для репо, проблем, тяг и действий — вам никогда не нужно использовать метод управления запросами тяги, когда вы хотите только перечислять проблемы. API Twilio разделяет обмен сообщениями, голос и верификацию на разные конечные точки. Стрип предлагает различные API для платежей, выставления счетов и подключения. Каждый ориентирован на одну бизнес-возможность.
Подумайте о том, чтобы посетить ссылку API Stripe , чтобы увидеть, как они избегают толстых интерфейсов.
GraphQL и ISP
GraphQL может изначально показаться нарушающим ISP, потому что одна конечная точка раскрывает всю схему. Однако хорошо разработанные API GraphQL применяют ISP на полевом уровне. Схема определяет отдельные типы и запросы для различных проблем, и клиенты могут запрашивать только те поля, которые им нужны. Такие инструменты, как Федерация Аполлона, делают это дальше, составляя унифицированный граф из нескольких подграфов, каждый из которых отвечает за ограниченный контекст — ISP на уровне микросервиса.
Микросервисы и ограниченный контекст
В микросервисных архитектурах каждая служба раскрывает свой собственный интерфейс (API). Обработке аутентификации пользователей не нужно знать об обновлениях инвентаря. Сохраняя сервисы небольшими и сфокусированными, вы, естественно, придерживаетесь ISP. Согласно статье Мартина Фаулера о микросервисах , это разложение является ключом к независимой развертываемости и масштабируемости.
SDK и библиотечный дизайн
Например, вместо одного центрального класса «ApiClient» с сотнями методов, предложите специализированные классы, такие как «OrdersClient», «ProductsClient» и «CustomersClient». Это именно то, что делает SDK AWS для JavaScript — каждая служба получает свой собственный класс клиентов.
Обычные подводные камни и как их избежать
Чрезмерная сегрегация
Слишком гранулированный подход может создать множество крошечных интерфейсов, которые сбивают с толку, чтобы перемещаться и поддерживать. Цель состоит не в том, чтобы иметь один интерфейс для каждого метода, а в том, чтобы группировать логически связанные операции, которые изменяются вместе. Хорошее эмпирическое правило: если два операции всегда используются вместе одним клиентом, они, вероятно, принадлежат одному и тому же интерфейсу.
Преждевременная гранулярность
Не перепроектируйте интерфейсы, прежде чем вы поймете потребности клиента. Начните с немного большего интерфейса и разделите его, только когда вы увидите конкретные доказательства различных ролей клиента или измените давление. Рефакторинг интерфейсов позже приемлем - особенно если у вас есть стратегии версионного копирования.
Игнорирование обратной совместимости
При разделении существующего интерфейса существующие клиенты могут сломаться, если они полагались на старый контракт. Всегда обесценивайтесь постепенно. Для REST вы можете редактировать свои конечные точки (например, '/v1/orders', '/v2/orders/read'). Для внутренних интерфейсов используйте шаблоны адаптера для моста старых и новых контрактов.
Накладные расходы на булинг и документацию
Больше интерфейсов означает больше документации. Инвестируйте в хорошие инструменты документации API (например, OpenAPI / Swagger или GraphQL) и убедитесь, что каждый интерфейс четко описан. Усилия окупаются в доверии и принятии разработчиком.
ISP и другие принципы Solid
Принцип единой ответственности (SRP)
ISP, естественно, согласуется с SRP. SRP говорит, что у модуля должна быть одна причина для изменения. ISP гарантирует, что интерфейс несет одну ответственность - обслуживает одну роль клиента. Когда вы следуете за SRP на уровне модуля, вы часто получаете интерфейсы, которые уже разделены.
Принцип замещения Лискова (LSP)
ISP не конфликтует с LSP. На самом деле, небольшие интерфейсы облегчают создание заменяемых реализаций. Если интерфейс имеет только два метода, любая реализация, которая выполняет эти методы, может быть заменена с уверенностью. Жирные интерфейсы часто заставляют разработчиков бросать нереализованные методы (например, бросать «NotImplementedException»), что нарушает LSP.
Открытый/закрытый принцип (OCP)
Разделенные интерфейсы поддерживают OCP, потому что вы можете добавлять новое поведение, создавая новые интерфейсы, а не модифицируя существующие. Например, добавление пакетной операции не требует изменения существующих интерфейсов чтения / записи - вы создаете новый интерфейс «BatchProcessor», который клиент может выбрать для реализации.
Принцип инверсии зависимостей (DIP)
ISP работает рука об руку с DIP: абстракции (интерфейсы) не должны зависеть от деталей; детали должны зависеть от абстракций.Когда эти абстракции очень сплочённы и разделены, вы достигаете максимальной гибкости в зависимости от проводки.
Тестирование API с ISP в уме
Применение ISP упрощает тестирование на нескольких уровнях:
- Единичные тесты: Каждый маленький интерфейс может быть легко высмеян. Тест для клиента только для чтения должен только высмеивать интерфейс считывания, а не весь API.
- Интеграционные тесты: Вы можете тестировать конечные точки изолированно. Тест на конечные точки записи не требует использования конечных точек чтения.
- Контрактное тестирование: С узкими интерфейсами контрактные тесты (например, с использованием Pact) становятся более целенаправленными.Каждый потребительский пакт охватывает только используемые им взаимодействия, снижая вероятность ложных срабатываний.
- Тесты производительности: Изолирование путей чтения и записи позволяет более точно моделировать реальные шаблоны использования.
Измерение влияния ISP
Как узнать, хорошо ли сегрегирован ваш дизайн API? Ищите эти показатели:
- Низкий «фанат» — типичная интеграция с клиентом затрагивает только несколько конечных точек или интерфейсов.
- Редкие изменения в общих интерфейсах — если интерфейс часто меняется по причинам, не связанным с его основным клиентом, он, вероятно, слишком широк.
- Немногие устаревшие методы — если ваш API накапливает много помеченных «@deprecated», которые являются остатками от жирных интерфейсов, сегрегация была слабой.
- Короткое время входа для новых разработчиков — узкий API легче изучить.
Заключение
Принцип разделения интерфейсов - это не просто академическое руководство - это практический инструмент для создания API, которые выдерживают испытание временем. Создавая небольшие, специфичные для ролей интерфейсы, вы уменьшаете взаимодействие, улучшаете опыт разработчиков и делаете свою систему более устойчивой к изменениям. Независимо от того, разрабатываете ли вы конечные точки REST, схемы GraphQL или SDK, вопрос «Мой клиент действительно нуждается в этом?» приведет вас к лучшим архитектурным решениям.
Помните, что ISP — это не жесткие правила, а интенциональность. Начните с клиентоориентированной перспективы, итерируйте на основе реальных моделей использования и не бойтесь рефакторировать интерфейсы по мере роста вашего понимания. Результатом будет API, с которым разработчики любят работать, который может развиваться, не нарушая мир.
Для дальнейшего чтения изучите статью ISP в Википедии и Роберта К. Мартина о SOLID . Эти ресурсы обеспечивают дополнительную глубину в отношении ISP к другим эвристикам дизайна.