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

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

Интерфейс — это именованная операция, которую модуль предоставляет другим модулям. Это единственный способ взаимодействия одного модуля с другим — процессы (Глава 7) ссылаются на интерфейсы, а не на модули напрямую.

module Payments {
aspect team: "Payments"
interface authorize
interface capture
interface getTransaction
interface paymentEvents
}

Четыре интерфейса у Payments. У каждого есть тип (interface, базовый тип) и имя. Другие модули могут вызывать Payments.authorize, делать запрос к Payments.getTransaction или потреблять Payments.paymentEvents.

Эта глава использует базовый тип interface. Стандартная библиотека (Глава 11) вводит подтипы интерфейсов, такие как rest_create, rest_read, kafka и grpc_server_stream, которые добавляют семантические различия и стилизацию рёбер.

interface — это базовый тип интерфейса. Он не несёт различия sync/async; он не управляет стилизацией рёбер. Он просто объявляет: «этот модуль предоставляет операцию с именем X».

module Orders {
interface createOrder
interface getOrder
interface orderEvents
}

Валидатор принимает объявление; рендерер диаграммы рисует ребро для каждого вызова, который процесс делает к Orders.createOrder и так далее. Является ли вызов синхронным RPC, асинхронной доставкой события или вызовом функции — для базового языка непрозрачно. Типы интерфейсов из стандартной библиотеки добавляют это различие.

Называйте интерфейс по его реальному протоколу

Заголовок раздела «Называйте интерфейс по его реальному протоколу»

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

  • REST<действие><Ресурс>: getOrder, createInvoice, listShipments.
  • RPC → глагол вызова: chargeCard, reserveSeat, cancelOrder.

Правило. Имена интерфейсов — в lowerCamelCase и зеркалят протокол: действие-плюс-ресурс для REST или глагол для RPC.

Это сохраняет модель читаемой для всех, кто касался реальной системы: имя на диаграмме — это имя в коде, в спецификации OpenAPI или в определении RPC. Расплывчатый handleStuff ничего не говорит ревьюеру; getOrder точно говорит, какой эндпоинт представляет ребро.

Интерфейс всегда объявляется его поставщиком. Поставщик — это модуль, который его выполняет. Шаг процесса всегда имеет вид Caller > Callee.Interface — правая часть называет интерфейс (а значит и поставщика); левая часть называет того, кто вызвал.

process Checkout {
Customer > Payments.authorize // Customer calls Payments
Payments > Ledger.record // Payments calls Ledger
}

Вы никогда не объявляете «интерфейс, через который Customer общается с Payments». Интерфейсы существуют на поставщике. Вызывающие обращаются к ним по полному имени (ModuleName.InterfaceName).

Это исключает распространённую ошибку моделирования: рисование стрелки «Customer→Payments» без указания того, что именно вызывается. В ArchLang так нельзя. Интерфейс должен сначала существовать на Payments; только тогда шаг процесса может его вызвать.

Тело интерфейса не обязательно. Если вам нечего добавлять, опустите его:

interface authorize

Когда тело нужно, чаще всего его содержимое — это описание и поля:

interface authorize {
"Authorize a payment hold. Returns a token used by capture."
timeout: "5s"
idempotent: true
}

Внутри тела можно:

  • Объявить описание (голую строку).
  • Задать поля (timeout: "5s", protocol: rest).
  • Добавить аспекты.

Нельзя поместить интерфейс внутрь интерфейса. Интерфейсы — листовые. Если нужно сгруппировать несколько связанных интерфейсов, используйте поверхность (Глава 6).

События — это асинхронные интерфейсы, достигаемые ребром процесса

Заголовок раздела «События — это асинхронные интерфейсы, достигаемые ребром процесса»

В ArchLang нет отдельной конструкции события или подписки. Событие — это просто интерфейс асинхронного типа — интерфейс kafka, amqp, webhook или sse из стандартной библиотеки (Глава 11), — достигаемый обычным ребром процесса >. Само ребро и есть подписка:

module Orders {
aspect team: "Commerce"
interface orderEvents // событие: асинхронный интерфейс (kafka в стандартной библиотеке)
}
module Shipping {
aspect team: "Fulfillment"
interface createShipment
}
process Fulfilment {
Orders > Shipping.createShipment // поведение Orders запускает обработчик Shipping
}

Одна и та же стрелка > несёт и синхронный RPC, и асинхронную доставку события; что именно — зависит от типа интерфейса на вызываемой стороне, а не от отдельного оператора. На интерфейсе нет постоянной «подписки» — зависимость рисуется только там, где её действительно использует процесс. Латентная подписка без потока — это зеркало неиспользуемого исходящего клиента: возможность, которую модель намеренно не рисует.

Оба соглашения моделирования валидны; выбирайте то, при котором стрелка указывает в ту сторону, в какую зависимость реально работает (язык в это не вмешивается):

  • Событие на стороне производителя. Интерфейс события живёт на производителе (Orders.orderEvents); потребитель тянет к нему ребро: Shipping > Orders.orderEvents. Добавление потребителя никогда не трогает файл производителя — ровно так и ведёт себя pub/sub.
  • Обработчик на стороне потребителя. Обработчик живёт на потребителе (Shipping.createShipment); поток производителя вызывает его: Orders > Shipping.createShipment.

В любом случае соединение — это шаг процесса, а Глава 26 разбирает полный событийно-управляемый конвейер от начала до конца.

Имена интерфейсов квалифицируются их модулем. Изнутри того же пакета можно обратиться к любому интерфейсу как Module.Interface (или Module.Surface.Interface, если он находится внутри поверхности — см. Главу 6). Модули в пространствах имён используют тот же путь через точку: Personal.Banking.Payments.authorize.

В описаниях (Глава 10) и в шагах процесса вы обращаетесь к интерфейсам по полному имени. В зрелой форме резолвер ожидает листовой интерфейс. Шаг, чья вызываемая сторона разрешается в модуль (X > Payments), — это не ошибка: он синтезирует анонимный интерфейс на этом модуле и отслеживает его как TODO — валидный черновик, который заполняется позже (Глава 7). Ворота слияния не пускают такие в завершённую модель.

Модули несут стабильные идентификаторы (#m4k29p); интерфейсы — нет. Их идентичность — это путь через точку внутри объемлющего модуля (Payments.authorize). Переименования обнаруживаются эвристиками по структуре и форме контракта во время диффа — см. Главу 13 для компромисса.

Практическое следствие: не пишите префиксы #xyz у интерфейсов. Форматтер их не добавит, и в грамматике для них нет места — парсер их отвергнет.

  • Интерфейс — это именованная операция, объявляемая его поставщиком.
  • Базовый тип — interface. Подтипы из стандартной библиотеки (rest_create, rest_read, kafka, grpc_server_stream, …) добавляют семантические различия — см. Главу 11.
  • Направление в процессе всегда Caller > Callee.Interface; интерфейс живёт на вызываемой стороне.
  • Интерфейсы — листовые: они не содержат других интерфейсов. Чтобы их сгруппировать, используйте поверхности.
  • Событие — это интерфейс асинхронного типа, достигаемый обычным ребром процесса > — нет ни subscribes:, ни отдельной конструкции события.
  • У интерфейсов нет стабильных идентификаторов; идентичность — по пути через точку.

Глава 6: Поверхности → — как группировать интерфейсы внутри поверхности модуля.