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

6. Поверхности

Модуль с двадцатью интерфейсами в плоском списке нечитаем. Поверхность — это именованная группировка интерфейсов (и вложенных поверхностей) внутри тела модуля. Это исключительно средство организации: у поверхностей нет семантики развёртывания, нет стабильных идентификаторов, нет присутствия за пределами модуля, в котором они живут.

module Orders {
aspect team: "Commerce"
surface ordersResource {
"Order CRUD operations"
base: "/orders"
interface create
interface get
interface update
interface delete
surface products {
base: "/products"
interface add
interface remove
}
}
surface webhooks {
interface subscribe
interface unsubscribe
}
}

Две поверхности верхнего уровня на Orders: ordersResource (со своей вложенной поверхностью products) и webhooks. Каждая группирует интерфейсы под именем; каждая может нести поля, аспекты и описание. Имена поверхностей — в lowerCamelCase, как и у интерфейсов, которые они группируют.

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

Поверхность — это домен интерфейсов: то же доменное мышление, что определяет, как вы разбиваете файлы и вкладываете модули, применённое на уровень ниже — к поверхности API модуля. Поверхность userInteraction держит операции взаимодействия с пользователем; поверхность webhooks держит операции вебхуков.

Поверхности дают вам место для того, чтобы:

  • Сгруппировать концептуально связанные интерфейсы (операции /orders вместе).
  • Прикрепить общие поля, например базовый путь, который должен применяться ко всему, что находится ниже.
  • Применить шаблон типа (Глава 16), чтобы можно было штамповать переиспользуемую поверхность crud, получающую фиксированный набор операций бесплатно — одна и та же стандартная форма применяется к разным модулям, вместо того чтобы переобъявлять её каждый раз.

Они не про развёртывание, владение или адресацию. Это относится к модулям.

Тест на группировку — это уменьшенный файловый тест: представьте, что выписываете каждую привязку интерфейса. Привязки, которые вы охотно держали бы в одном файле, относятся к одной поверхности; привязки, которые вы скорее разнесли бы по разным файлам, относятся к разным поверхностям. А интерфейс, который не вписывается ни в одну группу — не связан с остальными, — не нужно силой запихивать в неё. Оставьте его прямо на модуле или дайте ему собственную поверхность. Отдельная операция прекрасно живёт в теле модуля; тянитесь за поверхностью, когда существует реальная группа.

Правило. Модули содержат поверхности; поверхности не содержат модулей.

Поверхность может содержать другие поверхности и интерфейсы. Она не может содержать модуль. Если вам нужен подмодуль внутри сервиса, объявите его как вложенный модуль (Глава 4), а не как поверхность:

module Orders {
surface api {
interface create // ✅ интерфейсы внутри поверхности
}
module Worker { // ✅ вложенный модуль — НЕ внутри поверхности
interface runJob
}
// ❌ Это было бы ошибкой разбора:
// surface Bad {
// module Inside { ... }
// }
}

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

Шаги процессов и перекрёстные ссылки разрешаются через поверхности по точечным путям:

process Buy {
Customer > Orders.ordersResource.create
Customer > Orders.ordersResource.products.add
Customer > Orders.webhooks.subscribe
}

Полный путь — Module.Surface.[Surface.]Interface. Поверхности прозрачны для резолвера — они организуют исходный текст, но не вводят отдельное пространство имён, которое нужно было бы обходить.

surface — единственный тип поверхности, определённый в стандартной библиотеке. Вы можете объявлять собственные типы поверхностей, когда предпочтительна доменно-специфическая лексика:

type surface resource {
"An HTTP resource — appends to a base path"
append base
}
module Orders {
resource ordersResource {
base: "/orders"
interface post
interface get
}
}

resource теперь — определённый пользователем тип поверхности, который ведёт себя как surface плюс поле append base. (Глава 16 рассказывает об определении типов; Глава 18 — про append.)

Распространённые соглашения, которые вы встретите в реальных проектах:

  • resource — в стиле HTTP-ресурса, дописывает пути.
  • capability — группировка по возможностям, без семантики путей.
  • endpoint_group — группировка связанных интерфейсов под общей меткой.

Ни один из них не поставляется со стандартной библиотекой; команды определяют их по необходимости.

Тела поверхностей несут то же содержимое из полей и меток, что и модули:

surface ordersResource {
"Order CRUD operations"
base: "/orders"
version: v2
aspect {
domain: "Orders"
}
interface create
interface get
}

Аспекты, объявленные на поверхности, каскадируются на её вложенные интерфейсы и вложенные поверхности — именно так aspect { domain: "Orders" } на поверхности распространяется на каждую операцию внутри без повторов. Глава 18 полностью покрывает каскад.

У поверхностей, как и у интерфейсов, нет стабильных идентификаторов. Их идентичность — это точечный путь внутри объемлющего модуля. Обнаружение переименования использует структурные эвристики, как и у интерфейсов (Глава 13).

Поверхность, которой нечего добавить сверх шаблона её типа, может опустить тело:

module Orders {
resource ordersResource // surface using a 'resource' type, no instance additions
}

Это объявляет поверхность ordersResource, содержимое которой целиком приходит из типа resource. Мы встретим типы в Главе 15.

  • Поверхность группирует интерфейсы (и вложенные поверхности) внутри тела модуля.
  • Поверхности организационны: нет стабильных идентификаторов, нет семантики развёртывания.
  • Вложенность односторонняя — модули содержат поверхности, никогда наоборот.
  • Шаги процессов достигают интерфейсов внутри поверхностей по точечному пути (Module.Surface.Interface).
  • Пользовательские типы поверхностей (resource, capability) позволяют согласовать запись с доменной лексикой.

Глава 7: Процессы → — как описывается поведение и откуда на самом деле берутся стрелки зависимостей.