##// END OF EJS Templates
Rework variant artifacts materialization model...
Rework variant artifacts materialization model Refactor VariantArtifactsPlugin around a live outgoing artifacts context and split artifact publication into explicit internal services: outgoing variant registry, assembly binding, materialization policy hooks, primary-slot convention, and slot assembly handling. Introduce variant artifact slots as identity-first public API and expose materialized assembly handles through ArtifactAssemblies. Add replayable configuration hooks for outgoing configurations, outgoing slots, outgoing variants, and registered assemblies. Create consumable outgoing configurations per variant, bind the primary slot to the root outgoing artifact set, and publish non-primary slots as Gradle outgoing configuration variants. Add deterministic injective task names for slot assembly tasks, use Sync for directory assembly, and configure the default assembly output location under build/variant-assemblies. Make primary-slot selection finalize-on-read and provide a single-slot convention that fails when no unique default can be inferred. Mark artifact internal implementation package as non-public API.

File last commit:

r40:924d9107c025 default
r51:9db7822cd26c 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(...)`, не разрушая ленивую семантику там, где она действительно важна.
Именно так удаётся совместить:
* простое наблюдение
* чистую модель
* отсутствие собственных событий
* ленивую материализацию тяжёлого состояния