Глава 23. Инварианты модели: почему неправильное состояние невозможно
23.1. От философии к формальным правилам
На протяжении всей книги мы выстраивали модель мира.
Объект существует до своего описания. Описание изменяется, объект остаётся тем же. Исполнение является самостоятельным объектом. Экземпляр отличается от типа. Контекст существует отдельно от объекта. Связь является самостоятельной сущностью. Атрибут является утверждением, имеющим собственную историю.
Но философия без формальных правил остаётся пожеланием.
Модель данных становится защитой только тогда, когда она содержит инварианты — формальные утверждения о том, какие состояния системы допустимы, а какие нет.
Инвариант говорит не: «пользователю не следует делать этого».
Он говорит: «модель не должна позволять существовать такому состоянию».
Это принципиальное различие. Правило ограничивает действие. Инвариант ограничивает состояние.
23.2. Инвариант первый: объект первичен
KObject существует
↓
другие сущности относятся к нему
Но не наоборот.
KContext относится к KObject.
KAttribute относится к объекту, которому принадлежит утверждение.
KDefinition относится к объекту, который описывает.
KRelation связывает существующие объекты.
В допустимом состоянии модели зависимая сущность не может существовать без объекта или сущности, к которой она относится.
Это не означает, что все эти связи обязательно физически реализованы одним внешним ключом в одной базе данных. В распределённой системе разные части модели могут находиться в разных сервисах.
Инвариант определяет смысл допустимого состояния, а способ его обеспечения зависит от границы ответственности.
23.3. Инвариант второй: объект не смешивается со своим содержанием
KObject
id: 12345
Сам KObject не хранит имя, описание, владельца, Revision или значение свойства.
Это не означает, что система ничего не знает об объекте.
Наоборот.
Вся информация о нём существует вокруг KObject:
KObject: 12345
|
├── KProduct
├── KDefinition
├── KAttribute
├── KContext
└── KRelation
Каждая из этих сущностей отвечает на свой вопрос. Что это за тип объекта? Как он описан? Какими свойствами он обладает? В каких административных условиях существует? С какими объектами связан?
Сам KObject не должен превращаться в контейнер для всех этих сведений. Это защищает модель от смешения разных понятий.
23.4. Инвариант третий: Revision принадлежит описанию
Revision не принадлежит непосредственно KProduct. Revision принадлежит KDefinition.
KProduct
│
└── KDefinition
│
├── Revision A
├── Revision B
└── Revision C
Это означает, что изменение описания не записывается как изменение самого объекта.
Если конструктор изменил чертёж, это изменение Revision.
Если изменилось описание изделия, это изменение Definition.
Если изменилось само изделие как самостоятельный объект инженерного учёта, это уже другой объект.
Граница между этими случаями определяется не названием поля revision, а структурой модели.
Что это предотвращает?
Ситуацию, в которой одна и та же сущность одновременно означает изделие, его описание и версию описания.
23.5. Инвариант четвёртый: изменение, нарушающее взаимозаменяемость, не является Revision
Это следствие предыдущего принципа.
Если изменение сохраняет критерий взаимозаменяемости, оно может быть отражено новой Revision существующего описания.
Если изменение нарушает взаимозаменяемость, оно относится уже к другому объекту.
Упрощённо:
тот же объект
│
└── изменение описания
↓
Revision
другой объект
│
└── изменение,
нарушающее взаимозаменяемость
Revision не является универсальным механизмом записи любого изменения. Она предназначена для изменения описания.
Это предотвращает ситуацию, когда разные инженерные объекты начинают существовать под одной и той же сущностью только потому, что система умеет хранить версии.
23.6. Инвариант пятый: KContext не существует без KObject
KContext всегда относится к конкретному KObject.
KObject: 12345
↑
│ target_object_id
│
KContext
Нельзя иметь контекст, который не относится ни к одному объекту.
Но здесь важно сделать различие между логическим инвариантом и способом его реализации.
В распределённой архитектуре KObject и KContext могут принадлежать разным сервисам и находиться в разных базах данных.
Поэтому Context Service не может поставить обычный PostgreSQL FOREIGN KEY на таблицу KObject, если эта таблица принадлежит другой базе.
Тем не менее правило остаётся: KContext допустим только для существующего KObject.
Это уже системный инвариант. Он обеспечивается границами сервисов, API регистрации объекта и протоколом создания контекста, а не одним внешним ключом.
23.7. Инвариант шестой: атрибут является временным утверждением
KAttribute
object: 12345
type: material
value: 09Г2С
valid_from: 01.06.2031
source: конструктор Петров
basis: извещение №45
Атрибут не является просто значением. Он фиксирует утверждение:
у объекта было такое значение в определённый период, полученное из определённого источника и на определённом основании.
Поэтому атрибут не должен существовать без объекта, к которому он относится. Он также не должен терять временную информацию.
Что это предотвращает?
Ситуацию, в которой система знает только:
material = 09Г2С
но не знает:
- когда это значение стало действующим;
- когда перестало быть действующим;
- откуда оно получено;
- на каком основании появилось.
Для PLM это принципиально. Без времени система хранит последнее значение. С временем она хранит историю инженерного знания.
23.8. Инвариант седьмой: связь имеет смысл
KRelation
from: KObject 12345
to: KObject 67890
type: contains
Связь не является произвольной парой идентификаторов. Она имеет тип и смысл.
Не каждая связь допустима между любыми объектами. Например:
KProduct ──contains──> KProduct
может быть допустимой связью структуры изделия. А произвольное:
KUser ──contains──> KProduct
не приобретает инженерного смысла только потому, что технически два идентификатора существуют.
Следовательно, модель должна определять:
- какие типы связей существуют;
- какие объекты они соединяют;
- какие атрибуты принадлежат связи;
- какие ограничения действуют для конкретного типа связи.
Это защищает систему от отношений, которые технически можно записать, но которые не имеют смысла в предметной области.
23.9. Инвариант восьмой: исполнение является объектом
Исполнение является самостоятельным объектом.
Не набором параметров. Не строкой конфигурации. Не комбинацией выбранных опций.
KPExecutionGroup
│
├── KProduct — исполнение 1
├── KProduct — исполнение 2
└── KProduct — исполнение 3
У каждого исполнения есть собственное существование в модели. Это позволяет связать его с определением, документацией, структурой, атрибутами и другими объектами.
Что это предотвращает?
Ситуацию, когда производство получает условное:
«изготовить конфигурацию 047»
но в системе не существует самостоятельного объекта, который можно однозначно определить, описать и проследить.
23.10. Почему инварианты сильнее правил
Традиционная система часто работает следующим образом.Пользователь нажимает кнопку.
Система проверяет: допустимо ли это действие? Если нет — показывает ошибку.
Но интерфейс является внешним ограничением. Можно вызвать API напрямую. Можно импортировать данные из файла. Можно использовать другой клиент. Можно написать интеграцию, которая никогда не видела пользовательского интерфейса.
Поэтому настоящая защита должна находиться глубже интерфейса.
Инвариант говорит: это состояние системы недопустимо.
Но здесь необходимо сделать ещё одно уточнение.
Инвариант может быть обеспечен на разных уровнях.
уровень базы данных
↓
FK / UNIQUE / CHECK / ограничения
уровень сервиса
↓
транзакция / агрегат / API
уровень системы
↓
несколько сервисов /
протокол взаимодействия /
события / синхронизация
Поэтому фраза «инвариант делает состояние невозможным» означает:
система в целом не должна допускать такое состояние как допустимое состояние модели.
Это не всегда означает наличие одного SQL-ограничения, физически блокирующего запись.
23.11. Инвариант и граница сервиса
До этого момента мы рассматривали инвариант как свойство модели данных. Но в распределённой системе разные части одной модели могут находиться в разных сервисах и разных базах данных. В этом случае один PostgreSQL уже не способен выразить весь инвариант через внешний ключ.
Это не отменяет инвариант. Меняется место его обеспечения.
Часть инварианта может быть обеспечена структурой локальной модели. Другая часть — границей сервиса и его API. Ещё часть — согласованным взаимодействием нескольких сервисов.
Поэтому необходимо различать инвариант модели и механизм его реализации.
Инвариант отвечает на вопрос:
какое состояние системы допустимо?
Архитектура отвечает на другой вопрос:
где и каким механизмом это состояние обеспечивается?
Модель
↓
Инварианты
↓
границы ответственности
↓
локальные ограничения
+
межсервисные правила
Это различие принципиально для распределённой архитектуры. Предположим:
Object Registry
│
KObject
│
┌────────────┼────────────┐
↓ ↓ ↓
Design Context Relation
Service Service Service
│ │ │
Definition KContext KRelation
Revision
Каждый сервис владеет своей частью знания.
Design Service отвечает за Definition и Revision.
Context Service отвечает за KContext.
Relation Service отвечает за отношения.
Но объект остаётся одним и тем же объектом системы.
Поэтому инварианты должны существовать на двух уровнях.
Локальный инвариант может быть физически закреплён внутри одной транзакционной границы.
Например:
Revision → KDefinition
Если оба объекта находятся в одной базе, это может быть обеспечено внешним ключом.
Распределённый инвариант пересекает границы сервисов. Например:
KContext → KObject
Если KObject принадлежит Object Registry, Context Service не может использовать обычный FK к чужой базе.
Тогда инвариант обеспечивается самим протоколом системы:
регистрация KObject
↓
получение допустимого object_id
↓
создание KContext
↓
Context Service принимает только
существующий зарегистрированный объект
Так появляется важное архитектурное правило: граница сервиса не отменяет инвариант. Она определяет, каким способом этот инвариант обеспечивается.
Это и есть отличие модели от конкретной схемы базы данных.
Распределённый инвариант не означает, что каждый сервис знает всю модель. Он означает, что несколько сервисов совместно обеспечивают одно правило, каждый в пределах своей ответственности.
23.12. Кто отвечает за инвариант
В распределённой системе нельзя сказать, что «вся система отвечает за всё».
Это быстро приводит к ситуации, когда ни один сервис фактически ни за что не отвечает. Каждый инвариант должен иметь определённую границу ответственности.
Например:
Object Registry
↓
существование KObject
Design Service
↓
Definition / Revision
Context Service
↓
KContext и его административное состояние
Relation Service
↓
допустимые отношения
Сервис не должен произвольно изменять сущности другого сервиса. Он использует определённый интерфейс взаимодействия с владельцем соответствующего знания.
Так модель данных превращается в архитектурный принцип: границы знания определяют границы ответственности.
23.13. Инварианты и проекции
Есть ещё один слой модели, который возникает поверх основных объектов.
Основная модель хранит факты. Например:
KObject
KContext
KRelation
KAttribute
Но системе необходимо быстро отвечать на другой вопрос: может ли данный пользователь выполнить данное действие над этим объектом?
Для этого из основной модели строятся производные представления — проекции.
Упрощённо:
Основная модель
│
├── KContext
├── KRelation
├── KAttribute
└── другие данные
│
▼
события
│
▼
проекции
│
├── object scope
└── effective permissions
Проекция не становится новым источником истины. Она является производным представлением основной модели, подготовленным для конкретной задачи.
Это важно и для прав доступа.
KContext может содержать владельца и группу владения. Из этих и других данных строится область доступа объекта.
Из области доступа и назначенных прав формируется эффективное представление разрешений. Таким образом:
факты
↓
административное состояние
↓
проекции
↓
решение о доступе
Права не превращаются в свойства KObject. И изменение проекции не изменяет сам объект.
Это ещё один пример того, как одна модель может иметь несколько уровней представления, сохраняя единую семантику исходных данных.
23.14. Почему инварианты важны для долгоживущей системы
PLM-система может существовать десятилетиями.
Люди, которые её создавали, уходят. Меняются команды. Меняются технологии. Меняются интерфейсы. Меняются интеграции.
Но данные остаются.
Если допустимость состояния зависит только от знания конкретного разработчика, это знание постепенно теряется.
Если же оно закреплено в модели, система продолжает сохранять смысл данных. Через десять лет новый инженер должен понимать:
Revision — как Revision описания.
KContext — как административное состояние, связанное с объектом.
KAttribute — как временное утверждение.
KRelation — как типизированную связь.
KObject — как фундаментальный факт существования объекта.
Смысл не должен восстанавливаться из воспоминаний людей.
Он должен следовать из структуры модели.
23.15. Практический пример
Рассмотрим создание изделия.
Сначала система регистрирует объект.
KObject: 12345
Затем вместе с ним фиксируется его первоначальный административный контекст.
KObject: 12345
↑
KContext
owner: Иванов
group: Группа А
Затем появляется описание:
KObject: 12345
│
└── KDefinition
│
└── Revision A
Позже создаётся Revision B.
Объект остаётся тем же.
Затем появляются атрибуты.
KAttribute
material = 09Г2С
valid_from = ...
Затем возникают отношения с другими объектами.
KRelation
KObject 12345
│
└── contains
↓
KObject 67890
На каждом уровне модель не просто хранит запись.
Она фиксирует смысл.
Нельзя создать Revision без Definition.
Нельзя создать KContext без объекта.
Нельзя создать атрибут без объекта.
Нельзя создать отношение без связанных объектов и допустимого типа связи.
Именно совокупность этих ограничений создаёт целостную модель.
23.16. Что это предотвращает
Инварианты защищают не от ошибок интерфейса. Они защищают от ошибок смысла.
Без них система может содержать: Revision изделия, которой на самом деле является Revision документа.
Или: Context, который не относится ни к одному объекту.
Или: Attribute, для которого неизвестно, когда и откуда появилось значение.
Или: Relation, которая технически существует, но не имеет допустимого инженерного смысла.
Или: два разных инженерных объекта, записанных как один объект только потому, что система не различает изменение описания и изменение самого объекта.
Каждая такая ошибка может быть технически представлена в слабой модели.
В строгой модели для неё просто нет допустимого места.
23.17. Главный вывод
Инварианты являются формальным выражением модели мира.
Они говорят:
- объект существует независимо от своего содержания;
- зависимые сущности относятся к существующим объектам;
- Revision относится к описанию;
- изменение описания не смешивается с изменением самого объекта;
- KContext относится к KObject;
- атрибут хранит не только значение, но и время его действия;
- отношение имеет тип и инженерный смысл;
- исполнение является самостоятельным объектом.
Но в распределённой системе необходимо понимать слово «невозможно» правильно.
В одной базе данных невозможность может быть физически закреплена внешним ключом или другим ограничением.
В микросервисной системе часть инвариантов пересекает границы баз данных. Тогда они становятся распределёнными инвариантами и обеспечиваются совокупностью сервисов, их интерфейсов и протоколов взаимодействия.
Это не ослабляет модель.
Наоборот, это позволяет сохранить её смысл независимо от способа физического размещения данных. В результате модель данных выполняет гораздо более важную функцию, чем простое хранение информации.
Она определяет: какие состояния мира система вообще признаёт возможными.
Именно поэтому правильная модель способна сохранять смысл данных даже тогда, когда меняются приложения, сервисы, интерфейсы и люди, которые эти системы создавали.
В следующей главе: когда объект перестаёт существовать? Что происходит с объектом после завершения его жизненного цикла и почему «объект больше не существует» не означает «объекта никогда не существовало».