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

17. Обязательные пропуски

required-пропуск — это способ языка сказать «этот слот существует и должен быть заполнен, прежде чем модель станет валидной». Так автор типа сообщает каждому потомку: тебе нужно это решить; я не могу решить это за тебя.

type module service {
required cascade version
}

Этот тип объявляет поле version. Значения по умолчанию нет. Каждый экземпляр микросервиса должен либо предоставить значение, либо явно отбросить поле. Всё остальное — ошибка валидации.

Эта глава о точной семантике required и о единственных двух способах, которыми экземпляр (или потомок-подтип) может с ним справиться.

Ментальная модель, которая втягивает людей в неприятности: думать о required как о маркере, который можно добавить или убрать. Нельзя.

Смена мышления. required-пропуск не является отделимым маркером. Он и есть состояние отсутствия значения. Нет операции «понизить required до необязательного без заполнения», потому что у этой операции нет смысла — объявленное поле без значения и без флага required — это то же самое, что поле с required и без значения, что то же самое, что отсутствие поля вовсе (на уровне типа).

Поэтому, читая required version, не читайте «к полю version прикреплён флаг required». Читайте «поле version существует без значения, и это недопустимо в экземплярах». Флаг и пустота — это один и тот же факт.

Следствие: есть только два способа справиться с required-пропуском:

  1. Заполнить. Установить значение. Пропуск перестаёт быть пропуском.
  2. Отбросить. Удалить всё объявление полностью. Нет пропуска, потому что нет поля.

Вот и всё. Третьего варианта не существует.

Самый частый случай. Предоставьте значение:

type module service {
required cascade version
}
service Payments {
version: "2.1" // fulfills the blank
}

Или предоставьте содержимое для пропуска под-объявления:

type module service {
required database PrimaryStore
}
service Orders {
database PrimaryStore { // fulfills the blank with content
db_read read
db_write write
}
}

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

Если у вас действительно нет и не будет значения для обязательного поля — отбросьте объявление целиком:

type module service {
required database PrimaryStore
required cascade version
}
service StatelessRouter {
version: "1.0"
drop PrimaryStore // explicit acknowledgement: this service has no store
}

Отбрасывание намеренно делается громким. Оно вынуждает автора написать drop PrimaryStore, а не молча опустить поле. Следующий ревьюер видит отбрасывание и может спросить: «почему у этого сервиса нет основного хранилища?» — это полезный вопрос. Молчаливое пропускание скрыло бы его.

Чего нельзя:

// In a type body:
type module service {
required cascade version
}
// In an instance:
service Bad {
// Idea: "I want the version field to exist but not be required and not be set."
// There's no syntax for this. There's no concept for this. The thing
// you're describing isn't a thing.
}

Если у вас возникает желание «понизить до необязательного без заполнения», отступите и спросите: а каким было бы значение этого поля? Если ответ «пока никакое, но оно должно существовать» — у вас пропуск, заполните позже. Если ответ «у этого сервиса его действительно нет» — отбросьте.

К каждой части тела типа можно применить required:

type module service {
// Required field
required cascade version
// Required aspect
required aspect domain
// Required sub-declaration (interface)
required kafka heartbeat
// Required sub-declaration (sub-module)
required database PrimaryStore
}

Каждый из них требует от экземпляра отдельного решения «заполнить или отбросить».

Для вложенных аспектов точечная и блочная формы эквивалентны:

type module service {
required aspect domain
// is the same as:
aspect {
required domain
}
}

Набор действий против унаследованного пропуска

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

Когда подтип или экземпляр наследует required-пропуск, у него ровно шесть возможных действий:

ЦельСинтаксис
Уточнить до типа-подтипа, оставить пропускrequired <subtype-type> Name
Уточнить до типа-подтипа, заполнить<subtype-type> Name { ... }
Сменить на тип, не являющийся подтипом, оставить пропускoverride required <new-type> Name
Сменить на тип, не являющийся подтипом, заполнитьoverride <new-type> Name { ... }
Заполнить, сохранив унаследованный типName: value или Name { ... }
Полностью удалитьdrop Name

override — тема главы 19. Пока: это ключевое слово для смены унаследованного объявления на тип, не являющийся подтипом исходного.

Все шесть действий на одном пропуске:

type module service {
required database PrimaryStore
}
// 1. Refine to subtype type, keep blank — relational_db extends database
type service transactional_service {
required relational_db PrimaryStore
}
// 2. Refine to subtype type, fulfill
type service fulfilled_relational {
relational_db PrimaryStore { db_read read }
}
// 3. Switch to non-subtype type, keep blank
type service cache_still_blank {
override required cache PrimaryStore
}
// 4. Switch to non-subtype type, fulfill
type service cache_fulfilled {
override cache PrimaryStore { db_read lookup }
}
// 5. Fulfill keeping inherited type
type service ordinary {
database PrimaryStore { db_read read; db_write write }
}
// 6. Remove entirely
type service stateless {
drop PrimaryStore
}

Каждое допустимое действие против обязательного пропуска — одно из этих шести. Всё остальное — ошибка.

Подтип может добавить свои собственные required-пропуски, требуя от своих экземпляров (и любых дальнейших подтипов) больше информации:

type module service {
required cascade version
}
type service payments_service {
version: "2.1" // fulfills parent's blank
required ext.processor_vendor // adds a new blank
}

Перед экземпляром payments_service больше не стоит version (тип заполнил это), но стоит ext.processor_vendor. Экземпляр должен заполнить или отбросить его.

Так эволюционирует метамодель: обобщённый тип задаёт универсальные требования; подтипы накладывают на него доменно-специфичные требования.

Поведение каскада у обязательных пропусков

Заголовок раздела «Поведение каскада у обязательных пропусков»

Модификатор cascade — часть того, как поле объявляется на уровне типа. Вы не можете изменить его в подтипе или экземпляре — режим распространения зафиксирован у типа, который вводит поле.

type module service {
required cascade version // cascades through nested modules
}
service Payments {
version: "2.1"
component MetricsExporter {
// No 'version' set here; the cascade means resolved version is "2.1"
}
}

Глава 18 — глубокое погружение в каскад.

Валидатор выдаёт конкретные диагностики:

  • «Required field version is not fulfilled and not dropped» — вы забыли заполнить или отбросить пропуск.
  • «Cannot use required together with content»required component logs { rest_create Send } — то самое противоречие.
  • «Cannot drop field x: not inherited» — отбрасывать можно только то, что существует в области видимости.

Каждая указывает на место в исходнике и подсказывает исправление.

  • required-пропуск — это отсутствие значения, а не отделимый маркер.
  • Два способа с ним справиться: заполнить (установить значение или содержимое) или отбросить (удалить объявление).
  • «Понижение required без заполнения» — не операция; состояние, которое она создала бы, не имеет смысла.
  • Таблица из шести действий охватывает все допустимые действия против унаследованного обязательного пропуска.
  • Подтипы могут добавлять свои собственные обязательные пропуски для дальнейшей специализации.
  • Поведение каскада зафиксировано у типа, который вводит поле; подтипы не могут его изменить.

Глава 18: Распространение → — самая длинная глава в книге и смена мышления, благодаря которой метамодель складывается воедино: два компонуемых механизма распространения.