##// 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

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-подобной модели.

Есть коллекция объектов, на которую адаптер хочет подписаться:

projections.getProjections().all(projection -> {
    ...
});

Если projection — это тяжёлый объект, внутри которого уже лежат:

  • вычисленные bindings
  • провайдеры на source sets
  • derived state
  • полу-материализованные сущности

то all(...) начинает рано раскрывать то, что хотелось оставить ленивым.

Появляются симптомы:

  • ломается lazy semantics
  • растёт связность
  • становится важен порядок применения плагинов
  • хочется заводить собственные события: onCreated, onResolved, onMaterialized

Архитектура начинает скрипеть не потому, что all(...) плох, а потому что слишком много смысла засунуто в наблюдаемый объект.


Решение: наблюдать identity, а не состояние

Если сделать наблюдаемыми только identity objects, картина меняется.

Например:

public interface SourceSetProjection extends Named {
}

Такой объект содержит только identity:

  • name
  • возможно, ещё пару метаданных выбора
  • но не тяжёлое содержимое

Тогда подписка:

projections.getProjections().all(projection -> {
    ...
});

больше не страшна. Она eagerly materializes только дешёвые ключи, а не весь агрегатный граф.

Содержимое запрашивается отдельно:

Set<VariantLayerBinding> bindings = projections.getBindings(projection);
NamedDomainObjectProvider<GenericSourceSet> sourceSet =
    materializer.getSourceSet(projection.getName());

И вот эти вызовы уже могут быть:

  • lazy
  • вычисляемыми
  • кэшируемыми
  • привязанными к runtime lifecycle

Пример

Неудачный вариант

Представим такой интерфейс:

public interface SourceSetProjection extends Named {
    Set<VariantLayerBinding> getBindings();
    NamedDomainObjectProvider<GenericSourceSet> getSourceSet();
}

Проблемы тут сразу видны:

  • SourceSetProjection уже не identity object, а почти агрегат
  • внутри смешаны:

  • symbolic identity

  • relation data
  • runtime reference в чужой домен
  • подписка через all(...) начинает тащить за собой больше, чем хотелось бы

На словах объект называется “projection”, а по факту внутри у него уже полсистемы.


Более удачный вариант

Разделяем ответственность:

public interface SourceSetProjection extends Named {
}
public interface SourceSetProjections {
    NamedDomainObjectCollection<SourceSetProjection> getProjections();
    Set<VariantLayerBinding> getBindings(String sourceSetName);
}
public interface SourceSetMaterializer {
    NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName);
}

Теперь сценарий адаптера выглядит так:

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
  • Благодаря этому можно безопасно использовать replayable механизмы вроде all(...), не разрушая ленивую семантику там, где она действительно важна.

Именно так удаётся совместить:

  • простое наблюдение
  • чистую модель
  • отсутствие собственных событий
  • ленивую материализацию тяжёлого состояния