##// END OF EJS Templates
Refine variant artifacts publication lifecycle- Remove assembly task access from outgoing slot publication spec- Keep whenOutgoingSlot focused on publication attributes only- Decouple materialization policy handler from artifact assemblies- Drop eager afterEvaluate outgoing configuration realization- Add reference coverage for lazy Gradle outgoing variants- Exercise primary and secondary artifact resolution without forced realization- Keep slot body customization in ArtifactAssemblySpec
Refine variant artifacts publication lifecycle- Remove assembly task access from outgoing slot publication spec- Keep whenOutgoingSlot focused on publication attributes only- Decouple materialization policy handler from artifact assemblies- Drop eager afterEvaluate outgoing configuration realization- Add reference coverage for lazy Gradle outgoing variants- Exercise primary and secondary artifact resolution without forced realization- Keep slot body customization in ArtifactAssemblySpec

File last commit:

r40:924d9107c025 default
r52:3939ecb6e9a4 default
Show More
identity-first-model.ru.md
306 lines | 11.2 KiB | text/x-minidsrc | MarkdownLexer
/ identity-first-model.ru.md
cin
WIP variant sources model
r40 # Identity-first model: как сохранить lazy semantics без своих событий
В системах конфигурации и сборки почти всегда возникает одно и то же напряжение:
* хочется иметь **наблюдаемую модель**, на которую можно подписаться через что-то вроде `all(...)`
* но не хочется, чтобы такая подписка **ломала ленивость**
* ещё меньше хочется тащить в архитектуру собственную шину событий, порядок подписки и отдельный lifecycle
Один из рабочих способов решить это — **разделить identity и вычисляемое состояние**.
## Суть принципа
Идея простая:
* отдельный объект отвечает только за **identity** и минимальные метаданные выбора
* всё содержательное состояние агрегата получается **отдельными вызовами API**
* эти вызовы уже могут быть lazy, дорогими, вычисляемыми, кэшируемыми, провайдерными — какими угодно
То есть вместо одного “толстого” объекта мы получаем два слоя:
### 1. Identity layer
Лёгкие объекты, которые:
* дёшево создаются
* не мутируют
* безопасно наблюдаются eagerly
* годятся как ключи и точки подписки
### 2. State / aggregate access layer
Отдельные API, которые:
* по identity находят или вычисляют содержимое
* делают heavy work только по требованию
* могут сохранять ленивую семантику
Это особенно полезно там, где коллекция должна быть replayable, но при этом не должна тащить за собой дорогую материализацию.
---
## Проблема, которую это решает
Рассмотрим типичную ситуацию в Gradle-подобной модели.
Есть коллекция объектов, на которую адаптер хочет подписаться:
```java
projections.getProjections().all(projection -> {
...
});
```
Если `projection` — это тяжёлый объект, внутри которого уже лежат:
* вычисленные bindings
* провайдеры на source sets
* derived state
* полу-материализованные сущности
то `all(...)` начинает рано раскрывать то, что хотелось оставить ленивым.
Появляются симптомы:
* ломается lazy semantics
* растёт связность
* становится важен порядок применения плагинов
* хочется заводить собственные события: `onCreated`, `onResolved`, `onMaterialized`
Архитектура начинает скрипеть не потому, что `all(...)` плох, а потому что **слишком много смысла засунуто в наблюдаемый объект**.
---
## Решение: наблюдать identity, а не состояние
Если сделать наблюдаемыми только identity objects, картина меняется.
Например:
```java
public interface SourceSetProjection extends Named {
}
```
Такой объект содержит только identity:
* `name`
* возможно, ещё пару метаданных выбора
* но не тяжёлое содержимое
Тогда подписка:
```java
projections.getProjections().all(projection -> {
...
});
```
больше не страшна.
Она eagerly materializes только дешёвые ключи, а не весь агрегатный граф.
Содержимое запрашивается отдельно:
```java
Set<VariantLayerBinding> bindings = projections.getBindings(projection);
NamedDomainObjectProvider<GenericSourceSet> sourceSet =
materializer.getSourceSet(projection.getName());
```
И вот эти вызовы уже могут быть:
* lazy
* вычисляемыми
* кэшируемыми
* привязанными к runtime lifecycle
---
## Пример
### Неудачный вариант
Представим такой интерфейс:
```java
public interface SourceSetProjection extends Named {
Set<VariantLayerBinding> getBindings();
NamedDomainObjectProvider<GenericSourceSet> getSourceSet();
}
```
Проблемы тут сразу видны:
* `SourceSetProjection` уже не identity object, а почти агрегат
* внутри смешаны:
* symbolic identity
* relation data
* runtime reference в чужой домен
* подписка через `all(...)` начинает тащить за собой больше, чем хотелось бы
На словах объект называется “projection”, а по факту внутри у него уже полсистемы.
---
### Более удачный вариант
Разделяем ответственность:
```java
public interface SourceSetProjection extends Named {
}
```
```java
public interface SourceSetProjections {
NamedDomainObjectCollection<SourceSetProjection> getProjections();
Set<VariantLayerBinding> getBindings(String sourceSetName);
}
```
```java
public interface SourceSetMaterializer {
NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName);
}
```
Теперь сценарий адаптера выглядит так:
```java
projections.getProjections().all(projection -> {
Set<VariantLayerBinding> bindings =
projections.getBindings(projection.getName());
NamedDomainObjectProvider<GenericSourceSet> sourceSet =
materializer.getSourceSet(projection.getName());
// apply adapter-specific policy
});
```
Что изменилось:
* replayable подписка сохранилась
* eager materialization допустима, потому что `SourceSetProjection` дешёвый
* дорогое и вычисляемое состояние ушло в отдельные API
* materialization остаётся под контролем одного владельца
---
## Почему это лучше событий
Когда в модели смешаны identity и состояние, очень быстро хочется изобретать события:
* `projectionCreated`
* `projectionResolved`
* `sourceSetAvailable`
* `sourceSetMaterialized`
Потому что в какой-то момент становится важно, **когда именно** объект уже “достаточно готов”.
Если же наблюдаемый объект содержит только identity, а всё тяжёлое получается отдельными вызовами, событийная модель часто вообще не нужна.
Получается более спокойная схема:
* есть **identity registry**
* есть **lookup API** для связей
* есть **lazy materialization API** для тяжёлых сущностей
То есть вместо событий:
> “когда объект станет достаточно готов, я что-то сделаю”
получается обычный и понятный flow:
> “я вижу identity, а нужное состояние спрошу отдельно, когда оно мне действительно понадобится”
Это проще и для reasoning, и для тестирования, и для эволюции API.
---
## Что именно должно жить в identity object
Identity object не обязан быть “абсолютно пустым”.
Он может содержать **метаданные выбора**, если они:
* дешевы
* стабильны
* не требуют тяжёлой инициализации
* не превращают объект в агрегат
Например:
* `id`
* `name`
* `kind`
* `domain key`
* maybe `type`
Но он не должен содержать:
* вычисляемое содержимое агрегата
* ссылки времени выполнения на чужие домены
* lazy providers на heavy objects
* derived state, который может провоцировать раннюю материализацию
Хорошая практическая формула:
**identity object contains selection metadata; aggregate content is obtained separately.**
---
## Когда этот принцип особенно полезен
Он особенно хорош, если:
* есть replayable наблюдение через `all(...)`
* identity-объекты дешёвые и почти immutable
* содержимое агрегата может быть дорогим
* нужен clean split между symbolic model и runtime model
* хочется избежать собственной событийной шины
Для Gradle-подобных моделей это вообще очень естественный приём.
---
## Когда он не нужен
Если объект:
* маленький
* дешёвый
* уже сам по себе и есть всё его содержимое
то разделение на identity и отдельные lookup API может быть лишним.
То есть этот принцип полезен не как догма, а как инструмент.
Его стоит применять там, где реально есть риск смешения:
* ключа
* состояния
* вычисления
* runtime reference
---
## Практический итог
Если сформулировать коротко, то принцип такой:
1. **Identity objects** содержат только identity и дешёвые метаданные выбора.
2. **Агрегатное содержимое** не хранится внутри них, а получается отдельными API-вызовами.
3. Эти API уже могут выполнять:
* lazy resolution
* кэширование
* heavy computation
* materialization
4. Благодаря этому можно безопасно использовать replayable механизмы вроде `all(...)`, не разрушая ленивую семантику там, где она действительно важна.
Именно так удаётся совместить:
* простое наблюдение
* чистую модель
* отсутствие собственных событий
* ленивую материализацию тяжёлого состояния