17. Обязательные пропуски
required-пропуск — это способ языка сказать «этот слот существует и должен быть заполнен, прежде чем модель станет валидной». Так автор типа сообщает каждому потомку: тебе нужно это решить; я не могу решить это за тебя.
type module service { required cascade team}Этот тип объявляет поле team. Значения по умолчанию нет. Каждый экземпляр микросервиса должен либо предоставить значение, либо явно отбросить поле. Всё остальное — ошибка валидации.
Эта глава о точной семантике required и о единственных двух способах, которыми экземпляр (или потомок-подтип) может с ним справиться.
Что такое пропуск на самом деле
Ментальная модель, которая втягивает людей в неприятности: думать о required как о маркере, который можно добавить или убрать. Нельзя.
Смена мышления.
required-пропуск не является отделимым маркером. Он и есть состояние отсутствия значения. Нет операции «понизить required до необязательного без заполнения», потому что у этой операции нет смысла — объявленное поле без значения и без флагаrequired— это то же самое, что поле сrequiredи без значения, что то же самое, что отсутствие поля вовсе (на уровне типа).
Поэтому, читая required team, не читайте «к полю team прикреплён флаг required». Читайте «поле team существует без значения, и это недопустимо в экземплярах». Флаг и пустота — это один и тот же факт.
Следствие: есть только два способа справиться с required-пропуском:
- Заполнить. Установить значение. Пропуск перестаёт быть пропуском.
- Отбросить. Удалить всё объявление полностью. Нет пропуска, потому что нет поля.
Вот и всё. Третьего варианта не существует.
Заполнение
Самый частый случай. Предоставьте значение:
type module service { required cascade team}
service Payments { team: Payments // fulfills the blank}Или предоставьте содержимое для пропуска под-объявления:
type module service { required database PrimaryStore}
service Orders { database PrimaryStore { // fulfills the blank with content command Read command Write }}После заполнения потомки и подтипы вниз по цепочке больше не сталкиваются с пропуском. Он отвечен. Если вы хотели, чтобы они тоже что-то ответили, родительскому типу следовало оставить другой пропуск.
Отбрасывание
Если у вас действительно нет и не будет значения для обязательного поля — отбросьте объявление целиком:
type module service { required database PrimaryStore required cascade team}
service StatelessRouter { team: Platform drop PrimaryStore // explicit acknowledgement: this service has no store}Отбрасывание намеренно делается громким. Оно вынуждает автора написать drop PrimaryStore, а не молча опустить поле. Следующий ревьюер видит отбрасывание и может спросить: «почему у этого сервиса нет основного хранилища?» — это полезный вопрос. Молчаливое пропускание скрыло бы его.
Запрещённый третий вариант
Чего нельзя:
// In a type body:type module service { required cascade team}
// In an instance:service Bad { // Idea: "I want the team 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
К каждой части тела типа можно применить required:
type module service { // Required field required cascade team
// Required label required labels.domain
// Required sub-declaration (interface) required event Heartbeat
// Required sub-declaration (sub-module) required database PrimaryStore}Каждый из них требует от экземпляра отдельного решения «заполнить или отбросить».
Для вложенных меток точечная и блочная формы эквивалентны:
type module service { required labels.domain // is the same as: labels { required domain }}Набор действий против унаследованного пропуска
Когда подтип или экземпляр наследует required-пропуск, у него ровно шесть возможных действий:
| Цель | Синтаксис |
|---|---|
| Уточнить до вида-подтипа, оставить пропуск | required <subtype-kind> Name |
| Уточнить до вида-подтипа, заполнить | <subtype-kind> Name { ... } |
| Сменить на вид, не являющийся подтипом, оставить пропуск | override required <new-kind> Name |
| Сменить на вид, не являющийся подтипом, заполнить | override <new-kind> Name { ... } |
| Заполнить, сохранив унаследованный вид | Name: value или Name { ... } |
| Полностью удалить | drop Name |
override — тема главы 19. Пока: это ключевое слово для смены унаследованного объявления на вид, не являющийся подтипом исходного.
Все шесть действий на одном пропуске:
type module service { required database PrimaryStore}
// 1. Refine to subtype kind, keep blank — relational_db extends databasetype service transactionalService { required relational_db PrimaryStore}
// 2. Refine to subtype kind, fulfilltype service fulfilledRelational { relational_db PrimaryStore { command Read }}
// 3. Switch to non-subtype kind, keep blanktype service cacheStillBlank { override required cache PrimaryStore}
// 4. Switch to non-subtype kind, fulfilltype service cacheFulfilled { override cache PrimaryStore { command Lookup }}
// 5. Fulfill keeping inherited kindtype service ordinary { database PrimaryStore { command Read; command Write }}
// 6. Remove entirelytype service stateless { drop PrimaryStore}Каждое допустимое действие против обязательного пропуска — одно из этих шести. Всё остальное — ошибка.
Подтипы могут добавлять новые пропуски
Подтип может добавить свои собственные required-пропуски, требуя от своих экземпляров (и любых дальнейших подтипов) больше информации:
type module service { required cascade team}
type service payments_service { team: "Payments" // fulfills parent's blank required ext.processor_vendor // adds a new blank}Перед экземпляром payments_service больше не стоит team (тип заполнил это), но стоит ext.processor_vendor. Экземпляр должен заполнить или отбросить его.
Так эволюционирует метамодель: обобщённый тип задаёт универсальные требования; подтипы накладывают на него доменно-специфичные требования.
Поведение каскада у обязательных пропусков
Модификатор cascade — часть того, как поле объявляется на уровне типа. Вы не можете изменить его в подтипе или экземпляре — режим распространения зафиксирован у типа, который вводит поле.
type module service { required cascade team // cascades through nested modules}
service Payments { team: Payments
component MetricsExporter { // No 'team' set here; the cascade means resolved team is "Payments" }}Глава 18 — глубокое погружение в каскад.
Ошибки, которые вы увидите
Валидатор выдаёт конкретные диагностики:
- «Required field
teamis not fulfilled and not dropped» — вы забыли заполнить или отбросить пропуск. - «Cannot use
requiredtogether with content» —required component logs { command Send }— то самое противоречие. - «Cannot
dropfieldx: not inherited» — отбрасывать можно только то, что существует в области видимости.
Каждая указывает на место в исходнике и подсказывает исправление.
Резюме
required-пропуск — это отсутствие значения, а не отделимый маркер.- Два способа с ним справиться: заполнить (установить значение или содержимое) или отбросить (удалить объявление).
- «Понижение required без заполнения» — не операция; состояние, которое она создала бы, не имеет смысла.
- Таблица из шести действий охватывает все допустимые действия против унаследованного обязательного пропуска.
- Подтипы могут добавлять свои собственные обязательные пропуски для дальнейшей специализации.
- Поведение каскада зафиксировано у типа, который вводит поле; подтипы не могут его изменить.
Что дальше
Глава 18: Распространение → — самая длинная глава в книге и смена мышления, благодаря которой метамодель складывается воедино: два компонуемых механизма распространения.