# 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(...)`, не разрушая ленивую семантику там, где она действительно важна.

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

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