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

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 versionpayments_service уже заполнил его. Но экземпляр всё ещё может добавлять аспекты, переопределять версию или отбрасывать что-либо.

Подтипы используют те же операции уточнения / переопределения / отбрасывания, что и экземпляры. Здесь нет «режима подтипа» против «режима экземпляра» — операции единообразны. Глава 19 разбирает это.

Три причины, по которым язык пошёл по пути шаблонов форм, а не классов:

Моделированию нужны пропуски. В архитектурных документах есть обязательная информация, у которой нет подходящего значения по умолчанию. У каждого сервиса должна быть версия. Невозможно выбрать версию по умолчанию — значение «без версии» по умолчанию — это молчаливая ошибка. Правильная модель — «слот существует и должен быть заполнен». Это и есть пропуск. Классы в стиле ООП естественно не выражают пропусков; они выражают значения по умолчанию плюс переопределение.

Штамповка отлаживается. Когда у микросервиса разрешённое тело выглядит неожиданно, это можно проследить: «этот version: "2.1" пришёл из шаблона подтипа payments_service; этот widget: — из service; этот repo.url — из экземпляра». Штамповка шаблона — это линейная цепочка маленьких добавлений; наследование классов порождает вопрос о порядке разрешения методов, который трудно прочесть прямо из исходника.

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

Типы живут в .arch-файлах рядом с экземплярами:

// In acme.shared/types.arch
export type module payments_service {
required cascade version
aspect { security.zone: "PCI" }
}

Другие пакеты импортируют тип через механизм use из главы 12:

// In acme.shop/package.archspace
dependencies { 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: Определение типов → — напишите свой первый тип, на практике.