15. Зачем нужны типы?
Вы используете типы (service, database, rest_create) с главы 4. Все они приходили из стандартной библиотеки. Эта глава о том, что типы такое и откуда они берутся. Следующие четыре главы — о том, как определять свои собственные.
Это первая глава Части IV. Здесь центр тяжести книги смещается. Части I–III были посвящены написанию .arch-файлов с использованием уже имеющихся типов. Часть IV — о написании самих типов.
Смена мышления. Если вы пришли из объектно-ориентированного мира, слово «тип» в ArchLang означает не то, что вы думаете. Тип в ArchLang — это не класс. Тип — это даже, по сути, не категория. Тип — это шаблон формы: частично заполненный документ, который экземпляр завершает, заполняя пропуски.
Это различие определяет всё в Части IV. Держите его в уме.
Сначала тянитесь к простому
Заголовок раздела «Сначала тянитесь к простому»Прежде чем перейти к механике, самое важное правило этой части книги:
Правило. Простой модуль — это строительный блок по умолчанию; простой интерфейс — это соединение по умолчанию. Типизация — это сахар: тянитесь к ней только тогда, когда она окупается.
Язык спроектирован так, что голые модули и интерфейсы вполне самодостаточны. Вам не нужен пользовательский тип, чтобы смоделировать сервис, базу данных или очередь — простой module с хорошим описанием и правильной вложенностью говорит о многом.
Добавляйте пользовательский тип, только когда он даёт вам что-то конкретное:
- Подтипизирует сущности в семейство, разделяющее общую структуру,
- Прикрепляет пользовательские виджеты (рендеринг),
- Определяет пользовательские требования (обязательные пропуски, которые должен заполнить каждый экземпляр),
- Добавляет пользовательские поля, или
- Явно утверждает, что нечто является определённым типом — там, где само утверждение и есть ценность.
Если кандидат в типы не делает ничего из этого, не пишите его. Безповеденческий блок service, оборачивающий простой модуль и не добавляющий ни требования, ни виджета, ни поля, — это шум. Типы стандартной библиотеки, которыми вы пользовались (service, database, rest_create), окупаются именно потому, что несут виджеты и, иногда, требования. Ваши должны делать так же.
Что такое тип
Заголовок раздела «Что такое тип»type module service { required cascade version required aspect domain}Это объявление говорит: «Микросервис — это модуль, у которого обязательно должно быть поле version (которое каскадирует к потомкам) и аспект domain. Любой экземпляр микросервиса должен заполнить их или явно отбросить».
Экземпляр заполняет форму:
service Payments { version: "1.0" aspect { domain: "Payments" }}Тип предоставил форму; экземпляр предоставил содержание. Здесь нет наследования в смысле ООП — нет переопределения методов, нет виртуальной диспетчеризации. Есть штамповка шаблона: во время разбора тело типа отпечатывается на экземпляр так, будто записано там буквально. Экземпляр может уточнить, переопределить или отбросить отпечатанное, но отношение здесь — «этот экземпляр был отлит по этой форме», а не «этот экземпляр является членом этого класса».
Что заменяют типы
Заголовок раздела «Что заменяют типы»Если вы раньше пользовались инструментами для моделирования архитектуры, типы заменяют сразу несколько вещей:
| Их концепция | Концепция ArchLang |
|---|---|
| Архетипы | type module <type> |
| Шаблоны | type module <type> |
| Стереотипы | type module <type> |
Три разных механизма, разнесённые по разным инструментам, а часто и по разным возможностям одного инструмента, — все они служат одной цели: «модули такого характера разделяют общие значения по умолчанию и общие требования». ArchLang сводит их в один механизм с одним словарём.
Что может содержать тело типа
Заголовок раздела «Что может содержать тело типа»Тело типа выразительнее тела экземпляра, потому что оно может помечать что-либо как обязательное (обязательные пропуски, которые экземпляр должен заполнить):
type module service { // Default value — instances inherit; may override cascade widget: arch-service
// Mandatory blank — instances must fill or drop required cascade version required aspect domain
// Pre-filled sub-declaration — every service gets this component component metrics { rest_create emit }
// Mandatory blank sub-declaration — instance must refine or drop required database PrimaryStore}Здесь происходит шесть вещей:
- Значение по умолчанию (
cascade widget: ...) — значение, которое экземпляр наследует, но может изменить. - Обязательный пропуск поля (
required cascade version) — экземпляр должен заполнить. - Обязательный пропуск аспекта (
required aspect domain) — экземпляр должен заполнить. - Предзаполненное под-объявление (
component metrics { ... }) — экземпляр наследует целиком. - Обязательный пропуск под-объявления (
required database PrimaryStore) — экземпляр должен уточнить. - Модификатор распространения (
cascadeнаversion,widget) — рассматривается в главе 18.
Глава 16 проходит через объявление вашего первого типа. Глава 17 разбирает required. Глава 18 — глубокое погружение в cascade, append и аспекты.
Подтипы — это тоже шаблоны форм
Заголовок раздела «Подтипы — это тоже шаблоны форм»Тип может расширять другой тип, называя его родительским типом:
// Base type.type module service { required cascade version}
// Subtype — every payments_service gets all of service's stamp PLUS this.type service payments_service { version: "2.1" // fulfills the parent's blank aspect { security.zone: "PCI" } // adds a new aspect}Экземпляр payments_service наследует оба шаблона: service и payments_service. Перед экземпляром больше не стоит пропуск required version — payments_service уже заполнил его. Но экземпляр всё ещё может добавлять аспекты, переопределять версию или отбрасывать что-либо.
Подтипы используют те же операции уточнения / переопределения / отбрасывания, что и экземпляры. Здесь нет «режима подтипа» против «режима экземпляра» — операции единообразны. Глава 19 разбирает это.
Почему такой выбор
Заголовок раздела «Почему такой выбор»Три причины, по которым язык пошёл по пути шаблонов форм, а не классов:
Моделированию нужны пропуски. В архитектурных документах есть обязательная информация, у которой нет подходящего значения по умолчанию. У каждого сервиса должна быть версия. Невозможно выбрать версию по умолчанию — значение «без версии» по умолчанию — это молчаливая ошибка. Правильная модель — «слот существует и должен быть заполнен». Это и есть пропуск. Классы в стиле ООП естественно не выражают пропусков; они выражают значения по умолчанию плюс переопределение.
Штамповка отлаживается. Когда у микросервиса разрешённое тело выглядит неожиданно, это можно проследить: «этот version: "2.1" пришёл из шаблона подтипа payments_service; этот widget: — из service; этот repo.url — из экземпляра». Штамповка шаблона — это линейная цепочка маленьких добавлений; наследование классов порождает вопрос о порядке разрешения методов, который трудно прочесть прямо из исходника.
Это компонуется со структурным каскадом. В архитектуре есть второй механизм распространения: значения текут через вложенные модули (владелец, заданный на родителе, распространяется на дочерние компоненты). Это другая ось, нежели наследование типов. Шаблоны форм чисто компонуются со структурным каскадом — типы предоставляют структуру и значения по умолчанию, а структурный каскад заполняет значения во время выполнения, обходя дерево вложенности. ООП-наследование и структурное распространение не компонуются так же чисто. Глава 18 — та глава, где это становится очевидно.
Как ссылаться на типы
Заголовок раздела «Как ссылаться на типы»Типы живут в .arch-файлах рядом с экземплярами:
// In acme.shared/types.archexport type module payments_service { required cascade version aspect { security.zone: "PCI" }}Другие пакеты импортируют тип через механизм use из главы 12:
// In acme.shop/package.archspacedependencies { acme.shared: "../shared" }use payments_service from acme.sharedЗатем любой файл в acme.shop может объявить экземпляр:
payments_service Stripe { version: "3.0"}Модификатор export на типе делает его видимым для импортёров. Без export тип остаётся внутренним для объявляющего его пакета.
Стабильные идентификаторы у типов
Заголовок раздела «Стабильные идентификаторы у типов»Типы несут стабильные идентификаторы так же, как модули:
type #t017 module payments_service { ... }Форматтер выдаёт их при сохранении. Переименование типа не ломает его экземпляры — резолвер сопоставляет по идентификатору, а не по имени типа. См. главу 13.
- Простые модули и интерфейсы — это значение по умолчанию; типизация — это сахар. Добавляйте пользовательский тип, только чтобы подтипизировать, прикрепить виджеты, добавить требования или поля, либо утвердить «это является типом».
- Тип — это шаблон формы: частично заполненный документ, который экземпляр завершает.
- Типы — это не классы. Здесь нет диспетчеризации методов; есть штамповка шаблона.
- Тела типов могут содержать значения по умолчанию, обязательные пропуски (
required), предзаполненные под-объявления и модификаторы распространения. - Подтипы расширяют родителей, называя их родительским типом; применимы те же операции уточнения / переопределения / отбрасывания.
- Шаблоны форм компонуются со структурным каскадом — вторым механизмом распространения, рассмотренным в главе 18.
- Типы несут стабильные идентификаторы; форматтер выдаёт их при сохранении.
Что дальше
Заголовок раздела «Что дальше»Глава 16: Определение типов → — напишите свой первый тип, на практике.