⟨/⟩ 40_Arch/Tech/Angular/ADR_UI_008_State.cs

148 строк · в начало

using System;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd;

namespace Ban.Sdaid.Icd.Arch.Tech.Angular
{
    /// <summary>
    /// ADR-UI-008: Управление состоянием. Signals + injectable-сервисы на трёх уровнях
    /// (компонент / страница / приложение), без глобального store.
    /// </summary>
    public class ADR_UI_008_State : IAdrDocument, IFolder<ArchAngularFolder>
    {
        public string Name => "ADR-UI-008. Управление состоянием";

        public string Description =>
            @"Состояние живёт на нескольких уровнях: локальное (компонент), страничное (несколько
компонентов одной страницы), разделяемое (несколько частей приложения). Фиксируем один способ —
signals + injectable-сервисы, без параллельных подходов и без глобального store.";

        public string Version => "1.0";
        public string Status => "accepted";
        public string[] Comments => new[]
        {
            "2026-08-11. Принято перед реализацией состояния страницы «Ввод данных».",
        };

        public Type? Supersedes => null;

        public static string S1_Context = """
            ## Контекст и постановка задачи

            Состоянием надо управлять на трёх уровнях:

            - **локальное** — внутри компонента (текущий шаг, открытость блока, прогресс);
            - **страничное** — общее для нескольких компонентов одной страницы;
            - **разделяемое** — нужное нескольким частям приложения (профиль, конфиг).

            Нужен **единый способ** — без смеси signals / `BehaviorSubject` / глобального store.
            """;

        public static string S2_DecisionDrivers = """
            ## Драйверы решения

            1. **Минимум абстракций** — требований к undo/time-travel/оффлайну нет; глобальный store избыточен.
            2. **Явный поток данных** — компонент → сервис → API; без неявных глобальных «магнитов».
            3. **Нативные средства Angular** — signals + `computed()` + `toSignal()` покрывают почти всё.
            4. **Локальность** — состояние живёт там, где используется; удаление фичи удаляет её состояние.
            """;

        public static string S3_ConsideredOptions = """
            ## Рассмотренные варианты

            - **A. Глобальный store** (NgRx / elf) — универсально, но избыточно; лишняя зависимость.
            - **B. Signals + injectable-сервисы** — локальное в сигналах компонента; разделяемое — в
              `providedIn:'root'`-сервисе с сигналами.
            - **C. RxJS Subjects во всём** — проигрывает signals по эргономике в шаблонах и `computed`.
            """;

        public static string S4_DecisionOutcome = """
            ## Решение

            Выбран **Вариант B**: signals + injectable-сервисы.

            ### Уровни состояния

            | Уровень | Где живёт | Срок жизни |
            |---|---|---|
            | Компонентное | Сигналы внутри компонента | жизнь компонента |
            | Страничное | Per-page service в `providers` страницы (ADR-UI-013) | жизнь страницы |
            | Разделяемое | `@Injectable({ providedIn: 'root' })` сервис с сигналами | жизнь приложения |

            ### Компонентное состояние

            - `signal()` / `computed()` внутри компонента; изменения только через `set()` / `update()` (не
              мутируем объект).
            - Производное — через `computed()`, не копией в нескольких сигналах.

            ### Страничное состояние

            - Нетривиальная страница имеет **per-page service**, зарегистрированный в `providers` страницы
              (живёт ровно пока активна страница).
            - Внутри — сигналы; компонент и под-компоненты читают один экземпляр через `inject(...)`.
            - Данные из API кладутся в сигнал сервиса (или компонент подписывается через
              `takeUntilDestroyed()` и кладёт в свой сигнал).

            ### Разделяемое состояние

            - Только для данных, реально нужных нескольким частям (профиль, конфиг).
            - `@Injectable({ providedIn: 'root' })` в `src/app/services/`.
            - Контракт — **сигналы для чтения** (`asReadonly()`) + **методы для записи**. Прямая запись в
              сигнал извне запрещена (private signal + публичный readonly).

            ### Подписки и Observable

            - Угасание потоков — `takeUntilDestroyed()` + `DestroyRef` (без `until-destroy`, ADR-UI-001).
            - Observable → Signal — `toSignal(stream$)`, по возможности с явным `initialValue`.

            ### Чего не делаем

            - **Глобальный store** (NgRx/elf и аналоги).
            - **Запись через мутацию** — только `set`/`update`.
            - **Скрытые синглтоны** (`static`-поля, модули-синглтоны как имитация store) — разделяемое
              всегда в инжектируемом сервисе.
            - **Локальный store на странице через RxJS Subject** — замена: per-page service с сигналами.
            """;

        public static string S5_PositiveConsequences = """
            ## Положительные следствия

            - Один способ работы со состоянием — меньше развилок для разработчика и ИИ.
            - Сигналы интегрированы с Angular (change detection, шаблоны, `computed`) без лишних операторов.
            - Удаление страницы/фичи механически удаляет её состояние.
            """;

        public static string S6_NegativeConsequences = """
            ## Отрицательные следствия и компромиссы

            - Нет time-travel / Redux DevTools — при появлении требований к сложной отладке заведём
              отдельный ADR на store.
            - Оптимистичные обновления/откат реализуются вручную в сервисе.
            """;

        public static string S7_Validation = """
            ## Проверка

            - В коде нет импортов глобального store.
            - Компоненты с локальным состоянием используют сигналы (нет состояния в полях класса вне
              сигналов и форм).
            - Разделяемое состояние читается через `inject(<Service>).<signal>()` и пишется только методами
              сервиса.
            """;

        public static string S8_OpenQuestions = """
            ## Открытые вопросы / отложено

            - **Глобальный store** — пересмотрим при требованиях к time-travel / оффлайну / синхронизации вкладок.
            - **Кэширование данных между страницами** (справочники) — пока per-page; общий уровень — отдельным ADR.
            """;

        public static string S99_Related = $"""
            ## Связанные артефакты

            - {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_001_Stack)} — signals + сервисы, без store.
            - {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_009_Forms)} — форма как источник значений-сигналов.
            """;
    }
}