From da98889beb691565c26a50936addd901f949871e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=BD=E7=AC=94?= <46271592+Skymly@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:04:51 +0800 Subject: [PATCH] Simplify documentation to ADR, Design Doc, and Roadmap Remove in-repo RFC, Spec, Plan, and Review document types. Merge Spec API/diagnostic/invariant content into Design Docs, point task tracking and review at GitHub Issues and PRs, and update AGENTS, CONTRIBUTING, PR template, ADR metadata, and indexes so agents and humans follow the lighter conventions. --- .github/pull_request_template.md | 15 +- AGENTS.md | 52 +- CHANGELOG.md | 5 +- CONTRIBUTING.md | 21 +- README.md | 4 +- docs/DOCUMENTATION.md | 746 ++---------------- docs/README.md | 50 +- docs/ROADMAP.md | 15 +- .../adr/ADR-001-primitives-over-frameworks.md | 2 +- ...02-roslyn-incremental-source-generators.md | 2 +- .../ADR-003-dual-tfm-netstandard20-net80.md | 2 +- .../ADR-004-core-does-not-reference-msdi.md | 2 +- docs/adr/ADR-005-state-transition-table.md | 3 +- .../ADR-006-composite-parallel-traversal.md | 3 +- ...DR-007-composite-tree-schema-validation.md | 3 +- ...ADR-008-singleton-lifecycle-diagnostics.md | 7 +- docs/adr/README.md | 16 +- docs/adr/_template.md | 4 +- docs/design/ChainOfResponsibility.md | 92 ++- docs/design/Composite.md | 139 +++- docs/design/Decorator.md | 104 ++- docs/design/EventAggregator.md | 127 ++- docs/design/FactoryRegistry.md | 229 +++++- docs/design/README.md | 27 +- docs/design/StateTransitionTable.md | 394 ++++++++- docs/design/Strategy.md | 290 ++++++- docs/design/_template.md | 25 +- docs/plans/README.md | 19 - docs/plans/SingletonLifecycleDiagnostics.md | 54 -- docs/plans/_template.md | 39 - docs/plans/archive/.gitkeep | 0 ...-singleton-lifecycle-diagnostics-design.md | 44 -- docs/review/README.md | 19 - docs/review/_template.md | 31 - docs/review/archive/.gitkeep | 0 docs/rfc/README.md | 22 - docs/rfc/SingletonLifecycleDiagnostics.md | 217 ----- docs/rfc/_template.md | 46 -- .../rfc/archive/CompositeParallelTraversal.md | 306 ------- .../archive/CompositeTreeSchemaValidation.md | 292 ------- docs/rfc/archive/HierarchicalStateMachine.md | 551 ------------- docs/rfc/archive/StateTransitionTable.md | 373 --------- docs/spec/ChainOfResponsibility.md | 96 --- docs/spec/Composite.md | 144 ---- docs/spec/Decorator.md | 109 --- docs/spec/EventAggregator.md | 134 ---- docs/spec/FactoryRegistry.md | 232 ------ docs/spec/README.md | 23 - docs/spec/StateTransitionTable.md | 393 --------- docs/spec/Strategy.md | 294 ------- docs/spec/_template.md | 39 - 51 files changed, 1496 insertions(+), 4360 deletions(-) delete mode 100644 docs/plans/README.md delete mode 100644 docs/plans/SingletonLifecycleDiagnostics.md delete mode 100644 docs/plans/_template.md delete mode 100644 docs/plans/archive/.gitkeep delete mode 100644 docs/review/2026-07-08-singleton-lifecycle-diagnostics-design.md delete mode 100644 docs/review/README.md delete mode 100644 docs/review/_template.md delete mode 100644 docs/review/archive/.gitkeep delete mode 100644 docs/rfc/README.md delete mode 100644 docs/rfc/SingletonLifecycleDiagnostics.md delete mode 100644 docs/rfc/_template.md delete mode 100644 docs/rfc/archive/CompositeParallelTraversal.md delete mode 100644 docs/rfc/archive/CompositeTreeSchemaValidation.md delete mode 100644 docs/rfc/archive/HierarchicalStateMachine.md delete mode 100644 docs/rfc/archive/StateTransitionTable.md delete mode 100644 docs/spec/ChainOfResponsibility.md delete mode 100644 docs/spec/Composite.md delete mode 100644 docs/spec/Decorator.md delete mode 100644 docs/spec/EventAggregator.md delete mode 100644 docs/spec/FactoryRegistry.md delete mode 100644 docs/spec/README.md delete mode 100644 docs/spec/StateTransitionTable.md delete mode 100644 docs/spec/Strategy.md delete mode 100644 docs/spec/_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9745def..f1b8902 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -48,15 +48,8 @@ Closes # ## Documentation checklist - - -- [ ] New/changed public API → Spec updated (`docs/spec/`) -- [ ] New/changed diagnostic ID → Spec updated + `AnalyzerReleases.Unshipped.md` updated -- [ ] Implementation detail changed → Design Doc updated (`docs/design/`) -- [ ] New RFC → used `docs/rfc/_template.md` template -- [ ] RFC status changed → frontmatter updated + archived to `docs/rfc/archive/` if Implemented/Rejected -- [ ] New ADR → numbered from `docs/adr/README.md` next available -- [ ] New Plan / Review → used `docs/plans/_template.md` / `docs/review/_template.md` template -- [ ] Plan / Review status changed → frontmatter updated + archived + index README updated -- [ ] `CHANGELOG.md` `[Unreleased]` entry added + + +- [ ] Design Doc updated if API / diagnostic / implementation changed +- [ ] User-facing docs synced (separate PRs if multi-repo) - [ ] No documentation changes needed diff --git a/AGENTS.md b/AGENTS.md index dbbe7b5..1e57ac2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,7 +39,7 @@ DesignPatterns.slnx ├── DesignPatterns.Extensions.Autofac/ # Autofac 扩展 + Autofac 生成器 targets ├── DesignPatterns.Package/ # NuGet 元包(PackageId=Skymly.DesignPatterns) ├── tests/ # 单元 / 生成器 Verify / Analyzer / DI -├── docs/ # 维护者文档(DOCUMENTATION、DEVELOPMENT、ROADMAP、rfc/、adr/、spec/、design/、模式文档) +├── docs/ # 维护者文档(DOCUMENTATION、DEVELOPMENT、ROADMAP、adr/、design/) ├── .github/ # Issue/PR 模板、CI └── AGENTS.md ``` @@ -115,7 +115,7 @@ dotnet test DesignPatterns.slnx -c Release | State(M1–M2) | `ITransitionTable`、`[StateMachine]`、`[Transition]` | `StateTransitionGenerator` | | DI Health Checks | `AddDesignPatternsHealthChecks`、`IHealthCheck` | —(运行时扩展) | -模式文档:Spec(稳定契约)见 [docs/spec/](docs/spec/README.md),Design Doc(实现细节)见 [docs/design/](docs/design/README.md)。横切约定见 [docs/FactoryKeyConventions.md](docs/FactoryKeyConventions.md)。设计提案见 [docs/rfc/](docs/rfc/README.md),架构决策见 [docs/adr/](docs/adr/README.md)。 +模式文档见 [docs/design/](docs/design/README.md)。横切约定见 [docs/FactoryKeyConventions.md](docs/FactoryKeyConventions.md)。架构决策见 [docs/adr/](docs/adr/README.md)。 --- @@ -160,7 +160,7 @@ dotnet test DesignPatterns.slnx -c Release 诊断 ID 规范(**本表为唯一登记源**,其他文档不得另立分类): -- 下一个可用 ID:**DP067**;ID 一经发布不复用、不改语义(DP067–DP071 已由 [RFC: Singleton 生命周期诊断](docs/rfc/SingletonLifecycleDiagnostics.md) 预留)。 +- 下一个可用 ID:**DP067**;ID 一经发布不复用、不改语义(DP067–DP071 已由 [ADR-008](docs/adr/ADR-008-singleton-lifecycle-diagnostics.md) 预留)。 - 新增 / 修改诊断必须同步 [`DiagnosticIds.cs`](DesignPatterns.Diagnostics/DiagnosticIds.cs)、[`DesignPatternsDiagnosticDescriptors.cs`](DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs)(经 Compile Link 编入 SourceGenerators / Analyzers)与 [`AnalyzerReleases.Unshipped.md`](DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md)。 - 归属:DP006 / DP023 / DP024 / DP025 / DP033 / DP036 / DP044 / DP060 / DP061 / DP062 / DP066 属 **Analyzer**;其余属**生成器**。 - 文案:`messageFormat` 须含可操作建议;`description` 供 IDE 悬停;`helpLinkUri` 指向 [`DesignPatterns.Docs` diagnostics 页](https://skymly.github.io/DesignPatterns.Docs/diagnostics)(`#dp###` 片段,见 [`DiagnosticHelpLinks.cs`](DesignPatterns.Diagnostics/DiagnosticHelpLinks.cs))。 @@ -313,40 +313,28 @@ git push origin v0.1.0-preview3 --- -## 文档体系(文档驱动开发) +## 文档体系 -本仓库实行**文档驱动开发**:先文档后代码,任何非琐碎变更先满足文档前置条件(决策表见 [docs/DOCUMENTATION.md §11](docs/DOCUMENTATION.md#11-文档驱动开发流程))再进入实现。文档分为 7 种类型,完整规范见 [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md)。Agent 与人类开发者均须遵守。 +完整约定见 [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md)。仓内沉淀 ADR + Design Doc + Roadmap;任务与审查用 GitHub Issue / PR / Release。 -| 类型 | 目录 | 用途 | 关键规则 | -|------|------|------|----------| -| **RFC** | `docs/rfc/` | 设计提案与讨论 | 新增模式/诊断/破坏性 API 必须 RFC;模板 `docs/rfc/_template.md`;已实现移入 `archive/` | -| **ADR** | `docs/adr/` | 架构决策记录(不可变) | RFC Accepted → 产出 ADR;编号不复用;正文不修改,仅 Supersede | -| **Spec** | `docs/spec/` | 稳定契约(API 面、诊断 ID、不变量) | 变更需 RFC + ADR;随代码 PR 同步更新 | -| **Design Doc** | `docs/design/` | 实现细节、设计权衡、已知局限 | 随代码 PR 同步更新 | -| **Plan** | `docs/plans/` | 大型任务计划(跨多 PR) | 里程碑对齐单模块 PR 边界;Done/Cancelled 移入 `archive/`;小任务用 Issue 即可 | -| **Review** | `docs/review/` | 评审记录(设计/实现/发版/回顾) | Final 后正文不可变;行动项全部关闭移入 `archive/` | -| **Roadmap** | `docs/ROADMAP.md` | 功能与技术 backlog | 完成项移入「已完成(归档)」章节 | - -**归档统一规则**:归档 = 移动文件 + 更新状态字段 + 更新 README 索引,同一 PR 完成;归档后正文不再修改(仅修失效链接);归档不删除。 +| 载体 | 位置 | 用途 | +|------|------|------| +| **ADR** | `docs/adr/` | 架构决策(不可变;编号不复用;仅 Supersede) | +| **Design Doc** | `docs/design/` | 每域一份:API / 诊断 / 实现与权衡 | +| **Roadmap** | `docs/ROADMAP.md` | 宏观 backlog | +| **Issue / PR / Release** | GitHub | 任务、审查、版本历史 | -### Agent 文档工作流约定 +### Agent 文档工作流 | 场景 | Agent 行为 | |------|-----------| -| 新增诊断 ID | 确认是否有对应 RFC;无则提示需创建 RFC | -| 修改公共 API | 确认是否有对应 RFC + ADR;无则提示需创建 RFC | -| 跨多 PR 的大型任务 | 确认 `docs/plans/` 是否有对应 Plan;无则先建 Plan(经用户确认)再实现 | -| 创建 RFC | 使用 `docs/rfc/_template.md`;frontmatter 从 `Draft` 开始 | -| 创建 ADR | 编号取 `docs/adr/README.md` 中下一个可用编号 | -| 创建 Plan / Review | 使用对应 `_template.md`;Review 评审人注明为 Agent | -| RFC / Plan / Review 状态变更 | 更新 frontmatter `状态` + 日期;归档时移动到对应 `archive/` 并更新 README 索引 | -| Spec 变更 | 确认 RFC 已 Accepted;同步更新 Spec 版本号 | -| CHANGELOG | 在 `[Unreleased]` 下添加条目 | -| 文档目录 | 不在 `docs/` 之外创建文档文件(`.Local/` 除外) | - -### 模式文档结构 +| 新增诊断 ID / 改生成器行为 / 改公开 API | 同一 PR 更新对应 Design Doc;登记 `DiagnosticIds` + descriptors + `AnalyzerReleases`;更新 `AGENTS.md` 诊断表 | +| 破坏性或跨模块架构决策 | 记新 ADR(编号取 `docs/adr/README.md` 下一个可用编号);正文不可改,仅 Supersede | +| 发版 | 按 `docs/PUBLISHING.md`;`CHANGELOG` 迁出版本节;不擅自 bump/tag/publish | +| CHANGELOG | 用户可见变更写入 `[Unreleased]` | +| 文档目录 | 不在 `docs/` 之外新建文档(`.Local/` 除外) | -所有模式文档已拆分为 Spec(`docs/spec/`)+ Design Doc(`docs/design/`)。Spec 描述稳定契约(API 面、诊断 ID、不变量),Design Doc 描述实现细节(设计权衡、已知局限)。索引见 [docs/spec/README.md](docs/spec/README.md) 和 [docs/design/README.md](docs/design/README.md)。 +模式文档索引:[docs/design/README.md](docs/design/README.md)。 --- @@ -366,9 +354,9 @@ git push origin v0.1.0-preview3 Agent 行为准则——与「与用户沟通」并行生效: 1. **用户表述不清楚时,立刻询问**:不要基于猜测继续工作。用聚焦的问题(而非开放式提问)澄清意图,提供 2–4 个具体选项供用户选择。 -2. **用户表述不合理时,立刻指出并给出建议**:包括但不限于——违反已有 ADR(如 Core 引用 MSDI、厚重基类体系替代 primitives)、复用或改变已发布 `DP###` 诊断 ID 的语义、未走 RFC/ADR 流程变更 Spec 契约、跳过测试(单元 / 生成器 Verify 快照 / Analyzer)、单 PR 混合多个模块、破坏兼容基线(TFM / Roslyn 4.8.0)、过度设计。指出问题时必须说明**为什么不合理**,并给出合理替代方案。 +2. **用户表述不合理时,立刻指出并给出建议**:包括但不限于——违反已有 ADR(如 Core 引用 MSDI、厚重基类体系替代 primitives)、复用或改变已发布 `DP###` 诊断 ID 的语义、未更新 Design Doc / ADR 就变更已发布契约、跳过测试(单元 / 生成器 Verify 快照 / Analyzer)、单 PR 混合多个模块、破坏兼容基线(TFM / Roslyn 4.8.0)、过度设计。指出问题时必须说明**为什么不合理**,并给出合理替代方案。 3. **不要盲目执行**:即使能「做到」用户要求的事,如果认为方向有误,应先提出异议,等待用户确认后再动手。 -4. **发现矛盾时主动报告**:如果用户的新要求与已有 ADR / RFC / `AGENTS.md` 规则冲突,指出冲突点,由用户决定是否更新规则或调整需求(ADR 变更须走 Supersede 流程,见 [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md))。 +4. **发现矛盾时主动报告**:如果用户的新要求与已有 ADR / `AGENTS.md` 规则冲突,指出冲突点,由用户决定是否更新规则或调整需求(ADR 变更须走 Supersede 流程,见 [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md))。 ## 与用户沟通 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f9aebb..7c50996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Composite parallel traversal**: `CompositeTraverser.TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` — parallel tree traversal with `MaxDegreeOfParallelism` and `MaxParallelDepth` options. BFS same-level parallel, DFS child-parallel recursion with sequential fallback beyond depth threshold. `AggregateException` for error aggregation. `ConfigureAwait(false)` for async paths. `#if` split: `Parallel.ForEachAsync` on net8.0, `SemaphoreSlim` + `Task.WhenAll` on netstandard2.0. 17 new tests. Design RFC: [docs/rfc/CompositeParallelTraversal.md](docs/rfc/CompositeParallelTraversal.md). +- **Composite parallel traversal**: `CompositeTraverser.TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` — parallel tree traversal with `MaxDegreeOfParallelism` and `MaxParallelDepth` options. BFS same-level parallel, DFS child-parallel recursion with sequential fallback beyond depth threshold. `AggregateException` for error aggregation. `ConfigureAwait(false)` for async paths. `#if` split: `Parallel.ForEachAsync` on net8.0, `SemaphoreSlim` + `Task.WhenAll` on netstandard2.0. 17 new tests. See [ADR-006](docs/adr/ADR-006-composite-parallel-traversal.md). - **DP062 Phase 2 — generated RegisterDi coverage**: `CaptiveDependencyAnalyzer` now also scans `RegisterDi` calls from DesignPatterns source generators. Extracts `implementationLifetime` (or `lifetime` for single-param overloads) and applies it to all types bearing `[RegisterStrategy]`, `[RegisterFactory]`, `[RegisterEventHandler]`, `[Decorator]`, `[CompositePart]` attributes. 5 new Verify snapshot tests. -- **Singleton lifecycle diagnostics P1 — Autofac and factory delegate coverage**: `CaptiveDependencyAnalyzer` now collects Autofac registrations (`RegisterType` + fluent lifetime chain, `Register(c => ...)`) via symbol-name matching with no Autofac package reference, and reports the new **DP066** (Warning) when a Singleton factory delegate (`AddSingleton(sp => ...)` or Autofac `Register(...).SingleInstance()`) directly resolves a Scoped/Transient service via `GetRequiredService`/`GetService`/`Resolve`. MSDI factory and instance registrations now also feed the lifetime map, so Singletons constructor-depending on factory-registered Scoped/Transient services report DP062. Limitation: only direct resolution calls inside the delegate body are detected. 11 new Verify snapshot tests. RFC: [docs/rfc/SingletonLifecycleDiagnostics.md](docs/rfc/SingletonLifecycleDiagnostics.md), ADR-008. +- **Singleton lifecycle diagnostics P1 — Autofac and factory delegate coverage**: `CaptiveDependencyAnalyzer` now collects Autofac registrations (`RegisterType` + fluent lifetime chain, `Register(c => ...)`) via symbol-name matching with no Autofac package reference, and reports the new **DP066** (Warning) when a Singleton factory delegate (`AddSingleton(sp => ...)` or Autofac `Register(...).SingleInstance()`) directly resolves a Scoped/Transient service via `GetRequiredService`/`GetService`/`Resolve`. MSDI factory and instance registrations now also feed the lifetime map, so Singletons constructor-depending on factory-registered Scoped/Transient services report DP062. Limitation: only direct resolution calls inside the delegate body are detected. 11 new Verify snapshot tests. See [ADR-008](docs/adr/ADR-008-singleton-lifecycle-diagnostics.md). +- **Documentation system simplification**: Removed in-repo RFC / Spec / Plan / Review document types. Spec content merged into Design Docs; task tracking and review use GitHub Issues / PRs. See [docs/DOCUMENTATION.md](docs/DOCUMENTATION.md). ## [0.2.2] - 2026-06-30 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3eb70ac..d624d27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,18 +63,17 @@ dotnet test tests/DesignPatterns.SourceGenerators.Tests --filter FullyQualifiedN ## 文档 -本仓库文档体系分为 RFC、ADR、Spec、Design Doc、Roadmap 五种类型,完整规范见 [`docs/DOCUMENTATION.md`](docs/DOCUMENTATION.md)。人类开发者和 AI 编码助手均须遵守。 +文档约定见 [`docs/DOCUMENTATION.md`](docs/DOCUMENTATION.md)。仓内保留 ADR、Design Doc、Roadmap;任务与审查用 GitHub Issue / PR。 ### 文档工作流 | 变更类型 | 需要的文档操作 | |----------|----------------| -| 新增模式 / 诊断 ID / 破坏性 API | 创建 RFC → Accepted 后产出 ADR → 实现 PR 中更新 Spec + Design Doc | -| Bug fix | Issue + PR;如影响实现细节则更新 Design Doc | -| 公共 API 变更 | Spec 更新 + CHANGELOG 条目 | -| 诊断 ID 变更 | Spec 更新 + `AnalyzerReleases.Unshipped.md` 更新 | -| 用户可见变更 | 更新 [`CHANGELOG.md`](CHANGELOG.md)(英语,Keep a Changelog;发版时将 `[Unreleased]` 条目迁入版本节) | -| 新模式 | 在 [DesignPatterns.Samples](https://github.com/Skymly/DesignPatterns.Samples) 增加示例项目、更新本仓 `docs/` 与 [`AGENTS.md`](AGENTS.md) | +| 破坏性 API / 跨模块架构 | ADR + 更新对应 Design Doc | +| 公共 API / 诊断 / 实现细节变化 | 更新 Design Doc(+ `AnalyzerReleases` / `AGENTS.md` 诊断表,如适用) | +| Bug fix | Issue + PR;影响实现细节时更新 Design Doc | +| 用户可见变更 | 更新 [`CHANGELOG.md`](CHANGELOG.md)(英语,Keep a Changelog) | +| 新模式 | DesignPatterns.Samples 示例 + 本仓 Design Doc + [`AGENTS.md`](AGENTS.md) | | key 命名 | 遵循 [`docs/FactoryKeyConventions.md`](docs/FactoryKeyConventions.md) | | 架构 backlog | 见 [`docs/ROADMAP.md`](docs/ROADMAP.md) | @@ -82,9 +81,7 @@ dotnet test tests/DesignPatterns.SourceGenerators.Tests --filter FullyQualifiedN | 目录 | 用途 | |------|------| -| [`docs/DOCUMENTATION.md`](docs/DOCUMENTATION.md) | 文档体系标准(类型、生命周期、模板、归档) | -| [`docs/rfc/`](docs/rfc/README.md) | RFC — 设计提案与讨论 | -| [`docs/adr/`](docs/adr/README.md) | ADR — 架构决策记录 | -| [`docs/spec/`](docs/spec/README.md) | Spec — 稳定契约 | -| [`docs/design/`](docs/design/README.md) | Design Doc — 实现细节 | +| [`docs/DOCUMENTATION.md`](docs/DOCUMENTATION.md) | 文档约定 | +| [`docs/adr/`](docs/adr/README.md) | ADR — 架构决策 | +| [`docs/design/`](docs/design/README.md) | Design Doc — 每域设计 | | [`docs/ROADMAP.md`](docs/ROADMAP.md) | 路线图 | diff --git a/README.md b/README.md index 307f1ce..40db675 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,8 @@ DesignPatterns.slnx | [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | 环境、构建、测试、架构约定 | | [docs/PUBLISHING.md](docs/PUBLISHING.md) | NuGet 发版流程 | | [docs/FactoryKeyConventions.md](docs/FactoryKeyConventions.md) | Strategy / Factory key 命名约定 | -| [docs/spec/](docs/spec/README.md) | 模式规范(Spec)— 稳定契约:API 面、诊断 ID、不变量 | -| [docs/design/](docs/design/README.md) | 模式设计文档(Design Doc)— 实现细节、设计权衡、已知局限 | +| [docs/design/](docs/design/README.md) | 模式设计文档(Design Doc)— API / 诊断 / 实现与权衡 | +| [docs/adr/](docs/adr/README.md) | 架构决策记录(ADR) | | [docs/ROADMAP.md](docs/ROADMAP.md) | 功能与技术 backlog | | [CONTRIBUTING.md](CONTRIBUTING.md) | 贡献与测试说明 | | [AGENTS.md](AGENTS.md) | AI 编码助手项目上下文 | diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 6b853f4..6f82c44 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -1,726 +1,76 @@ -# 文档体系标准 +# 文档约定 -> **权威源**。本文档定义本仓库**文档驱动开发(Documentation-Driven Development)**体系:所有文档的类型、结构、生命周期、归档规则,以及以文档为先导的开发流程。人类开发者和 AI 编码助手(Agent)均须遵守。`AGENTS.md`「文档体系」章节为本文档的精简摘要。 +> **权威源**。定义本仓库文档载体与维护规则。人类开发者与 AI 编码助手均须遵守。`AGENTS.md`「文档体系」为本文摘要。 > -> - **核心原则**:**先文档后代码**——任何非琐碎变更,先确定它需要哪些文档、文档达到要求状态后才动代码(决策表见 [§11](#11-文档驱动开发流程))。 -> - **语言**:内部维护者文档以**中文**为主;面向库使用者的文档在 [DesignPatterns.Docs](https://github.com/Skymly/DesignPatterns.Docs) 仓库。 +> - **语言**:内部维护者文档以**中文**为主;面向库使用者的文档在 [DesignPatterns.Docs](https://github.com/Skymly/DesignPatterns.Docs)。 > - **冲突优先级**:`AGENTS.md` > `docs/DOCUMENTATION.md` > 其他文档。 ---- +## 1. 文档载体 -## 1. 文档类型总览 - -| 类型 | 目录 | 用途 | 稳定性 | 变更门槛 | -|------|------|------|--------|----------| -| **RFC** | `docs/rfc/` | 设计提案与讨论记录 | 提案阶段,频繁迭代 | 自由修改(Review 前) | -| **ADR** | `docs/adr/` | 架构决策记录(不可变卡片) | 已决策,仅追加 | 仅 Supersede,不修改原文 | -| **Spec** | `docs/spec/` | 稳定契约(API 面、诊断 ID、不变量) | 版本化稳定 | 需 RFC + ADR 方可变更 | -| **Design Doc** | `docs/design/` | 实现细节、设计权衡、已知局限 | 随实现演进 | PR 随代码同步更新 | -| **Roadmap** | `docs/ROADMAP.md` | 功能与技术 backlog | 滚动维护 | 维护者评审 | -| **Plan** | `docs/plans/`(大型)/ GitHub Issue(小型) | 任务计划(目标、步骤、验收) | 短生命周期 | 计划内自由更新 | -| **Review** | `docs/review/` | 评审记录(设计 / 实现 / 阶段回顾) | Final 后不可变 | 仅勾选行动项与修复链接 | +| 载体 | 位置 | 用途 | +|------|------|------| +| **ADR** | `docs/adr/` | 架构决策(不可变卡片) | +| **Design Doc** | `docs/design/` | 每域一份:API 面 + 诊断/契约 + 实现细节 + 设计权衡 | +| **Roadmap** | `docs/ROADMAP.md` | 宏观规划与 backlog 排序 | +| **Issue** | GitHub Issues | 需求、Bug、任务追踪 | +| **PR** | GitHub Pull Requests | 变更审查 | +| **Release** | GitHub Releases | 版本历史 | -### 1.1 不作为独立文档类型 +### 不作为独立文档类型 | 内容 | 载体 | |------|------| -| 编码规范、兼容基线、打包、测试 | `AGENTS.md`(权威源) | +| 编码规范、兼容基线、打包、测试 | `AGENTS.md` | | 开发环境、构建、仓库布局 | `docs/DEVELOPMENT.md` | | 发布流程 | `docs/PUBLISHING.md` | -| 变更日志 | `CHANGELOG.md`(Keep a Changelog 格式) | +| 变更日志 | `CHANGELOG.md` | | 贡献流程 | `CONTRIBUTING.md` | ---- - -## 2. RFC — Request for Comments - -### 2.1 用途 - -对**有设计争议或影响面较大**的变更提出设计方案,供讨论与决策。小改动(bug fix、单方法新增)无需 RFC,直接 Issue + PR。 - -### 2.2 何时需要 RFC - -| 场景 | 需要 RFC? | -|------|-----------| -| 新增模式(新源生成器 + 诊断) | ✅ 必须 | -| 新增或变更公共 API(破坏性) | ✅ 必须 | -| 新增诊断 ID(`DP###`) | ✅ 必须 | -| 跨模块架构变更 | ✅ 必须 | -| 单模块内 bug fix | ❌ Issue + PR | -| 单模块内新增非破坏性 API | ❌ Issue + PR(但需在 Design Doc 记录) | -| 文档/测试/重构 | ❌ Issue + PR | -| 工程整改(CI、构建脚本) | ⚠️ 视影响面,由维护者判断 | - -### 2.3 文件命名 - -``` -docs/rfc/.md -``` - -示例:`CompositeParallelTraversal.md`、`HierarchicalStateMachine.md`。 - -### 2.4 Frontmatter 标准 - -每份 RFC **必须**以如下元数据块开头(blockquote 格式,字段固定): - -```markdown -> **状态**:Draft | Review | Accepted | Rejected | Implemented | Superseded -> **类型**:Feature | Pattern | Architecture | Process -> **创建**:YYYY-MM-DD -> **更新**:YYYY-MM-DD -> **作者**:维护者 / 贡献者 -> **关联 Roadmap**:FXX(如有) -> **关联 Issue**:#XXX(如有) -> **衍生 ADR**:ADR-XXX(Accepted 后填写) -``` - -### 2.5 正文章节模板 - -```markdown -# RFC: <标题> - -> (frontmatter) - -## 摘要 -一段话说明提案内容。 - -## 动机 -为什么需要这个变更?现有局限是什么? - -## 非目标 -明确不做什么,避免范围蔓延。 - -## 设计方案 -### 概念模型 -### API 设计 -### 诊断影响 -### 实现方案 - -## 替代方案 -考虑过但否决的方案及原因。 - -## 开放问题 -讨论中尚未决策的问题(Review 阶段)。 - -## 决策记录 -Review 结束后记录最终决策与理由(Accepted 阶段填写)。 - -## 参考 -``` - -### 2.6 生命周期 - -``` -Draft → Review → Accepted → Implemented → (archive) - ↘ Rejected → (archive) -``` - -| 状态 | 含义 | 操作 | -|------|------|------| -| **Draft** | 作者撰写中,未公开征求意见 | 可自由修改 | -| **Review** | 公开征求意见 | 更新 frontmatter `状态`;讨论在 GitHub Issue 或 PR Comments | -| **Accepted** | 决策通过,待实现 | 填写「决策记录」章节;创建衍生 ADR;关联 Roadmap 项标记「进行中」 | -| **Rejected** | 决策否决 | 记录否决理由;移入 `archive/` | -| **Implemented** | 已实现并合并 | 关联 PR 号;移入 `archive/` | -| **Superseded** | 被后续 RFC 取代 | 标注取代者链接;移入 `archive/` | - -### 2.7 归档规则 - -- 状态变为 **Rejected**、**Implemented** 或 **Superseded** 时,文件移入 `docs/rfc/archive/`。 -- 归档文件**不再修改**(除修正链接失效)。 -- `docs/rfc/README.md` 状态板保留归档条目的链接。 - ---- - -## 3. ADR — Architecture Decision Record - -### 3.1 用途 - -记录**最终架构决策**的简短不可变卡片。ADR 不是讨论场所——讨论在 RFC 中完成,ADR 只记录结论。 - -### 3.2 与 RFC 的关系 - -- RFC **Accepted** → 产出一份 ADR(在 RFC frontmatter `衍生 ADR` 字段和 ADR `关联 RFC` 字段双向链接)。 -- 少数情况下,无需完整 RFC 的小决策也可直接写 ADR(如编码风格选择),但需在 ADR 中说明为何跳过 RFC。 - -### 3.3 文件命名 - -``` -docs/adr/ADR--.md -``` - -编号从 `001` 开始,零填充三位,**不复用编号**。示例:`ADR-001-roslyn-source-generators.md`。 - -### 3.4 格式模板 - -```markdown -# ADR-NNN: <标题> - -| 字段 | 值 | -|------|-----| -| **状态** | Accepted | Superseded by ADR-XXX | Deprecated | -| **日期** | YYYY-MM-DD | -| **关联 RFC** | [docs/rfc/XXX.md](../rfc/XXX.md)(或「无 — 直接决策」) | - -## 背景 - -为什么需要做这个决策?当时的约束和问题是什么? - -## 决策 - -最终决定了什么?一句话概括 + 补充说明。 - -## 后果 - -这个决策带来的正面和负面影响。 - -## 参考 - -- 相关 ADR、文档、外部链接 -``` - -### 3.5 不可变原则 - -- ADR 一旦 **Accepted**,正文**不修改**。 -- 若决策被推翻,创建新 ADR 并将旧 ADR 状态改为 `Superseded by ADR-XXX`,旧 ADR 正文仍不修改。 -- 编号**永不复用**。 - ---- - -## 4. Spec — 规范文档 - -### 4.1 用途 - -定义模式的**稳定契约**:公共 API 面、诊断 ID、不变量、兼容基线。Spec 是「接口」级别的文档——描述 **what**,不描述 **how**。 - -### 4.2 与 Design Doc 的关系 - -| 维度 | Spec | Design Doc | -|------|------|------------| -| 回答 | What(契约是什么) | How + Why(怎么实现、为什么这样做) | -| 稳定性 | 高——变更需 RFC + ADR | 中——随实现演进 | -| 读者 | 库使用者 + 维护者 | 维护者 | -| 位置 | `docs/spec/` | `docs/design/` | - -### 4.3 文件命名 - -``` -docs/spec/.md -``` - -与模式名一致。示例:`docs/spec/Strategy.md`。 - -### 4.4 章节模板 - -```markdown -# Spec: <模式名> - -> **版本**:vX.Y(与 NuGet 包版本对齐) -> **关联 Design Doc**:[docs/design/.md](../design/.md) -> **关联 ADR**:ADR-XXX(如有) - -## API 面 - -### 运行时接口 -(接口定义、方法签名、泛型约束) - -### 特性(Attribute) -(特性类、属性、构造函数签名) - -### 生成器产出 -(生成的类名、方法签名、命名空间) - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| DPXXX | Warning | ... | ... | - -## 不变量 - -1. ... -2. ... - -## 兼容基线 - -- netstandard2.0 / net8.0 -- ... - -## 不在范围内 - -- ... -``` - -### 4.5 变更门槛 - -- **新增 API** 或 **新增诊断 ID**:需 RFC → ADR → Spec 更新。 -- **破坏性变更 API**:需 RFC → ADR → Spec 更新 + CHANGELOG `Breaking` 条目。 -- **文档修正**(措辞、示例):直接 PR,无需 RFC。 - ---- - -## 5. Design Doc — 设计文档 - -### 5.1 用途 - -记录模式的**实现细节**、设计权衡、已知局限、与生态的边界。Design Doc 是「实现」级别的文档——描述 **how** 和 **why**。 - -### 5.2 文件命名 - -``` -docs/design/.md -``` - -与对应 Spec 同名。示例:`docs/design/Strategy.md`。 - -### 5.3 章节模板 - -```markdown -# Design Doc: <模式名> - -> **关联 Spec**:[docs/spec/.md](../spec/.md) -> **关联 RFC**:[docs/rfc/XXX.md](../rfc/XXX.md)(如有) -> **关联 ADR**:ADR-XXX(如有) - -## 概述 - -## 设计目标 - -## 实现概览 - -### 运行时 -(关键类、数据结构、算法) - -### 源生成器 -(增量管线阶段、EquatableArray 结构) - -### 诊断 -(检测逻辑、报告位置) - -## 设计权衡 - -### 选择了 X 而非 Y,因为... -(链接到 ADR) - -## 与生态的边界 - -## 已知局限 - -## 参考 -``` - -### 5.4 变更门槛 +**判断原则**:若信息在 Issue / PR / Release 中已完整表达,不必再写仓内文件。仓内只记跨多个 Issue/PR 的累积知识,以及不应随讨论漂移的决策。 -- 随代码 PR 同步更新,无需独立 RFC。 -- 但若实现变更导致 Spec 契约变更,则需走 RFC 流程。 +## 2. ADR ---- +- **何时写**:破坏性 API、跨模块架构、诊断语义边界、与已有 ADR 冲突需改决策时。 +- **编号**:从 ADR-001 起,三位零填充,**不复用**;下一个编号见 [adr/README.md](adr/README.md)。 +- **正文不可变**:Accepted 后不改决策正文;仅允许修正失效链接。 +- **变更方式**:新决策写新 ADR,旧 ADR 标 `Superseded by ADR-NNN`。 +- **模板**:[adr/_template.md](adr/_template.md)。 -## 6. Roadmap +## 3. Design Doc -### 6.1 用途 +- **何时更新**:公开 API、诊断 ID、生成器行为或实现权衡变化时,在**同一 PR** 更新对应 `docs/design/.md`。 +- **内容**:API 面、诊断表、不变量/兼容基线、实现概览、设计权衡、已知局限。 +- **模板**:[design/_template.md](design/_template.md);索引:[design/README.md](design/README.md)。 -维护 `docs/ROADMAP.md` 作为功能与技术 backlog 的滚动清单。 +## 4. Roadmap -### 6.2 生命周期 +- 维护宏观排序与探索候选;完成项移入「已完成(归档)」。 +- 具体任务与验收在 **GitHub Issue** 跟踪,不在仓内另建 Plan 文档。 -``` -候选 → 排期 → 进行中 → 已完成(归档) - ↘ 暂缓 / 明确不做 -``` - -- **候选**:在 Roadmap 中登记,未排期。 -- **排期**:分配到 FXX 阶段,准备启动。 -- **进行中**:创建 RFC(如需要)+ Issue + 分支。 -- **已完成**:移入 Roadmap「已完成(归档)」章节。 -- **暂缓 / 明确不做**:移入对应章节,记录理由。 - -### 6.3 与其他文档的联动 - -| Roadmap 状态 | 关联文档 | -|--------------|----------| -| 排期 → 进行中 | 创建 RFC(如需要)→ Accepted → 产出 ADR | -| 进行中 → 已完成 | Spec 更新 + Design Doc 更新 + CHANGELOG 条目 + RFC 归档 | - ---- - -## 7. Plan — 任务计划 - -### 7.1 载体(双轨) - -| 规模 | 载体 | 判断标准 | -|------|------|----------| -| **小型任务** | GitHub Issue | 单 PR 可完成(bug fix、单方法新增、文档修正) | -| **大型任务** | `docs/plans/.md` + 主 Issue | 跨多 PR、跨多阶段、或由 RFC 衍生的实现计划 | - -大型任务的 Plan 文档是**执行的唯一真相源**:目标、里程碑拆解、验收标准、进度状态都记录在文档中;主 Issue 仅作 GitHub 侧跟踪入口,链接到 Plan 文档。 - -### 7.2 文件命名 - -``` -docs/plans/.md -``` - -与关联 RFC 同名(如有)。示例:`docs/plans/HierarchicalStateMachine.md`。 - -### 7.3 Frontmatter 标准 - -```markdown -> **状态**:Active | Done | Cancelled -> **创建**:YYYY-MM-DD -> **更新**:YYYY-MM-DD -> **关联 RFC**:[docs/rfc/XXX.md](../rfc/XXX.md)(如有) -> **关联 Issue**:#XXX -> **关联 Roadmap**:FXX(如有) -``` - -### 7.4 正文章节模板 - -```markdown -# Plan: <标题> - -> (frontmatter) - -## 目标 -一段话说明要交付什么。 - -## 非目标 -本计划不覆盖的内容。 - -## 里程碑拆解 - -| 阶段 | 内容 | 模块(AGENTS.md 边界) | 状态 | PR | -|------|------|------------------------|------|-----| -| P1 | ... | Runtime | [ ] | — | -| P2 | ... | SourceGenerators | [ ] | — | - -## 验收标准 -- [ ] ... - -## 风险与依赖 - -## 变更记录 -计划执行中的重大调整(范围增减、顺序变更)在此追加,不删除原文。 -``` +## 5. 同步 checklist -### 7.5 生命周期与归档 +变更公共 API / 诊断 / 生成代码时,按需同步: -``` -Active → Done → archive/ - ↘ Cancelled → archive/(记录取消原因) -``` - -- 所有里程碑完成且验收标准全部满足 → 状态改 `Done`,移入 `docs/plans/archive/`。 -- 中途取消 → 状态改 `Cancelled`,在「变更记录」中写明原因后归档。 -- 小型任务的 Issue 不归档(GitHub close 即完成)。 - -### 7.6 何时需要 Issue - -- 所有 PR 必须关联一个 Issue(bug fix 可使用 `bug_report` 模板,功能使用 `feature_request` 模板)。 -- 大型特性(跨多 PR)使用一个主 Issue + `docs/plans/` 计划文档。 - ---- - -## 8. Review — 评审记录 - -### 8.1 用途 +1. 本仓 `docs/design/.md` +2. `DesignPatterns.Diagnostics/DiagnosticIds.cs` + descriptors + `AnalyzerReleases.*.md` +3. `AGENTS.md` 诊断表(若新增 `DP###`) +4. `CHANGELOG.md` `[Unreleased]` +5. 用户向文档:[DesignPatterns.Docs](https://github.com/Skymly/DesignPatterns.Docs)(**独立 PR**) +6. 示例:[DesignPatterns.Samples](https://github.com/Skymly/DesignPatterns.Samples)(若行为可见变化) -记录**结构化评审结论**,使评审意见可追溯、行动项可跟踪。区别于 PR 内的行内 code review(即时、随 PR 关闭而结束),Review 文档记录**跨 PR 或里程碑级**的评审。 - -### 8.2 何时需要 Review 文档 - -| 场景 | 需要 Review 文档? | -|------|--------------------| -| RFC 进入 Review 阶段的设计评审 | ✅(评审结论落文档,RFC「决策记录」引用) | -| 大型 Plan 完成后的实现回顾 | ✅ | -| 发版前的 API 面 / 打包审查 | ✅ | -| 定期技术债 / 代码质量审查 | ✅ | -| 单 PR 的常规 code review | ❌ PR Comments 即可 | - -### 8.3 文件命名 - -``` -docs/review/-.md -``` - -示例:`docs/review/2026-07-08-composite-schema-validation-design.md`。 - -### 8.4 Frontmatter 标准 - -```markdown -> **状态**:Draft | Final -> **类型**:Design | Implementation | Release | Retrospective -> **日期**:YYYY-MM-DD -> **评审对象**:RFC / Plan / PR 范围 / 版本号 -> **评审人**:维护者 / Agent(注明) -``` - -### 8.5 正文章节模板 - -```markdown -# Review: <标题> - -> (frontmatter) - -## 评审范围 -评审了什么(文档、代码范围、版本)。 - -## 结论 -通过 | 有条件通过 | 不通过,一句话概括。 - -## 发现 - -| # | 级别 | 发现 | 建议 | -|---|------|------|------| -| 1 | Blocker / Major / Minor / Nit | ... | ... | - -## 行动项 - -- [ ] #1 → Issue #XXX / PR #XXX -- [ ] #2 → ... - -## 参考 -``` - -### 8.6 生命周期与归档 - -``` -Draft → Final → (行动项全部关闭后)archive/ -``` - -- **Final 后正文不可变**:新发现另起新 Review;仅允许勾选行动项 checkbox、补充 Issue/PR 链接。 -- 行动项全部关闭后移入 `docs/review/archive/`。 -- 级别定义:**Blocker**(不修复不得合并/发版)、**Major**(须建 Issue 跟踪)、**Minor**(建议修复)、**Nit**(可忽略)。 - ---- - -## 9. 归档机制(统一规则) - -各类型文档共用同一套归档纪律: - -| 类型 | 归档目录 | 归档触发 | 归档后可改动 | -|------|----------|----------|--------------| -| RFC | `docs/rfc/archive/` | Implemented / Rejected / Superseded | 仅修失效链接 | -| Plan | `docs/plans/archive/` | Done / Cancelled | 仅修失效链接 | -| Review | `docs/review/archive/` | Final 且行动项全部关闭 | 仅修失效链接 | -| ADR | 不移动(原地 Supersede) | — | 仅状态字段改 `Superseded by ADR-XXX` | -| Spec / Design Doc | 不归档(活文档) | 模式被移除时随代码删除 | 随实现演进 | -| Roadmap 条目 | `ROADMAP.md`「已完成(归档)」章节 | 功能落地 | 仅追加 | - -通用规则: - -1. **归档 = 移动文件 + 更新状态字段 + 更新对应 README 索引**,三者必须在同一 PR 完成。 -2. 归档文件**正文不再修改**(唯一例外:修正失效链接)。 -3. 归档不删除——历史决策与讨论过程是资产;需要检索时从各目录 README 的索引进入。 -4. 引用归档文件时使用 `archive/` 路径;归档时须全仓搜索旧路径并修正引用。 - ---- - -## 10. 目录结构 +## 6. 目录结构 ``` docs/ -├── DOCUMENTATION.md # 本文件 — 文档体系标准 -├── README.md # 文档索引 -├── DEVELOPMENT.md # 开发手册 -├── ROADMAP.md # 路线图 -├── PUBLISHING.md # 发布流程 -├── rfc/ -│ ├── README.md # RFC 索引 + 状态板 -│ ├── _template.md # RFC 模板 -│ ├── .md # 进行中的 RFC -│ └── archive/ -│ └── .md # 已实现/已否决的 RFC +├── DOCUMENTATION.md +├── README.md +├── ROADMAP.md ├── adr/ -│ ├── README.md # ADR 索引 -│ └── ADR-NNN-.md # 架构决策记录 -├── spec/ -│ ├── README.md # Spec 索引 -│ ├── _template.md # Spec 模板 -│ └── <PatternName>.md # 模式规范 -├── design/ -│ ├── README.md # Design Doc 索引 -│ └── <PatternName>.md # 模式设计文档 -├── plans/ -│ ├── README.md # Plan 状态板 -│ ├── _template.md # Plan 模板 -│ ├── <ActivePlan>.md # 进行中的大型任务计划 -│ └── archive/ -│ └── <DonePlan>.md # 已完成/已取消的计划 -├── review/ -│ ├── README.md # Review 索引 -│ ├── _template.md # Review 模板 -│ ├── <YYYY-MM-DD>-<topic>.md # 行动项未关闭的评审 -│ └── archive/ -│ └── <ClosedReview>.md # 行动项全部关闭的评审 -└── (横切约定文档保留在 docs/ 根目录) - ├── FactoryKeyConventions.md - ├── AppSettings.md - ├── Autofac.md - ├── Configuration.md - └── PluginAssemblies.md -``` - -### 10.1 现有模式文档的迁移(已完成) - -所有 7 个模式文档已从 `docs/<PatternName>.md` 拆分迁移至 `docs/spec/` + `docs/design/`: - -| 原文件 | Spec | Design Doc | -|--------|------|------------| -| `docs/Strategy.md` | `docs/spec/Strategy.md` | `docs/design/Strategy.md` | -| `docs/ChainOfResponsibility.md` | `docs/spec/ChainOfResponsibility.md` | `docs/design/ChainOfResponsibility.md` | -| `docs/Composite.md` | `docs/spec/Composite.md` | `docs/design/Composite.md` | -| `docs/FactoryRegistry.md` | `docs/spec/FactoryRegistry.md` | `docs/design/FactoryRegistry.md` | -| `docs/Decorator.md` | `docs/spec/Decorator.md` | `docs/design/Decorator.md` | -| `docs/EventAggregator.md` | `docs/spec/EventAggregator.md` | `docs/design/EventAggregator.md` | -| `docs/StateTransitionTable.md` | `docs/spec/StateTransitionTable.md` | `docs/design/StateTransitionTable.md` | - -迁移遵循以下规则: -- **API 面、诊断 ID 表、不变量、兼容基线** → Spec -- **实现概览、设计权衡、与生态边界、已知局限、示例** → Design Doc -- 横切约定文档(`FactoryKeyConventions.md`、`AppSettings.md` 等)保留在 `docs/` 根目录,不迁移。 -- 旧文件已删除。 - ---- - -## 11. 文档驱动开发流程 - -**先文档后代码**:动手写代码前,先按下表判定变更所需的文档前置条件;前置文档未达到要求状态,不进入实现阶段。人类开发者与 Agent 一体遵守。 - -### 11.1 变更类型 → 文档前置条件决策表 - -| 变更类型 | RFC | ADR | Plan | Review | 实现 PR 须同步 | -|----------|-----|-----|------|--------|----------------| -| 新增模式(生成器 + 诊断) | ✅ 必须 Accepted | ✅ RFC 衍生 | ✅ `docs/plans/` | ✅ 设计评审 | Spec 新建 + Design Doc 新建 + CHANGELOG | -| 破坏性公共 API 变更 | ✅ 必须 Accepted | ✅ | 视规模 | ✅ 设计评审 | Spec + CHANGELOG `Breaking` | -| 新增诊断 ID | ✅ 必须 Accepted | ✅ | 视规模 | 建议 | Spec + `AnalyzerReleases.Unshipped.md` + CHANGELOG | -| 非破坏性 API 新增(单模块) | ❌ | ❌ | ❌(Issue 即可) | ❌ | Design Doc + CHANGELOG | -| Bug fix | ❌ | ❌ | ❌(Issue 即可) | ❌ | CHANGELOG(如用户可见) | -| 重构(无行为变更) | ❌ | 视架构影响 | ❌ | ❌ | Design Doc(如实现结构变化) | -| 发版 | ❌ | ❌ | ❌ | ✅ Release 审查 | CHANGELOG 版本化 | - -### 11.2 新功能/新模式完整流程 - -``` -1. Roadmap 候选 → 排期 -2. 创建 RFC(Draft)→ 公开 Review -3. 设计评审 → Review 文档(Final)→ RFC Accepted → 产出 ADR → Roadmap 标记「进行中」 -4. 创建 Plan(docs/plans/,跨多 PR 时)+ 主 Issue -5. 按 Plan 里程碑实现 → 每个 PR 随代码更新 Spec + Design Doc -6. 全部合并 → RFC 标记 Implemented → 移入 rfc/archive/ -7. Plan 标记 Done → 移入 plans/archive/;可选实现回顾 Review -8. CHANGELOG 条目 → Roadmap 标记「已完成」 -``` - -### 11.3 Bug Fix 流程 - +│ ├── README.md +│ ├── _template.md +│ └── ADR-NNN-*.md +└── design/ + ├── README.md + ├── _template.md + └── <Domain>.md ``` -1. Issue(bug_report) -2. PR(修复 + 测试 + Design Doc 更新如需要) -3. 合并 → CHANGELOG 条目 -``` - -### 11.4 文档变更流程 - -``` -1. Issue(说明文档变更内容与原因) -2. PR(文档修改) -3. 合并 -``` - -### 11.5 发版流程(文档侧) - -``` -1. Release Review(docs/review/,检查 API 面 / CHANGELOG / 打包) -2. Blocker 行动项清零 -3. 按 docs/PUBLISHING.md 执行发版 -4. CHANGELOG [Unreleased] → 版本化章节 -``` - -### 11.6 Agent 工作流约定 - -AI 编码助手(Agent)在本仓库工作时,除遵守 `AGENTS.md` 的全部规则外,还须: - -| 场景 | Agent 行为 | -|------|-----------| -| 新增诊断 ID | 必须先确认是否有对应 RFC;无则提示需创建 RFC | -| 修改公共 API | 必须先确认是否有对应 RFC + ADR;无则提示需创建 RFC | -| 接到跨多 PR 的大型任务 | 先确认 `docs/plans/` 是否有对应 Plan;无则先建 Plan(经用户确认)再实现 | -| 创建 RFC | 使用 `docs/rfc/_template.md` 模板;frontmatter 从 `Draft` 开始 | -| 创建 ADR | 使用 `docs/adr/` 目录;编号取 `docs/adr/README.md` 中下一个可用编号 | -| 创建 Plan | 使用 `docs/plans/_template.md`;里程碑对齐 AGENTS.md 单模块 PR 边界 | -| 创建 Review | 使用 `docs/review/_template.md`;评审人注明为 Agent | -| RFC / Plan / Review 状态变更 | 更新 frontmatter `状态` + 日期;归档时移动文件到对应 `archive/` 并更新 README 索引 | -| Spec 变更 | 确认关联 RFC 已 Accepted;同步更新 Spec 版本号 | -| Design Doc 变更 | 随代码 PR 同步更新 | -| ROADMAP 变更 | 完成项移入「已完成(归档)」;新增项放入对应阶段 | -| CHANGELOG | 在 `[Unreleased]` 下添加条目 | -| 文档目录 | 不在 `docs/` 之外创建文档文件(`.Local/` 除外,见 AGENTS.md) | - -### 11.7 人类开发者工作流约定 - -人类开发者除遵守 `CONTRIBUTING.md` 的全部规则外,还须: - -| 场景 | 开发者行为 | -|------|-----------| -| 发起 RFC | 在 `docs/rfc/` 创建文件,提交 PR 标记 `Draft` → `Review` | -| 评审 RFC | 在 RFC PR 的 Comments 中讨论;重大设计评审落 `docs/review/` 文档;决策后更新状态为 `Accepted`/`Rejected` | -| 创建 ADR | RFC Accepted 后,在 `docs/adr/` 创建 ADR 文件,双向链接 | -| 启动大型任务 | 创建 `docs/plans/` 计划文档 + 主 Issue | -| 归档 RFC / Plan / Review | 达到归档条件后移入对应 `archive/`,更新 README 索引 | -| Spec 变更评审 | 确认 RFC + ADR 齐备后批准 Spec 更新 | -| Roadmap 维护 | 定期评审 Roadmap,更新状态 | - ---- - -## 12. 文档与 Git 的关系 - -| 文档类型 | 提交方式 | -|----------|----------| -| RFC | 独立 PR(`docs: RFC <name>`)或随实现 PR | -| ADR | 随 RFC PR 或独立 PR(`docs: ADR-NNN <title>`) | -| Spec | 随实现 PR(与代码变更同一 PR) | -| Design Doc | 随实现 PR(与代码变更同一 PR) | -| Plan | 独立 PR(`docs: plan <name>`);状态更新可随实现 PR | -| Review | 独立 PR(`docs: review <topic>`) | -| Roadmap | 随实现 PR 或独立 PR | -| CHANGELOG | 随实现 PR | - -### 12.1 PR 模板中的文档 checklist - -PR 模板包含文档变更 checklist,提交者须勾选: - -- [ ] 新增/变更公共 API → Spec 已更新 -- [ ] 新增/变更诊断 ID → Spec 已更新 + `AnalyzerReleases.Unshipped.md` 已更新 -- [ ] 新增/变更实现细节 → Design Doc 已更新 -- [ ] 新增 RFC → 使用 `_template.md` 模板 -- [ ] RFC / Plan / Review 状态变更 → frontmatter 已更新 + 归档操作已完成 -- [ ] CHANGELOG `[Unreleased]` 已添加条目 - ---- - -## 13. 文档质量检查清单 - -提交文档相关 PR 前,检查: - -- [ ] 文件位置正确(RFC 在 `docs/rfc/`,ADR 在 `docs/adr/`,Spec 在 `docs/spec/`,Design Doc 在 `docs/design/`,Plan 在 `docs/plans/`,Review 在 `docs/review/`) -- [ ] Frontmatter 格式符合标准(字段完整、日期格式 `YYYY-MM-DD`) -- [ ] 章节结构符合模板 -- [ ] 交叉链接有效(RFC ↔ ADR ↔ Spec ↔ Design Doc ↔ Plan ↔ Review 双向链接) -- [ ] `docs/README.md` 索引已更新 -- [ ] `docs/rfc/README.md` 状态板已更新(RFC 状态变更时) -- [ ] `docs/adr/README.md` 索引已更新(新增 ADR 时) -- [ ] `docs/plans/README.md` 状态板已更新(Plan 状态变更时) -- [ ] `docs/review/README.md` 索引已更新(Review 状态变更时) -- [ ] 语言:内部文档中文为主,技术术语可用英文 -- [ ] 无 AI/LLM 工具名称(遵守 `AGENTS.md` 隐私规则) -- [ ] 无私有工作区路径(遵守 `AGENTS.md` 隐私规则) - ---- - -## 14. 参考 - -- [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) -- [Semantic Versioning](https://semver.org/spec/v2.0.0.html) -- [RFC Process (Rust)](https://github.com/rust-lang/rfcs) — RFC 生命周期参考 -- [ADR (Michael Nygard)](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions) — ADR 概念起源 diff --git a/docs/README.md b/docs/README.md index 1834dd3..cd7d27d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,53 +2,37 @@ 本目录面向**维护者与贡献者**(中文为主)。面向库使用者的英文/中文指南见 [DesignPatterns.Docs](https://github.com/Skymly/DesignPatterns.Docs)(VitePress 站点:[skymly.github.io/DesignPatterns.Docs](https://skymly.github.io/DesignPatterns.Docs/))。 -> **文档体系标准**:[DOCUMENTATION.md](DOCUMENTATION.md) — 定义所有文档的类型、结构、生命周期与归档规则。人类开发者和 AI 编码助手均须遵守。 +> **文档约定**:[DOCUMENTATION.md](DOCUMENTATION.md) — ADR / Design Doc / Roadmap;任务与审查用 GitHub Issue / PR。 ## 入门 | 文档 | 说明 | |------|------| -| [DOCUMENTATION.md](DOCUMENTATION.md) | **文档体系标准**(类型、生命周期、模板、归档、工作流) | +| [DOCUMENTATION.md](DOCUMENTATION.md) | 文档约定 | | [DEVELOPMENT.md](DEVELOPMENT.md) | 环境、构建、测试、架构约定 | -| [CONTRIBUTING.md](../CONTRIBUTING.md) | 贡献流程、测试与 Roslyn 组件结构 | -| [PUBLISHING.md](PUBLISHING.md) | NuGet 预览/正式发版流程 | +| [CONTRIBUTING.md](../CONTRIBUTING.md) | 贡献流程 | +| [PUBLISHING.md](PUBLISHING.md) | NuGet 发版流程 | | [ROADMAP.md](ROADMAP.md) | 功能与技术 backlog | | [../AGENTS.md](../AGENTS.md) | AI 编码助手上下文(诊断 ID 权威登记) | -## 设计提案与决策 +## 决策与设计 | 目录 | 说明 | |------|------| -| [rfc/](rfc/README.md) | RFC — 设计提案与讨论记录([状态板](rfc/README.md)) | -| [adr/](adr/README.md) | ADR — 架构决策记录([索引](adr/README.md)) | - -## 计划与评审 - -| 目录 | 说明 | -|------|------| -| [plans/](plans/README.md) | Plan — 大型任务计划(跨多 PR;[状态板](plans/README.md)) | -| [review/](review/README.md) | Review — 评审记录(设计/实现/发版/回顾;[索引](review/README.md)) | - -## 规范与设计文档 - -| 目录 | 说明 | -|------|------| -| [spec/](spec/README.md) | Spec — 模式稳定契约(API 面、诊断 ID、不变量) | -| [design/](design/README.md) | Design Doc — 实现细节、设计权衡、已知局限 | - -> 所有模式文档已从 `docs/<PatternName>.md` 拆分迁移至 `spec/` + `design/`,见 [spec/README.md](spec/README.md)。 +| [adr/](adr/README.md) | ADR — 架构决策记录 | +| [design/](design/README.md) | Design Doc — 每域 API / 诊断 / 实现 | ## 模式索引 -| 模式 | Spec | Design Doc | -|------|------|------------| -| Strategy | [spec/Strategy.md](spec/Strategy.md) | [design/Strategy.md](design/Strategy.md) | -| Chain of Responsibility | [spec/ChainOfResponsibility.md](spec/ChainOfResponsibility.md) | [design/ChainOfResponsibility.md](design/ChainOfResponsibility.md) | -| Composite | [spec/Composite.md](spec/Composite.md) | [design/Composite.md](design/Composite.md) | -| Factory Registry | [spec/FactoryRegistry.md](spec/FactoryRegistry.md) | [design/FactoryRegistry.md](design/FactoryRegistry.md) | -| Decorator | [spec/Decorator.md](spec/Decorator.md) | [design/Decorator.md](design/Decorator.md) | -| Event Aggregator | [spec/EventAggregator.md](spec/EventAggregator.md) | [design/EventAggregator.md](design/EventAggregator.md) | -| State Transition Table | [spec/StateTransitionTable.md](spec/StateTransitionTable.md) | [design/StateTransitionTable.md](design/StateTransitionTable.md) | +| 模式 | Design Doc | +|------|------------| +| Strategy | [design/Strategy.md](design/Strategy.md) | +| Chain of Responsibility | [design/ChainOfResponsibility.md](design/ChainOfResponsibility.md) | +| Composite | [design/Composite.md](design/Composite.md) | +| Factory Registry | [design/FactoryRegistry.md](design/FactoryRegistry.md) | +| Decorator | [design/Decorator.md](design/Decorator.md) | +| Event Aggregator | [design/EventAggregator.md](design/EventAggregator.md) | +| State Transition Table | [design/StateTransitionTable.md](design/StateTransitionTable.md) | ## 横切约定 @@ -75,4 +59,4 @@ | 维护者 / 深度 API | 本目录 `docs/` | 中文设计说明 | | 跨工具 AI 约束 | 根目录 `AGENTS.md` | 中文 | -修改诊断 ID、CodeFix 或公共 API 时,同步更新:`DiagnosticIds.cs`、`DesignPatterns.Docs` 的 [diagnostics](https://github.com/Skymly/DesignPatterns.Docs/blob/main/docs/diagnostics.md) 页,以及(若影响 key 约定)本目录 [FactoryKeyConventions.md](FactoryKeyConventions.md)。 +修改诊断 ID、CodeFix 或公共 API 时,同步更新:`DiagnosticIds.cs`、对应 Design Doc、`DesignPatterns.Docs` 的 [diagnostics](https://github.com/Skymly/DesignPatterns.Docs/blob/main/docs/diagnostics.md) 页,以及(若影响 key 约定)本目录 [FactoryKeyConventions.md](FactoryKeyConventions.md)。 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ef634ab..10fe97e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -20,7 +20,7 @@ | Event Aggregator | `{Event}EventHandlerRegistry` | | State | `{StateEnum}TransitionTable`、partial `{Holder}` 便捷方法 | -新增生成器必须沿用此命名风格(详见 [Decorator.md](design/Decorator.md))。诊断 ID 续接现有区段,下一个可用 ID 为 **DP067**,DP067–DP071 已由 [Singleton 生命周期诊断 RFC](rfc/SingletonLifecycleDiagnostics.md) 预留(DP066 为 Singleton 工厂委托 captive dependency,DP063–DP065 为 Composite 树 schema 校验,DP060–DP062 为 DI 生命周期校验,DP056–DP059 为 State hierarchy,DP053–DP055 为 Factory async + pooling 签名/池化校验,DP050–DP052 为 Handler guard 签名校验,DP047–DP049 为 Strategy guard 签名校验,DP044–DP046 为 EventAggregator 源生成器 + Analyzer 诊断,DP042–DP043 为 Decorator DI + async 签名校验,DP040–DP041 为 Composite DI + visitor 覆盖校验,DP037–DP039 为 State entry/exit action 诊断,DP032–DP035 为 State guard 诊断,DP036 为 State 字面量边校验;ID 一经发布不复用,详见 [AGENTS.md](../AGENTS.md))。 +新增生成器必须沿用此命名风格(详见 [Decorator.md](design/Decorator.md))。诊断 ID 续接现有区段,下一个可用 ID 为 **DP067**,DP067–DP071 已由 [ADR-008](adr/ADR-008-singleton-lifecycle-diagnostics.md) 预留(DP066 为 Singleton 工厂委托 captive dependency,DP063–DP065 为 Composite 树 schema 校验,DP060–DP062 为 DI 生命周期校验,DP056–DP059 为 State hierarchy,DP053–DP055 为 Factory async + pooling 签名/池化校验,DP050–DP052 为 Handler guard 签名校验,DP047–DP049 为 Strategy guard 签名校验,DP044–DP046 为 EventAggregator 源生成器 + Analyzer 诊断,DP042–DP043 为 Decorator DI + async 签名校验,DP040–DP041 为 Composite DI + visitor 覆盖校验,DP037–DP039 为 State entry/exit action 诊断,DP032–DP035 为 State guard 诊断,DP036 为 State 字面量边校验;ID 一经发布不复用,详见 [AGENTS.md](../AGENTS.md))。 诊断 ID 预分配(F2+ 增强项,登记后不提前占用,实现时按序领取): @@ -99,20 +99,20 @@ 候选池(仅登记,未排期):Observer / 轻量 pub-sub 扩展、Builder 生成器、ObjectPool(探索源生成池化策略)、Resilience primitive(探索编译期策略组合)、Command 路由(探索与 MediatR 的差异点)。**范围包含 GoF 但不局限于 GoF**:并发模式、反应式模式、函数式模式等候选同样欢迎。**未通过准入前不实现。** -长期探索候选(高潜力、高复杂度,需 RFC 评审): +长期探索候选(高潜力、高复杂度,需 Issue + ADR 评审): | 候选 | 说明 | 探索价值 | |------|------|----------| | ~~State 层级状态机~~ | ~~`[StateMachine(..., Hierarchical = true)]` 支持嵌套状态 + 通配转换,生成器展平为快表~~ — **已在 v3 实现**(v3.1 运行时 `IStateHierarchy`、v3.2 `[StateParent]` + 展平 + DP056–DP059、v3.3 LCA + action 链合成、v3.4 DI + 示例 + 文档) | ⭐⭐⭐ 与 Stateless 重叠但展示「编译期展平层级」技术 | | ~~Composite 并行遍历~~ | ~~`TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` + `MaxDegreeOfParallelism` + `MaxParallelDepth`~~ — **Phase 1 已实现**(BFS 同层并行 / DFS 子节点并行递归 + 深度回退 / `AggregateException` / `ConfigureAwait(false)` / `#if` TFM 分裂) | ⭐⭐ 大树场景实用;AOT 友好并行调度 | -| Composite 懒加载 | `[CompositePart(..., LazyChildren = true)]` + `AssembleAsync` + `ICompositeLazyNode` | ⭐⭐ 大树按需展开;Phase 3 独立 RFC | +| Composite 懒加载 | `[CompositePart(..., LazyChildren = true)]` + `AssembleAsync` + `ICompositeLazyNode` | ⭐⭐ 大树按需展开;独立 Issue + ADR | | ~~Composite 树 schema 校验~~ | ~~编译期校验 max depth / parent-child 类型兼容性 / 节点计数~~ — **已实现**(`[CompositeSchema(MaxDepth, MaxNodes)]` 契约级约束 + `[CompositePart(AllowedChildTypes)]` 节点级约束 + DP063–DP065) | ⭐⭐ 结构错误编译期捕获 | | Decorator 组合 / 嵌套 | `DecoratorStackBuilder.Compose(otherStack)`,Analyzer 校验栈间类型兼容 | ⭐⭐ 可复用装饰器组合 | | MSDI keyed services(.NET 8+) | 生成 keyed registration 代码(`#if NET8_0_OR_GREATER`),与 Autofac keyed 对称 | ⭐⭐ 补齐 MSDI keyed 缺口 | | DI 健康检查 + 生命周期校验 | 生成 `IHealthCheck` 校验注册表键可解析;Analyzer 警告无效 lifetime 组合(Singleton registry + Transient impl) | ⭐⭐ 生产场景刚需 | -| Singleton 生命周期诊断 | Analyzer 检测 singleton 捕获 scoped/transient 引用;async 初始化支持 | ⭐⭐ 防 DI 反模式 | [~] [RFC](rfc/SingletonLifecycleDiagnostics.md) / [Plan](plans/SingletonLifecycleDiagnostics.md) | +| Singleton 生命周期诊断 | Analyzer 检测 singleton 捕获 scoped/transient 引用;async 初始化支持。[~] P1 已完成(DP066);P2/P3 见 [ADR-008](adr/ADR-008-singleton-lifecycle-diagnostics.md) | ⭐⭐ 防 DI 反模式 | -State 转换表 v1 已于 0.1.0-preview4 发布;v2(guard 委托、DI 集成、DP036 字面量边校验)已实现,见 [StateTransitionTable.md](design/StateTransitionTable.md) 与 [rfc/StateTransitionTable.md](rfc/StateTransitionTable.md)。EventAggregator 联动示例仍为候选。 +State 转换表 v1 已于 0.1.0-preview4 发布;v2(guard 委托、DI 集成、DP036 字面量边校验)已实现,见 [StateTransitionTable.md](design/StateTransitionTable.md)。EventAggregator 联动示例仍为候选。 --- @@ -205,6 +205,7 @@ State 转换表 v1 已于 0.1.0-preview4 发布;v2(guard 委托、DI 集成 - F4+ Singleton captive dependency 诊断:`CaptiveDependencyAnalyzer`(DP062 Warning)— 编译期扫描 MSDI 注册调用构建 Type→Lifetime 映射,检查 Singleton 实现构造函数参数是否依赖 Scoped/Transient 服务。 - F4+ DP062 Phase 2:扩展覆盖生成器 `RegisterDi` 调用 — 从 `[RegisterStrategy]`/`[RegisterFactory]`/`[RegisterEventHandler]`/`[Decorator]`/`[CompositePart]` 特性提取实现类型,应用 `implementationLifetime`。 - F4+ DP062 过度注册修复:按模式分组特性标注类型(`RegistrationCategory`),`RegisterDi` containing type 名匹配正确类别,避免跨模式 lifetime 污染。 -- F5 Composite 并行遍历 Phase 1:`TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` + `MaxDegreeOfParallelism` + `MaxParallelDepth` + `AggregateException` + `ConfigureAwait(false)` + `#if` TFM 分裂。设计 RFC:[docs/rfc/CompositeParallelTraversal.md](rfc/CompositeParallelTraversal.md)。 +- F5 Composite 并行遍历 Phase 1:`TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` + `MaxDegreeOfParallelism` + `MaxParallelDepth` + `AggregateException` + `ConfigureAwait(false)` + `#if` TFM 分裂。见 [ADR-006](adr/ADR-006-composite-parallel-traversal.md)。 - 发版 `0.2.2`:含 DP062 Singleton captive dependency 诊断(tag `v0.2.2`,稳定版)。 -- F5+ Composite 树 schema 校验:`[CompositeSchema(MaxDepth, MaxNodes)]` 契约级约束 + `[CompositePart(AllowedChildTypes)]` 节点级约束 + DP063(max depth exceeded Warning)/ DP064(child type not allowed Error)/ DP065(node count exceeded Warning)。设计 RFC:[docs/rfc/CompositeTreeSchemaValidation.md](rfc/CompositeTreeSchemaValidation.md)。 +- F5+ Composite 树 schema 校验:`[CompositeSchema(MaxDepth, MaxNodes)]` 契约级约束 + `[CompositePart(AllowedChildTypes)]` 节点级约束 + DP063(max depth exceeded Warning)/ DP064(child type not allowed Error)/ DP065(node count exceeded Warning)。见 [ADR-007](adr/ADR-007-composite-tree-schema-validation.md)。 +- 文档体系精简:移除 RFC / Spec / Plan / Review 仓内类型;Spec 合入 Design Doc;任务与审查改用 GitHub Issue / PR。见 [DOCUMENTATION.md](DOCUMENTATION.md)。 diff --git a/docs/adr/ADR-001-primitives-over-frameworks.md b/docs/adr/ADR-001-primitives-over-frameworks.md index bfe556b..4569f5b 100644 --- a/docs/adr/ADR-001-primitives-over-frameworks.md +++ b/docs/adr/ADR-001-primitives-over-frameworks.md @@ -4,7 +4,7 @@ |------|-----| | **状态** | Accepted | | **日期** | 2026-06-14 | -| **关联 RFC** | 无 — 项目创立时的核心设计原则 | +| **关联 Issue** | —(项目创立时直接决策) | ## 背景 diff --git a/docs/adr/ADR-002-roslyn-incremental-source-generators.md b/docs/adr/ADR-002-roslyn-incremental-source-generators.md index e9b073a..825ac5b 100644 --- a/docs/adr/ADR-002-roslyn-incremental-source-generators.md +++ b/docs/adr/ADR-002-roslyn-incremental-source-generators.md @@ -4,7 +4,7 @@ |------|-----| | **状态** | Accepted | | **日期** | 2026-06-14 | -| **关联 RFC** | 无 — 项目创立时的技术选型 | +| **关联 Issue** | —(项目创立时直接决策) | ## 背景 diff --git a/docs/adr/ADR-003-dual-tfm-netstandard20-net80.md b/docs/adr/ADR-003-dual-tfm-netstandard20-net80.md index a995668..621be58 100644 --- a/docs/adr/ADR-003-dual-tfm-netstandard20-net80.md +++ b/docs/adr/ADR-003-dual-tfm-netstandard20-net80.md @@ -4,7 +4,7 @@ |------|-----| | **状态** | Accepted | | **日期** | 2026-06-14 | -| **关联 RFC** | 无 — 项目创立时的兼容性决策 | +| **关联 Issue** | —(项目创立时直接决策) | ## 背景 diff --git a/docs/adr/ADR-004-core-does-not-reference-msdi.md b/docs/adr/ADR-004-core-does-not-reference-msdi.md index 4ad677e..e1eaad4 100644 --- a/docs/adr/ADR-004-core-does-not-reference-msdi.md +++ b/docs/adr/ADR-004-core-does-not-reference-msdi.md @@ -4,7 +4,7 @@ |------|-----| | **状态** | Accepted | | **日期** | 2026-06-14 | -| **关联 RFC** | 无 — 项目创立时的架构边界决策 | +| **关联 Issue** | —(项目创立时直接决策) | ## 背景 diff --git a/docs/adr/ADR-005-state-transition-table.md b/docs/adr/ADR-005-state-transition-table.md index 1cff716..6dd711c 100644 --- a/docs/adr/ADR-005-state-transition-table.md +++ b/docs/adr/ADR-005-state-transition-table.md @@ -4,7 +4,7 @@ |------|-----| | **状态** | Accepted | | **日期** | 2026-06-14 | -| **关联 RFC** | [docs/rfc/archive/StateTransitionTable.md](../rfc/archive/StateTransitionTable.md)、[docs/rfc/archive/HierarchicalStateMachine.md](../rfc/archive/HierarchicalStateMachine.md) | +| **关联 Issue** | —(历史 RFC 已随文档体系精简移除) | ## 背景 @@ -34,5 +34,4 @@ DesignPatterns 缺少有限状态机 primitive。现有库(Stateless、Akka.NE ## 参考 -- [docs/spec/StateTransitionTable.md](../spec/StateTransitionTable.md) - [docs/design/StateTransitionTable.md](../design/StateTransitionTable.md) diff --git a/docs/adr/ADR-006-composite-parallel-traversal.md b/docs/adr/ADR-006-composite-parallel-traversal.md index e18689c..ba315e5 100644 --- a/docs/adr/ADR-006-composite-parallel-traversal.md +++ b/docs/adr/ADR-006-composite-parallel-traversal.md @@ -4,7 +4,7 @@ |------|-----| | **状态** | Accepted | | **日期** | 2026-06-30 | -| **关联 RFC** | [docs/rfc/archive/CompositeParallelTraversal.md](../rfc/archive/CompositeParallelTraversal.md) | +| **关联 Issue** | —(历史 RFC 已随文档体系精简移除) | ## 背景 @@ -34,5 +34,4 @@ Composite 模式已有串行遍历(`Traverse` / `TraverseAsync`),但大树 ## 参考 -- [docs/spec/Composite.md](../spec/Composite.md) - [docs/design/Composite.md](../design/Composite.md) diff --git a/docs/adr/ADR-007-composite-tree-schema-validation.md b/docs/adr/ADR-007-composite-tree-schema-validation.md index 00de761..29797ca 100644 --- a/docs/adr/ADR-007-composite-tree-schema-validation.md +++ b/docs/adr/ADR-007-composite-tree-schema-validation.md @@ -4,7 +4,7 @@ |------|-----| | **状态** | Accepted | | **日期** | 2026-07-07 | -| **关联 RFC** | [docs/rfc/archive/CompositeTreeSchemaValidation.md](../rfc/archive/CompositeTreeSchemaValidation.md) | +| **关联 Issue** | —(历史 RFC 已随文档体系精简移除) | ## 背景 @@ -34,5 +34,4 @@ ## 参考 -- [docs/spec/Composite.md](../spec/Composite.md) - [docs/design/Composite.md](../design/Composite.md) diff --git a/docs/adr/ADR-008-singleton-lifecycle-diagnostics.md b/docs/adr/ADR-008-singleton-lifecycle-diagnostics.md index 72f4c3b..8f02af0 100644 --- a/docs/adr/ADR-008-singleton-lifecycle-diagnostics.md +++ b/docs/adr/ADR-008-singleton-lifecycle-diagnostics.md @@ -4,11 +4,11 @@ |------|-----| | **状态** | Accepted | | **日期** | 2026-07-08 | -| **关联 RFC** | [docs/rfc/SingletonLifecycleDiagnostics.md](../rfc/SingletonLifecycleDiagnostics.md) | +| **关联 Issue** | —(历史 RFC / Plan / Review 已随文档体系精简移除;决策以本文为准) | ## 背景 -DP062 已覆盖 MSDI 构造函数注入的 Singleton captive dependency,但 Autofac 注册与 `AddSingleton` 工厂委托不在检测范围内;`[GenerateSingleton]` 缺少 async 初始化路径,与 DI 容器单例混用时无警告;DEVELOPMENT.md 声明的「静态可变单例提示」尚无对应 Analyzer。设计评审([2026-07-08](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md))确认了三阶段扩展方案并修正两处初稿设计错误。 +DP062 已覆盖 MSDI 构造函数注入的 Singleton captive dependency,但 Autofac 注册与 `AddSingleton` 工厂委托不在检测范围内;`[GenerateSingleton]` 缺少 async 初始化路径,与 DI 容器单例混用时无警告;DEVELOPMENT.md 声明的「静态可变单例提示」尚无对应 Analyzer。设计评审确认了三阶段扩展方案并修正两处初稿设计错误(async 不可用属性 getter、不存在的 MSDI 字段注入)。 ## 决策 @@ -38,5 +38,4 @@ DP062 已覆盖 MSDI 构造函数注入的 Singleton captive dependency,但 Au - [ADR-001 Primitives over frameworks](ADR-001-primitives-over-frameworks.md) - [ADR-004 Core does not reference MSDI](ADR-004-core-does-not-reference-msdi.md) -- [设计评审记录](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md) -- [实现计划](../plans/SingletonLifecycleDiagnostics.md) +- [docs/ROADMAP.md](../ROADMAP.md)(Singleton 生命周期诊断条目) diff --git a/docs/adr/README.md b/docs/adr/README.md index a7af4ee..ab2f1a3 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -1,23 +1,23 @@ # ADR 索引 -架构决策记录(Architecture Decision Record)。ADR 是不可变卡片,记录最终决策;讨论在 RFC 中完成。 +架构决策记录(Architecture Decision Record)。ADR 是不可变卡片,记录最终决策;讨论与任务跟踪在 GitHub Issue / PR 中完成。 -- **格式与生命周期**:见 [DOCUMENTATION.md](../DOCUMENTATION.md#3-adr--architecture-decision-record) +- **格式与规则**:见 [DOCUMENTATION.md](../DOCUMENTATION.md#2-adr) - **模板**:[_template.md](_template.md) - **编号规则**:从 ADR-001 开始,零填充三位,**不复用编号** ## 决策列表 -| ADR | 标题 | 状态 | 日期 | 关联 RFC | -|-----|------|------|------|----------| +| ADR | 标题 | 状态 | 日期 | 关联 Issue | +|-----|------|------|------|------------| | [ADR-001](ADR-001-primitives-over-frameworks.md) | Primitives over frameworks | Accepted | 2026-06-14 | — | | [ADR-002](ADR-002-roslyn-incremental-source-generators.md) | Roslyn incremental source generators | Accepted | 2026-06-14 | — | | [ADR-003](ADR-003-dual-tfm-netstandard20-net80.md) | Dual TFM — netstandard2.0 + net8.0 | Accepted | 2026-06-14 | — | | [ADR-004](ADR-004-core-does-not-reference-msdi.md) | Core does not reference MSDI | Accepted | 2026-06-14 | — | -| [ADR-005](ADR-005-state-transition-table.md) | State transition table | Accepted | 2026-06-14 | StateTransitionTable, HierarchicalStateMachine | -| [ADR-006](ADR-006-composite-parallel-traversal.md) | Composite parallel traversal | Accepted | 2026-06-30 | CompositeParallelTraversal | -| [ADR-007](ADR-007-composite-tree-schema-validation.md) | Composite tree schema validation | Accepted | 2026-07-07 | CompositeTreeSchemaValidation | -| [ADR-008](ADR-008-singleton-lifecycle-diagnostics.md) | Singleton lifecycle diagnostics | Accepted | 2026-07-08 | SingletonLifecycleDiagnostics | +| [ADR-005](ADR-005-state-transition-table.md) | State transition table | Accepted | 2026-06-14 | — | +| [ADR-006](ADR-006-composite-parallel-traversal.md) | Composite parallel traversal | Accepted | 2026-06-30 | — | +| [ADR-007](ADR-007-composite-tree-schema-validation.md) | Composite tree schema validation | Accepted | 2026-07-07 | — | +| [ADR-008](ADR-008-singleton-lifecycle-diagnostics.md) | Singleton lifecycle diagnostics | Accepted | 2026-07-08 | — | ## 下一个可用编号 diff --git a/docs/adr/_template.md b/docs/adr/_template.md index 60b4331..fc3e75e 100644 --- a/docs/adr/_template.md +++ b/docs/adr/_template.md @@ -4,7 +4,7 @@ |------|-----| | **状态** | Accepted | | **日期** | YYYY-MM-DD | -| **关联 RFC** | [docs/rfc/XXX.md](../rfc/XXX.md)(或「无 — 直接决策」) | +| **关联 Issue** | #XXX(或「无 — 直接决策」) | ## 背景 @@ -20,4 +20,4 @@ ## 参考 -- 相关 ADR、文档、外部链接 +- 相关 ADR、Design Doc、外部链接 diff --git a/docs/design/ChainOfResponsibility.md b/docs/design/ChainOfResponsibility.md index 424e94f..75d04bc 100644 --- a/docs/design/ChainOfResponsibility.md +++ b/docs/design/ChainOfResponsibility.md @@ -1,7 +1,6 @@ # Design Doc: Chain of Responsibility -> **关联 Spec**:[docs/spec/ChainOfResponsibility.md](../spec/ChainOfResponsibility.md) -> **关联 RFC**:[docs/rfc/XXX.md](../rfc/XXX.md)(如有) +> **版本**:v0.2.2(与 NuGet 包版本对齐) > **关联 ADR**:ADR-XXX(如有) ## 概述 @@ -15,6 +14,93 @@ 3. **异步一等**:`ValueTask` + `CancellationToken` 4. 不依赖 DI;handler 由调用方手动组装 +## API 面 + +### 运行时接口 + +运行时类型位于 `DesignPatterns/Behavioral/`。 + +| 类型 | 说明 | +|------|------| +| `HandlerDelegate<TContext>` | 调用链中下一阶段的委托 | +| `IHandler<TContext>` | 单个 handler:`InvokeAsync(context, next, ct)` | +| `HandlerPipelineBuilder<TContext>` | `Use(handler)` / `Use(delegate)`,`Build()` | +| `HandlerPipeline<TContext>` | 不可变管道,`InvokeAsync(context, ct)`、`InvokeTracedAsync`(见下文) | +| `HandlerPipelineTrace` | 一次 traced 调用的逐步结果 | +| `HandlerPipelineStep` | 单步:索引、handler 显示名、状态 | +| `HandlerPipelineStepStatus` | `Completed` / `ShortCircuited` / `NotReached` | + +`HandlerPipelineStepStatus` 取值含义: + +| `HandlerPipelineStepStatus` | 含义 | +|----------------------------|------| +| **Completed** | Handler 已执行且调用了 `next` | +| **ShortCircuited** | Handler 已执行但**未**调用 `next`(管道在此 handler 之后不再向下) | +| **NotReached** | Handler **未执行**(因更早的 handler 短路) | + +`InvokeTracedAsync` 不改变执行顺序与短路语义,仅额外返回 `HandlerPipelineTrace`;委托 handler 在 trace 中显示名为 `"<delegate>"`。 + +`HandlerPipelineTrace.WasShortCircuited` 为「是否存在任一步为 `ShortCircuited`」;终端 handler 不调用 `next` 时也会为 `true`,不宜单独作为「业务短路」判据。 + +### 特性(Attribute) + +```csharp +// 泛型(推荐,C# 11+ / net8.0) +[HandlerOrder<RequestContext>(10)] +public sealed class LoggingHandler : IHandler<RequestContext> { } + +// 非泛型(netstandard2.0) +[HandlerOrder(10, typeof(RequestContext))] +public sealed class LoggingHandler : IHandler<RequestContext> { } +``` + +数值越小越先执行(与手动 `Use` 注册顺序一致)。 + +同一 handler 类可标注多个 `[HandlerOrder<...>]`(`AllowMultiple = true`),分别加入不同 `TContext` 的生成管道,例如共享日志 handler 同时实现 `IHandler<RequestContext>` 与 `IHandler<AuditContext>`。同一 context 下重复的 Order 仍报 **DP005**。 + +### 生成器产出 + +对每种 `TContext` 生成 `{Context}HandlerPipeline`: + +```csharp +public static partial class RequestContextHandlerPipeline +{ + public static HandlerPipeline<RequestContext> Instance { get; } +} +``` + +引用 `DesignPatterns.Extensions.DependencyInjection` 后,`{Context}HandlerPipeline` 额外生成: + +```csharp +RequestContextHandlerPipeline.RegisterDi(services); +var pipeline = provider.GetRequiredService<HandlerPipeline<RequestContext>>(); +``` + +`Create(IServiceProvider sp)` 按 `[HandlerOrder]` 顺序 `GetRequiredService` 各 handler。Singleton 管道在首次构建时固定 handler 实例;需要每次解析新管道时使用 `registryLifetime: ServiceLifetime.Transient`。 + +手动注册:`services.AddHandlerPipeline<TContext>(builder => ...)`(扩展包)。 + +## 诊断 + +| ID | 级别 | 触发条件 | 消息格式 | +|----|------|----------|----------| +| DP005 | Error | 同一 context 下重复的 Order | ... | +| DP008 | Error | 标注 `[HandlerOrder]` 但未实现 `IHandler<TContext>` | ... | +| DP009 | Error | 标注 `[HandlerOrder]` 的 handler 缺少 public 无参构造 | ... | +| DP024 | Info | Analyzer:实现了 `IHandler<TContext>` 但未加 `[HandlerOrder]`(该 context 已有其它 handler 注册) | ... | +| DP024 | Info | CodeFix:一键添加 `[HandlerOrder(order, typeof(TContext))]` | ... | + +## 不变量 / 兼容基线 + +1. Handler **调用** `await next(context, ct)` → 继续后续 handler;Handler **不调用** `next` → 后续 handler 不再执行(inbound 与 outbound 均跳过)——即**通过是否调用 `next` 实现短路**,不强制业务 context 实现额外接口。 +2. 空管道:`InvokeAsync` / `InvokeTracedAsync` 正常完成,trace 为空。 +3. `Use(null)` 抛 `ArgumentNullException`。 + +### 兼容基线 + +- netstandard2.0 + net8.0(两者均须可用并随包分发) +- Roslyn 组件 4.8.0(`Microsoft.CodeAnalysis.CSharp` / Workspaces;Analyzers 3.3.4) + ## 实现概览 ### 运行时 @@ -51,7 +137,7 @@ if (trace.Steps.Any(s => s.Status == HandlerPipelineStepStatus.NotReached)) **`{Context}HandlerPipeline` 生成**:对每种 `TContext` 生成 `public static partial class {Context}HandlerPipeline`,暴露 `Instance`(`HandlerPipeline<TContext>`);引用 DI 扩展包后额外生成 `RegisterDi(services)` 与 `Create(IServiceProvider sp)`。 -### 诊断 +### 诊断检测细节 | ID | 说明 | |----|------| diff --git a/docs/design/Composite.md b/docs/design/Composite.md index 4781c78..85c39c2 100644 --- a/docs/design/Composite.md +++ b/docs/design/Composite.md @@ -1,6 +1,6 @@ # Design Doc: Composite -> **关联 Spec**:[docs/spec/Composite.md](../spec/Composite.md) +> **版本**:v0.2.2(与 NuGet 包版本对齐) > **关联 ADR**:[ADR-006](../adr/ADR-006-composite-parallel-traversal.md)、[ADR-007](../adr/ADR-007-composite-tree-schema-validation.md) ## 概述 @@ -14,6 +14,140 @@ Composite 模式将对象组织成树形结构,以统一方式对待单个对 3. **Catalog 装配**:`[CompositePart]` 生成 flat catalog,`BuildRoot()` 组装单根树,`BuildForest()` 组装多根森林 4. 不依赖 DI;树由编译期定义、运行时一次性装配 +## API 面 + +### 运行时接口 + +命名空间:`DesignPatterns.Structural` + +| 类型 | 签名 | 说明 | +|------|------|------| +| `ICompositeNode<TSelf>` | `IReadOnlyList<TSelf> Children { get; }` where `TSelf : ICompositeNode<TSelf>` | 节点契约:通过 `Children` 区分叶子与分支,空列表为叶子 | +| `ICompositeBuildable<TNode>` | `void SetChildren(IReadOnlyList<TNode> children)` where `TNode : ICompositeNode<TNode>` | 装配时接收子节点;实现应在装配后冻结 children,后续调用视为无效 | +| `CompositeTraverser` | `static void Traverse<TNode>(TNode root, Action<TNode, int, int> visitor, CompositeTraversalOptions<TNode>? options = null)` | 同步遍历单根树(visitor: node, depth, siblingIndex) | +| | `static void TraverseForest<TNode>(IReadOnlyList<TNode> roots, Action<TNode, int, int> visitor, CompositeTraversalOptions<TNode>? options = null)` | 同步遍历森林(多根树) | +| | `static ValueTask TraverseAsync<TNode>(TNode root, Func<TNode, int, int, CancellationToken, ValueTask> visitor, CompositeTraversalOptions<TNode>? options = null, CancellationToken cancellationToken = default)` | 异步遍历单根树 | +| | `static ValueTask TraverseForestAsync<TNode>(IReadOnlyList<TNode> roots, Func<TNode, int, int, CancellationToken, ValueTask> visitor, CompositeTraversalOptions<TNode>? options = null, CancellationToken cancellationToken = default)` | 异步遍历森林 | +| | `static void TraverseParallel<TNode>(TNode root, Action<TNode, int, int> visitor, CompositeTraversalOptions<TNode>? options = null)` | 并行遍历单根树(访问顺序非确定) | +| | `static void TraverseForestParallel<TNode>(IReadOnlyList<TNode> roots, Action<TNode, int, int> visitor, CompositeTraversalOptions<TNode>? options = null)` | 并行遍历森林(根间串行,子树内并行) | +| | `static ValueTask TraverseParallelAsync<TNode>(TNode root, Func<TNode, int, int, CancellationToken, ValueTask> visitor, CompositeTraversalOptions<TNode>? options = null, CancellationToken cancellationToken = default)` | 异步并行遍历单根树 | +| | `static ValueTask TraverseForestParallelAsync<TNode>(IReadOnlyList<TNode> roots, Func<TNode, int, int, CancellationToken, ValueTask> visitor, CompositeTraversalOptions<TNode>? options = null, CancellationToken cancellationToken = default)` | 异步并行遍历森林 | +| `CompositeTraversalOptions<TNode>` | `CompositeTraversalOrder Order { get; set; }`(默认 `DepthFirstPreOrder`) | 遍历顺序 | +| | `int? MaxDepth { get; set; }`(`null` = 无限制) | 最大访问深度(0 = 仅根) | +| | `bool VisitLeavesOnly { get; set; }` | 仅访问叶子节点 | +| | `Func<TNode, bool>? ShouldSkipSubtree { get; set; }` | 返回 `true` 时跳过该子树 | +| | `int? MaxDegreeOfParallelism { get; set; }`(`null` = `Environment.ProcessorCount`) | 并行度上限 | +| | `int MaxParallelDepth { get; set; }`(默认 32) | 并行递归深度上限,超过后回退串行 | +| `CompositeTreeBuilder<TNode>` | `CompositeTreeBuilder<TNode> Leaf(TNode node)` | 添加叶子节点 | +| | `CompositeTreeBuilder<TNode> Branch(TNode node, Action<CompositeTreeBuilder<TNode>> configure)` | 添加分支节点(嵌套 builder 配置子节点) | +| | `TNode Build()` | 构建单根树(要求恰好一个顶层节点) | +| `CompositeCatalogEntry<TNode>` | `CompositeCatalogEntry(string key, string? parentKey, int order, Type implementationType)` | Catalog 条目 | +| | `string Key { get; }` | 唯一键 | +| | `string? ParentKey { get; }` | 父键(`null` 为根候选) | +| | `int Order { get; }` | 同父兄弟间排序(升序) | +| | `Type ImplementationType { get; }` | 实现类型 | +| `CompositeCatalogAssembler` | `static TNode Assemble<TNode>(IReadOnlyList<CompositeCatalogEntry<TNode>> entries)` where `TNode : class, ICompositeNode<TNode>` | 从 catalog 装配单根树(`Activator.CreateInstance`) | +| | `static TNode Assemble<TNode>(IReadOnlyList<CompositeCatalogEntry<TNode>> entries, IServiceProvider serviceProvider)` | 从 catalog 装配单根树(DI 解析节点) | +| | `static IReadOnlyList<TNode> AssembleForest<TNode>(IReadOnlyList<CompositeCatalogEntry<TNode>> entries)` | 从 catalog 装配多根森林(`Activator.CreateInstance`) | +| | `static IReadOnlyList<TNode> AssembleForest<TNode>(IReadOnlyList<CompositeCatalogEntry<TNode>> entries, IServiceProvider serviceProvider)` | 从 catalog 装配多根森林(DI 解析节点) | + +`CompositeTraversalOrder` 枚举值:`DepthFirstPreOrder`、`DepthFirstPostOrder`、`BreadthFirst`。 + +### 特性(Attribute) + +命名空间:`DesignPatterns.Structural` + +#### `CompositePartAttribute`(非泛型,netstandard2.0) + +```csharp +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class CompositePartAttribute : Attribute +{ + public CompositePartAttribute(string key, Type @for); + + public string Key { get; } + public Type For { get; } + public string? ParentKey { get; set; } // null = 根候选 + public int Order { get; set; } // 同父兄弟间排序,升序 + public Type[]? AllowedChildTypes { get; set; } // 允许的子节点实现类型;null = 不限制 +} +``` + +#### `CompositePartAttribute<TContract>`(泛型,C# 11+ / net8.0) + +```csharp +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class CompositePartAttribute<TContract> : Attribute +{ + public CompositePartAttribute(string key); + + public string Key { get; } + public string? ParentKey { get; set; } + public int Order { get; set; } + public Type[]? AllowedChildTypes { get; set; } +} +``` + +#### `CompositeSchemaAttribute`(契约级约束) + +```csharp +[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class CompositeSchemaAttribute : Attribute +{ + public int MaxDepth { get; set; } // 最大树深度(root = 1);0 = 无限制;默认 0 + public int MaxNodes { get; set; } // 最大节点总数(所有根合计);0 = 无限制;默认 0 +} +``` + +标注于 composite 契约接口或基类,启用编译期树结构 schema 校验。未标注时行为不变。 + +### 生成器产出 + +源生成器 `CompositePartGenerator` 对每个 composite contract 生成以下类型(`IPaymentStrategy` → `PaymentStrategyCompositeKeys`;`IMenuNode` → `MenuNodeCompositeKeys`): + +| 生成类型 | 说明 | +|----------|------| +| `{Contract}CompositeKeys` | key 常量(`public const string`) | +| `{Contract}CompositeCatalog` | entry 列表 + `BuildRoot()` + `BuildForest()` 方法 | + +`{Contract}CompositeCatalog` 方法: + +| 方法 | 说明 | +|------|------| +| `BuildRoot()` | 从 catalog 装配单根树;要求 catalog 中**恰好一个** `ParentKey == null` 的根,否则运行时抛 `CompositeAssemblyException` | +| `BuildForest()` | 从 catalog 装配多根森林;一个或多个 `ParentKey == null`,按 `Order` 再 key 排序 | + +## 诊断 + +| ID | 级别 | 触发条件 | 消息格式 | +|----|------|----------|----------| +| DP010 | Error | 同一 contract 下 key 重复 | `Composite key '{0}' is already registered for contract '{1}'. Use a unique key or remove the duplicate [CompositePart] attribute.` | +| DP011 | Error | `ParentKey` 引用的 key 不存在 | `Composite parent key '{0}' was not found for contract '{1}'. Register the parent part first or correct the ParentKey value.` | +| DP012 | Error | parent 链形成环 | `Composite key '{0}' participates in a parent-key cycle for contract '{1}'. Remove or reassign ParentKey values to break the cycle.` | +| DP013 | Error | 标注类型未实现 composite contract | `Type '{0}' does not implement composite contract '{1}'. Implement the contract or fix the [CompositePart] contract argument.` | +| DP014 | Error | 缺少 public 无参构造 | `Type '{0}' must declare a public parameterless constructor for generated composite catalogs.` | +| DP015 | Error | 未实现 `ICompositeBuildable<TContract>` | `Type '{0}' must implement ICompositeBuildable<{1}> to be used with generated composite catalogs.` | +| DP040 | Error | `BuildRoot(IServiceProvider)` 时节点类型未注册到 DI 容器 | `Composite node type '{0}' was not registered in the service collection. Call {1}.RegisterDi(services) before BuildRoot(serviceProvider), or register the type manually.` | +| DP041 | Info | Visitor 覆盖不全(保留 ID — C# 编译器通过接口实现 CS0535 自动强制覆盖,诊断不实际触发) | `Visitor '{0}' does not implement all Visit methods of '{1}'. The C# compiler enforces full coverage via interface implementation (CS0535); this diagnostic is reserved for future use.` | +| DP063 | Warning | `[CompositeSchema(MaxDepth)]` 约束被超过 | `Composite tree for contract '{0}' has depth {1}, exceeding the maximum depth of {2} declared by [CompositeSchema]. Consider flattening the tree structure or increasing MaxDepth.` | +| DP064 | Error | 子节点实现类型不在父节点 `AllowedChildTypes` 集合中 | `Composite part '{0}' (type '{1}') is not in the AllowedChildTypes of its parent '{2}' (type '{3}'). Add '{1}' to the parent's AllowedChildTypes or change the ParentKey to a compatible parent.` | +| DP065 | Warning | `[CompositeSchema(MaxNodes)]` 约束被超过 | `Composite contract '{0}' has {1} parts, exceeding the maximum of {2} declared by [CompositeSchema]. Consider splitting into multiple contracts or increasing MaxNodes.` | + +## 不变量 / 兼容基线 + +1. **单根约束(`BuildRoot`)**:`BuildRoot()` / `Assemble` 要求 catalog 中恰好一个 `ParentKey == null` 的根;零根或多根时运行时抛 `CompositeAssemblyException`。 +2. **`ICompositeBuildable<TContract>` 要求**:catalog 装配的节点类型必须实现 `ICompositeBuildable<TContract>`,`TContract` 为 composite 接口(如 `IMenuNode`)。 +3. **public 无参构造**:catalog 装配的节点类型必须声明 public 无参构造函数(与 Strategy/Handler 生成器一致),供 `Activator.CreateInstance` 实例化。 +4. **`SetChildren` 一次性**:`SetChildren` 在装配时调用一次;实现类应在装配后冻结 children,后续调用视为无效。 +5. **多根森林(`BuildForest`)**:`BuildForest()` / `AssembleForest` 允许一个或多个 `ParentKey == null` 的根,按 `Order` 升序再 key 排序返回。 +6. **并行遍历顺序非确定**:`TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` 的访问顺序非确定;需要顺序时使用 `Traverse` / `TraverseAsync`。 + +### 兼容基线 + +- `netstandard2.0` + `net8.0`(两者均须可用并随包分发) +- 泛型 `CompositePartAttribute<TContract>` 仅在 C# 11+ / net7.0+ 可用(`#if NET7_0_OR_GREATER`);netstandard2.0 使用非泛型 `CompositePartAttribute(string key, Type @for)` +- 并行遍历:net8.0 用 `Parallel.ForEachAsync`,netstandard2.0 用 `SemaphoreSlim` + `Task.WhenAll`(`#if` 分裂) + ## 实现概览 ### 运行时 @@ -117,7 +251,7 @@ public sealed class SettingsMenu : IMenuNode, ICompositeBuildable<IMenuNode> { } 所有 schema 约束均为 opt-in — 未标注 `[CompositeSchema]` 的契约行为不变。校验在编译期完成,运行时零开销。 -### 诊断 +### 诊断检测细节 诊断检测逻辑与报告位置: @@ -186,7 +320,6 @@ Composite 关注树形结构与统一遍历;Chain 关注线性有序执行与 - GoF: Composite (Structural) - [ADR-006: Composite parallel traversal](../adr/ADR-006-composite-parallel-traversal.md) - [ADR-007: Composite tree schema validation](../adr/ADR-007-composite-tree-schema-validation.md) -- [Spec: Composite](../spec/Composite.md) - [docs/DEVELOPMENT.md](../DEVELOPMENT.md) - 示例:[DesignPatterns.Samples.Composite](https://github.com/Skymly/DesignPatterns.Samples/tree/main/DesignPatterns.Samples.Composite) - **Catalog 森林**:`MenuNodeCompositeCatalog.BuildForest()` + `TraverseForest`(多根 `[CompositePart]`;多根时 `BuildRoot()` 抛错) diff --git a/docs/design/Decorator.md b/docs/design/Decorator.md index 388ea75..e9b1d5f 100644 --- a/docs/design/Decorator.md +++ b/docs/design/Decorator.md @@ -1,7 +1,6 @@ # Design Doc: Decorator -> **关联 Spec**:[docs/spec/Decorator.md](../spec/Decorator.md) -> **关联 RFC**:[docs/rfc/XXX.md](../rfc/XXX.md)(如有) +> **版本**:v0.2.2(与 NuGet 包版本对齐) > **关联 ADR**:ADR-XXX(如有) ## 概述 @@ -15,6 +14,103 @@ Decorator(装饰器)在**不修改核心类型**的前提下,为同一契 3. **零 DI 依赖**:手动组装;DI 扩展留 P3 4. **编译期胶水**:生成 `{Contract}DecoratorStack.Build(core)` 与 `{Contract}DecoratorOrder` 常量,不生成装饰方法体 +## API 面 + +### 运行时接口 + +运行时类型位于 `DesignPatterns/Structural/`。 + +| 类型 | 说明 | +|------|------| +| `IDecorator<TService>` | `TService Decorate(TService inner)` | +| `IDecoratorOf<TService>` | 可选标记接口(生成器不强制) | +| `DecoratorStackBuilder<TService>` | `Add<TDecorator>()` / `Add(instance)` / `Add(..., Func<bool>)` / `Build(core)` | + +`DecoratorStackBuilder<TService>` 方法签名: + +| 方法 | 说明 | +|------|------| +| `Add<TDecorator>()` where `TDecorator : TService, IDecorator<TService>, new()` | 注册一个装饰器类型(无参构造) | +| `Add(TService decorator)` | 注册一个装饰器实例(须实现 `IDecorator<TService>`) | +| `Add<TDecorator>(Func<bool> predicate)` | 注册一个带运行时谓词的装饰器类型;谓词在 `Build` 时求值,为 `false` 时跳过该装饰器 | +| `Add(TService decorator, Func<bool> predicate)` | 注册一个带运行时谓词的装饰器实例 | +| `Build(TService core)` | 按 `Add` 顺序(`Order` 越小越靠外)包装 `core`,返回最外层 `TService` | + +### 特性(Attribute) + +```csharp +// 泛型(推荐,C# 11+ / net8.0) +[Decorator<IPaymentService>(10)] +public sealed class LoggingPaymentDecorator : IPaymentService, IDecorator<IPaymentService> { ... } + +// 非泛型(netstandard2.0) +[Decorator(10, typeof(IPaymentService))] +public sealed class LoggingPaymentDecorator : IPaymentService, IDecorator<IPaymentService> { ... } +``` + +`[Decorator]` / `[Decorator<TService>]` 构造函数签名: + +| 特性 | 构造函数 | 说明 | +|------|----------|------| +| `DecoratorAttribute<TService>` | `(int order)` | 泛型形式,C# 11+ / net8.0 | +| `DecoratorAttribute` | `(int order, Type serviceType)` | 非泛型形式,netstandard2.0 | + +`Order` 数值越小越靠外(先接到调用,再委托向内)。 + +### 生成器产出 + +对 `IPaymentService` → `PaymentServiceDecoratorStack` 与 `PaymentServiceDecoratorOrder`: + +```csharp +public static partial class PaymentServiceDecoratorStack +{ + public static IPaymentService Build(IPaymentService core) { ... } +} + +public static partial class PaymentServiceDecoratorOrder +{ + public const int LoggingPaymentDecorator = 10; + public const int MetricsPaymentDecorator = 20; +} +``` + +- `{Contract}DecoratorStack.Build(core)`:按 `[Decorator]` 的 `Order` 升序包装 `core`,返回最外层 `TService`。注册全部 `[Decorator]` 类型,**不**应用运行时谓词。 +- `{Contract}DecoratorOrder`:为每个装饰器生成 `public const int`,常量名取装饰器**简单类型名**。同一 contract 下类型名须唯一(跨命名空间同名会导致重复常量编译错误)。 + +特性中可引用常量替代魔法数: + +```csharp +[Decorator<IPaymentService>(PaymentServiceDecoratorOrder.LoggingPaymentDecorator)] +public sealed class LoggingPaymentDecorator : IPaymentService, IDecorator<IPaymentService> { ... } +``` + +## 诊断 + +| ID | 级别 | 触发条件 | 消息格式 | +|----|------|----------|----------| +| DP016 | Error | 同一 contract 下重复 `Order` | `Decorator order '{0}' is already used for service contract '{1}'. Assign a unique order value or remove the duplicate [Decorator] attribute.` | +| DP017 | Error | 标注 `[Decorator]` 但未实现 service contract | `Type '{0}' does not implement service contract '{1}'. Implement the contract or fix the [Decorator] service argument.` | +| DP018 | Error | 标注 `[Decorator]` 但未实现 `IDecorator<TService>` | `Type '{0}' does not implement IDecorator<{1}>. Implement IDecorator<{1}> to participate in generated decorator stacks.` | +| DP019 | Error | 标注 `[Decorator]` 的装饰器缺少 public 无参构造 | `Type '{0}' must declare a public parameterless constructor for generated decorator stacks.` | +| DP042 | Error | async 装饰器 `DecorateAsync` 签名不匹配 | `Async decorator '{0}' must implement 'ValueTask<{1}> DecorateAsync({1} inner, CancellationToken cancellationToken = default)'. Fix the IAsyncDecorator<{1}> implementation.` | +| DP043 | Warning | 装饰器无法从 DI 容器解析(无 public 无参构造) | `Decorator '{0}' has no public parameterless constructor. Register it in the DI container via RegisterDi(), or add a parameterless constructor for Build() without IServiceProvider.` | + +CodeFix:DP017(加契约接口)、DP019(加无参构造)。DP018 需手写 `Decorate` 方法。 + +## 不变量 / 兼容基线 + +1. `Build(core)` 要求 `core` 非 `null`,否则抛 `ArgumentNullException`。 +2. `Order` 越小越靠外(先接到调用,再委托向内);先注册的装饰器包装在后注册的外层。 +3. 无装饰器时 `Build(core)` 返回同一 `core` 引用(不创建包装)。 +4. `DecoratorOrder` 常量名取装饰器**简单类型名**;同一 contract 下类型名须唯一。 +5. `Add(null)` / `Add(..., null)` 谓词抛 `ArgumentNullException`。 +6. 生成器产出的 `{Contract}DecoratorStack.Build(core)` 注册全部 `[Decorator]` 类型,条件开关(`Func<bool>`)仅适用于手动 `DecoratorStackBuilder` 组装。 + +### 兼容基线 + +- netstandard2.0 + net8.0(两者均须可用并随包分发) +- Roslyn 组件 4.8.0(`Microsoft.CodeAnalysis.CSharp` / Workspaces;Analyzers 3.3.4) + ## 实现概览 ### 运行时 @@ -66,7 +162,7 @@ var service = new DecoratorStackBuilder<IPaymentService>() public sealed class LoggingPaymentDecorator : IPaymentService, IDecorator<IPaymentService> { ... } ``` -### 诊断 +### 诊断检测细节 | ID | 说明 | |----|------| @@ -122,7 +218,5 @@ Decorator 聚焦「同一契约的横切增强」,不替代 ASP.NET Core 中 ## 参考 - GoF: Decorator (Structural) -- [docs/spec/Decorator.md](../spec/Decorator.md) -- [docs/spec/ChainOfResponsibility.md](../spec/ChainOfResponsibility.md) - [docs/DEVELOPMENT.md](../DEVELOPMENT.md) - [AGENTS.md](../../AGENTS.md) diff --git a/docs/design/EventAggregator.md b/docs/design/EventAggregator.md index f7d71f4..8920898 100644 --- a/docs/design/EventAggregator.md +++ b/docs/design/EventAggregator.md @@ -1,7 +1,6 @@ # Design Doc: Event Aggregator -> **关联 Spec**:[docs/spec/EventAggregator.md](../spec/EventAggregator.md) -> **关联 RFC**:[docs/rfc/XXX.md](../rfc/XXX.md)(如有) +> **版本**:v0.2.2 > **关联 ADR**:ADR-XXX(如有) ## 概述 @@ -15,6 +14,128 @@ Event Aggregator 提供进程内、轻量的 **发布/订阅(pub/sub)** 机 3. 线程安全:订阅表在锁下修改,发布时对处理器列表做快照 4. 不侵入 Core 的 DI 依赖;可选 `AddEventAggregator` 扩展 +## API 面 + +### 运行时接口 + +| 类型 | 职责 | +|------|------| +| `IEventHandler<TEvent>` | 处理指定事件类型 | +| `IEventAggregator` | 订阅、取消订阅、发布 | +| `EventAggregator` | 默认实现 | + +#### 事件处理器 + +```csharp +public interface IEventHandler<in TEvent> +{ + ValueTask HandleAsync(TEvent evt, CancellationToken cancellationToken = default); +} +``` + +任意类实现该接口即可作为处理器;**不要求**继承基类或注册特性。 + +#### 聚合器 + +```csharp +public interface IEventAggregator +{ + void Subscribe<TEvent>(IEventHandler<TEvent> handler); + void Unsubscribe<TEvent>(IEventHandler<TEvent> handler); + ValueTask PublishAsync<TEvent>(TEvent evt, CancellationToken cancellationToken = default); +} +``` + +事件类型 `TEvent` 可以是 `class`、`struct` 或 `record`;同一聚合器上可并存多种 `TEvent`。 + +#### 基本用法 + +```csharp +var aggregator = new EventAggregator(); + +aggregator.Subscribe(new EmailNotificationHandler()); +aggregator.Subscribe(new AuditLogHandler()); + +await aggregator.PublishAsync(new OrderPlacedEvent("ORD-001", 99.99m)); + +aggregator.Unsubscribe(auditHandler); +await aggregator.PublishAsync(new OrderPlacedEvent("ORD-002", 49.99m)); +``` + +### 特性(Attribute) + +| 特性 | 形态 | 适用 TFM | +|------|------|----------| +| `[RegisterEventHandler(typeof(FooEvent))]` | 非泛型 | 全部(netstandard2.0+) | +| `[RegisterEventHandler<FooEvent>]` | 泛型 | `#if NET7_0_OR_GREATER`(C# 11+ generic attributes) | + +与 `[RegisterStrategy]` / `[RegisterFactory]` 的双形态模式一致。 + +### 生成器产出 + +`RegisterEventHandlerGenerator` 在编译期扫描 `[RegisterEventHandler]` 特性,按 **event type 分组** 生成 `{Event}EventHandlerRegistry` 静态类(`Event` 后缀自动剥离,如 `OrderPlacedEvent` → `OrderPlacedEventHandlerRegistry`): + +| 成员 | 路径 | 条件 | +|------|------|------| +| `SubscribeAll(IEventAggregator)` | 静态:`new Handler()` + `Subscribe<TEvent>` | handler 有公共无参构造 | +| `RegisterDi(IServiceCollection, ServiceLifetime)` | DI:注册 handler 实现到容器 | `DesignPatterns_EnableDiIntegration=true` | +| `SubscribeAll(IEventAggregator, IServiceProvider)` | DI:从容器解析 handler + `Subscribe<TEvent>` | `DesignPatterns_EnableDiIntegration=true` | + +**静态路径**仅包含有公共无参构造的 handler;**DI 路径**包含全部有效 handler。这与其他生成器的无参构造约束一致(DP007/DP009/DP014/DP019/DP022 同源)。 + +#### 静态路径(无 DI 依赖) + +```csharp +public record OrderPlacedEvent(string OrderId); + +[RegisterEventHandler<OrderPlacedEvent>] +public sealed class LogOrderHandler : IEventHandler<OrderPlacedEvent> +{ + public ValueTask HandleAsync(OrderPlacedEvent evt, CancellationToken ct = default) => default; +} + +// 启动时 +var aggregator = new EventAggregator(); +OrderPlacedEventHandlerRegistry.SubscribeAll(aggregator); +await aggregator.PublishAsync(new OrderPlacedEvent("ORD-001")); +``` + +#### DI 路径(两步) + +```csharp +// 1. 注册聚合器 + handler 实现 +services.AddEventAggregator(); +OrderPlacedEventHandlerRegistry.RegisterDi(services); + +// 2. 启动时从容器解析并订阅 +var provider = services.BuildServiceProvider(); +var aggregator = provider.GetRequiredService<IEventAggregator>(); +OrderPlacedEventHandlerRegistry.SubscribeAll(aggregator, provider); +``` + +`RegisterDi` 默认 `implementationLifetime: ServiceLifetime.Transient`(每次解析新实例,因 handler 通常无状态)。 + +## 诊断 + +| ID | 级别 | 触发条件 | 消息格式 | +|----|------|----------|----------| +| DP044 | Info | 实现 `IEventHandler<T>` 但未标注 `[RegisterEventHandler]` | 提示未注册的 EventHandler | +| DP045 | Error | 同一 handler 类对同一 event type 重复标注 `[RegisterEventHandler]` | 报告重复标注 | +| DP046 | Error | 标注 `[RegisterEventHandler<T>]` 但类未实现 `IEventHandler<T>` | 报告契约不匹配 | + +## 不变量 / 兼容基线 + +1. 同一事件类型的处理器按 **订阅顺序** 依次调用。 +2. `PublishAsync` 在发布前对处理器列表做 **快照**,随后在锁外调用,避免在 `HandleAsync` 执行期间持有锁。 +3. 同一 `handler` 实例可重复 `Subscribe`(会多次出现在列表中)。 +4. `Unsubscribe` 移除**第一次**匹配项。 +5. 无处理器时 `PublishAsync` 立即完成(空订阅)。 + +### 兼容基线 + +- netstandard2.0 / net8.0(运行时核心,两者均须可用并随包分发) +- DI 集成:独立包 `DesignPatterns.Extensions.DependencyInjection`,`services.AddEventAggregator()` 注册 `IEventAggregator` → `EventAggregator`(默认 Singleton) + ## 实现概览 ### 运行时 @@ -54,7 +175,7 @@ services.AddEventAggregator(); // 默认 Singleton - **DI 路径**:`RegisterDi(IServiceCollection, ServiceLifetime)` 注册 handler 实现到容器;`SubscribeAll(IEventAggregator, IServiceProvider)` 从容器解析 handler + `Subscribe<TEvent>`。条件为 `DesignPatterns.EnableDiIntegration=true`。 - 静态路径仅包含有公共无参构造的 handler;DI 路径包含全部有效 handler。这与其他生成器的无参构造约束一致(DP007/DP009/DP014/DP019/DP022 同源)。 -### 诊断 +### 诊断检测细节 | ID | 归属 | 语义 | |----|------|------| diff --git a/docs/design/FactoryRegistry.md b/docs/design/FactoryRegistry.md index 139ec4d..aa2a8c7 100644 --- a/docs/design/FactoryRegistry.md +++ b/docs/design/FactoryRegistry.md @@ -1,7 +1,6 @@ # Design Doc: Factory Registry -> **关联 Spec**:[docs/spec/FactoryRegistry.md](../spec/FactoryRegistry.md) -> **关联 RFC**:[docs/rfc/XXX.md](../rfc/XXX.md)(如有) +> **版本**:v0.2.2(与 NuGet 包版本对齐) > **关联 ADR**:ADR-XXX(如有) ## 概述 @@ -18,6 +17,227 @@ Factory Registry 按 key 解析**工厂实现**,每次 `Create` 调用对应 f 2. 显式失败:`FactoryNotFoundException` 带 key 信息 3. 不侵入 Core 的 DI 依赖;生命周期由 factory 委托或 DI 扩展包负责 +## API 面 + +### 运行时接口 + +命名空间:`DesignPatterns.Creational` + +#### 注册表接口 + +```csharp +namespace DesignPatterns.Creational; + +/// <summary> +/// 按 key 解析工厂实现的只读注册表。每次 Create 调用对应 factory 委托得到新产品实例。 +/// </summary> +public interface IFactoryRegistry<TKey, TProduct> : IReadOnlyRegistry<TKey, TProduct> + where TKey : notnull +{ + bool TryCreate(TKey key, [MaybeNullWhen(false)] out TProduct product); + TProduct Create(TKey key); // 找不到抛 FactoryNotFoundException + IReadOnlyCollection<TKey> Keys { get; } +} +``` + +> **注意**:`IFactoryRegistry` 继承 `IReadOnlyRegistry<TKey, TProduct>`,与 `IStrategyRegistry` 共享该抽象。 + +#### 不可变实现 + +```csharp +public sealed class FactoryRegistry<TKey, TProduct> : IFactoryRegistry<TKey, TProduct> + where TKey : notnull +``` + +net8.0+ 上内部使用 `FrozenDictionary` 优化查找。 + +#### Builder(手动注册,无生成器时) + +```csharp +public sealed class FactoryRegistryBuilder<TKey, TProduct> + where TKey : notnull +{ + // 每次 Create(key) 调用该 factory + public FactoryRegistryBuilder<TKey, TProduct> Register(TKey key, Func<TProduct> factory); + + // factory 接收 key + public FactoryRegistryBuilder<TKey, TProduct> Register(TKey key, Func<TKey, TProduct> factory); + + public IFactoryRegistry<TKey, TProduct> Build(); +} +``` + +- 重复 key 在 `Register` 时抛 `ArgumentException`(**运行时**检测)。 + +#### 异常 + +```csharp +public sealed class FactoryNotFoundException : Exception +{ + public FactoryNotFoundException(); + public FactoryNotFoundException(string message); + public FactoryNotFoundException(string message, Exception innerException); + public static FactoryNotFoundException ForKey<TKey>(TKey key) where TKey : notnull; +} +``` + +`ForKey` 生成消息:`"No factory registered for key '{key}'."` + +#### 异步工厂(可选) + +```csharp +public interface IAsyncFactory<TProduct> +{ + ValueTask<TProduct> CreateAsync(CancellationToken cancellationToken = default); +} + +public interface IAsyncFactoryRegistry<TKey, TProduct> : IReadOnlyRegistry<TKey, TProduct> + where TKey : notnull +{ + ValueTask<(bool Success, TProduct? Product)> TryCreateAsync( + TKey key, CancellationToken cancellationToken = default); + ValueTask<TProduct> CreateAsync( + TKey key, CancellationToken cancellationToken = default); +} +``` + +`FactoryRegistryAsyncExtensions.AsAsync()` 可将同步 `IFactoryRegistry` 适配为 `IAsyncFactoryRegistry`。 + +### 特性(Attribute) + +命名空间:`DesignPatterns.Creational` + +#### 泛型版本(C# 11 / .NET 7+) + +```csharp +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] +public sealed class RegisterFactoryAttribute<TContract> : Attribute +{ + public string Key { get; } + public bool IsAsync { get; set; } + public int PoolSize { get; set; } + + public RegisterFactoryAttribute(string key); +} +``` + +#### 非泛型版本(netstandard2.0 / C# 7.3) + +```csharp +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] +public sealed class RegisterFactoryAttribute : Attribute +{ + public string Key { get; } + public Type Contract { get; } + public bool IsAsync { get; set; } + public int PoolSize { get; set; } + + public RegisterFactoryAttribute(string key, Type contract); +} +``` + +#### 属性说明 + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `Key` | `string` | (构造函数必填) | 用于解析此工厂的 key | +| `Contract` | `Type` | (非泛型构造函数必填) | 工厂契约类型(接口或基类) | +| `IsAsync` | `bool` | `false` | 标记为异步工厂;`true` 时须实现 `IAsyncFactory<TProduct>`,生成器额外输出 `IAsyncFactoryRegistry`。`false` 时异步检测基于 `IAsyncFactory<TProduct>` 实现自动判断 | +| `PoolSize` | `int` | `0` | 启用对象池的最大池大小(per key)。`0` 禁用池化;正值使生成器输出 `IPooledFactoryRegistry`。仅异步工厂有效 | + +#### 用法示例 + +```csharp +// 泛型 Attribute(C# 11+) +[RegisterFactory<IProductFactory>("standard")] +public class StandardFactory : IProductFactory { ... } + +// 非泛型(netstandard2.0) +[RegisterFactory("standard", typeof(IProductFactory))] +public class StandardFactory : IProductFactory { ... } +``` + +工厂契约一般为**接口**(或基类);实现类需 **public 无参构造**,以便生成器注册 `() => new Implementation()`。 + +### 生成器产出 + +对每个契约 `TContract`(命名规则与 Strategy 相同:去掉前缀 `I` 等),生成器输出: + +#### 1. 强类型 Key 常量 + +```csharp +// ProductFactoryKeys.g.cs +public static partial class ProductFactoryKeys +{ + public const string Standard = "standard"; + public const string Premium = "premium"; +} +``` + +#### 2. 静态注册表(无 DI 场景) + +```csharp +// ProductFactoryRegistry.g.cs +public static partial class ProductFactoryRegistry +{ + public static IFactoryRegistry<string, IProductFactory> Create() { ... } +} +``` + +`Create()` 返回的注册表在每次 `Create(key)` 时**新建**产品实例(与 Strategy 注册表返回单例实例不同)。 + +#### 3. DI 集成(引用 `DesignPatterns.Extensions.DependencyInjection` 时) + +引用扩展包会自动 Import `build/DesignPatterns.Extensions.DependencyInjection.targets`,设置 `DesignPatterns_EnableDiIntegration=true`,生成器额外输出: + +```csharp +public static partial class ProductFactoryRegistry +{ + // Create() 仍保留(new() 静态实例,无 DI 生命周期) + + public static IFactoryRegistry<string, IProductFactory> Create(IServiceProvider serviceProvider) { ... } + + public static IServiceCollection RegisterDi( + IServiceCollection services, + ServiceLifetime implementationLifetime = ServiceLifetime.Transient, + ServiceLifetime registryLifetime = ServiceLifetime.Singleton); +} +``` + +> **注意**:`implementationLifetime` 默认值为 `Transient`(与 Strategy/Chain 等模式默认 `Singleton` 不同),因为工厂语义是每次 `Create` 返回新实例。 + +手动 Builder 仍可用:`services.AddFactoryRegistry<string, IProduct>(builder => { ... })`(扩展包),与生成器互不冲突。 + +## 诊断 + +| ID | 级别 | 触发条件 | 消息格式 | +|----|------|----------|----------| +| DP020 | Error | 同一 `TContract` 下 key 重复 | `Factory key '{0}' is already registered for contract '{1}'. Use a unique key or remove the duplicate [RegisterFactory] attribute.` | +| DP021 | Error | 标记的类未实现指定的 `TContract` | `Type '{0}' does not implement factory contract '{1}'. Implement the contract or fix the [RegisterFactory] contract argument.` | +| DP022 | Error | 标记的类缺少 public 无参构造 | `Type '{0}' must declare a public parameterless constructor for [RegisterFactory] static registration, or enable DI integration.` | +| DP023 | Info | 实现了某工厂契约但未加 `[RegisterFactory]` | `Type '{0}' implements factory contract '{1}' but has no [RegisterFactory] attribute. Add [RegisterFactory("key", typeof(...))] with a unique key and the contract type to register it.` | +| DP053 | Error | `IsAsync=true` 但未实现 `IAsyncFactory<TProduct>` | `Factory '{0}' is marked with IsAsync=true but does not implement IAsyncFactory<{1}>. Implement IAsyncFactory<{1}> or set IsAsync=false.` | +| DP054 | Error | `PoolSize` 为负数 | `Factory '{0}' has PoolSize={1}, which is negative. PoolSize must be >= 0 (0 disables pooling).` | +| DP055 | Warning | `PoolSize` 过大(可能导致内存过高) | `Factory '{0}' has PoolSize={1}, which may cause excessive memory usage. Consider a smaller value (recommended: 1-100).` | + +> **归属**:DP023 属 **Analyzer**(含 CodeFix,可一键添加 `[RegisterFactory("suggested-key", typeof(TContract))]`,与 DP006 对称);DP020 / DP021 / DP022 / DP053 / DP054 / DP055 属**生成器**。 + +## 不变量 / 兼容基线 + +1. **每次 Create 返回新实例**:`Create(key)` / `TryCreate(key, out _)` 每次调用均执行 factory 委托,得到新产品实例(与 Strategy 注册表返回同一实例不同)。 +2. **重复 key 抛 ArgumentException**:`FactoryRegistryBuilder.Register` 在运行时检测重复 key,抛 `ArgumentException`(`"A factory is already registered for key '{key}'."`)。编译期由 DP020 检测。 +3. **`IFactoryRegistry` 继承 `IReadOnlyRegistry`**:与 `IStrategyRegistry` 共享 `IReadOnlyRegistry<TKey, TValue>` 抽象(提供 `Keys` 和 `TryGet`)。 +4. **标记的类须实现指定的 `TContract`**(DP021)。 +5. **标记的类须有 public 无参构造**:生成器使用 `new()` 实例化(DP022),或启用 DI 集成由容器解析。 +6. **key 唯一性**:同一 `TContract` 下 key 不可重复(DP020 在编译期强制)。 + +### 兼容基线 + +- 运行时 TFM:`netstandard2.0` + `net8.0`(两者均须可用并随包分发)。 +- 泛型 Attribute(`RegisterFactoryAttribute<TContract>`)需要 C# 11+ / `net7.0+`;`netstandard2.0` 目标下用 `#if NET7_0_OR_GREATER` 条件编译。 +- 非泛型 `RegisterFactoryAttribute` 始终可用,功能等价。 +- net8.0 上 `FactoryRegistry<TKey, TProduct>` 内部使用 `FrozenDictionary` 优化查找。 + ## 实现概览 ### 运行时 @@ -86,7 +306,7 @@ public static partial class ProductFactoryRegistry **池化路径**:当 `PoolSize > 0` 且工厂为异步时,生成器输出 `IPooledFactoryRegistry`,按 key 维护对象池。 -### 诊断 +### 诊断检测细节 | ID | 严重性 | 来源 | 触发条件 | |----|--------|------|----------| @@ -153,8 +373,6 @@ services.AddFactoryRegistry<string, IProduct>(builder => { ... }); // 手动 Bui | 未注册实现 | DP006 (Info) | DP023 (Info) | | DI 默认 lifetime | Singleton | Transient | -详见 [Strategy.md](../spec/Strategy.md)。 - ## 已知局限 - **不做抽象工厂族**:不提供多个产品类型族(Abstract Factory)的完整框架 @@ -165,5 +383,4 @@ services.AddFactoryRegistry<string, IProduct>(builder => { ... }); // 手动 Bui ## 参考 - GoF: Factory Method / 注册表变体(Creational) -- [Strategy.md](../spec/Strategy.md) — 策略注册表对比 - [AGENTS.md](../../AGENTS.md) — 项目规则与里程碑 diff --git a/docs/design/README.md b/docs/design/README.md index 9f9543e..30d9c97 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -1,23 +1,18 @@ # Design Doc 索引 -设计文档(Design Document)— 记录模式的实现细节、设计权衡、已知局限。 +设计文档(Design Document)是各模式的单一事实来源,记录公开 API、诊断、不变量,以及实现细节、设计权衡和已知局限。 -- **格式与变更门槛**:见 [DOCUMENTATION.md](../DOCUMENTATION.md#5-design-doc--设计文档) +- **格式**:见 [DOCUMENTATION.md](../DOCUMENTATION.md) - **模板**:[_template.md](_template.md) -- **与 Spec 的关系**:[Spec](../spec/) 描述 **what**(契约),Design Doc 描述 **how** + **why**(实现) ## 已有 Design Doc -| 模式 | Design Doc | Spec | 关联 ADR | -|------|------------|------|----------| -| Strategy | [Strategy.md](Strategy.md) | [Strategy.md](../spec/Strategy.md) | — | -| Chain of Responsibility | [ChainOfResponsibility.md](ChainOfResponsibility.md) | [ChainOfResponsibility.md](../spec/ChainOfResponsibility.md) | — | -| Composite | [Composite.md](Composite.md) | [Composite.md](../spec/Composite.md) | [ADR-006](../adr/ADR-006-composite-parallel-traversal.md)、[ADR-007](../adr/ADR-007-composite-tree-schema-validation.md) | -| Factory Registry | [FactoryRegistry.md](FactoryRegistry.md) | [FactoryRegistry.md](../spec/FactoryRegistry.md) | — | -| Decorator | [Decorator.md](Decorator.md) | [Decorator.md](../spec/Decorator.md) | — | -| Event Aggregator | [EventAggregator.md](EventAggregator.md) | [EventAggregator.md](../spec/EventAggregator.md) | — | -| State Transition Table | [StateTransitionTable.md](StateTransitionTable.md) | [StateTransitionTable.md](../spec/StateTransitionTable.md) | [ADR-005](../adr/ADR-005-state-transition-table.md) | - -## 迁移状态 - -所有 7 个模式文档已从 `docs/<PatternName>.md` 拆分迁移至 `docs/spec/` + `docs/design/`。旧文件已删除。 +| 模式 | Design Doc | 关联 ADR | +|------|------------|----------| +| Strategy | [Strategy.md](Strategy.md) | — | +| Chain of Responsibility | [ChainOfResponsibility.md](ChainOfResponsibility.md) | — | +| Composite | [Composite.md](Composite.md) | [ADR-006](../adr/ADR-006-composite-parallel-traversal.md)、[ADR-007](../adr/ADR-007-composite-tree-schema-validation.md) | +| Factory Registry | [FactoryRegistry.md](FactoryRegistry.md) | — | +| Decorator | [Decorator.md](Decorator.md) | — | +| Event Aggregator | [EventAggregator.md](EventAggregator.md) | — | +| State Transition Table | [StateTransitionTable.md](StateTransitionTable.md) | [ADR-005](../adr/ADR-005-state-transition-table.md) | diff --git a/docs/design/StateTransitionTable.md b/docs/design/StateTransitionTable.md index b5d9ed5..b142e32 100644 --- a/docs/design/StateTransitionTable.md +++ b/docs/design/StateTransitionTable.md @@ -1,8 +1,7 @@ # Design Doc: State 转换表 -> **关联 Spec**:[docs/spec/StateTransitionTable.md](../spec/StateTransitionTable.md) +> **版本**:v0.2.2(与 NuGet 包版本对齐) > **关联 ADR**:[ADR-005](../adr/ADR-005-state-transition-table.md) -> **关联 RFC(归档)**:[docs/rfc/archive/StateTransitionTable.md](../rfc/archive/StateTransitionTable.md)、[docs/rfc/archive/HierarchicalStateMachine.md](../rfc/archive/HierarchicalStateMachine.md) ## 概述 @@ -24,6 +23,388 @@ State 转换表为 **(当前状态, 触发器) → 下一状态** 的有限图 5. 层次状态机(v3)通过编译期展平实现,运行时无层级开销 6. 同时支持同步与异步路径(entry/exit action 仅 async 路径触发) +## API 面 + +### 运行时接口 + +命名空间:`DesignPatterns.Behavioral` + +#### 转换表 + +```csharp +namespace DesignPatterns.Behavioral; + +/// <summary> +/// 只读转换表:(当前状态, 触发器) → 下一状态。 +/// </summary> +public interface ITransitionTable<TState, TTrigger> + where TState : struct, Enum + where TTrigger : struct, Enum +{ + bool TryTransition(TState from, TTrigger trigger, out TState to); + bool CanTransitionFrom(TState from, TTrigger trigger); + IReadOnlyCollection<TTrigger> GetAllowedTriggers(TState from); + TState InitialState { get; } +} +``` + +- `TryTransition`:命中边且 guard 通过时返回 `true` 并设置 `to`;否则返回 `false`。 +- `GetAllowedTriggers`:返回从 `from` 出发的所有合法触发器。 +- `CanTransitionFrom`:仅接收 state(无 trigger),判断是否存在以 `from` 为起点的边。 + +`TransitionTable<TState, TTrigger>` 为不可变实现;net8.0 上内部使用 `FrozenDictionary` 优化查找。 + +#### Builder(手动注册,无生成器时) + +```csharp +public sealed class TransitionTableBuilder<TState, TTrigger> + where TState : struct, Enum + where TTrigger : struct, Enum +{ + public TransitionTableBuilder<TState, TTrigger> WithInitial(TState initial); + public TransitionTableBuilder<TState, TTrigger> Add( + TState from, TTrigger trigger, TState to, + Func<TState, TTrigger, bool>? guard = null, + Action<TState, TState, TTrigger>? onEnterSync = null, + Action<TState, TState, TTrigger>? onExitSync = null, + Func<TState, TState, TTrigger, CancellationToken, ValueTask>? onEnterAsync = null, + Func<TState, TState, TTrigger, CancellationToken, ValueTask>? onExitAsync = null); + public ITransitionTable<TState, TTrigger> Build(); +} +``` + +> **v3.4 重载简化**:`Add` 的多个重载合并为单一签名,所有可选参数(`guard`、`onEnterSync`、`onExitSync`、`onEnterAsync`、`onExitAsync`)均有默认值 `null`,调用方可按命名参数传任意子集,无需为占位提供 `null`。 + +手动 Builder 用法: + +```csharp +using DesignPatterns.Behavioral; + +public enum OrderStatus { Draft, Submitted, Paid } +public enum OrderTrigger { Submit, Pay } + +var table = new TransitionTableBuilder<OrderStatus, OrderTrigger>() + .WithInitial(OrderStatus.Draft) + .Add(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted) + .Add(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid) + .Build(); + +if (table.TryTransition(OrderStatus.Draft, OrderTrigger.Submit, out var next)) +{ + // next == Submitted +} + +// 非法边返回 false;Transition() 扩展抛 InvalidTransitionException +``` + +#### Transition 扩展 + +```csharp +public static class TransitionTableExtensions +{ + // 非法边时抛 InvalidTransitionException(含当前态与触发器信息) + public static TState Transition<TState, TTrigger>( + this ITransitionTable<TState, TTrigger> table, TState from, TTrigger trigger) + where TState : struct, Enum + where TTrigger : struct, Enum; +} +``` + +#### Guard 委托 + +`TransitionTableBuilder.Add` 提供 `guard` 参数;`TransitionEdge<TState, TTrigger>.Guard` 存储委托。`TransitionTable.TryTransition` 在命中边后调用 guard;guard 返回 `false` 时该次转换视为不存在。 + +```csharp +var table = new TransitionTableBuilder<OrderStatus, OrderTrigger>() + .WithInitial(OrderStatus.Draft) + .Add(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted, + guard: (state, trigger) => !string.IsNullOrEmpty(orderId)) + .Build(); +``` + +#### Entry / Exit Actions + +`TransitionTableBuilder.Add` 提供 `onEnterSync` / `onExitSync` / `onEnterAsync` / `onExitAsync` 参数。Action 仅通过 async 路径(`TryTransitionAsync`)触发;同步 `TryTransition` 不调用 action。 + +```csharp +var table = new TransitionTableBuilder<OrderStatus, OrderTrigger>() + .WithInitial(OrderStatus.Draft) + .Add( + OrderStatus.Draft, + OrderTrigger.Submit, + OrderStatus.Submitted, + guard: null, + onEnterSync: (from, to, trigger) => Console.WriteLine($"Entering {to}"), + onExitSync: (from, to, trigger) => Console.WriteLine($"Exiting {from}")) + .Build(); +``` + +执行顺序(`TryTransitionAsync`):guard → OnExit(sync → async)→ OnEnter(sync → async)→ 返回结果。 + +> **部分执行**:若 OnExit 成功后 OnEnter 抛异常,异常传播给调用方,OnExit 副作用已发生。调用方需自行处理补偿。`TransitionResult<TState>` 不暴露执行进度。 + +#### IStateMachine 实例包装器 + +```csharp +public interface IStateMachine<TState, TTrigger> + where TState : struct, Enum + where TTrigger : struct, Enum +{ + TState CurrentState { get; } + bool TryTransition(TTrigger trigger, out TState to); + ValueTask<TransitionResult<TState>> TryTransitionAsync(TTrigger trigger, CancellationToken ct = default); +} + +public sealed class StateMachine<TState, TTrigger> : IStateMachine<TState, TTrigger> + where TState : struct, Enum + where TTrigger : struct, Enum +{ + public StateMachine(ITransitionTable<TState, TTrigger> table); + public TState CurrentState { get; } // 初始 == table.InitialState + public bool TryTransition(TTrigger trigger, out TState to); + public ValueTask<TransitionResult<TState>> TryTransitionAsync(TTrigger trigger, CancellationToken ct = default); +} +``` + +`TryTransition` 委托给底层 table;成功时更新 `CurrentState`。`TryTransitionAsync` 同理,并触发 entry/exit action。 + +```csharp +var machine = new StateMachine<OrderStatus, OrderTrigger>(table); +// machine.CurrentState == table.InitialState + +if (machine.TryTransition(OrderTrigger.Submit, out var next)) +{ + // machine.CurrentState == OrderStatus.Submitted +} +``` + +> **线程安全**:`StateMachine<TState,TTrigger>` 非线程安全,设计为单线程使用。多线程场景应在调用方同步,或每线程使用独立实例。 + +#### IStateHierarchy(v3 层次状态) + +```csharp +public interface IStateHierarchy<TState> + where TState : struct, Enum +{ + TState? GetParent(TState state); + bool IsInState(TState state, TState ancestor); + IReadOnlyList<TState> GetAncestors(TState state); +} +``` + +- `GetParent`:返回直接父状态,无父时返回 `null`。 +- `IsInState`:判断 `state` 是否为 `ancestor` 的后代(含自身)。 +- `GetAncestors`:从 `state` 向上到根的祖先链(不含 `state` 自身)。 + +### 特性(Attribute) + +命名空间:`DesignPatterns.Behavioral` + +#### [StateMachine] + +```csharp +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class StateMachineAttribute : Attribute +{ + public Type State { get; } // state enum 类型 + public Type Trigger { get; } // trigger enum 类型 + public object? Initial { get; set; } // 初始状态(state enum 成员) + public bool Hierarchical { get; set; } // v3:启用层次状态模式 + public StateMachineAttribute(Type state, Type trigger); +} +``` + +#### [Transition] + +```csharp +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] +public sealed class TransitionAttribute : Attribute +{ + public object From { get; } // state enum 成员 + public object Trigger { get; } // trigger enum 成员 + public object To { get; } // state enum 成员 + public string? Guard { get; set; } // holder 类上的 static guard 方法名 + public string? OnEnter { get; set; } // holder 类上的 static entry action 方法名 + public string? OnExit { get; set; } // holder 类上的 static exit action 方法名 + public TransitionAttribute(object from, object trigger, object to); +} +``` + +#### [StateParent](v3 层次状态) + +```csharp +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] +public sealed class StateParentAttribute : Attribute +{ + public object Child { get; } // state enum 成员 + public object Parent { get; } // state enum 成员 + public StateParentAttribute(object child, object parent); +} +``` + +#### 用法示例 + +```csharp +[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft)] +[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted)] +[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid)] +public static partial class OrderMachine; + +// 生成:OrderStatusTransitionTable.Instance +// holder:OrderMachine.TryTransition(...)、OrderMachine.InitialState +``` + +Guard + Entry/Exit 示例: + +```csharp +[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft)] +[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted, Guard = nameof(CanSubmit))] +[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid, + OnEnter = nameof(OnSubmitted), OnExit = nameof(OnLeaveDraft))] +public static partial class OrderMachine +{ + public static bool CanSubmit(OrderStatus state, OrderTrigger trigger) => true; + public static void OnSubmitted(OrderStatus from, OrderStatus to, OrderTrigger trigger) { } + public static void OnLeaveDraft(OrderStatus from, OrderStatus to, OrderTrigger trigger) { } +} +``` + +层次状态示例(v3): + +```csharp +[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft, Hierarchical = true)] +[StateParent(OrderStatus.Submitted, OrderStatus.Active)] +[StateParent(OrderStatus.Paid, OrderStatus.Active)] +[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted)] +[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid)] +[Transition(OrderStatus.Active, OrderTrigger.Cancel, OrderStatus.Cancelled, OnExit = nameof(OnExitActive))] +public static partial class OrderMachine +{ + public static void OnExitActive(OrderStatus from, OrderStatus to, OrderTrigger trigger) { } +} +``` + +### 生成器产出 + +对 state enum `OrderStatus`: + +| 生成物 | 名称 | +|--------|------| +| 转换表类型 | `OrderStatusTransitionTable` | +| 单例 | `OrderStatusTransitionTable.Instance` | +| Holder 便捷 API | `OrderMachine.TryTransition`、`InitialState` | + +#### {StateEnum}TransitionTable + +```csharp +public sealed partial class OrderStatusTransitionTable + : ITransitionTable<OrderStatus, OrderTrigger> + , IStateHierarchy<OrderStatus> // v3:层次模式时实现 +{ + public static OrderStatusTransitionTable Instance { get; } + public OrderStatus InitialState { get; } + public bool TryTransition(OrderStatus from, OrderTrigger trigger, out OrderStatus to); + public bool CanTransitionFrom(OrderStatus from, OrderTrigger trigger); + public IReadOnlyCollection<OrderTrigger> GetAllowedTriggers(OrderStatus from); + // v3 层次模式时额外实现 IStateHierarchy<OrderStatus> + public OrderStatus? GetParent(OrderStatus state); + public bool IsInState(OrderStatus state, OrderStatus ancestor); + public IReadOnlyList<OrderStatus> GetAncestors(OrderStatus state); +} +``` + +#### Holder 便捷方法 + +生成器在 holder partial 类上输出: + +- `TryTransition(...)` — 委托到 `Instance` +- `InitialState` — 委托到 `Instance.InitialState` + +#### RegisterDi(引用 `DesignPatterns.Extensions.DependencyInjection` 时) + +当消费项目引用 `DesignPatterns.Extensions.DependencyInjection`(自动 Import `build/DesignPatterns.Extensions.DependencyInjection.targets`)时,生成器在转换表类上额外输出: + +```csharp +public static IServiceCollection RegisterDi( + IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Singleton) +{ + services.TryAdd(new ServiceDescriptor( + typeof(ITransitionTable<OrderStatus, OrderTrigger>), + _ => Instance, + lifetime)); + // v3 层次模式时额外注册 IStateHierarchy<OrderStatus> + return services; +} +``` + +调用方式: + +```csharp +OrderStatusTransitionTable.RegisterDi(services); +// 或链式 +services.AddOtherStuff().RegisterDi<OrderStatus, OrderTrigger>(); +``` + +启用开关:MSBuild 属性 `DesignPatterns_EnableDiIntegration`(引用 DI 扩展包时自动为 `true`)。 + +#### 手动注册扩展 + +`DesignPatterns.Extensions.DependencyInjection.DesignPatternsServiceCollectionExtensions` 提供 `AddTransitionTable` 扩展方法,用于注册预构建的表实例: + +```csharp +services.AddTransitionTable(manualTable); +services.AddTransitionTable(OrderStatusTransitionTable.Instance, ServiceLifetime.Singleton); +``` + +使用 `TryAdd` 语义,重复注册不会覆盖。 + +v3 层次模式下使用 `AddStateHierarchy<TState, TTrigger>` 扩展方法(从容器解析 table 并转型)和 `AddStateMachine<TState, TTrigger>` 扩展方法。 + +## 诊断 + +| ID | 级别 | 触发条件 | 消息格式 | +|----|------|----------|----------| +| **DP026** | Error | 重复边 `(from, trigger)` | 重复的转换边 | +| **DP027** | Error | `[Transition]` 的 state 非 enum 成员 | state 非 enum 成员 | +| **DP028** | Error | trigger 非 enum 成员 | trigger 非 enum 成员 | +| **DP029** | Error | `Initial` 非 state enum 成员 | Initial 非 state enum 成员 | +| **DP030** | Error | holder 非 static partial class | holder 须为 static partial class | +| **DP031** | Info | state 从未作为 `from` 出现(终态提示) | state 从未作为 from 出现 | +| **DP032** | Error | guard 方法在 holder 类上未找到 | guard 方法未找到 | +| **DP034** | Error | guard 方法非 static | guard 方法须为 static | +| **DP035** | Error | guard 方法签名错误(须 `bool Method(TState, TTrigger)`) | guard 方法签名错误 | +| **DP036** | Info | `TryTransition` 字面量 (state, trigger) 对未声明(与 DP025 对称) | 字面量边未声明 | +| **DP037** | Error | entry/exit action 方法在 holder 类上未找到 | action 方法未找到 | +| **DP038** | Error | entry/exit action 方法非 static(防御性;CS0708 先于生成器拒绝) | action 方法须为 static | +| **DP039** | Error | entry/exit action 方法签名错误(须 `void Method(TState, TState, TTrigger)` 或 `ValueTask Method(TState, TState, TTrigger, CancellationToken)`) | action 方法签名错误 | +| **DP056** | Error | `[StateParent]` 父链存在循环 | 父链循环 | +| **DP057** | Error | `[StateParent]` 的 child/parent 非 state enum 成员 | 非法 enum 成员 | +| **DP058** | Error | `[StateParent]` 自引用(child == parent) | 自引用父 | +| **DP059** | Error | `[StateParent]` 的 parent 从未作为任何边的 state 出现(孤立父声明) | 孤立父声明 | + +常量与文案:[`DesignPatterns.Diagnostics/DiagnosticIds.cs`](../../DesignPatterns.Diagnostics/DiagnosticIds.cs)、[`DesignPatternsDiagnosticDescriptors.cs`](../../DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs)。 + +用户向说明:[DesignPatterns.Docs — diagnostics](https://skymly.github.io/DesignPatterns.Docs/diagnostics#state-transition-table-dp026-dp031)。 + +## 不变量 / 兼容基线 + +1. `TState` 和 `TTrigger` 均须为 **enum**(`where TState : struct, Enum`)。 +2. Guard 方法签名须为 `bool Method(TState, TTrigger)`;须 **static**;须在 holder 类上声明。 +3. Entry/Exit action 方法签名须为 `void Method(TState from, TState to, TTrigger trigger)`(sync)或 `ValueTask Method(TState from, TState to, TTrigger trigger, CancellationToken)`(async);须 **static**;须在 holder 类上声明。 +4. `StateMachine<TState, TTrigger>` **非线程安全**,设计为单线程使用。多线程场景应在调用方同步,或每线程使用独立实例。 +5. 执行顺序(`TryTransitionAsync`):guard → OnExit(sync → async)→ OnEnter(sync → async)→ 返回结果。 +6. Entry/Exit action 仅通过 async 路径(`TryTransitionAsync`)触发;同步 `TryTransition` 不调用 action。 +7. 部分执行语义:若 OnExit 成功后 OnEnter 抛异常,异常传播给调用方,OnExit 副作用已发生;`TransitionResult<TState>` 不暴露执行进度。 +8. `TransitionTable<TState, TTrigger>` 不可变;`Build()` 后不可修改。 +9. DI 注册使用 `TryAdd` 语义,重复注册不会覆盖。 + +### 兼容基线 + +- netstandard2.0 + net8.0(两者均须可用并随包分发) +- Roslyn 组件 4.8.0(`Microsoft.CodeAnalysis.CSharp` / Workspaces;Analyzers 3.3.4) +- 生成器实现:`IIncrementalGenerator` + `ForAttributeWithMetadataName` + ## 实现概览 ### 运行时 @@ -52,13 +433,13 @@ State 转换表为 **(当前状态, 触发器) → 下一状态** 的有限图 - **Entry/Exit action 生成**:`[Transition]` 的 `OnEnter` / `OnExit` 命名属性指定 holder 类上的 static 方法名。校验 action 方法须 static(DP038)、签名 `void Method(TState, TState, TTrigger)` 或 `ValueTask Method(TState, TState, TTrigger, CancellationToken)`(DP039)、在 holder 类上声明(DP037)。 - **`[StateParent]` 收集**(v3.2):收集 holder 类上的 `[StateParent]` 特性,构建父子关系图。诊断 DP056–DP059 覆盖:循环父链、自引用父、未知子/父状态、孤立父声明。 - **`HierarchyFlattener` 边继承展平**(v3.2):`[StateParent]` 声明的父子关系在编译期被展平——父状态上的边自动继承到所有子状态。例如 `Active + Cancel → Cancelled` 会被展平为 `Submitted + Cancel → Cancelled` 和 `Paid + Cancel → Cancelled`,无需重复声明。 -- **LCA 算法 + entry/exit action 链合成**(v3.3):当层次模式下一条展平后的边跨越多个层级时,源生成器按 RFC §8 的 LCA(最低公共祖先)算法计算 exit/enter 链并合成复合委托(composite delegate): +- **LCA 算法 + entry/exit action 链合成**(v3.3):当层次模式下一条展平后的边跨越多个层级时,源生成器按 LCA(最低公共祖先)算法计算 exit/enter 链并合成复合委托(composite delegate): - **Exit 链**:从 `from` 向上到 LCA(不含 LCA),按从子到父的顺序执行 - **Enter 链**:从 LCA 向下到 `to`(不含 LCA),按从父到子的顺序执行 - **复合委托**:仅当链有 2+ action 时生成 `CompositeExit_{From}_{Trigger}` / `CompositeEnter_{From}_{Trigger}` 静态方法;1 action 直接用原引用;0 action 用 null - 复合委托对运行时透明:`TransitionEdge` 结构不变,复合委托是单个 `Action<TState,TState,TTrigger>` 或 `Func<...,ValueTask>` - **边界场景(RFC §8.4)**: + **边界场景**: | 场景 | Exit 链 | Enter 链 | |------|---------|----------| @@ -69,7 +450,7 @@ State 转换表为 **(当前状态, 触发器) → 下一状态** 的有限图 - **DI 集成生成**:当消费项目引用 `DesignPatterns.Extensions.DependencyInjection`(自动 Import `build/DesignPatterns.Extensions.DependencyInjection.targets`,设置 `DesignPatterns_EnableDiIntegration=true`)时,生成器在转换表类上额外输出 `RegisterDi` 方法。v3 层次模式下 `RegisterDi` 自动注册 `ITransitionTable` + `IStateHierarchy`。 -### 诊断 +### 诊断检测细节 #### 生成器诊断 @@ -187,9 +568,6 @@ Entry/exit action 选择 **部分执行**语义——若 OnExit 成功后 OnEnte ## 参考 - [ADR-005: State transition table](../adr/ADR-005-state-transition-table.md) -- [RFC: StateTransitionTable(归档)](../rfc/archive/StateTransitionTable.md) -- [RFC: HierarchicalStateMachine(归档)](../rfc/archive/HierarchicalStateMachine.md) -- [Spec: State 转换表](../spec/StateTransitionTable.md) — 稳定契约(API 面、诊断 ID、不变量) - [AGENTS.md](../../AGENTS.md) — 项目规则与里程碑 - [docs/DEVELOPMENT.md](../DEVELOPMENT.md) — 通用开发约定 - [Autofac.md](../Autofac.md) — Autofac 扩展 diff --git a/docs/design/Strategy.md b/docs/design/Strategy.md index c549eea..90a0167 100644 --- a/docs/design/Strategy.md +++ b/docs/design/Strategy.md @@ -1,6 +1,6 @@ # Design Doc: Strategy -> **关联 Spec**:[docs/spec/Strategy.md](../spec/Strategy.md) +> **版本**:v0.2.2(与 NuGet 包版本对齐) ## 概述 @@ -17,6 +17,288 @@ Strategy 模式允许在运行时按条件选择算法/行为实现,避免大 3. 不侵入实现类的继承链 4. 同时支持同步与异步策略 +## API 面 + +### 运行时接口 + +命名空间:`DesignPatterns.Behavioral` + +#### 策略接口(可选标记接口) + +```csharp +namespace DesignPatterns.Behavioral; + +/// <summary> +/// 标记性策略接口。不强制实现,但配合源生成器时提供更好的类型约束。 +/// </summary> +public interface IStrategy<in TInput, out TOutput> +{ + TOutput Execute(TInput input); +} + +/// <summary> +/// 异步策略接口。 +/// </summary> +public interface IAsyncStrategy<in TInput, TOutput> +{ + ValueTask<TOutput> ExecuteAsync(TInput input, CancellationToken ct = default); +} +``` + +> **注意**:`[RegisterStrategy]` 不要求实现 `IStrategy<,>`。任何接口/基类均可作为策略契约。 +> +> `IStrategy<,>` / `IAsyncStrategy<,>` 为**可选标记接口**,便于表达同步/异步算法形状;注册表与生成器不依赖它们。用法见 `tests/DesignPatterns.Tests/Behavioral/StrategyMarkerInterfaceTests.cs`。 + +#### 注册表 + +```csharp +namespace DesignPatterns.Behavioral; + +/// <summary> +/// 按 key 解析策略实现的只读注册表。 +/// </summary> +public interface IStrategyRegistry<TKey, TStrategy> + where TKey : notnull +{ + bool TryGet(TKey key, [MaybeNullWhen(false)] out TStrategy strategy); + TStrategy Get(TKey key); // 找不到抛 StrategyNotFoundException + bool TryGetWithGuard(TKey key, [MaybeNullWhen(false)] out TStrategy strategy); + IReadOnlyCollection<TKey> Keys { get; } +} +``` + +> **注意**:`TStrategy` 为不变(invariant),非协变 `out`——`TryGetWithGuard` 的 `out TStrategy` 参数阻止协变。 + +提供不可变实现 `StrategyRegistry<TKey, TStrategy>`,net8.0 上内部使用 `FrozenDictionary` 优化查找。 + +#### Builder(手动注册,无生成器时) + +```csharp +public sealed class StrategyRegistryBuilder<TKey, TStrategy> where TKey : notnull +{ + public StrategyRegistryBuilder<TKey, TStrategy> Register(TKey key, TStrategy strategy); + public StrategyRegistryBuilder<TKey, TStrategy> Register(TKey key, Func<TStrategy> factory); + public IStrategyRegistry<TKey, TStrategy> Build(); +} +``` + +#### 异步策略解析 + +`IAsyncStrategy<TInput, TOutput>` 与 `[RegisterStrategy]` 使用**同一套** Keys / Registry / `RegisterDi` 路径;契约可以是继承 `IAsyncStrategy<,>` 的专用接口,无需额外 attribute 或并行注册表类型。 + +`StrategyRegistryExtensions` 提供按 key 解析并执行的便利方法: + +```csharp +// 注册表值为 IAsyncStrategy<TInput, TOutput> +var result = await registry.ExecuteAsync(PaymentAsyncStrategyKeys.Stripe, amount); + +// 注册表值为继承 IAsyncStrategy<,> 的契约(需显式指定 TContract / TOutput / TInput) +var length = await registry.ExecuteAsync<ITextProcessor, int, string>( + TextProcessorKeys.Length, "hello"); + +// 或等价写法 +var length = await registry.Get(TextProcessorKeys.Length).ExecuteAsync("hello"); +``` + +`TryExecuteAsync` 与 `ExecuteAsync` 对称,未命中 key 时返回 `false` 而不抛异常。 + +DI 场景:`{Contract}Registry.RegisterDi(services)` 后从容器解析 `IStrategyRegistry<string, TContract>`,再 `await registry.ExecuteAsync<...>(...)` 或 `Get(key).ExecuteAsync(...)`。 + +#### Guard 谓词 + +`TryGetWithGuard` 在解析策略时额外评估注册的 guard 谓词。guard 返回 `false` 时该策略视为未注册(返回 `false`)。 + +```csharp +var builder = new StrategyRegistryBuilder<string, IPaymentStrategy>() + .Register("alipay", new AlipayPayment(), guard: key => isEnabled("alipay")); +var registry = builder.Build(); + +// guard 通过时返回策略;guard 返回 false 时返回 false +if (registry.TryGetWithGuard("alipay", out var strategy)) { ... } +``` + +> **设计约束**:guard 签名为 `Func<TKey, bool>`(仅接收 key),非 `Func<TInput, bool>`。注册表层面不知道 `TInput`(`TStrategy` 不要求实现 `IStrategy<TInput, TOutput>`),因此无法基于输入判断。基于输入的动态路由是业务逻辑,不在此库范围。 + +源生成器支持:`[RegisterStrategy<TContract>("key", Guard = nameof(CanEnable))]`,生成器校验 guard 方法签名(DP047-DP049)。 + +### 特性(Attribute) + +命名空间:`DesignPatterns.Behavioral` + +#### 泛型版本(C# 11 / .NET 7+) + +```csharp +/// <summary> +/// 标记一个类为某策略接口的实现,并注册到编译期生成的策略注册表中。 +/// </summary> +/// <typeparam name="TContract">策略契约接口。</typeparam> +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] +public sealed class RegisterStrategyAttribute<TContract> : Attribute +{ + /// <summary> + /// 用于解析此策略的 key。 + /// </summary> + public string Key { get; } + + /// <summary> + /// 可选:实现类上的 static guard 方法名。设置后该方法须有签名 + /// <c>static bool Method(TKey key)</c>。guard 返回 false 时该策略视为未注册。 + /// </summary> + public string? Guard { get; set; } + + public RegisterStrategyAttribute(string key) + { + Key = key; + } +} +``` + +#### 非泛型版本(netstandard2.0 / C# 7.3) + +```csharp +/// <summary> +/// 非泛型版本,用于不支持泛型 Attribute 的目标框架。 +/// </summary> +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] +public sealed class RegisterStrategyAttribute : Attribute +{ + public string Key { get; } + + /// <summary> + /// 策略契约接口类型。 + /// </summary> + public Type For { get; } + + /// <summary> + /// 可选:实现类上的 static guard 方法名。签名须为 <c>static bool Method(TKey key)</c>。 + /// </summary> + public string? Guard { get; set; } + + public RegisterStrategyAttribute(string key, Type @for) + { + Key = key; + For = @for; + } +} +``` + +#### 用法示例 + +```csharp +// 泛型 Attribute(推荐,C# 11+) +[RegisterStrategy<IPaymentStrategy>("alipay")] +public class AlipayPayment : IPaymentStrategy { ... } + +[RegisterStrategy<IPaymentStrategy>("wechat")] +public class WechatPayment : IPaymentStrategy { ... } + +// 非泛型(netstandard2.0 / C# 7.3) +[RegisterStrategy("alipay", typeof(IPaymentStrategy))] +public class AlipayPayment : IPaymentStrategy { ... } +``` + +### 生成器产出 + +对于每个 `TContract`,生成器输出: + +#### 1. 强类型 Key 常量 + +```csharp +// PaymentStrategyKeys.g.cs +public static partial class PaymentStrategyKeys +{ + public const string Alipay = "alipay"; + public const string Wechat = "wechat"; +} +``` + +命名规则:`{接口名去掉前缀I和后缀Strategy}Keys`,可通过特性参数 override。 + +#### 2. 静态注册表(无 DI 场景) + +```csharp +// PaymentStrategyRegistry.g.cs +public static partial class PaymentStrategyRegistry +{ + private static readonly IStrategyRegistry<string, IPaymentStrategy> _instance = + new StrategyRegistry<string, IPaymentStrategy>( + new Dictionary<string, IPaymentStrategy> + { + ["alipay"] = new AlipayPayment(), + ["wechat"] = new WechatPayment(), + }); + + public static IStrategyRegistry<string, IPaymentStrategy> Instance => _instance; +} +``` + +#### 3. DI 集成(引用 `DesignPatterns.Extensions.DependencyInjection` 时) + +引用扩展包会自动 Import `build/DesignPatterns.Extensions.DependencyInjection.targets`,设置 `DesignPatterns_EnableDiIntegration=true`,生成器额外输出: + +```csharp +public static partial class PaymentStrategyRegistry +{ + // Instance 仍保留(new() 静态实例,无 DI 生命周期) + + public static IStrategyRegistry<string, IPaymentStrategy> Create(IServiceProvider serviceProvider) => + new ServiceProviderStrategyRegistry<string, IPaymentStrategy>(serviceProvider, _diEntries); + + public static IServiceCollection RegisterDi( + IServiceCollection services, + ServiceLifetime implementationLifetime = ServiceLifetime.Singleton, + ServiceLifetime registryLifetime = ServiceLifetime.Singleton); +} +``` + +推荐用法: + +```csharp +var services = new ServiceCollection(); +PaymentStrategyRegistry.RegisterDi(services); +var registry = services.BuildServiceProvider() + .GetRequiredService<IStrategyRegistry<string, IPaymentStrategy>>(); +``` + +| 生命周期组合 | 行为 | +|--------------|------| +| Singleton 实现 + Singleton 注册表 | 默认;`TryGet` 每次解析同一实现实例 | +| Transient 实现 + Singleton 注册表 | `TryGet` 每次从容器取新实例(推荐需要可变策略时) | +| Transient 注册表 | 每次解析注册表时重建 `Create(sp)` | + +手动 Builder 仍可用:`services.AddStrategyRegistry<string, IPaymentStrategy>(...)`(见扩展包),与生成器互不冲突。 + +## 诊断 + +| ID | 级别 | 触发条件 | 消息格式 | +|----|------|----------|----------| +| DP003 | Error | 同一 `TContract` 下 key 重复 | 编译期检测冲突 | +| DP004 | Error | 标记的类未实现指定的 `TContract` | 接口不匹配 | +| DP006 | Info | 实现了某策略契约但未加 `[RegisterStrategy]` | 建议添加特性 | +| DP007 | Error | 标记的类缺少 public 无参构造 | 无法 `new()` 实例化 | +| DP047 | Error | `Guard` 指定的方法在实现类上未找到 | 添加 static 方法或移除 Guard | +| DP048 | Error | `Guard` 指定的方法非 static | 改为 static | +| DP049 | Error | `Guard` 指定的方法签名错误(须 `static bool Method(TKey key)`) | 修正参数类型或返回类型 | + +> **注意**:DP005 属于 Handler(`[HandlerOrder]` 重复 Order),不属于 Strategy。 + +## 不变量 / 兼容基线 + +1. **key 唯一性**:同一 `TContract` 下 key 不可重复(DP003 在编译期强制)。 +2. **`TStrategy` 不变(invariant)**:`IStrategyRegistry<TKey, TStrategy>` 的 `TStrategy` 非协变 `out`——`TryGetWithGuard` 的 `out TStrategy` 参数阻止协变。 +3. **guard 签名固定为 `Func<TKey, bool>`**:guard 仅接收 key,不接收 `TInput`。注册表层面不知道 `TInput`(`TStrategy` 不要求实现 `IStrategy<TInput, TOutput>`)。 +4. **`[RegisterStrategy]` 不要求实现 `IStrategy<,>`**:任何接口/基类均可作为策略契约。 +5. **标记的类须有 public 无参构造**:生成器使用 `new()` 实例化(DP007)。 +6. **标记的类须实现指定的 `TContract`**(DP004)。 +7. **`IStrategy<,>` / `IAsyncStrategy<,>` 为可选标记接口**:注册表与生成器不依赖它们。 + +### 兼容基线 + +- 运行时 TFM:`netstandard2.0` + `net8.0`(两者均须可用并随包分发)。 +- 泛型 Attribute(`RegisterStrategyAttribute<TContract>`)需要 C# 11+ / `net7.0+`;`netstandard2.0` 目标下用 `#if NET7_0_OR_GREATER` 条件编译。 +- 非泛型 `RegisterStrategyAttribute` 始终可用,功能等价。 +- net8.0 上 `StrategyRegistry<TKey, TStrategy>` 内部使用 `FrozenDictionary` 优化查找。 + ## 实现概览 ### 运行时 @@ -39,7 +321,7 @@ Strategy 模式允许在运行时按条件选择算法/行为实现,避免大 2. **静态注册表**(`{ContractName}Registry`),包含 `Instance` 静态属性(`new()` 静态单例,无 DI 生命周期)。 3. **DI 集成方法**(引用 `DesignPatterns.Extensions.DependencyInjection` 时自动启用):`Create(IServiceProvider)` 返回 `ServiceProviderStrategyRegistry`,`RegisterDi(IServiceCollection, ...)` 注册到容器。 -### 诊断 +### 诊断检测细节 | 检测点 | 逻辑 | 报告位置 | |--------|------|----------| @@ -116,8 +398,6 @@ guard 签名固定为 `Func<TKey, bool>`(仅接收 key),不接收 `TInput` | 共享抽象 | `IStrategyRegistry` 继承 `IReadOnlyRegistry` | `IFactoryRegistry` 继承 `IReadOnlyRegistry` | | DI | `PaymentStrategyRegistry.RegisterDi(services)` | `ProductFactoryRegistry.RegisterDi(services)` | -详见 [FactoryRegistry.md](../spec/FactoryRegistry.md)。 - ## 已知局限 - **不做 DI 生命周期管理**:注册表不替代 DI 容器的生命周期管理;`Instance` 为 `new()` 静态单例,无 DI 生命周期。 @@ -131,7 +411,5 @@ guard 签名固定为 `Func<TKey, bool>`(仅接收 key),不接收 `TInput` - GoF: Strategy (Behavioral) - [AGENTS.md](../../AGENTS.md) — 项目规则与里程碑 - [docs/DEVELOPMENT.md](../DEVELOPMENT.md) — 通用开发约定 -- [Spec: Strategy](../spec/Strategy.md) — 稳定契约(API 面、诊断 ID、不变量) -- [FactoryRegistry.md](../spec/FactoryRegistry.md) — Factory Registry 模式文档 - [Autofac.md](../Autofac.md) — Autofac 扩展 - [PluginAssemblies.md](../PluginAssemblies.md) — 插件程序集布局 diff --git a/docs/design/_template.md b/docs/design/_template.md index 50527de..402c9ec 100644 --- a/docs/design/_template.md +++ b/docs/design/_template.md @@ -1,32 +1,33 @@ # Design Doc: <模式名> -> **关联 Spec**:[docs/spec/<PatternName>.md](../spec/<PatternName>.md) -> **关联 RFC**:[docs/rfc/XXX.md](../rfc/XXX.md)(如有) > **关联 ADR**:ADR-XXX(如有) +> **关联 Issue**:#XXX(可选) ## 概述 ## 设计目标 -## 实现概览 +## API 面 -### 运行时 +(公开运行时 API、Attribute 与生成器产出) -(关键类、数据结构、算法) +## 诊断 -### 源生成器 +(诊断 ID、级别、触发条件与消息) -(增量管线阶段、EquatableArray 结构) +## 不变量 / 兼容基线 -### 诊断 +(语义约束、目标框架与版本兼容要求) -(检测逻辑、报告位置) +## 实现概览 -## 设计权衡 +### 运行时 + +### 源生成器 -### 选择了 X 而非 Y,因为... +### 诊断检测逻辑 -(链接到 ADR) +## 设计权衡 ## 与生态的边界 diff --git a/docs/plans/README.md b/docs/plans/README.md deleted file mode 100644 index 8b8f36f..0000000 --- a/docs/plans/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Plan 状态板 - -大型任务计划(跨多 PR / 跨多阶段)。小型任务直接使用 GitHub Issue,不建计划文档。 - -- **格式与生命周期**:见 [DOCUMENTATION.md](../DOCUMENTATION.md#7-plan--任务计划) -- **模板**:[_template.md](_template.md) -- **归档**:已完成 / 已取消的 Plan 移入 [archive/](archive/) - -## 活跃 Plan - -| Plan | 标题 | 状态 | 创建日期 | 关联 RFC | -|------|------|----------|----------|----------| -| [SingletonLifecycleDiagnostics.md](SingletonLifecycleDiagnostics.md) | Singleton 生命周期诊断 | Active | 2026-07-08 | [RFC](../rfc/SingletonLifecycleDiagnostics.md) | - -## 归档 Plan - -| Plan | 标题 | 最终状态 | 创建日期 | 关联 RFC | -|------|------|----------|----------|----------| -| — | — | — | — | — | diff --git a/docs/plans/SingletonLifecycleDiagnostics.md b/docs/plans/SingletonLifecycleDiagnostics.md deleted file mode 100644 index c2a4346..0000000 --- a/docs/plans/SingletonLifecycleDiagnostics.md +++ /dev/null @@ -1,54 +0,0 @@ -# Plan: Singleton 生命周期诊断 - -> **状态**:Active -> **创建**:2026-07-08 -> **更新**:2026-07-08 -> **关联 RFC**:[docs/rfc/SingletonLifecycleDiagnostics.md](../rfc/SingletonLifecycleDiagnostics.md) -> **关联 Issue**:(待创建) -> **关联 Roadmap**:F3 — Singleton 生命周期诊断 - -## 目标 - -按 RFC 三阶段交付 Singleton 生命周期编译期诊断:P1 补齐 DP062 缺口 → P2 `[GenerateSingleton]` 增强 → P3 静态可变单例反模式检测。每阶段独立 PR,遵守 AGENTS.md 单模块边界。 - -## 非目标 - -- 不在本 Plan 内发版(发版由维护者单独决策)。 -- 不在首版实现 CodeFix(除非某阶段 Review 结论要求)。 - -## 里程碑拆解 - -| 阶段 | 内容 | 模块(AGENTS.md 边界) | 状态 | PR | -|------|------|------------------------|------|-----| -| **P0** | RFC Draft → 设计评审([Review](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md))→ Accepted → [ADR-008](../adr/ADR-008-singleton-lifecycle-diagnostics.md) | Docs | [x] | (直接提交) | -| **P1** | 扩展 captive dependency:Autofac / RegisterAutofac 注册收集(符号名匹配)、工厂委托 lambda 分析;DP066;CaptiveDependencyAnalyzer 测试 | Analyzers | [x] | (直接提交) | -| **P2a** | `GenerateSingletonAttribute.InitializeAsync` + 生成 `GetInstanceAsync`(`Lazy<Task<T>>`)+ 生成器 DP067 | Runtime + SourceGenerators(拆 2 PR) | [ ] | — | -| **P2b** | DP068 DI 混用警告 + DP069 ThreadSafe 提示 | Analyzers | [ ] | — | -| **P2** | Spec `docs/spec/Singleton.md` + Design Doc + Sample 更新(可选) | Docs | [ ] | — | -| **P3** | DP070 / DP071 静态可变单例启发式检测 | Analyzers | [ ] | — | -| **收尾** | CHANGELOG、`AGENTS.md` 诊断表、DesignPatterns.Docs diagnostics 页、RFC → Implemented 归档 | Docs | [ ] | — | - -## 验收标准 - -- [x] P1:Autofac `SingleInstance` 构造函数 captive + 工厂委托 captive(DP066)有 Verify 测试;DP066 已登记并发布到 `AnalyzerReleases` -- [ ] P2a:`InitializeAsync` 指定后生成 `GetInstanceAsync` 且不生成同步 `Instance`;无效签名报 DP067;netstandard2.0 / net8.0 测试通过 -- [ ] P2b:同时 `[GenerateSingleton]` + `AddSingleton` 报 DP068;`ThreadSafe=false` 场景报 DP069 -- [ ] P3:典型 static mutable singleton 样本报 DP070;与 DI 双重注册报 DP071;误报样本不报 -- [ ] 全仓 `./build.ps1 --target Ci --configuration Release` 零警告 -- [ ] RFC 状态 → Implemented;本 Plan → Done 并归档 - -## 风险与依赖 - -| 风险 | 缓解 | -|------|------| -| P1 工厂委托 lambda 静态分析覆盖不全(间接调用、方法组) | 首版仅分析直接 `GetRequiredService` / `GetService` 调用;限制记入 Design Doc | -| P2a `Lazy<Task<T>>` 初始化失败缓存异常 Task | Design Doc 记录该 `Lazy` 语义;必要时用 `LazyThreadSafetyMode.PublicationOnly` 权衡 | -| P3 启发式误报 | 默认 Info;实现 PR 用 Samples 与测试固件调参 | - -## 变更记录 - -| 日期 | 调整 | 原因 | -|------|------|------| -| 2026-07-08 | 初版 Plan 自 RFC Draft 创建 | 用户选定完整 RFC 三阶段范围 | -| 2026-07-08 | P1 删除字段注入项(DP066 改为工厂委托 captive);P2a 改 `GetInstanceAsync` 设计 | 设计评审 Blocker #1/#2([Review](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md)) | -| 2026-07-08 | P1 完成:Autofac 注册收集 + DP066 工厂委托分析 + 11 个 Verify 测试 | P1 实现落地 | diff --git a/docs/plans/_template.md b/docs/plans/_template.md deleted file mode 100644 index 48a49ba..0000000 --- a/docs/plans/_template.md +++ /dev/null @@ -1,39 +0,0 @@ -# Plan: <标题> - -> **状态**:Active -> **创建**:YYYY-MM-DD -> **更新**:YYYY-MM-DD -> **关联 RFC**:[docs/rfc/XXX.md](../rfc/XXX.md)(如有) -> **关联 Issue**:#XXX -> **关联 Roadmap**:FXX(如有) - -## 目标 - -一段话说明要交付什么。 - -## 非目标 - -本计划不覆盖的内容。 - -## 里程碑拆解 - -<!-- 每个里程碑对应一个 PR,模块须对齐 AGENTS.md 单模块 PR 边界 --> - -| 阶段 | 内容 | 模块(AGENTS.md 边界) | 状态 | PR | -|------|------|------------------------|------|-----| -| P1 | ... | Runtime | [ ] | — | -| P2 | ... | SourceGenerators | [ ] | — | - -## 验收标准 - -- [ ] ... - -## 风险与依赖 - -## 变更记录 - -<!-- 计划执行中的重大调整(范围增减、顺序变更)在此追加,不删除原文 --> - -| 日期 | 调整 | 原因 | -|------|------|------| -| — | — | — | diff --git a/docs/plans/archive/.gitkeep b/docs/plans/archive/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/review/2026-07-08-singleton-lifecycle-diagnostics-design.md b/docs/review/2026-07-08-singleton-lifecycle-diagnostics-design.md deleted file mode 100644 index 89851a3..0000000 --- a/docs/review/2026-07-08-singleton-lifecycle-diagnostics-design.md +++ /dev/null @@ -1,44 +0,0 @@ -# Review: Singleton 生命周期诊断 — RFC 设计评审 - -> **状态**:Final -> **类型**:Design -> **日期**:2026-07-08 -> **评审对象**:[docs/rfc/SingletonLifecycleDiagnostics.md](../rfc/SingletonLifecycleDiagnostics.md)(Draft 初版) -> **评审人**:Agent(对照代码库核查) - -## 评审范围 - -RFC Draft 初版全部章节:P1 DP062 缺口补齐、P2 `[GenerateSingleton]` 增强、P3 静态可变单例检测、诊断 ID 分配(DP066–DP071)、开放问题 1–4。核查依据:`CaptiveDependencyAnalyzer.cs`(DP062 现实现)、`DesignPatterns.Analyzers.csproj`(依赖结构)、`SingletonSyntaxFactory.cs` / `GenerateSingletonAttribute.cs`(生成器现状)。 - -## 结论 - -**有条件通过** — 修正 2 个 Blocker、1 个 Major 后可进入 Accepted;诊断 ID 分配与三阶段结构合理,无需推翻。 - -## 发现 - -| # | 级别 | 发现 | 建议 | -|---|------|------|------| -| 1 | Blocker | P2a 设计「`Instance` getter 在首次访问时 await 初始化」不可实现——C# 属性 getter 不能 `await`;若同步阻塞(`.GetAwaiter().GetResult()`)则违反「异步一等」原则且有死锁风险 | 指定 `InitializeAsync` 时**不生成**同步 `Instance`,改为生成 `static ValueTask<T> GetInstanceAsync(CancellationToken)`,内部用 `Lazy<Task<T>>`(netstandard2.0 可用);两种形态互斥,语义清晰 | -| 2 | Blocker | P1「字段/属性注入」以 `[FromServices]` 为触发条件不成立——`[FromServices]` 是 ASP.NET Core MVC 参数绑定特性,MSDI 本身**没有**字段/属性注入;按原设计 DP066 永远不会在纯 MSDI 代码中触发 | 首版删除字段/属性注入项;Autofac `PropertiesAutowired()` 属性注入列为「不在范围(后续评估)」。DP066 改指**工厂委托 captive dependency**(`AddSingleton<T>(sp => ...)` lambda 内解析 Scoped/Transient),该项原本悬而未决,正好补位 | -| 3 | Major | 开放问题 2(Analyzer 是否引用 Autofac 程序集)实际已有先例可循——现有 DP060/DP062 对 MSDI **未加包引用**,通过 `ToDisplayString()` 符号全名匹配(`CaptiveDependencyAnalyzer.cs:293`);Analyzer 程序集 `IncludeBuildOutput=false`,不应携带第三方依赖 | Autofac 检测同样用**符号全名字符串匹配**(`Autofac.ContainerBuilder`、`SingleInstance` 等),不新增 PackageReference;开放问题 2 关闭 | -| 4 | Minor | DP069 触发条件「含 async 成员或实现 IDisposable」与线程安全风险相关性弱——`IDisposable` 与 `LazyThreadSafetyMode.None` 并无直接冲突 | 触发条件改为「`ThreadSafe = false` 且类型含**非 readonly 实例字段**(可变状态)」,直指竞态本质;保持 Info 级别 | -| 5 | Minor | 开放问题 1(`Lazy<Task<T>>` vs 自定义 `AsyncLazy`)可直接决策——`Lazy<Task<T>>` 在 netstandard2.0 完全可用,且本库「primitives 而非框架」原则不支持为单一场景新增公共 primitive | 采用 `Lazy<Task<T>>`(生成代码内部实现细节,不暴露公共类型);开放问题 1 关闭 | -| 6 | Nit | DP070 启发式仅覆盖 static 字段,遗漏 `public static T Instance { get; set; }` 可变属性形态 | 启发式补充「public static 带 setter 的引用类型属性」 | -| 7 | Nit | 开放问题 4(CodeFix)建议直接定稿 | 首版仅诊断,不做 CodeFix;后续按需求迭代(与 DP010–012 处理方式一致) | - -## 行动项 - -- [x] #1 → RFC P2a 改为 `GetInstanceAsync` 设计(本次评审随附修改) -- [x] #2 → RFC P1 重构:删除字段注入、DP066 重定义为工厂委托 captive(本次评审随附修改) -- [x] #3 → RFC 非目标与开放问题更新:Autofac 符号名匹配定稿(本次评审随附修改) -- [x] #4 → RFC DP069 触发条件修正(本次评审随附修改) -- [x] #5 → RFC 开放问题 1 关闭(本次评审随附修改) -- [x] #6 → RFC DP070 启发式补充(本次评审随附修改) -- [x] #7 → RFC 开放问题 4 关闭(本次评审随附修改) - -## 参考 - -- [CaptiveDependencyAnalyzer.cs](../../DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs) — DP062 符号匹配实现 -- [DesignPatterns.Analyzers.csproj](../../DesignPatterns.Analyzers/DesignPatterns.Analyzers.csproj) — Analyzer 依赖结构(无 MSDI/Autofac 包引用) -- [ADR-001 Primitives over frameworks](../adr/ADR-001-primitives-over-frameworks.md) -- [ADR-004 Core does not reference MSDI](../adr/ADR-004-core-does-not-reference-msdi.md) diff --git a/docs/review/README.md b/docs/review/README.md deleted file mode 100644 index 4cc613d..0000000 --- a/docs/review/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Review 索引 - -评审记录 — 设计评审、实现回顾、发版审查、技术债审查。单 PR 的常规 code review 在 PR Comments 中进行,不建文档。 - -- **格式与生命周期**:见 [DOCUMENTATION.md](../DOCUMENTATION.md#8-review--评审记录) -- **模板**:[_template.md](_template.md) -- **归档**:Final 且行动项全部关闭的 Review 移入 [archive/](archive/) - -## 行动项未关闭 - -| Review | 类型 | 结论 | 日期 | -|--------|------|------|------| -| [2026-07-08-singleton-lifecycle-diagnostics-design.md](2026-07-08-singleton-lifecycle-diagnostics-design.md) | Design | 有条件通过(修订已随评审完成,待 RFC Accepted 后归档) | 2026-07-08 | - -## 归档 Review - -| Review | 类型 | 结论 | 日期 | -|--------|------|------|------| -| — | — | — | — | diff --git a/docs/review/_template.md b/docs/review/_template.md deleted file mode 100644 index b595f20..0000000 --- a/docs/review/_template.md +++ /dev/null @@ -1,31 +0,0 @@ -# Review: <标题> - -> **状态**:Draft -> **类型**:Design | Implementation | Release | Retrospective -> **日期**:YYYY-MM-DD -> **评审对象**:RFC / Plan / PR 范围 / 版本号 -> **评审人**:维护者 / Agent(注明) - -## 评审范围 - -评审了什么(文档、代码范围、版本)。 - -## 结论 - -通过 | 有条件通过 | 不通过,一句话概括。 - -## 发现 - -<!-- 级别:Blocker(不修复不得合并/发版)、Major(须建 Issue 跟踪)、Minor(建议修复)、Nit(可忽略) --> - -| # | 级别 | 发现 | 建议 | -|---|------|------|------| -| 1 | Blocker / Major / Minor / Nit | ... | ... | - -## 行动项 - -<!-- Final 后正文不可变;仅允许勾选 checkbox、补充 Issue/PR 链接 --> - -- [ ] #1 → Issue #XXX / PR #XXX - -## 参考 diff --git a/docs/review/archive/.gitkeep b/docs/review/archive/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/rfc/README.md b/docs/rfc/README.md deleted file mode 100644 index 55ae7b5..0000000 --- a/docs/rfc/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# RFC 状态板 - -Request for Comments — 设计提案与讨论记录。 - -- **格式与生命周期**:见 [DOCUMENTATION.md](../DOCUMENTATION.md#2-rfc--request-for-comments) -- **模板**:[_template.md](_template.md) -- **归档**:已实现 / 已否决 / 已取代的 RFC 移入 [archive/](archive/) - -## 活跃 RFC - -| RFC | 标题 | 状态 | 创建日期 | 关联 Plan | -|-----|------|------|----------|-----------| -| [SingletonLifecycleDiagnostics.md](SingletonLifecycleDiagnostics.md) | Singleton 生命周期诊断 | Accepted | 2026-07-08 | [Plan](../plans/SingletonLifecycleDiagnostics.md) | - -## 归档 RFC - -| RFC | 标题 | 最终状态 | 创建日期 | 衍生 ADR | -|-----|------|----------|----------|----------| -| [StateTransitionTable.md](archive/StateTransitionTable.md) | State 转换表 | Implemented | 2026-06-14 | ADR-005 | -| [HierarchicalStateMachine.md](archive/HierarchicalStateMachine.md) | State 层级状态机 | Implemented | 2026-06-28 | ADR-005 | -| [CompositeParallelTraversal.md](archive/CompositeParallelTraversal.md) | Composite 并行遍历 | Implemented | 2026-06-30 | ADR-006 | -| [CompositeTreeSchemaValidation.md](archive/CompositeTreeSchemaValidation.md) | Composite 树 schema 校验 | Implemented | 2026-07-07 | ADR-007 | diff --git a/docs/rfc/SingletonLifecycleDiagnostics.md b/docs/rfc/SingletonLifecycleDiagnostics.md deleted file mode 100644 index aac0494..0000000 --- a/docs/rfc/SingletonLifecycleDiagnostics.md +++ /dev/null @@ -1,217 +0,0 @@ -# RFC: Singleton 生命周期诊断 - -> **状态**:Accepted -> **类型**:Feature -> **创建**:2026-07-08 -> **更新**:2026-07-08(设计评审修订并 Accepted,见[评审记录](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md)) -> **作者**:维护者 -> **关联 Roadmap**:F3(长期探索候选 — Singleton 生命周期诊断) -> **关联 Issue**:(待创建) -> **衍生 ADR**:[ADR-008](../adr/ADR-008-singleton-lifecycle-diagnostics.md) - -## 摘要 - -在现有 **DP062**(Singleton 构造函数捕获 Scoped/Transient 服务)基础上,分三阶段扩展 Singleton 生命周期相关编译期诊断:补齐 DI 注册覆盖缺口、增强 `[GenerateSingleton]` 生命周期安全、检测静态可变单例反模式。目标是在不引入厚重框架的前提下,最大化「Analyzer + 源生成器」协同的 DI 反模式防护价值。 - -## 动机 - -| 现状 | 局限 | -|------|------| -| **DP062** 已覆盖 MSDI `AddSingleton` / `TryAdd` 及生成器 `RegisterDi` 的**构造函数注入** captive dependency | 未覆盖 Autofac `RegisterAutofac` 注册、`AddSingleton` 工厂委托注册 | -| **`[GenerateSingleton]`** 生成 `Lazy<T>` + `Instance`,与 DI 生命周期**显式无关**(见特性 XML 文档) | 无 async 初始化路径;与 DI 混用时无警告;`ThreadSafe = false` 无场景提示 | -| **DEVELOPMENT.md** 已声明「诊断可提示静态可变单例,推荐 DI 生命周期」 | 尚无对应 Analyzer | - -生产场景中 Singleton 生命周期错误(captive dependency、静态可变状态、编译期单例与容器单例混用)是高频 DI 反模式;本 RFC 将 ROADMAP 候选「Singleton 生命周期诊断」落地为可排期的三阶段计划。 - -## 非目标 - -- 不实现完整 DI 容器或替换 MSDI / Autofac。 -- 不将 `[GenerateSingleton]` 与 DI 生命周期**合并**为统一单例模型(二者语义保持分离)。 -- 不检测所有可能的运行时 captive dependency(如 `IServiceProvider` 手动 resolve、反射注入)。 -- **Analyzer 不引用 Autofac 包**——沿用 DP060/DP062 对 MSDI 的做法:符号全名字符串匹配(`ToDisplayString()`),Analyzer 程序集保持零第三方依赖(`IncludeBuildOutput=false`)。 -- 不检测字段/属性注入——MSDI 无此机制;Autofac `PropertiesAutowired()` 属性注入列为后续评估项,首版不做。 -- 不重复 **DP060**(`RegisterDi` registry lifetime > implementation lifetime)或 **DP061**(wasteful lifetime mismatch)的语义。 - ---- - -## 设计方案 - -### 概念模型 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ DI 容器注册(MSDI / Autofac / RegisterDi / RegisterAutofac)│ -│ → 构建 Type → Lifetime 映射 │ -│ → 对 Singleton 实现检查依赖来源(ctor / 工厂委托 lambda) │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ [GenerateSingleton](编译期 Lazy<T>,非 DI) │ -│ → 可选 async 初始化钩子 │ -│ → 与 DI 注册混用警告 │ -│ → ThreadSafe=false 场景提示 │ -└─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ 静态可变单例反模式(无特性、无 DI) │ -│ → 检测 public static 可变字段 / 典型 ServiceLocator 形态 │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 阶段 P1 — 补齐 DP062 缺口(Analyzer 模块) - -**目标**:扩展 `CaptiveDependencyAnalyzer`(或同模块姊妹 Analyzer),不修改 DP062 已发布语义。 - -| 增强项 | 说明 | 诊断 | -|--------|------|------| -| **Autofac 注册收集** | 识别 `RegisterType<T>().SingleInstance()`、`Register(_ => ...).SingleInstance()`、`RegisterAutofac` 生成方法内的 `SingleInstance()` / `InstancePerDependency()` 等;映射 implementation type → lifetime。检测采用**符号全名匹配**(如 `Autofac.ContainerBuilder`),不引入 Autofac 包引用 | 复用 **DP062**(同一 captive dependency 语义,构造函数注入) | -| **工厂委托注册** | `AddSingleton<T>(sp => new T(...))` / `AddSingleton(sp => ...)` 中静态分析 lambda 体内的 `GetRequiredService<X>()` / `GetService<X>()` 调用;若 `X` 在 lifetime map 中为 Scoped/Transient → 报告 | **DP066**(新,独立 ID:修复建议与构造函数场景不同) | -| **多构造函数** | 文档化限制:当前取首个 public 实例构造函数;评估是否改为「最长 public 构造函数」对齐 `ActivatorUtilities` | 无新 ID;Design Doc 记录 | - -**不变量**: - -- DP062 消息格式与严重性(Warning)不变。 -- Analyzer 仍放在 `DesignPatterns.Analyzers`,遵守 ADR-004;对 MSDI / Autofac 均为符号名匹配,无包引用。 - -> 评审备注:初稿的「字段/属性注入(`[FromServices]`)」项经评审移除——MSDI 没有字段/属性注入机制,`[FromServices]` 是 ASP.NET Core MVC 参数绑定特性;见[评审记录 #2](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md)。 - -### 阶段 P2 — `[GenerateSingleton]` 增强(SourceGenerators + Analyzers) - -**目标**:增强编译期单例生成器的生命周期安全,不与 DI Singleton 混淆。 - -#### P2a — Async 初始化(SourceGenerators 模块) - -新增可选特性属性: - -```csharp -public sealed class GenerateSingletonAttribute : Attribute -{ - // 现有 - public bool ThreadSafe { get; set; } = true; - - /// <summary> - /// Optional static method name for async initialization invoked once - /// before the instance is returned from the generated GetInstanceAsync(). - /// When set, the synchronous Instance property is not generated. - /// Signature: static ValueTask|Task MethodName(T instance, CancellationToken ct) - /// </summary> - public string? InitializeAsync { get; set; } -} -``` - -生成器行为(**两种形态互斥**): - -- 未指定 `InitializeAsync`(默认):生成现有 `Lazy<T>` + 同步 `Instance` 属性,行为不变。 -- 指定 `InitializeAsync`:**不生成**同步 `Instance`,改为生成: - -```csharp -private static readonly Lazy<Task<T>> _instance = new(async () => -{ - var instance = new T(); - await T.<InitializeAsync>(instance, CancellationToken.None).ConfigureAwait(false); - return instance; -}, LazyThreadSafetyMode.ExecutionAndPublication); - -/// <summary>Gets the asynchronously initialized singleton instance.</summary> -public static ValueTask<T> GetInstanceAsync() => new(_instance.Value); -``` - -- 生成器校验 `InitializeAsync` 方法存在、static、签名匹配 `static ValueTask|Task Method(T instance, CancellationToken ct)`(**DP067** Error,复用 State guard 签名校验基础设施)。 -- `Lazy<Task<T>>` 为生成代码内部实现,不新增公共 primitive(netstandard2.0 可用;遵守 ADR-001)。 - -> 评审备注:初稿「`Instance` getter 首次访问时 await」不可实现(属性 getter 不能 await,同步阻塞违反异步一等原则);改为互斥的 `GetInstanceAsync` 设计,见[评审记录 #1](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md)。 - -#### P2b — DI 混用与 ThreadSafe 提示(Analyzers 模块) - -| ID | 严重性 | 触发条件 | -|----|--------|----------| -| **DP068** | Warning | 类型同时标注 `[GenerateSingleton]` 且在编译单元内被 MSDI `AddSingleton` / `RegisterDi` / Autofac `SingleInstance` 注册(双重单例,实例不一致风险) | -| **DP069** | Info | `[GenerateSingleton(ThreadSafe = false)]` 且类型含**非 readonly 实例字段**(可变状态 + 无发布屏障 → 竞态风险) | - -**非目标(P2)**:不自动生成 DI 注册代码;不将 `Instance` 注册到容器。 - -### 阶段 P3 — 静态可变单例反模式(Analyzers 模块) - -**目标**:落实 DEVELOPMENT.md「诊断可提示静态可变单例,推荐 DI 生命周期」。 - -| ID | 严重性 | 触发条件 | -|----|--------|----------| -| **DP070** | Info | `public static` 非 readonly 引用类型字段,**或 `public static` 带 setter 的引用类型属性**,且类型名/成员名匹配常见单例命名(`Instance`、`Singleton`、`_instance`)或类为 `sealed` 且仅含 static 访问器 | -| **DP071** | Warning | 上述字段且类型在 DI 容器中也有 Singleton 注册(双重单例形态) | - -**抑制**:`[SuppressMessage]` 或 `#pragma` 标准 Roslyn 抑制;不新增自定义 Suppress 特性。 - -**非目标(P3)**:不检测所有 static mutable 字段(误报控制优先);不强制迁移到 DI。 - ---- - -## 诊断 ID 汇总 - -| ID | 名称 | 严重性 | 归属 | 阶段 | -|----|------|--------|------|------| -| DP062 | CaptiveDependency | Warning | Analyzers | 已有 | -| DP066 | FactoryDelegateCaptiveDependency | Warning | Analyzers | P1 | -| DP067 | GenerateSingletonInvalidInitializer | Error | SourceGenerators | P2a | -| DP068 | GenerateSingletonDiMixing | Warning | Analyzers | P2b | -| DP069 | GenerateSingletonThreadSafetyHint | Info | Analyzers | P2b | -| DP070 | StaticMutableSingleton | Info | Analyzers | P3 | -| DP071 | StaticMutableSingletonWithDi | Warning | Analyzers | P3 | - -> **ID 登记**:实现前须同步 `DiagnosticIds.cs`、`DesignPatternsDiagnosticDescriptors.cs`、`AnalyzerReleases.Unshipped.md`、`AGENTS.md` 诊断表、`docs/spec/`(若影响公共 API)及 DesignPatterns.Docs diagnostics 页。 - ---- - -## API 变更 - -| 变更 | 类型 | 模块 | -|------|------|------| -| `GenerateSingletonAttribute.InitializeAsync` | 非破坏性新增 | Runtime (`DesignPatterns/`) | -| 无其他公共运行时 API 变更 | — | — | - -Spec:若 P2a 落地,须新建或扩展 `docs/spec/Singleton.md`(当前无独立 Spec 文件)。 - ---- - -## 替代方案 - -| 方案 | 否决原因 | -|------|----------| -| 仅文档说明,不新增诊断 | 无法发挥编译期协同探索价值;与项目方针不符 | -| 将 `[GenerateSingleton]` 废弃,统一 DI Singleton | 破坏已有 Sample / 测试;`GenerateSingleton` 与 DI 生命周期语义 intentionally 分离 | -| 单一 mega-Analyzer 覆盖全部阶段 | 违反单模块 PR 边界;难测试、难回滚 | -| 扩展 DP062 语义覆盖工厂委托而不新 ID | 工厂委托与构造函数注入的修复建议不同(改 lambda vs 改构造函数/生命周期);独立 ID 便于 help link 与后续 CodeFix 演进 | - ---- - -## 开放问题 - -1. ~~**P2a Async 初始化**:`Lazy<Task<T>>` vs 自定义 `AsyncLazy` primitive~~ — **已决策**:采用 `Lazy<Task<T>>`(生成代码内部实现,不新增公共 primitive,ADR-001 约束)。见[评审记录 #5](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md)。 -2. ~~**P1 Autofac**:Analyzer 是否引用 Autofac 程序集~~ — **已决策**:符号全名字符串匹配,零包引用(对齐 DP060/DP062 的 MSDI 处理)。见[评审记录 #3](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md)。 -3. **P3 误报率**:`DP070` 启发式规则需在实现 PR 中用真实代码库样本调优(默认 Info 已定,调参留在 P3 实现 PR)。 -4. ~~**CodeFix**~~ — **已决策**:首版仅诊断,不做 CodeFix;后续按需求迭代。见[评审记录 #7](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md)。 - ---- - -## 决策记录 - -**2026-07-08 设计评审**([评审记录](../review/2026-07-08-singleton-lifecycle-diagnostics-design.md),结论:有条件通过): - -1. P2a 改为互斥 `GetInstanceAsync` 设计(原 `Instance` await 方案不可实现)。 -2. P1 移除字段/属性注入项(MSDI 无此机制);DP066 重定义为工厂委托 captive dependency。 -3. Autofac 检测用符号全名匹配,Analyzer 零第三方包引用。 -4. DP069 触发条件改为「非 readonly 实例字段」;DP070 启发式补充可变静态属性。 -5. 首版仅诊断无 CodeFix;`Lazy<Task<T>>` 为内部实现。 - -**2026-07-08 维护者确认 Accepted**,产出 [ADR-008](../adr/ADR-008-singleton-lifecycle-diagnostics.md)。 - ---- - -## 参考 - -- [ADR-004 Core does not reference MSDI](../adr/ADR-004-core-does-not-reference-msdi.md) -- [CaptiveDependencyAnalyzer.cs](../../DesignPatterns.Analyzers/CaptiveDependencyAnalyzer.cs)(DP062 实现) -- [GenerateSingletonAttribute.cs](../../DesignPatterns/Creational/GenerateSingletonAttribute.cs) -- [ROADMAP.md](../ROADMAP.md) — Singleton 生命周期诊断候选 -- [DEVELOPMENT.md](../DEVELOPMENT.md) — Singleton 诊断说明 diff --git a/docs/rfc/_template.md b/docs/rfc/_template.md deleted file mode 100644 index 5a262c8..0000000 --- a/docs/rfc/_template.md +++ /dev/null @@ -1,46 +0,0 @@ -# RFC: <标题> - -> **状态**:Draft -> **类型**:Feature | Pattern | Architecture | Process -> **创建**:YYYY-MM-DD -> **更新**:YYYY-MM-DD -> **作者**:维护者 / 贡献者 -> **关联 Roadmap**:FXX(如有) -> **关联 Issue**:#XXX(如有) -> **衍生 ADR**:ADR-XXX(Accepted 后填写) - -## 摘要 - -一段话说明提案内容。 - -## 动机 - -为什么需要这个变更?现有局限是什么? - -## 非目标 - -明确不做什么,避免范围蔓延。 - -## 设计方案 - -### 概念模型 - -### API 设计 - -### 诊断影响 - -### 实现方案 - -## 替代方案 - -考虑过但否决的方案及原因。 - -## 开放问题 - -讨论中尚未决策的问题(Review 阶段)。 - -## 决策记录 - -Review 结束后记录最终决策与理由(Accepted 阶段填写)。 - -## 参考 diff --git a/docs/rfc/archive/CompositeParallelTraversal.md b/docs/rfc/archive/CompositeParallelTraversal.md deleted file mode 100644 index d141b5b..0000000 --- a/docs/rfc/archive/CompositeParallelTraversal.md +++ /dev/null @@ -1,306 +0,0 @@ -# RFC: Composite 并行遍历 - -> **状态**:Implemented -> **类型**:Feature -> **创建**:2026-06-30 -> **更新**:2026-07-07 -> **作者**:维护者 -> **关联 Roadmap**:F5 -> **关联 Issue**:— -> **衍生 ADR**:[ADR-006](../../adr/ADR-006-composite-parallel-traversal.md) - -## 概述 - -为 Composite 模式新增并行遍历能力(`TraverseParallel` / `TraverseParallelAsync`),支持 BFS 同层并行、DFS 子节点并行递归,配合 `MaxDegreeOfParallelism` 限流与 `MaxParallelDepth` 递归深度保护。 - -懒加载(`ICompositeLazyNode` + `[CompositePart(LazyChildren)]`)作为 Phase 3 独立 RFC,不在本纪要范围。 - -## 设计会议纪要 - -### 参与角色 - -| 角色 | 职责 | -|------|------| -| 运行时库架构师 | API 设计、接口契约、向后兼容性 | -| 并发工程师 | 并发原语、同步机制、性能、死锁/竞态 | -| 风险分析师 / 魔鬼代言人 | 失败模式、竞态条件、误用场景 | -| 真实用户场景分析师 | 实际使用模式、用户心智模型 | - ---- - -## 一、达成共识的议题 - -### 1.1 线程安全责任归属:用户责任 + 文档契约 - -四角色一致同意:库不做运行时防御,线程安全是用户责任。 - -| 维度 | 共识 | -|------|------| -| `Children` 读取 | 组装后视为不可变;并行读取安全(前提:无写入);不做防御性复制 | -| `ShouldSkipSubtree` | 用户责任;不做预评估(O(N) 额外遍历违背并行初衷) | -| visitor 回调 | 纯文档契约;不加 `ISafeVisitor` 标记接口 | -| `IReadOnlyList<T>` | `List<T>` 并行 `foreach` 读取安全(无写入时);.NET 运行时保证 | - -- **架构师**:"库做防御性复制会破坏性能目标、掩盖用户错误。" -- **魔鬼**:"库无法防止用户错误——只能通过文档和 API 设计引导正确使用。" -- **用户分析师**:"模仿 `Parallel.ForEach` 的文档风格——.NET 开发者看到 `TraverseParallel` 会自然联想到线程安全要求。" - -### 1.2 API 形态:独立方法 - -四角色一致同意:使用 `TraverseParallel` / `TraverseParallelAsync` 独立方法,不修改现有 `Traverse` 语义。 - -``` -TraverseParallel(root, visitor, options) // 同步并行 -TraverseParallelAsync(root, asyncVisitor, options, ct) // 异步并行 -TraverseForestParallel(roots, visitor, options) -TraverseForestParallelAsync(roots, asyncVisitor, options, ct) -``` - -**架构师**:"`MaxDegreeOfParallelism` 仅对并行有效,加到 Options 会污染同步遍历的语义。" - -### 1.3 异常传播:`AggregateException` - -四角色一致同意:收集所有异常,抛 `AggregateException`。 - -- **并发工程师**:`ConcurrentQueue<Exception>` 收集 → `AggregateException` 抛出。 -- **架构师**:提供 `ExceptionMode` 选项(Aggregate / FirstException / Suppress)。 -- **魔鬼**:`ContinueOnError` 选项允许部分失败后继续。 - -**合并方案**:Phase 1 默认 `AggregateException`;`ContinueOnError` / `ExceptionMode` 延迟到 Phase 2。 - -### 1.4 async 路径:`ConfigureAwait(false)` - -四角色一致同意:异步并行遍历内部使用 `ConfigureAwait(false)`。 - -- **魔鬼**:"WPF/WinForms 中 `await` 默认捕获 `SynchronizationContext`,多个并行 visitor 同时回到 UI 线程会串行化——完全违背并行初衷。" -- **并发工程师**:`SemaphoreSlim.WaitAsync(ct).ConfigureAwait(false)` + `asyncVisitor(...).ConfigureAwait(false)`。 - -### 1.5 递归深度限制 - -四角色一致同意:限制并行递归深度,超过阈值回退到顺序遍历。 - -- **并发工程师**:`MaxParallelDepth = 32`,超过后顺序递归。 -- **魔鬼**:并行递归 DFS 在循环引用"树"中会创建无限 Task,耗尽线程池——但循环检测默认关闭(开销大)。 - -### 1.6 netstandard2.0 兼容 - -四角色一致同意:`#if` 分支。 - -| TFM | 同步并行 | 异步并行 | -|-----|----------|----------| -| net8.0 | `Parallel.ForEach` | `Parallel.ForEachAsync`(.NET 6+) | -| netstandard2.0 | `Parallel.ForEach` | `SemaphoreSlim` + `Task.WhenAll` | - ---- - -## 二、存在分歧的议题 - -### 2.1 `MaxDegreeOfParallelism = 1` 的行为 - -| 角色 | 立场 | -|------|------| -| 魔鬼 | 抛 `ArgumentException` 或文档警告"不保证顺序" | -| 架构师 | 不特殊处理,用户应直接用 `Traverse` | -| 并发工程师 | 不特殊处理,`Parallel.ForEach` 的 `MaxDop=1` 已有定义 | - -**决议**:不抛异常,文档明确"`MaxDegreeOfParallelism = 1` 不保证顺序;如需顺序请用 `Traverse`"。 - -### 2.2 是否提供线程安全收集器 - -| 角色 | 立场 | -|------|------| -| 用户分析师 | **必须提供** `CollectParallel` API + `CompositeTraversalCollector<T>` | -| 架构师 | 纯文档契约,不加额外 API | -| 魔鬼 | 提供收集器重载,但不强制 | -| 并发工程师 | 不涉及(超出线程安全范围) | - -**用户分析师的关键论据**: - -```csharp -// 用户最自然的写法——会崩溃 -var errors = new List<string>(); -CompositeTraverser.TraverseParallel(root, (node, _, _) => { - if (!node.IsValid()) errors.Add($"{node.Key}: invalid"); -}); -``` - -**决议**:Phase 1 不提供收集器(保持 API 最小化);文档示例展示 `ConcurrentBag<T>` 用法;Phase 2 根据用户反馈再考虑 `CollectParallel`。 - -### 2.3 `ExceptionMode` vs `ContinueOnError` - -| 角色 | 立场 | -|------|------| -| 架构师 | `ParallelExceptionMode` 枚举(Aggregate / First / Suppress) | -| 魔鬼 | `ContinueOnError` 布尔选项 | -| 并发工程师 | 默认 `AggregateException`,不需要选项 | - -**决议**:Phase 1 只实现 `AggregateException`(默认行为)。`ContinueOnError` / `ExceptionMode` 延迟到 Phase 2 根据用户反馈再加。 - -### 2.4 默认并行度 - -| 角色 | 立场 | -|------|------| -| 魔鬼 | `Math.Min(ProcessorCount, 4)` 保守默认 | -| 架构师 | `Environment.ProcessorCount` | -| 并发工程师 | `Environment.ProcessorCount` | -| 用户分析师 | `Environment.ProcessorCount`(与 `Parallel.ForEach` 一致) | - -**魔鬼的关键论据**:Docker 容器中 `ProcessorCount` 可能返回宿主机 CPU 数而非容器限制。 - -**决议**:默认 `Environment.ProcessorCount`(与 .NET 生态一致);文档警告容器化环境需手动设置。 - -### 2.5 循环检测 - -| 角色 | 立场 | -|------|------| -| 魔鬼 | 提供 `DetectCycles` 选项(默认关闭) | -| 其他三角色 | 不需要(树结构假设无循环) | - -**决议**:不实现循环检测。文档明确"Composite 树必须无循环引用"。顺序遍历的栈溢出是快速失败,已足够。 - -### 2.6 测试可重复性 - -| 角色 | 立场 | -|------|------| -| 魔鬼 | `ForceSequentialForTesting` 选项 | -| 架构师 | 不需要选项,测试用 `ConcurrentBag` + count 断言 | -| 并发工程师 | `MaxDegreeOfParallelism = 1` 用于逻辑测试 | - -**决议**:不提供 `ForceSequentialForTesting`。测试策略: -- 逻辑正确性:`ConcurrentBag<T>` + count + 集合断言(不断言顺序) -- 竞态测试:`MaxDegreeOfParallelism = Environment.ProcessorCount` + 多次运行 - ---- - -## 三、最终技术方案 - -### 3.1 `CompositeTraversalOptions` 新增属性 - -```csharp -/// <summary> -/// Maximum degree of parallelism for parallel traversals. -/// null means Environment.ProcessorCount. -/// WARNING: Setting to 1 does NOT guarantee visitation order; -/// use Traverse() for ordered traversal. -/// </summary> -public int? MaxDegreeOfParallelism { get; set; } - -/// <summary> -/// Maximum depth for parallel recursion. Beyond this depth, -/// traversal falls back to sequential. Default is 32. -/// </summary> -public int MaxParallelDepth { get; set; } = 32; -``` - -### 3.2 同步并行实现 - -| 遍历顺序 | 策略 | -|----------|------| -| BFS | 逐层提取 → `Parallel.ForEach` 同层并行 → 收集下一层 | -| DFS PreOrder | 根节点同步访问 → 子节点 `Parallel.ForEach` 并行递归 | -| DFS PostOrder | 子节点 `Parallel.ForEach` 并行递归 → 全部完成后访问根节点 | -| 深度 > MaxParallelDepth | 回退到顺序递归 | - -### 3.3 异步并行实现 - -| TFM | 策略 | -|-----|------| -| net8.0 | `Parallel.ForEachAsync` + `ConfigureAwait(false)` | -| netstandard2.0 | `SemaphoreSlim` + `Task.WhenAll` + `ConfigureAwait(false)` | - -### 3.4 异常处理 - -```csharp -var exceptions = new ConcurrentQueue<Exception>(); - -// 每个 visitor 调用: -try { visitor(node, depth, idx); } -catch (Exception ex) { exceptions.Enqueue(ex); } - -// 遍历结束后: -if (!exceptions.IsEmpty) - throw new AggregateException(exceptions); -``` - -### 3.5 取消传播 - -- 直接传递 `CancellationToken`,不使用 `CreateLinkedTokenSource` -- `Parallel.ForEach` / `SemaphoreSlim.WaitAsync` 自动响应取消 -- 已启动的 visitor 继续完成;未启动的任务取消 - -### 3.6 文档契约(XML doc) - -``` -TraverseParallel: - "The visitor may be invoked concurrently from multiple threads. - Ensure thread safety: use ConcurrentBag<T> for accumulation, - lock/SemaphoreSlim for shared state. Order is non-deterministic; - use Traverse() for ordered traversal." - -MaxDegreeOfParallelism: - "null = Environment.ProcessorCount. Setting to 1 does NOT - guarantee order. In containerized environments, manually set - this to match CPU limits." -``` - ---- - -## 四、风险矩阵与决议 - -| 风险 | 严重度 | 库层面解决 | 决议 | -|------|--------|-----------|------| -| visitor 共享状态崩溃 | 高 | 否(用户责任) | 文档警告 + `ConcurrentBag` 示例 | -| `ShouldSkipSubtree` 竞态 | 中 | 否(用户责任) | 文档警告 | -| `Children` 懒加载非线程安全 | 高 | 否(用户责任) | 文档要求组装后不可变 | -| 死锁 | 中 | 库内部无锁 | 文档警告避免 visitor+skip 共享锁 | -| `MaxDop=1` 误导 | 中 | 文档 | 不抛异常,文档明确 | -| async `SynchronizationContext` | 高 | 是 | `ConfigureAwait(false)` | -| 异常资源泄漏 | 中 | 否(用户责任) | 文档警告 + `using` | -| 循环引用 | 高 | 否 | 文档要求无循环 | -| 容器 `ProcessorCount` | 中 | 部分 | 文档警告 | -| 测试可重复性 | 中 | 否 | 测试策略指南 | - ---- - -## 五、行动计划 - -| Phase | 内容 | 优先级 | -|-------|------|--------| -| **Phase 1** | `TraverseParallel` + `TraverseParallelAsync` + `TraverseForestParallel` + `TraverseForestParallelAsync` + `MaxDegreeOfParallelism` + `MaxParallelDepth` + `AggregateException` + `ConfigureAwait(false)` + 文档 + 测试 | 立即 | -| **Phase 2** | `ContinueOnError` 选项 + `CollectParallel` API + `ExceptionMode` 枚举 | 根据用户反馈 | -| **Phase 3** | 懒加载(`ICompositeLazyNode` + `[CompositePart(LazyChildren)]` + 生成器) | 独立 RFC | - ---- - -## 六、用户场景分析摘要 - -### 适合并行遍历的场景 - -| 场景 | 原因 | -|------|------| -| 配置树校验(CPU 密集) | 各节点校验独立,并行收益明显 | -| 远程 API 调用(I/O 密集 async) | 并发度从 1 提升到 N,延迟降低数量级 | -| 日志/遥测收集 | `ILogger` 实现线程安全 | -| Forest 遍历(多棵独立树) | 最安全的并行场景,无共享状态 | -| DI 容器解析 | `IServiceProvider` 线程安全 | - -### 不适合并行遍历的场景 - -| 场景 | 原因 | -|------|------| -| UI 菜单树渲染 | UI dispatcher 单线程瓶颈,并行收益有限 | -| 写入同一个 `Stream` | `Stream.Write` 非线程安全 | -| 向 `StringBuilder` 追加 | `StringBuilder` 非线程安全 | -| 向 `List<T>.Add` 收集 | `List<T>.Add` 非线程安全(最常见误用) | - -### 用户最常见误用模式 - -```csharp -// ❌ 会崩溃——List<T>.Add 非线程安全 -var results = new List<string>(); -CompositeTraverser.TraverseParallel(root, (node, _, _) => results.Add(node.Name)); - -// ✅ 正确——ConcurrentBag<T> 线程安全 -var results = new ConcurrentBag<string>(); -CompositeTraverser.TraverseParallel(root, (node, _, _) => results.Add(node.Name)); -``` diff --git a/docs/rfc/archive/CompositeTreeSchemaValidation.md b/docs/rfc/archive/CompositeTreeSchemaValidation.md deleted file mode 100644 index 12a0476..0000000 --- a/docs/rfc/archive/CompositeTreeSchemaValidation.md +++ /dev/null @@ -1,292 +0,0 @@ -# RFC: Composite 树 schema 校验 - -> **状态**:Implemented -> **类型**:Feature -> **创建**:2026-07-15 -> **更新**:2026-07-07 -> **作者**:维护者 -> **关联 Roadmap**:F5+ -> **关联 Issue**:#218 -> **衍生 ADR**:[ADR-007](../../adr/ADR-007-composite-tree-schema-validation.md) - -## 概述 - -为 Composite 模式新增编译期树结构 schema 校验:**最大深度**、**父子类型兼容性**、**节点计数**。在源生成器阶段从 flat parent-key 映射计算树结构并报告诊断,无需运行时开销。 - -现有 Composite 诊断(DP010–DP015, DP040–DP041)覆盖键唯一性、父键存在性、循环检测、契约实现、构造函数、DI 注册等基础校验,但**不**校验树的整体结构形态。本 RFC 补齐这一层。 - -## 设计目标 - -| 目标 | 说明 | -|------|------| -| 编译期捕获结构错误 | 深度过大可能导致栈溢出;类型不匹配的父子关系是设计错误;节点过多暗示架构问题 | -| 零运行时开销 | 所有校验在源生成器阶段完成,不增加运行时成本 | -| 向后兼容 | 所有新约束均为 opt-in,不标注则不校验(现有代码行为不变) | -| 探索价值 | 展示「源生成器从 flat 声明重建树拓扑并在编译期校验」的编译期 + 运行时协同模式 | - ---- - -## 一、API 设计 - -### 1.1 `CompositeSchemaAttribute`(契约级约束) - -新增特性,标注在**契约接口/基类**上,声明该契约的全局树结构约束: - -```csharp -[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -public sealed class CompositeSchemaAttribute : Attribute -{ - /// <summary>Maximum tree depth (root = depth 1). 0 = no limit. Default = 0.</summary> - public int MaxDepth { get; set; } - - /// <summary>Maximum total node count across all roots. 0 = no limit. Default = 0.</summary> - public int MaxNodes { get; set; } -} -``` - -**用法示例**: - -```csharp -[CompositeSchema(MaxDepth = 10, MaxNodes = 500)] -public interface IMenuNode : ICompositeNode<IMenuNode> { } -``` - -### 1.2 `CompositePartAttribute.AllowedChildTypes`(节点级约束) - -在现有 `CompositePartAttribute` 上新增可选属性,声明该节点允许的子节点实现类型: - -```csharp -// 新增到现有 CompositePartAttribute -public Type[]? AllowedChildTypes { get; set; } -``` - -**用法示例**: - -```csharp -[CompositePart("root", typeof(IMenuNode))] -public partial class MenuRoot : IMenuNode, ICompositeBuildable<IMenuNode> { } - -[CompositePart("panel", typeof(IMenuNode), ParentKey = "root", AllowedChildTypes = new[] { typeof(PanelNode), typeof(LeafNode) })] -public partial class PanelNode : IMenuNode, ICompositeBuildable<IMenuNode> { } - -[CompositePart("leaf", typeof(IMenuNode), ParentKey = "panel")] -// OK: LeafNode 的父节点 panel 允许 LeafNode -public partial class LeafNode : IMenuNode, ICompositeBuildable<IMenuNode> { } - -[CompositePart("section", typeof(IMenuNode), ParentKey = "root", AllowedChildTypes = new[] { typeof(SectionNode) })] -// ERROR: SectionNode 的父节点 root 未在 AllowedChildTypes 中声明 SectionNode -public partial class SectionNode : IMenuNode, ICompositeBuildable<IMenuNode> { } -``` - -**向后兼容**:`AllowedChildTypes = null`(默认)表示不限制——任何实现契约的类型都可作为子节点,与现有行为一致。 - -### 1.3 泛型变体同步 - -`CompositePartAttribute<TContract>` 也新增 `AllowedChildTypes` 属性。`CompositeSchemaAttribute` 不需要泛型变体(它标注在契约类型本身上,无需泛型参数)。 - ---- - -## 二、诊断 ID - -| ID | 名称 | 严重性 | 归属 | 触发条件 | -|----|------|--------|------|----------| -| DP063 | CompositeTreeMaxDepthExceeded | Warning | SourceGenerators | 树的实际深度超过 `[CompositeSchema(MaxDepth)]` 声明值 | -| DP064 | CompositeChildTypeNotAllowed | Error | SourceGenerators | 子节点的实现类型不在父节点的 `AllowedChildTypes` 集合中 | -| DP065 | CompositeNodeCountExceeded | Warning | SourceGenerators | 契约的总节点数超过 `[CompositeSchema(MaxNodes)]` 声明值 | - -### 2.1 诊断消息格式 - -**DP063**: -- Title: "Composite tree max depth exceeded" -- Message: "Composite tree for contract '{0}' has depth {1}, exceeding the maximum depth of {2} declared by [CompositeSchema]. Consider flattening the tree structure or increasing MaxDepth." -- Description: "Excessively deep composite trees can cause stack overflows in recursive traversal. Review the tree structure or adjust the MaxDepth constraint." - -**DP064**: -- Title: "Composite child type not allowed by parent" -- Message: "Composite part '{0}' (type '{1}') is not in the AllowedChildTypes of its parent '{2}' (type '{3}'). Add '{1}' to the parent's AllowedChildTypes or change the ParentKey to a compatible parent." -- Description: "Parent-child type compatibility is enforced at compile time when AllowedChildTypes is specified. Ensure child implementation types are declared in the parent's AllowedChildTypes array." - -**DP065**: -- Title: "Composite node count exceeds limit" -- Message: "Composite contract '{0}' has {1} parts, exceeding the maximum of {2} declared by [CompositeSchema]. Consider splitting into multiple contracts or increasing MaxNodes." -- Description: "Excessive node counts may indicate architectural issues or cause performance problems in tree assembly and traversal." - -### 2.2 诊断报告位置 - -- **DP063**:报告在**最深的叶子节点**的 `[CompositePart]` 特性位置(指向造成超限的路径终点) -- **DP064**:报告在**子节点**的 `[CompositePart]` 特性位置(指向不被允许的子节点) -- **DP065**:报告在**契约类型**声明位置(`[CompositeSchema]` 标注的接口/类) - ---- - -## 三、生成器实现 - -### 3.1 数据流 - -现有管线不变。新增校验在 `ReportDiagnostics` 阶段执行(与现有 DP010–DP012 同层): - -``` -ForAttributeWithMetadataName("CompositePartAttribute") ← 现有 -ForAttributeWithMetadataName("CompositePartAttribute`1") ← 现有 - → Transform → CompositeRegistration[] ← 现有 - → Collect + Combine ← 现有 - → ReportDiagnostics ← 新增 schema 校验 - → Emit ← 现有 -``` - -### 3.2 深度计算 - -从 flat parent-key 映射计算树深度(复用现有 `BuildParentMap` 基础设施): - -```csharp -// 伪代码 -static int ComputeDepth(string key, IReadOnlyDictionary<string, string?> parentByKey) -{ - var depth = 1; // root = depth 1 - var children = entries.Where(e => e.ParentKey == key); - foreach (var child in children) - { - depth = Math.Max(depth, 1 + ComputeDepth(child.Key, parentByKey)); - } - return depth; -} -``` - -**优化**:使用 memoization 避免重复计算(节点数 N,O(N) 时间 + O(N) 空间)。 - -**安全**:循环检测已在 DP012 完成,深度计算不会无限递归。 - -### 3.3 父子类型兼容性校验 - -```csharp -// 伪代码 -foreach (var entry in registrations) -{ - if (entry.ParentKey is null) continue; - if (!entryByKey.TryGetValue(entry.ParentKey, out var parent)) continue; // DP011 已处理 - - var allowedTypes = parent.AllowedChildTypes; - if (allowedTypes is null || allowedTypes.Length == 0) continue; // 不限制 - - if (!allowedTypes.Contains(entry.ImplementationType)) - { - context.ReportDiagnostic(DP064, entry.Location, ...); - } -} -``` - -**注意**:`AllowedChildTypes` 存储的是 `Type[]`,在生成器中需要从特性参数提取 `INamedTypeSymbol` 并比较完全限定名。 - -### 3.4 节点计数 - -```csharp -// 伪代码 -var schemaAttr = contractType.GetAttributes() - .FirstOrDefault(a => a.AttributeClass?.Name == "CompositeSchemaAttribute"); - -if (schemaAttr != null) -{ - var maxNodes = ExtractIntArgument(schemaAttr, "MaxNodes"); - if (maxNodes > 0 && registrations.Count > maxNodes) - { - context.ReportDiagnostic(DP065, contractTypeLocation, ...); - } -} -``` - -### 3.5 `CompositeSchemaAttribute` 提取 - -`CompositeSchemaAttribute` 标注在契约接口上,不在 `[CompositePart]` 的目标类上。生成器需要额外提取: - -1. 在 Transform 阶段,从 `INamedTypeSymbol contractType` 的 `GetAttributes()` 查找 `CompositeSchemaAttribute` -2. 提取 `MaxDepth` 和 `MaxNodes` 值 -3. 存入 `CompositeRegistration` 或单独的 schema 模型 - -**模型扩展**:在 `CompositeRegistration` 中新增 `int? SchemaMaxDepth` 和 `int? SchemaMaxNodes` 字段(nullable,null = 未标注 `[CompositeSchema]`)。或者使用单独的 `CompositeSchemaInfo` 模型,按契约分组。 - -选择后者(单独模型),避免每个 `CompositeRegistration` 重复存储: - -```csharp -private sealed record CompositeSchemaInfo( - ContractInfo Contract, - int? MaxDepth, - int? MaxNodes, - LocationInfo? Location); -``` - -在 Combine 阶段,按 Contract 分组提取 schema 信息,与 registrations 一起传入 ReportDiagnostics。 - ---- - -## 四、向后兼容性 - -| 变更 | 影响 | -|------|------| -| 新增 `CompositeSchemaAttribute` | 无影响——未标注时不触发任何新诊断 | -| `CompositePartAttribute.AllowedChildTypes` 新属性 | 无影响——`null` 默认值表示不限制 | -| 新增 DP063/DP064/DP065 | 无影响——仅在 opt-in 约束被声明时才报告 | - -**无破坏性变更**:所有现有 `[CompositePart]` 代码无需修改即可继续工作。 - ---- - -## 五、测试计划 - -### 5.1 生成器单元测试(Verify + 诊断断言) - -| 测试 | 验证点 | -|------|--------| -| `ReportsDp063_MaxDepthExceeded` | `[CompositeSchema(MaxDepth = 2)]` + 3 层树 → DP063 Warning | -| `ReportsDp063_NotExceeded_WhenWithinLimit` | `[CompositeSchema(MaxDepth = 5)]` + 3 层树 → 无 DP063 | -| `ReportsDp064_ChildTypeNotAllowed` | 父节点 `AllowedChildTypes = [A, B]`,子节点类型 C → DP064 Error | -| `ReportsDp064_Allowed_WhenTypeInList` | 父节点 `AllowedChildTypes = [A, B]`,子节点类型 B → 无 DP064 | -| `ReportsDp064_NotChecked_WhenAllowedChildTypesNull` | 父节点未设 `AllowedChildTypes` → 无 DP064(任意类型均可) | -| `ReportsDp065_NodeCountExceeded` | `[CompositeSchema(MaxNodes = 3)]` + 5 个节点 → DP065 Warning | -| `ReportsDp065_NotExceeded_WhenWithinLimit` | `[CompositeSchema(MaxNodes = 10)]` + 5 个节点 → 无 DP065 | -| `NoSchemaAttribute_NoNewDiagnostics` | 无 `[CompositeSchema]` + 深树 + 多节点 → 无 DP063/DP065 | -| `MaxDepth_ForestMode_ComputesPerTree` | 多根森林,最深树超过限制 → DP063 报告最深树 | -| `AllowedChildTypes_GenericAttribute` | 泛型 `[CompositePart<T>]` + `AllowedChildTypes` → DP064 正确触发 | - -### 5.2 运行时测试 - -无需新增运行时测试——所有校验在编译期完成,运行时行为不变。 - -### 5.3 集成测试 - -新增一个使用 `[CompositeSchema]` + `AllowedChildTypes` 的集成测试项目,验证端到端生成 + 组装 + 遍历流程。 - ---- - -## 六、实现范围 - -| 文件 | 变更 | -|------|------| -| `DesignPatterns/Structural/CompositeSchemaAttribute.cs` | **新增** — 契约级特性 | -| `DesignPatterns/Structural/CompositePartAttribute.cs` | **修改** — 新增 `AllowedChildTypes` 属性 | -| `DesignPatterns/Structural/CompositePartAttribute.TContract.cs` | **修改** — 新增 `AllowedChildTypes` 属性 | -| `DesignPatterns.Diagnostics/DiagnosticIds.cs` | **修改** — 新增 DP063/DP064/DP065 常量 | -| `DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs` | **修改** — 新增 3 个描述符 | -| `DesignPatterns.SourceGenerators/Generators/CompositePartGenerator.cs` | **修改** — 提取 schema、深度计算、类型校验、节点计数 | -| `DesignPatterns.SourceGenerators/AnalyzerReleases.Unshipped.md` | **修改** — 新增 DP063/DP064/DP065 条目 | -| `tests/DesignPatterns.SourceGenerators.Tests/Generators/CompositePartGeneratorTests.cs` | **修改** — 新增 10 个测试 | -| `tests/DesignPatterns.Tests/Structural/CompositeSchemaAttributeTests.cs` | **新增** — 特性构造测试 | -| `docs/spec/Composite.md` + `docs/design/Composite.md` | **修改** — 文档补充 schema 校验说明 | -| `docs/ROADMAP.md` | **修改** — 标记完成 | - -**PR 边界**:跨 Runtime + Diagnostics + SourceGenerators 三个模块。按 AGENTS.md 规范,应拆分为: -1. **Runtime PR**:`CompositeSchemaAttribute` + `CompositePartAttribute.AllowedChildTypes` -2. **Diagnostics + SourceGenerators PR**:DP063/DP064/DP065 + 生成器校验逻辑 + 测试 - -或合并为一个 PR(因为特性 + 诊断 + 校验紧密耦合,拆分后第一个 PR 无法独立验证)。选择合并。 - ---- - -## 七、探索价值 - -本 RFC 展示以下编译期 + 运行时协同技术: - -1. **从 flat 声明重建树拓扑**:源生成器从 `ParentKey` 字符串引用重建完整树结构,在编译期计算深度、验证类型兼容性——无需运行时反射或动态分析 -2. **opt-in 约束的渐进式类型安全**:`AllowedChildTypes` 允许用户逐步收紧类型约束,从「任意契约实现」到「特定实现类型集合」,编译期捕获设计错误 -3. **契约级 vs 节点级约束的分层**:`CompositeSchemaAttribute`(契约级全局约束)与 `AllowedChildTypes`(节点级局部约束)正交组合,覆盖不同粒度的校验需求 - -与 Stateless 等运行时库的对比:本方案将结构校验前移到编译期,运行时零开销;Stateless 等库在运行时动态验证状态机配置,有反射和异常成本。 diff --git a/docs/rfc/archive/HierarchicalStateMachine.md b/docs/rfc/archive/HierarchicalStateMachine.md deleted file mode 100644 index 8ac0470..0000000 --- a/docs/rfc/archive/HierarchicalStateMachine.md +++ /dev/null @@ -1,551 +0,0 @@ -# RFC: State 层级状态机(编译期展平) - -> **状态**:Implemented -> **类型**:Pattern -> **创建**:2026-06-28 -> **更新**:2026-07-07 -> **作者**:维护者 -> **关联 Roadmap**:F3 -> **关联 Issue**:— -> **衍生 ADR**:[ADR-005](../../adr/ADR-005-state-transition-table.md)(与 StateTransitionTable RFC 共享 ADR) - ---- - -## 1. 摘要 - -在现有 State 转换表(v2:扁平 enum 图 + guard + entry/exit action + DI)基础上,增加 **层级状态(hierarchical states)** 支持:允许状态声明父状态,子状态自动继承父状态的转换边,父状态的 entry/exit action 在进入/离开子状态时按 UML 状态机语义链式触发。 - -**核心技术路线**:**编译期展平(compile-time flattening)**——源生成器在编译期将层级关系展开为扁平查找表,运行时 API 保持 `Dictionary<(TState, TTrigger), …>` 的 O(1) 查找性能,不引入运行时层级引擎。这与 ROADMAP 中「生成器展平为快表」的描述一致。 - -**不是**完整 UML 状态机框架:不做历史状态(history)、并发状态(orthogonal regions)、持久化快照。与 Stateless 的边界见 §9。 - ---- - -## 2. 动机 - -### 2.1 现有局限 - -当前 State 转换表(v2)是**扁平图**:每个 `(state, trigger)` 至多一条边,无父子关系。当领域模型存在自然层级时(见下例),扁平表需要为每个子状态重复声明相同的转换,导致冗余且易遗漏。 - -```text -订单状态层级: - Active - ├── Submitted - └── Paid - Cancelled - Draft - -问题:Submitted 和 Paid 都应响应 Cancel → Cancelled。 -扁平表需要两条独立边: - [Transition(Submitted, Cancel, Cancelled)] - [Transition(Paid, Cancel, Cancelled)] -层级表只需一条父级边: - [Transition(Active, Cancel, Cancelled)] ← 子状态自动继承 -``` - -### 2.2 为何现在 - -- F1/F2/F2+ 全部完成,0.2.0-preview3 已发布,功能主线收敛。 -- State v2(guard + entry/exit + DI + tracing)已稳定,层级状态是 v1 RFC §2.3 明确列出的「暂不做」项中**探索价值最高**的(⭐⭐⭐)。 -- 生成器管线已模块化(Transform → Validate → Emit),扩展层级元数据收集与展平逻辑的侵入性低。 -- 与 Stateless 重叠但展示「编译期展平层级」技术——这正是本库「编译期 + 运行时协同」探索方针的核心体现。 - -### 2.3 非目标 - -- **历史状态**(shallow/deep history)——UML 超集,运行时需要记住最后活跃子状态,与「编译期展平」路线冲突。 -- **并发状态**(orthogonal regions / parallel states)——需要多活跃状态,当前 `IStateMachine.CurrentState` 是单值。 -- **运行时动态层级修改**——层级在编译期固定;运行时不可增删父子关系。 -- **持久化快照 / 分布式状态 / 超时转换**——与 v1 非目标一致。 -- **`string` / `int` 状态键**——仍仅支持 `enum`。 - ---- - -## 3. 设计原则 - -1. **编译期展平,运行时扁平**:生成器在编译期将层级关系展开为扁平边表;运行时查找逻辑与 v2 完全一致(O(1) Dictionary lookup),不引入层级遍历开销。 -2. **子优先继承**:子状态的直接边覆盖父状态的继承边(most-specific wins)。 -3. **Action 链由生成器合成**:entry/exit action 链在编译期合成为复合委托,运行时 `TransitionEdge` 结构不变(仍为单个 `OnEnter` / `OnExit` 委托,但委托体内部按序调用链)。 -4. **向后兼容**:未声明层级的现有状态机行为不变;层级是 opt-in。 -5. **层级元数据可选暴露**:生成器额外输出 `IStateHierarchy<TState>` 实现,供 `IsInState` / `GetParent` 查询;不影响转换查找路径。 - ---- - -## 4. 概念模型 - -```text - Cancel trigger - [Active] ─────────────────► [Cancelled] - │ - ├── [Submitted] ──Pay──► [Paid] - │ │ - │ Submit - │ │ - │ ▼ - │ (self-loop or edge) - │ - └── [Paid] - - [Draft] ──Submit──► [Submitted] -``` - -- **父状态(parent state)**:一个 enum 成员,声明为其他状态的父。父状态本身可以是叶子(有出边)也可以是纯容器(无自身边,仅提供继承边)。 -- **子状态(child state)**:声明了 `Parent` 的状态。子状态继承父状态的所有转换边(除非自身有同 trigger 的直接边)。 -- **祖先链(ancestor chain)**:从当前状态向上到根的路径:`Submitted → Active → (root)`。 -- **LCA(最低公共祖先)**:转换 `from → to` 时,from 和 to 的最近公共祖先。exit action 从 from 向上执行到 LCA(不含 LCA);entry action 从 LCA 向下执行到 to(不含 LCA)。 - -### 4.1 转换解析规则 - -当 `TryTransition(current, trigger)` 被调用时: - -1. 查找 `(current, trigger)` 的直接边 → 命中则使用 -2. 若未命中,查找 `(parent(current), trigger)` → 命中则使用 -3. 继续向上直到根 → 命中则使用 -4. 全部未命中 → 返回 `false` - -**编译期展平**:生成器在编译期执行上述解析,为每个 `(state, trigger)` 对预计算有效边,写入扁平表。运行时只需一次 Dictionary 查找。 - -### 4.2 Entry/Exit Action 链 - -转换 `Submitted → Cancelled`(LCA = root,即无公共祖先): - -```text -1. Exit Submitted (子 exit action) -2. Exit Active (父 exit action) -3. Enter Cancelled (entry action) -``` - -转换 `Submitted → Paid`(LCA = Active,父未变): - -```text -1. Exit Submitted (子 exit action) -2. Enter Paid (子 entry action) -(Active 的 exit/enter 不触发——父状态未变) -``` - -**编译期合成**:生成器为每条展平后的边计算 action 链,生成复合委托: - -```csharp -// 生成代码(示意) -static void ExitChain_Submitted_To_Cancelled(TState from, TState to, TTrigger trigger) -{ - OnExitSubmitted(from, to, trigger); // 子 exit - OnExitActive(from, to, trigger); // 父 exit -} -static void EnterChain_Submitted_To_Cancelled(TState from, TState to, TTrigger trigger) -{ - OnEnterCancelled(from, to, trigger); -} -``` - ---- - -## 5. 运行时 API(草案) - -### 5.1 层级元数据接口(新增) - -```csharp -namespace DesignPatterns.Behavioral; - -/// <summary> -/// Optional hierarchy metadata for hierarchical state machines. -/// Generated tables implement this interface when Hierarchical = true. -/// </summary> -public interface IStateHierarchy<in TState> - where TState : struct, Enum -{ - /// <summary> - /// Returns the parent state of <paramref name="state"/>, or null if it is a root. - /// </summary> - TState? GetParent(TState state); - - /// <summary> - /// Returns true if <paramref name="current"/> is <paramref name="ancestor"/> - /// or a descendant of <paramref name="ancestor"/>. - /// </summary> - bool IsInState(TState current, TState ancestor); - - /// <summary> - /// Returns the ancestor chain from <paramref name="state"/> up to root (exclusive of root). - /// </summary> - IReadOnlyList<TState> GetAncestors(TState state); -} -``` - -### 5.2 ITransitionTable 扩展 - -`ITransitionTable<TState, TTrigger>` **不**新增成员。层级表的生成实现额外实现 `IStateHierarchy<TState>`: - -```csharp -// 生成产物 -public sealed partial class OrderStatusTransitionTable - : ITransitionTable<OrderStatus, OrderTrigger> - , IStateHierarchy<OrderStatus> // 仅 Hierarchical = true 时 -{ ... } -``` - -消费者可通过模式匹配访问层级元数据: - -```csharp -if (table is IStateHierarchy<OrderStatus> hierarchy) -{ - bool isActive = hierarchy.IsInState(current, OrderStatus.Active); -} -``` - -### 5.3 扩展方法 - -```csharp -namespace DesignPatterns.Behavioral; - -public static class StateHierarchyExtensions -{ - /// <summary> - /// Attempts to cast the table to IStateHierarchy and query IsInState. - /// Returns false if the table is not hierarchical. - /// </summary> - public static bool IsInState<TState, TTrigger>( - this ITransitionTable<TState, TTrigger> table, - TState current, TState ancestor) - where TState : struct, Enum - where TTrigger : struct, Enum - => table is IStateHierarchy<TState> h && h.IsInState(current, ancestor); -} -``` - -### 5.4 手动 Builder 扩展 - -```csharp -public sealed class TransitionTableBuilder<TState, TTrigger> - where TState : struct, Enum - where TTrigger : struct, Enum -{ - // 现有 API 不变 - - /// <summary>Declares a parent-child relationship. Opt-in for hierarchical mode.</summary> - public TransitionTableBuilder<TState, TTrigger> WithParent( - TState child, TState parent); -} -``` - -`Build()` 返回的 `ITransitionTable` 实现在 `WithParent` 被调用后额外实现 `IStateHierarchy<TState>`。手动 Builder 不做展平(用户需自行添加所有有效边);层级元数据仅用于 `IsInState` 查询。 - ---- - -## 6. 编译期 API(草案) - -### 6.1 新增特性 - -```csharp -namespace DesignPatterns.Behavioral; - -/// <summary> -/// Declares a parent state for a child state in a hierarchical state machine. -/// Apply on the holder class, once per child state that has a parent. -/// </summary> -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class StateParentAttribute : Attribute -{ - public StateParentAttribute(object child, object parent) { ... } -} -``` - -`[StateMachine]` 新增属性: - -```csharp -public sealed class StateMachineAttribute : Attribute -{ - // 现有成员不变 - - /// <summary> - /// Enables hierarchical state machine mode. - /// When true, [StateParent] attributes are collected and transitions are flattened. - /// Default: false (backward compatible). - /// </summary> - public bool Hierarchical { get; set; } -} -``` - -### 6.2 用法示例 - -```csharp -public enum OrderStatus -{ - Draft, - Active, // composite/parent state - Submitted, - Paid, - Cancelled, -} - -public enum OrderTrigger -{ - Submit, - Pay, - Cancel, -} - -[StateMachine( - typeof(OrderStatus), - typeof(OrderTrigger), - Initial = OrderStatus.Draft, - Hierarchical = true)] -[StateParent(OrderStatus.Submitted, OrderStatus.Active)] -[StateParent(OrderStatus.Paid, OrderStatus.Active)] - -// 子状态直接边 -[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted)] -[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid)] - -// 父级继承边:Submitted 和 Paid 都继承此边 -[Transition(OrderStatus.Active, OrderTrigger.Cancel, OrderStatus.Cancelled)] - -// 父级 entry/exit action(子状态进入/离开时链式触发) -[Transition(OrderStatus.Active, OrderTrigger.Cancel, OrderStatus.Cancelled, - OnExit = nameof(OnExitActive))] -public static partial class OrderStatusMachine; - -static partial void OnExitActive(OrderStatus from, OrderStatus to, OrderTrigger trigger) - => Console.WriteLine($"Exiting Active (was in {from})"); -``` - -### 6.3 生成产物 - -| 生成类型 | 说明 | -|----------|------| -| `OrderStatusTransitionTable` | 扁平表(展平后)+ `IStateHierarchy<OrderStatus>` 实现 | -| `OrderStatusMachine`(partial 扩展) | 便捷方法(与 v2 一致) | -| `OrderStatusStateMachine` | `IStateMachine<OrderStatus, OrderTrigger>` 包装器 | - -展平后的表等效于: - -```text -(Draft, Submit) → Submitted -(Submitted, Pay) → Paid -(Submitted, Cancel) → Cancelled ← 从 Active 继承 -(Paid, Cancel) → Cancelled ← 从 Active 继承 -``` - -### 6.4 生成器实现要点 - -在现有 `StateTransitionGenerator` 管线上扩展: - -1. **Transform 阶段**: - - 当 `Hierarchical = true` 时,额外收集 `[StateParent]` 特性 - - 构建 `Dictionary<TState, TState>` parent map - - 检测循环(DP056) - -2. **Validate 阶段**(新增诊断): - - DP056:层级循环(A → B → A) - - DP057:`[StateParent]` 的 child 或 parent 不是声明的状态 enum 成员 - - DP058:`[StateParent]` 的 parent 等于 child(自引用) - - DP059:父状态从未作为 `from` 出现且无子状态(Info,孤立父状态) - -3. **Flatten 阶段**(新增): - - 对每个 `(state, trigger)` 对,沿祖先链向上查找有效边 - - 子状态直接边优先(覆盖父级边) - - 为每条展平边计算 entry/exit action 链(LCA 算法) - - 生成复合委托包装 action 链 - -4. **Emit 阶段**: - - 输出扁平表(与 v2 格式一致,但边数可能更多) - - 额外输出 `IStateHierarchy<TState>` 实现(`GetParent` / `IsInState` / `GetAncestors`) - - 生成 `StateParentMap` 静态字段供层级查询 - ---- - -## 7. 诊断 ID(DP056–DP059) - -下一可用 ID:**DP056**。 - -| ID | 级别 | 归属 | 触发条件 | -|----|------|------|----------| -| **DP056** | Error | 生成器 | 层级循环:状态 A 的父链经 B、C…回到 A | -| **DP057** | Error | 生成器 | `[StateParent]` 的 child 或 parent 不是 `[StateMachine]` 声明的状态 enum 成员 | -| **DP058** | Error | 生成器 | `[StateParent]` 的 parent 等于 child(自引用) | -| **DP059** | Info | 生成器 | 父状态从未作为 `from` 出现且无子状态声明它为父(孤立父状态) | - -**CodeFix**:暂不提供(层级关系需人工判断)。 - ---- - -## 8. Entry/Exit Action 链算法 - -### 8.1 LCA 计算 - -```text -function LCA(a, b): - ancestors_a = [a] + GetAncestors(a) - ancestors_b = [b] + GetAncestors(b) - return first common element in ancestors_a ∩ ancestors_b - (or null if no common ancestor = root) -``` - -### 8.2 Exit 链(from → LCA,不含 LCA) - -```text -function ExitChain(from, lca): - chain = [] - current = from - while current != lca and current != null: - if HasOnExit(current): chain.append(GetOnExit(current)) - current = GetParent(current) - return chain // 执行顺序:from → ... → child-of-lca -``` - -### 8.3 Enter 链(LCA → to,不含 LCA,逆序执行) - -```text -function EnterChain(to, lca): - chain = [] - current = to - while current != lca and current != null: - if HasOnEnter(current): chain.prepend(GetOnEnter(current)) - current = GetParent(current) - return chain // 执行顺序:child-of-lca → ... → to -``` - -### 8.4 同级转换(LCA = from 或 LCA = to) - -- `from == to`(自环):exit(from) → enter(from) -- `LCA == from`(进入后代):仅 enter 链(from 的 exit 不触发) -- `LCA == to`(从后代回到祖先):仅 exit 链(to 的 enter 不触发) - ---- - -## 9. 与 Stateless 的边界 - -| | 本库层级状态机 | [Stateless](https://github.com/dotnet-state-machine/stateless) | -|--|-------------------|----------------------------------------------------------------| -| 层级模型 | 编译期声明 + 展平 | 运行时 fluent API(`SubstateOf`) | -| 查找性能 | O(1) 扁平表(展平后) | O(depth) 运行时向上查找 | -| 层级修改 | 编译期固定 | 运行时可配置 | -| 历史状态 | 不支持 | 支持(`OnEntry` from history) | -| 并发状态 | 不支持 | 支持(`DefineParallelState`) | -| Action 链 | 编译期合成委托 | 运行时动态调用 | -| 诊断 | 编译期 DP056–DP059 | 无编译期诊断 | -| 适用 | 层级固定、追求零运行时开销 | 需要运行时灵活性 / 历史状态 / 并发 | - -**指导**:层级关系在编译期已知且不需运行时修改 → 本库;需要 `SubstateOf` 动态配置 / 历史状态 / 并发区域 → Stateless。 - ---- - -## 10. 交付计划 - -| 阶段 | 内容 | 依赖 | -|------|------|------| -| **v3.1** | 运行时 `IStateHierarchy<TState>` + `TransitionTableBuilder.WithParent` + `IsInState` 扩展方法 | 无 | -| **v3.2** | 生成器:`[StateParent]` 收集 + 展平算法 + DP056–DP059 诊断 | v3.1 | -| **v3.3** | 生成器:entry/exit action 链合成 + LCA 算法 | v3.2 | -| **v3.4** | DI 集成(`IStateHierarchy` 注册与解析) + Sample + Docs | v3.3 | - -每个阶段独立 PR,遵守单模块 PR 边界。 - ---- - -## 11. 开放问题(已决策) - -> 以下 5 个开放问题经 4 位评审委员(运行时性能 / API 设计 / 生成器实现 / 领域建模)并行评审后达成共识,转为最终决策。 - -### Q1:父状态能否也是活跃状态?→ **决策:允许** - -**问题**:`Active` 既是 `Submitted`/`Paid` 的父,能否也是 `TryTransition` 的 `from`(即直接处于 `Active` 状态而非其子状态)? - -**决策**:允许。父状态本身可以是叶子状态。`IsInState(Active, Active)` 返回 `true`。展平时 `(Active, trigger)` 的直接边与子状态继承的边独立存在。 - -**评审共识**(4/4 一致): -- 运行时:零开销,无需特殊处理,展平算法统一处理所有状态 -- API 设计:与 enum 模型一致,无需引入「container-only」概念 -- 生成器:最简路径,`StateMachineModel` 结构不变 -- 领域建模:符合 UML 状态图语义,Stateless 同样允许;拒绝则需人造「默认子状态」增加复杂度 - -### Q2:多重继承?→ **决策:不支持** - -**问题**:一个子状态能否有多个父状态? - -**决策**:不支持。单继承(每个状态至多一个 parent)。多重继承引入 LCA 歧义和复杂度,不符合「primitive」定位。需要多重继承的场景用 Stateless。 - -**评审共识**(4/4 一致): -- 运行时:单继承 = `Dictionary<TState, TState>` O(depth) 遍历;多继承 = DAG 图遍历 + 菱形歧义 -- API 设计:`GetParent(TState) → TState?` 简洁;多继承需 `GetParents → IReadOnlyList` + 冲突解决规则 -- 生成器:单继承展平算法线性;多继承需拓扑排序 + C3 线性化,生成代码体积爆炸 -- 领域建模:UML 和 Stateless 均用单继承树结构;真实领域模型几乎总是树形 - -### Q3:通配转换(wildcard transition)?→ **决策:v3 不做** - -**问题**:ROADMAP 提及「通配转换」。是否需要一个特殊的「任意状态」枚举值或特性来声明全局 fallback 边? - -**决策**:v3 不做。父级继承边已覆盖大部分「通配」语义(在父状态上声明边即对所有子状态生效)。如果后续有需求,可评估 `[Transition(Any, trigger, to)]` 语法。 - -**评审共识**(4/4 一致): -- 运行时:通配需运行时 fallback 逻辑,破坏 O(1) 查找;父级继承通过编译期展平保持 O(1) -- API 设计:显式优于隐式;父级声明使继承关系清晰可追溯 -- 生成器:通配需特殊语法解析 + 优先级排序 + 歧义诊断,管线复杂度中等 -- 领域建模:UML 无通配转换;Stateless 不支持;父级继承是「Ultimate Hook」模式的正确表达 - -### Q4:`GetAllowedTriggers` 是否包含继承的触发器?→ **决策:包含** - -**问题**:当 `Submitted` 没有直接 `Cancel` 边但 `Active` 有时,`GetAllowedTriggers(Submitted)` 是否返回 `Cancel`? - -**决策**:包含。展平后 `Submitted` 的有效边包含继承的 `Cancel`,`GetAllowedTriggers` 返回所有有效触发器。 - -**评审共识**(4/4 一致): -- 运行时:与编译期展平一致,`_triggersByState` 字典预构建,零运行时遍历 -- API 设计:方法名是 `GetAllowedTriggers` 非 `GetDirectlyDeclaredTriggers`;语义正确性要求返回有效触发器 -- 生成器:直接查询展平表,无需额外逻辑——实际上比排除继承触发器更简单 -- 领域建模:UML 状态「is in」所有超态;Stateless `PermittedTriggers` 包含继承触发器;UI 按钮启用/禁用依赖此行为 - -### Q5:`TransitionTrace` 如何表示 action 链?→ **决策:状态列表 + count + 失败定位** - -**问题**:现有 `TransitionTrace` 有 `OnExitCompleted` / `OnEnterCompleted` 两个 bool。层级 action 链可能有多个 exit/enter action。 - -**决策**:综合 4 位委员意见,采用状态列表为主 + count 快速路径 + 简化失败定位: - -```csharp -public readonly struct TransitionTrace<TState> - where TState : struct, Enum -{ - // 现有字段保留(向后兼容) - public bool Succeeded { get; } - public TState NextState { get; } - public bool OnExitCompleted { get; } // = ExitActions 全部完成 - public bool OnEnterCompleted { get; } // = EnterActions 全部完成 - public Exception? Exception { get; } - - // 新增:性能梯度(bool → count → 状态列表 → 失败定位) - public int ExitActionCount { get; } // 零分配快速路径 - public int EnterActionCount { get; } - public IReadOnlyList<TState> StatesExited { get; } // 语义清晰:哪些状态被 exit - public IReadOnlyList<TState> StatesEntered { get; } // 语义清晰:哪些状态被 enter - public string? FailedActionName { get; } // 定位失败点(简化版) - public TState? FailedActionState { get; } // 失败发生在哪个状态 -} -``` - -**设计理由**: -- **状态列表(enum 值)而非方法名(string)**:用户关心的是层级路径(哪些状态被 exit/enter),而非内部方法名;enum 值零字符串分配,AOT 友好 -- **Count 字段**:提供零分配快速路径,无需访问列表即可判断是否有 action -- **失败定位**:`FailedActionName` + `FailedActionState` 精确指出哪个 action 在哪个状态失败,无需完整 `ActionTrace` 结构体的开销 -- **现有 bool 字段保留**:`OnExitCompleted` / `OnEnterCompleted` 作为聚合 bool 保持向后兼容 - -**评审分歧与综合**: -- A(运行时):建议加 count 字段作为零分配路径 → **采纳** -- B(API 设计):建议结构化 `ActionTrace`(含 `Completed` + `Exception?`)→ **简化采纳**:用 `FailedActionName` + `FailedActionState` 定位失败点,避免完整结构体开销 -- C(生成器):建议用状态列表(`TState`)而非方法名(`string`)→ **采纳**:语义更清晰,性能更好 -- D(领域建模):接受原案(方法名列表)→ **升级**:状态列表比方法名更有语义价值 - ---- - -## 12. 向后兼容性 - -| 现有 API | 影响 | -|----------|------| -| `ITransitionTable<TState, TTrigger>` | **不变**(无新成员) | -| `TransitionTableBuilder<TState, TTrigger>` | 新增 `WithParent` 方法(additive) | -| `IStateMachine<TState, TTrigger>` | **不变** | -| `[StateMachine]` | 新增 `Hierarchical` 属性(默认 `false`) | -| `[Transition]` | **不变** | -| 现有扁平状态机 | **零影响**(`Hierarchical` 默认 `false`,生成器行为不变) | -| `TransitionTrace<TState>` | 可能扩展(Q5),additive | -| `TransitionEdge<TState, TTrigger>` | **不变**(action 链由生成器合成为单个委托) | - -**破坏性变更**:无。层级模式完全 opt-in。 diff --git a/docs/rfc/archive/StateTransitionTable.md b/docs/rfc/archive/StateTransitionTable.md deleted file mode 100644 index 8d1d71f..0000000 --- a/docs/rfc/archive/StateTransitionTable.md +++ /dev/null @@ -1,373 +0,0 @@ -# RFC: State 转换表(有限状态转换 primitive) - -> **状态**:Implemented -> **类型**:Pattern -> **创建**:2026-06-14 -> **更新**:2026-07-07 -> **作者**:维护者 / 贡献者 -> **关联 Roadmap**:F3 -> **关联 Issue**:— -> **衍生 ADR**:[ADR-005](../../adr/ADR-005-state-transition-table.md) - ---- - -## 1. 摘要 - -为 DesignPatterns 增加 **有限状态转换表(finite state transition table)** primitive:用 **enum 状态 + enum 触发器** 描述合法边,编译期生成冻结查找表与强类型 API,编译期诊断拦截重复边、非法枚举成员与明显结构问题。 - -**不是**通用状态机框架(层次状态、历史、持久化、重试、entry/exit 动作 DSL)。定位与 Strategy/Factory 同级:**声明式标注 + 生成胶水 + 可选手动 Builder**。 - ---- - -## 2. 动机 - -### 2.1 库内缺口 - -| 已有能力 | 表达力 | -|----------|--------| -| Strategy / Factory | key → 实现(无「当前态」) | -| Chain / Decorator | 线性顺序(非分支图) | -| Composite | 树结构(非状态图) | -| EventAggregator | 事件广播(无状态合法性) | - -许多领域问题本质是 **(当前状态, 触发器) → 下一状态** 的有限图:订单生命周期、插件启停、连接状态、向导步骤、任务阶段。手写 `switch` 或 `Dictionary<(State, Trigger), State>` 易漏边、难测试、重构 enum 时无编译期保护。 - -### 2.2 为何现在 - -- F1/F2 主线已收敛,F3 准入评审通过本 RFC 后可排期。 -- 与现有生成器模式(partial holder、`ForAttributeWithMetadataName`、`*Keys` 常量风格)一致,可复用注册表类基础设施思路。 -- 不重复 MediatR(命令路由)或 Polly(弹性);与 **Stateless** 等库边界清晰(见 §8)。 - -### 2.3 非目标(v1 明确不做) - -- 层次 / 并发 / 历史状态(UML 状态机超集) -- 持久化快照、分布式状态、超时转换 -- ~~Entry / exit / internal transition 动作~~(v1 非目标;**v2 已实现** entry/exit action 委托 + `[Transition(OnEnter/OnExit)]` 源生成,见 [StateTransitionTable.md](../../design/StateTransitionTable.md)) -- Guard 的编译期表达式求值(v1 无 guard;v2 仅运行时委托) -- `string` / `int` 状态键(v1 仅 **enum**;与 DP025 字面量键校验正交,后续再评) -- 反射扫描或 AppDomain 自动发现转换 - ---- - -## 3. 设计原则 - -1. **表驱动,非继承**:状态行为留在 consumer 类型中;库只提供 **合法性表 + TryTransition**。 -2. **编译期优先**:非法边、重复边、引用未知 enum 成员 → **Error**;孤立状态 → **Info**(可配置关闭)。 -3. **组合优于继承**:不引入 `StateBase` / `AbstractState` 体系。 -4. **与生态共存**:表可嵌入领域服务;需要完整状态机 DSL 时继续用 Stateless 等,本库不竞争。 -5. **分阶段交付**:v1 纯表;v2 guard + DI;v3 再评 string trigger / 与 EventAggregator 联动示例。 - ---- - -## 4. 概念模型 - -```text - trigger T1 trigger T2 - [S0] ──────────────► [S1] ──────────────► [S2] - (唯一边) (唯一边) -``` - -- **状态 `TState`**:`enum`(consumer 定义) -- **触发器 `TTrigger`**:**独立** `enum`(consumer 定义;v1 不与 `TState` 共用同一 enum) -- **边**:至多一条 `(TState from, TTrigger trigger) → TState to`;**允许自环**(`from == to`) -- **初始态**:机器元数据指定一个 `TState` 常量 -- **终态**:v1 **不**建模终态 attribute 或专用规则;无出边的状态自然表示终态 - ---- - -## 5. 运行时 API(草案) - -命名空间:`DesignPatterns.Behavioral`(与 Strategy、Chain、EventAggregator 同级)。 - -### 5.1 核心接口 - -```csharp -namespace DesignPatterns.Behavioral; - -/// <summary> -/// Immutable transition table for enum state and trigger types. -/// </summary> -public interface ITransitionTable<TState, TTrigger> - where TState : struct, Enum - where TTrigger : struct, Enum -{ - TState InitialState { get; } - - /// <summary> - /// Returns false when (current, trigger) is not a declared transition. - /// </summary> - bool TryTransition(TState current, TTrigger trigger, out TState next); - - /// <summary> - /// Triggers that have at least one outgoing edge from <paramref name="current"/>. - /// Order is declaration order in source. - /// </summary> - IReadOnlyList<TTrigger> GetAllowedTriggers(TState current); - - /// <summary> - /// Whether any edge leaves <paramref name="current"/>. - /// </summary> - bool CanTransitionFrom(TState current); -} -``` - -### 5.2 异常类型(可选) - -```csharp -public sealed class InvalidTransitionException<TState, TTrigger> : Exception - where TState : struct, Enum - where TTrigger : struct, Enum; -``` - -提供扩展方法 `Transition(...)` 在失败时抛上述异常;与 `StrategyRegistry.Get` 对称。 - -### 5.3 手动 Builder(无生成器时) - -```csharp -public sealed class TransitionTableBuilder<TState, TTrigger> - where TState : struct, Enum - where TTrigger : struct, Enum -{ - public TransitionTableBuilder<TState, TTrigger> WithInitial(TState initial); - public TransitionTableBuilder<TState, TTrigger> Add(TState from, TTrigger trigger, TState to); - public ITransitionTable<TState, TTrigger> Build(); -} -``` - -- 重复 `(from, trigger)` → `ArgumentException`(与 Factory Builder 一致) -- net8.0 实现内部可用 `FrozenDictionary<(TState, TTrigger), TState>`;netstandard2.0 用 `Dictionary` - -### 5.4 与 EventAggregator / Strategy 的组合(consumer 侧) - -```csharp -if (table.TryTransition(order.Status, OrderTrigger.Pay, out var next)) -{ - order.Status = next; - await aggregator.PublishAsync(new OrderPaidEvent(order.Id), ct); -} -``` - -库 **不** 内建发布事件或调用 Strategy;RFC 仅在 Sample 演示组合。 - ---- - -## 6. 编译期 API(草案) - -### 6.1 特性 - -```csharp -namespace DesignPatterns.Behavioral; - -/// <summary> -/// Marks a partial static holder for generated transition table metadata. -/// Apply exactly once per state machine. -/// </summary> -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -public sealed class StateMachineAttribute : Attribute -{ - public StateMachineAttribute(Type stateType, Type triggerType) { ... } - - /// <summary>Initial state enum member.</summary> - public object Initial { get; set; } = null!; - - /// <summary>Optional machine name for generated type names; default = state enum name sans leading 'E'.</summary> - public string? Name { get; set; } -} - -/// <summary> -/// Declares one directed edge. Repeat on the same partial holder class. -/// </summary> -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class TransitionAttribute : Attribute -{ - public TransitionAttribute(object from, object trigger, object to) { ... } -} -``` - -**约束(生成器 Error)** - -- Holder 必须为 `static partial class`(**一 holder 一机**;v1 不支持同一 holder 多台状态机) -- `StateMachineAttribute.StateType` / `TriggerType` 须为 **两个独立** enum -- `TransitionAttribute` 的 `from` / `trigger` / `to` 须为该 enum 的**命名常量**(`OrderStatus.Draft`),非任意 int cast -- 同一 holder 上 `(from, trigger)` 唯一 -- `from == to` 的自环边合法 - -### 6.2 用法示例 - -```csharp -public enum OrderStatus { Draft, Submitted, Paid, Cancelled } - -public enum OrderTrigger { Submit, Pay, Cancel } - -[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft)] -[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted)] -[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid)] -[Transition(OrderStatus.Draft, OrderTrigger.Cancel, OrderStatus.Cancelled)] -public static partial class OrderStatusMachine; -``` - -`[Transition]` 与 `[StateMachine]` 一样标注在 **holder 类声明**上(`AllowMultiple`),不是方法或嵌套类型。 - -### 6.3 生成产物命名 - -对 `OrderStatus` + holder `OrderStatusMachine`: - -| 生成类型 | 说明 | -|----------|------| -| `OrderStatusTransitionTable` | `ITransitionTable<OrderStatus, OrderTrigger>` 实现 + `static Instance` | -| `OrderStatusMachine`(同名 partial 扩展) | 便捷:`public static bool TryTransition(...)` 转发到 `TransitionTable.Instance` | - -v1 **不**生成 `{State}Triggers` 或 `{State}Keys` 常量类——触发器与状态均直接使用 consumer 定义的 enum 成员。 - -命名规则写入 [ROADMAP.md](../../ROADMAP.md)「命名规则」表: - -| 模式 | 生成类型示例 | -|------|----------------| -| State | `{StateEnum}TransitionTable`、partial `{Holder}` 便捷方法 | - -### 6.4 生成器实现要点 - -- 新增 `StateTransitionGenerator`:`IIncrementalGenerator` + `ForAttributeWithMetadataName` -- 收集同一 partial holder 上所有 `TransitionAttribute`(需 syntax 或 semantic 聚合;参考 `HandlerOrder` 多特性模式) -- 输出单文件 partial:`TransitionTable` + 扩展方法 -- 注册 `[Generator]` / `AnalyzerReleases.Unshipped.md` - ---- - -## 7. 诊断 ID(预留 DP026–DP032) - -下一可用 ID:**DP026**([AGENTS.md](../../../AGENTS.md))。实现前在 `DiagnosticIds.cs` 正式登记。 - -| ID | 级别 | 归属 | 触发条件 | -|----|------|------|----------| -| **DP026** | Error | 生成器 | 重复 `(from, trigger)` | -| **DP027** | Error | 生成器 | `Transition` 中 `from`/`to` 不是 `StateMachine` 声明的状态 enum 成员 | -| **DP028** | Error | 生成器 | `trigger` 不是声明的触发器 enum 成员 | -| **DP029** | Error | 生成器 | `Initial` 不是状态 enum 成员 | -| **DP030** | Error | 生成器 | `[StateMachine]` holder 不是 `static partial class` | -| **DP031** | Info | 生成器 | 状态 enum 成员从未作为 `from` 出现(**孤立态**,排除 `Initial` 仅作终态场景) | -| **DP032** | Error | 生成器(v2) | guard 方法在 holder 类上未找到 | -| **DP034** | Error | 生成器(v2) | guard 方法非 static | -| **DP035** | Error | 生成器(v2) | guard 方法签名错误(须 `bool Method(TState, TTrigger)`) | -| **DP036** | Info | Analyzer(v2) | `TryTransition` 字面量 `(state, trigger)` 对未声明(与 DP025 对称) | - -**CodeFix(v1)** - -- DP026:无自动修复(删重复 attribute) -- DP027–DP029:若 typo 接近某 enum 成员,可选「替换为最近成员」CodeFix(复用 CorrectRegistryKey 距离算法) - -**明确不做 CodeFix** - -- DP031 孤立态(需业务判断是否故意终态) - ---- - -## 8. 与 Stateless / 手写表 的边界 - -| | 本库 State 转换表 | [Stateless](https://github.com/dotnet-state-machine/stateless) | -|--|-------------------|----------------------------------------------------------------| -| 模型 | 扁平 enum 图 | 状态机 + guard + 行为 + 子状态 | -| 编译期 | 边表 + DP | 主要为运行时 fluent API | -| 体积 | 单表 + TryTransition | 功能完整 | -| 适用 | 合法边校验、轻量 domain | 复杂工作流、回调、触发器参数 | - -**指导**:边数 < 30、无 guard 的 domain → 本库;需要 `OnEntry` / `PermitReentry` / 子状态 → Stateless。 - ---- - -## 9. 示例场景(Samples 规划) - -| Sample 项目 | 演示 | -|-------------|------| -| `DesignPatterns.Samples.StateMachine` | 订单状态 + `TryTransition` + 非法触发单元测试 | -| (可选)与 EventAggregator | 转换成功后 `PublishAsync`(Sample 内组合,非库 API) | -| (可选)手动 Builder | 与生成表等价的手写 `TransitionTableBuilder` 测试 | - ---- - -## 10. DI 集成(v2,已实现) - -```csharp -// 生成器在转换表类上输出 RegisterDi(引用 DesignPatterns.Extensions.DependencyInjection 时自动启用) -OrderStatusTransitionTable.RegisterDi(services); // Singleton ITransitionTable<,> - -// 或手动注册预构建的表 -services.AddTransitionTable(OrderStatusTransitionTable.Instance); -``` - -启用开关:MSBuild 属性 `DesignPatterns_EnableDiIntegration`(引用 DI 扩展包时自动为 `true`)。详见 [StateTransitionTable.md](../../design/StateTransitionTable.md#v2di-集成)。 - ---- - -## 11. 实现分期 - -| 阶段 | 范围 | 交付 | -|------|------|------| -| **M0 评审** | 本 RFC 定稿 | Issue + 标签 `rfc-approved` | -| **M1 Runtime** | `ITransitionTable`、`TransitionTableBuilder`、异常、单元测试 | PR → Runtime 模块 | [x] | -| **M2 Generator** | `[StateMachine]`、`[Transition]`、`{State}TransitionTable`、DP026–DP031 | PR → SourceGenerators + Diagnostics | [x] | -| **M3 Sample + Docs** | Samples 项目、DesignPatterns.Docs 用户页、`docs/StateTransitionTable.md` | 分 PR | -| **M4 v2 Guard 运行时** | `TransitionEdge.Guard` 委托、`TransitionTableBuilder.Add` guard 重载 | PR #124 | [x] | -| **M4 v2 Guard 生成器** | `[Transition] Guard` 属性、DP032/DP034/DP035 诊断 | PR #125 | [x] | -| **M4 v2 DI 集成** | 生成 `RegisterDi` 方法、`AddTransitionTable` 扩展 | PR #126 | [x] | -| **M4 v2 字面量边校验** | DP036 `StateTransitionLiteralEdgeAnalyzer` | PR #127 | [x] | - -**模块边界**(强制):M1 / M2 / M3 各为独立 PR,符合 [AGENTS.md](../../../AGENTS.md) 跨模块规则。 - ---- - -## 12. 测试策略 - -| 层级 | 内容 | -|------|------| -| 单元 | `TryTransition` 命中/未命中、`GetAllowedTriggers` 顺序、Builder 重复边 | -| 生成器 Verify | 表源码快照、DP026–DP030 消息与位置 | -| 集成 | 生成 `TransitionTable` → 运行时行为与手写 Builder 一致 | -| Analyzer(v2) | DP032 字面量边校验 | - ---- - -## 13. v1 设计决策(已确认) - -| # | 决策 | 结论 | -|---|------|------| -| Q1 | 触发器是否必须独立 enum? | **是** — `TState` 与 `TTrigger` 必须为两个独立 enum | -| Q2 | 是否生成 `{State}Triggers` const 类? | **否** — v1 不生成;直接使用 consumer 的 trigger enum | -| Q3 | DP031 孤立态默认级别 | **Info** | -| Q4 | `Transition` 是否允许 `from == to`(自环)? | **允许** | -| Q5 | 终态是否要在表中有出边 / 是否建模终态? | **不强制、不建模** — 无出边即自然终态 | -| Q6 | 一个 holder 是否只允许一台状态机? | **是** — 一 holder 一机 | - -后续若需 string trigger 或 trigger 常量类,单独开 v2 RFC 修订,不在 v1 范围。 - ---- - -## 14. 评审检查清单 - -- [x] 与「不做全量 GoF / 不做 MediatR」无冲突 -- [x] 命名与现有 `{Contract}Keys` / `{Contract}Registry` 可区分(v1 不生成 Keys/Triggers 常量) -- [x] DP026–DP031 区间无与既有 ID 重叠 -- [x] netstandard2.0 + net8.0 双 TFM 表实现可行 -- [x] v1 设计决策(§13 Q1–Q6)已确认 -- [ ] 至少一个 Skymly 产品场景愿意在 preview 包上试用(AnyTool 插件生命周期 / AgentDesk 任务态等) -- [ ] M1→M2→M3 分期与独立 PR 启动 - ---- - -## 15. 参考 - -- [Strategy.md](../../spec/Strategy.md) — 注册表 + 生成器模式参考 -- [FactoryKeyConventions.md](../../FactoryKeyConventions.md) — 键约定(State v1 不用 string key) -- [EventAggregator.md](../../spec/EventAggregator.md) — 组合用 pub/sub -- [ROADMAP.md](../../ROADMAP.md) F3 准入标准 -- Stateless 文档 — 生态边界对照 - ---- - -## 变更记录 - -| 日期 | 说明 | -|------|------| -| 2026-06-14 | 初稿(Draft) | -| 2026-06-15 | v1 设计决策确认(§13 Q1–Q6) | -| 2026-06-22 | v2 已实现:guard 委托(PR #124/#125)、DI 集成(PR #126)、DP036 字面量边校验(PR #127) | diff --git a/docs/spec/ChainOfResponsibility.md b/docs/spec/ChainOfResponsibility.md deleted file mode 100644 index 56aa126..0000000 --- a/docs/spec/ChainOfResponsibility.md +++ /dev/null @@ -1,96 +0,0 @@ -# Spec: Chain of Responsibility - -> **版本**:v0.2.2(与 NuGet 包版本对齐) -> **关联 Design Doc**:[docs/design/ChainOfResponsibility.md](../design/ChainOfResponsibility.md) -> **关联 ADR**:ADR-XXX(如有) - -## API 面 - -### 运行时接口 - -运行时类型位于 `DesignPatterns/Behavioral/`。 - -| 类型 | 说明 | -|------|------| -| `HandlerDelegate<TContext>` | 调用链中下一阶段的委托 | -| `IHandler<TContext>` | 单个 handler:`InvokeAsync(context, next, ct)` | -| `HandlerPipelineBuilder<TContext>` | `Use(handler)` / `Use(delegate)`,`Build()` | -| `HandlerPipeline<TContext>` | 不可变管道,`InvokeAsync(context, ct)`、`InvokeTracedAsync`(见下文) | -| `HandlerPipelineTrace` | 一次 traced 调用的逐步结果 | -| `HandlerPipelineStep` | 单步:索引、handler 显示名、状态 | -| `HandlerPipelineStepStatus` | `Completed` / `ShortCircuited` / `NotReached` | - -`HandlerPipelineStepStatus` 取值含义: - -| `HandlerPipelineStepStatus` | 含义 | -|----------------------------|------| -| **Completed** | Handler 已执行且调用了 `next` | -| **ShortCircuited** | Handler 已执行但**未**调用 `next`(管道在此 handler 之后不再向下) | -| **NotReached** | Handler **未执行**(因更早的 handler 短路) | - -`InvokeTracedAsync` 不改变执行顺序与短路语义,仅额外返回 `HandlerPipelineTrace`;委托 handler 在 trace 中显示名为 `"<delegate>"`。 - -`HandlerPipelineTrace.WasShortCircuited` 为「是否存在任一步为 `ShortCircuited`」;终端 handler 不调用 `next` 时也会为 `true`,不宜单独作为「业务短路」判据。 - -### 特性(Attribute) - -```csharp -// 泛型(推荐,C# 11+ / net8.0) -[HandlerOrder<RequestContext>(10)] -public sealed class LoggingHandler : IHandler<RequestContext> { } - -// 非泛型(netstandard2.0) -[HandlerOrder(10, typeof(RequestContext))] -public sealed class LoggingHandler : IHandler<RequestContext> { } -``` - -数值越小越先执行(与手动 `Use` 注册顺序一致)。 - -同一 handler 类可标注多个 `[HandlerOrder<...>]`(`AllowMultiple = true`),分别加入不同 `TContext` 的生成管道,例如共享日志 handler 同时实现 `IHandler<RequestContext>` 与 `IHandler<AuditContext>`。同一 context 下重复的 Order 仍报 **DP005**。 - -### 生成器产出 - -对每种 `TContext` 生成 `{Context}HandlerPipeline`: - -```csharp -public static partial class RequestContextHandlerPipeline -{ - public static HandlerPipeline<RequestContext> Instance { get; } -} -``` - -引用 `DesignPatterns.Extensions.DependencyInjection` 后,`{Context}HandlerPipeline` 额外生成: - -```csharp -RequestContextHandlerPipeline.RegisterDi(services); -var pipeline = provider.GetRequiredService<HandlerPipeline<RequestContext>>(); -``` - -`Create(IServiceProvider sp)` 按 `[HandlerOrder]` 顺序 `GetRequiredService` 各 handler。Singleton 管道在首次构建时固定 handler 实例;需要每次解析新管道时使用 `registryLifetime: ServiceLifetime.Transient`。 - -手动注册:`services.AddHandlerPipeline<TContext>(builder => ...)`(扩展包)。 - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| DP005 | Error | 同一 context 下重复的 Order | ... | -| DP008 | Error | 标注 `[HandlerOrder]` 但未实现 `IHandler<TContext>` | ... | -| DP009 | Error | 标注 `[HandlerOrder]` 的 handler 缺少 public 无参构造 | ... | -| DP024 | Info | Analyzer:实现了 `IHandler<TContext>` 但未加 `[HandlerOrder]`(该 context 已有其它 handler 注册) | ... | -| DP024 | Info | CodeFix:一键添加 `[HandlerOrder(order, typeof(TContext))]` | ... | - -## 不变量 - -1. Handler **调用** `await next(context, ct)` → 继续后续 handler;Handler **不调用** `next` → 后续 handler 不再执行(inbound 与 outbound 均跳过)——即**通过是否调用 `next` 实现短路**,不强制业务 context 实现额外接口。 -2. 空管道:`InvokeAsync` / `InvokeTracedAsync` 正常完成,trace 为空。 -3. `Use(null)` 抛 `ArgumentNullException`。 - -## 兼容基线 - -- netstandard2.0 + net8.0(两者均须可用并随包分发) -- Roslyn 组件 4.8.0(`Microsoft.CodeAnalysis.CSharp` / Workspaces;Analyzers 3.3.4) - -## 不在范围内 - -- Autofac / DryIoc 扩展(可选,后续) diff --git a/docs/spec/Composite.md b/docs/spec/Composite.md deleted file mode 100644 index 8f016b7..0000000 --- a/docs/spec/Composite.md +++ /dev/null @@ -1,144 +0,0 @@ -# Spec: Composite - -> **版本**:v0.2.2(与 NuGet 包版本对齐) -> **关联 Design Doc**:[docs/design/Composite.md](../design/Composite.md) -> **关联 ADR**:[ADR-006](../adr/ADR-006-composite-parallel-traversal.md)、[ADR-007](../adr/ADR-007-composite-tree-schema-validation.md) - -## API 面 - -### 运行时接口 - -命名空间:`DesignPatterns.Structural` - -| 类型 | 签名 | 说明 | -|------|------|------| -| `ICompositeNode<TSelf>` | `IReadOnlyList<TSelf> Children { get; }` where `TSelf : ICompositeNode<TSelf>` | 节点契约:通过 `Children` 区分叶子与分支,空列表为叶子 | -| `ICompositeBuildable<TNode>` | `void SetChildren(IReadOnlyList<TNode> children)` where `TNode : ICompositeNode<TNode>` | 装配时接收子节点;实现应在装配后冻结 children,后续调用视为无效 | -| `CompositeTraverser` | `static void Traverse<TNode>(TNode root, Action<TNode, int, int> visitor, CompositeTraversalOptions<TNode>? options = null)` | 同步遍历单根树(visitor: node, depth, siblingIndex) | -| | `static void TraverseForest<TNode>(IReadOnlyList<TNode> roots, Action<TNode, int, int> visitor, CompositeTraversalOptions<TNode>? options = null)` | 同步遍历森林(多根树) | -| | `static ValueTask TraverseAsync<TNode>(TNode root, Func<TNode, int, int, CancellationToken, ValueTask> visitor, CompositeTraversalOptions<TNode>? options = null, CancellationToken cancellationToken = default)` | 异步遍历单根树 | -| | `static ValueTask TraverseForestAsync<TNode>(IReadOnlyList<TNode> roots, Func<TNode, int, int, CancellationToken, ValueTask> visitor, CompositeTraversalOptions<TNode>? options = null, CancellationToken cancellationToken = default)` | 异步遍历森林 | -| | `static void TraverseParallel<TNode>(TNode root, Action<TNode, int, int> visitor, CompositeTraversalOptions<TNode>? options = null)` | 并行遍历单根树(访问顺序非确定) | -| | `static void TraverseForestParallel<TNode>(IReadOnlyList<TNode> roots, Action<TNode, int, int> visitor, CompositeTraversalOptions<TNode>? options = null)` | 并行遍历森林(根间串行,子树内并行) | -| | `static ValueTask TraverseParallelAsync<TNode>(TNode root, Func<TNode, int, int, CancellationToken, ValueTask> visitor, CompositeTraversalOptions<TNode>? options = null, CancellationToken cancellationToken = default)` | 异步并行遍历单根树 | -| | `static ValueTask TraverseForestParallelAsync<TNode>(IReadOnlyList<TNode> roots, Func<TNode, int, int, CancellationToken, ValueTask> visitor, CompositeTraversalOptions<TNode>? options = null, CancellationToken cancellationToken = default)` | 异步并行遍历森林 | -| `CompositeTraversalOptions<TNode>` | `CompositeTraversalOrder Order { get; set; }`(默认 `DepthFirstPreOrder`) | 遍历顺序 | -| | `int? MaxDepth { get; set; }`(`null` = 无限制) | 最大访问深度(0 = 仅根) | -| | `bool VisitLeavesOnly { get; set; }` | 仅访问叶子节点 | -| | `Func<TNode, bool>? ShouldSkipSubtree { get; set; }` | 返回 `true` 时跳过该子树 | -| | `int? MaxDegreeOfParallelism { get; set; }`(`null` = `Environment.ProcessorCount`) | 并行度上限 | -| | `int MaxParallelDepth { get; set; }`(默认 32) | 并行递归深度上限,超过后回退串行 | -| `CompositeTreeBuilder<TNode>` | `CompositeTreeBuilder<TNode> Leaf(TNode node)` | 添加叶子节点 | -| | `CompositeTreeBuilder<TNode> Branch(TNode node, Action<CompositeTreeBuilder<TNode>> configure)` | 添加分支节点(嵌套 builder 配置子节点) | -| | `TNode Build()` | 构建单根树(要求恰好一个顶层节点) | -| `CompositeCatalogEntry<TNode>` | `CompositeCatalogEntry(string key, string? parentKey, int order, Type implementationType)` | Catalog 条目 | -| | `string Key { get; }` | 唯一键 | -| | `string? ParentKey { get; }` | 父键(`null` 为根候选) | -| | `int Order { get; }` | 同父兄弟间排序(升序) | -| | `Type ImplementationType { get; }` | 实现类型 | -| `CompositeCatalogAssembler` | `static TNode Assemble<TNode>(IReadOnlyList<CompositeCatalogEntry<TNode>> entries)` where `TNode : class, ICompositeNode<TNode>` | 从 catalog 装配单根树(`Activator.CreateInstance`) | -| | `static TNode Assemble<TNode>(IReadOnlyList<CompositeCatalogEntry<TNode>> entries, IServiceProvider serviceProvider)` | 从 catalog 装配单根树(DI 解析节点) | -| | `static IReadOnlyList<TNode> AssembleForest<TNode>(IReadOnlyList<CompositeCatalogEntry<TNode>> entries)` | 从 catalog 装配多根森林(`Activator.CreateInstance`) | -| | `static IReadOnlyList<TNode> AssembleForest<TNode>(IReadOnlyList<CompositeCatalogEntry<TNode>> entries, IServiceProvider serviceProvider)` | 从 catalog 装配多根森林(DI 解析节点) | - -`CompositeTraversalOrder` 枚举值:`DepthFirstPreOrder`、`DepthFirstPostOrder`、`BreadthFirst`。 - -### 特性(Attribute) - -命名空间:`DesignPatterns.Structural` - -#### `CompositePartAttribute`(非泛型,netstandard2.0) - -```csharp -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -public sealed class CompositePartAttribute : Attribute -{ - public CompositePartAttribute(string key, Type @for); - - public string Key { get; } - public Type For { get; } - public string? ParentKey { get; set; } // null = 根候选 - public int Order { get; set; } // 同父兄弟间排序,升序 - public Type[]? AllowedChildTypes { get; set; } // 允许的子节点实现类型;null = 不限制 -} -``` - -#### `CompositePartAttribute<TContract>`(泛型,C# 11+ / net8.0) - -```csharp -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -public sealed class CompositePartAttribute<TContract> : Attribute -{ - public CompositePartAttribute(string key); - - public string Key { get; } - public string? ParentKey { get; set; } - public int Order { get; set; } - public Type[]? AllowedChildTypes { get; set; } -} -``` - -#### `CompositeSchemaAttribute`(契约级约束) - -```csharp -[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -public sealed class CompositeSchemaAttribute : Attribute -{ - public int MaxDepth { get; set; } // 最大树深度(root = 1);0 = 无限制;默认 0 - public int MaxNodes { get; set; } // 最大节点总数(所有根合计);0 = 无限制;默认 0 -} -``` - -标注于 composite 契约接口或基类,启用编译期树结构 schema 校验。未标注时行为不变。 - -### 生成器产出 - -源生成器 `CompositePartGenerator` 对每个 composite contract 生成以下类型(`IPaymentStrategy` → `PaymentStrategyCompositeKeys`;`IMenuNode` → `MenuNodeCompositeKeys`): - -| 生成类型 | 说明 | -|----------|------| -| `{Contract}CompositeKeys` | key 常量(`public const string`) | -| `{Contract}CompositeCatalog` | entry 列表 + `BuildRoot()` + `BuildForest()` 方法 | - -`{Contract}CompositeCatalog` 方法: - -| 方法 | 说明 | -|------|------| -| `BuildRoot()` | 从 catalog 装配单根树;要求 catalog 中**恰好一个** `ParentKey == null` 的根,否则运行时抛 `CompositeAssemblyException` | -| `BuildForest()` | 从 catalog 装配多根森林;一个或多个 `ParentKey == null`,按 `Order` 再 key 排序 | - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| DP010 | Error | 同一 contract 下 key 重复 | `Composite key '{0}' is already registered for contract '{1}'. Use a unique key or remove the duplicate [CompositePart] attribute.` | -| DP011 | Error | `ParentKey` 引用的 key 不存在 | `Composite parent key '{0}' was not found for contract '{1}'. Register the parent part first or correct the ParentKey value.` | -| DP012 | Error | parent 链形成环 | `Composite key '{0}' participates in a parent-key cycle for contract '{1}'. Remove or reassign ParentKey values to break the cycle.` | -| DP013 | Error | 标注类型未实现 composite contract | `Type '{0}' does not implement composite contract '{1}'. Implement the contract or fix the [CompositePart] contract argument.` | -| DP014 | Error | 缺少 public 无参构造 | `Type '{0}' must declare a public parameterless constructor for generated composite catalogs.` | -| DP015 | Error | 未实现 `ICompositeBuildable<TContract>` | `Type '{0}' must implement ICompositeBuildable<{1}> to be used with generated composite catalogs.` | -| DP040 | Error | `BuildRoot(IServiceProvider)` 时节点类型未注册到 DI 容器 | `Composite node type '{0}' was not registered in the service collection. Call {1}.RegisterDi(services) before BuildRoot(serviceProvider), or register the type manually.` | -| DP041 | Info | Visitor 覆盖不全(保留 ID — C# 编译器通过接口实现 CS0535 自动强制覆盖,诊断不实际触发) | `Visitor '{0}' does not implement all Visit methods of '{1}'. The C# compiler enforces full coverage via interface implementation (CS0535); this diagnostic is reserved for future use.` | -| DP063 | Warning | `[CompositeSchema(MaxDepth)]` 约束被超过 | `Composite tree for contract '{0}' has depth {1}, exceeding the maximum depth of {2} declared by [CompositeSchema]. Consider flattening the tree structure or increasing MaxDepth.` | -| DP064 | Error | 子节点实现类型不在父节点 `AllowedChildTypes` 集合中 | `Composite part '{0}' (type '{1}') is not in the AllowedChildTypes of its parent '{2}' (type '{3}'). Add '{1}' to the parent's AllowedChildTypes or change the ParentKey to a compatible parent.` | -| DP065 | Warning | `[CompositeSchema(MaxNodes)]` 约束被超过 | `Composite contract '{0}' has {1} parts, exceeding the maximum of {2} declared by [CompositeSchema]. Consider splitting into multiple contracts or increasing MaxNodes.` | - -## 不变量 - -1. **单根约束(`BuildRoot`)**:`BuildRoot()` / `Assemble` 要求 catalog 中恰好一个 `ParentKey == null` 的根;零根或多根时运行时抛 `CompositeAssemblyException`。 -2. **`ICompositeBuildable<TContract>` 要求**:catalog 装配的节点类型必须实现 `ICompositeBuildable<TContract>`,`TContract` 为 composite 接口(如 `IMenuNode`)。 -3. **public 无参构造**:catalog 装配的节点类型必须声明 public 无参构造函数(与 Strategy/Handler 生成器一致),供 `Activator.CreateInstance` 实例化。 -4. **`SetChildren` 一次性**:`SetChildren` 在装配时调用一次;实现类应在装配后冻结 children,后续调用视为无效。 -5. **多根森林(`BuildForest`)**:`BuildForest()` / `AssembleForest` 允许一个或多个 `ParentKey == null` 的根,按 `Order` 升序再 key 排序返回。 -6. **并行遍历顺序非确定**:`TraverseParallel` / `TraverseParallelAsync` / `TraverseForestParallel` / `TraverseForestParallelAsync` 的访问顺序非确定;需要顺序时使用 `Traverse` / `TraverseAsync`。 - -## 兼容基线 - -- `netstandard2.0` + `net8.0`(两者均须可用并随包分发) -- 泛型 `CompositePartAttribute<TContract>` 仅在 C# 11+ / net7.0+ 可用(`#if NET7_0_OR_GREATER`);netstandard2.0 使用非泛型 `CompositePartAttribute(string key, Type @for)` -- 并行遍历:net8.0 用 `Parallel.ForEachAsync`,netstandard2.0 用 `SemaphoreSlim` + `Task.WhenAll`(`#if` 分裂) - -## 不在范围内 - -- 运行时动态增删节点(树由编译期定义、运行时一次性装配) -- DI 自动注册 `BuildRoot()` / `BuildForest()` 结果(DI 扩展不自动注册装配产物) diff --git a/docs/spec/Decorator.md b/docs/spec/Decorator.md deleted file mode 100644 index d242378..0000000 --- a/docs/spec/Decorator.md +++ /dev/null @@ -1,109 +0,0 @@ -# Spec: Decorator - -> **版本**:v0.2.2(与 NuGet 包版本对齐) -> **关联 Design Doc**:[docs/design/Decorator.md](../design/Decorator.md) -> **关联 ADR**:ADR-XXX(如有) - -## API 面 - -### 运行时接口 - -运行时类型位于 `DesignPatterns/Structural/`。 - -| 类型 | 说明 | -|------|------| -| `IDecorator<TService>` | `TService Decorate(TService inner)` | -| `IDecoratorOf<TService>` | 可选标记接口(生成器不强制) | -| `DecoratorStackBuilder<TService>` | `Add<TDecorator>()` / `Add(instance)` / `Add(..., Func<bool>)` / `Build(core)` | - -`DecoratorStackBuilder<TService>` 方法签名: - -| 方法 | 说明 | -|------|------| -| `Add<TDecorator>()` where `TDecorator : TService, IDecorator<TService>, new()` | 注册一个装饰器类型(无参构造) | -| `Add(TService decorator)` | 注册一个装饰器实例(须实现 `IDecorator<TService>`) | -| `Add<TDecorator>(Func<bool> predicate)` | 注册一个带运行时谓词的装饰器类型;谓词在 `Build` 时求值,为 `false` 时跳过该装饰器 | -| `Add(TService decorator, Func<bool> predicate)` | 注册一个带运行时谓词的装饰器实例 | -| `Build(TService core)` | 按 `Add` 顺序(`Order` 越小越靠外)包装 `core`,返回最外层 `TService` | - -### 特性(Attribute) - -```csharp -// 泛型(推荐,C# 11+ / net8.0) -[Decorator<IPaymentService>(10)] -public sealed class LoggingPaymentDecorator : IPaymentService, IDecorator<IPaymentService> { ... } - -// 非泛型(netstandard2.0) -[Decorator(10, typeof(IPaymentService))] -public sealed class LoggingPaymentDecorator : IPaymentService, IDecorator<IPaymentService> { ... } -``` - -`[Decorator]` / `[Decorator<TService>]` 构造函数签名: - -| 特性 | 构造函数 | 说明 | -|------|----------|------| -| `DecoratorAttribute<TService>` | `(int order)` | 泛型形式,C# 11+ / net8.0 | -| `DecoratorAttribute` | `(int order, Type serviceType)` | 非泛型形式,netstandard2.0 | - -`Order` 数值越小越靠外(先接到调用,再委托向内)。 - -### 生成器产出 - -对 `IPaymentService` → `PaymentServiceDecoratorStack` 与 `PaymentServiceDecoratorOrder`: - -```csharp -public static partial class PaymentServiceDecoratorStack -{ - public static IPaymentService Build(IPaymentService core) { ... } -} - -public static partial class PaymentServiceDecoratorOrder -{ - public const int LoggingPaymentDecorator = 10; - public const int MetricsPaymentDecorator = 20; -} -``` - -- `{Contract}DecoratorStack.Build(core)`:按 `[Decorator]` 的 `Order` 升序包装 `core`,返回最外层 `TService`。注册全部 `[Decorator]` 类型,**不**应用运行时谓词。 -- `{Contract}DecoratorOrder`:为每个装饰器生成 `public const int`,常量名取装饰器**简单类型名**。同一 contract 下类型名须唯一(跨命名空间同名会导致重复常量编译错误)。 - -特性中可引用常量替代魔法数: - -```csharp -[Decorator<IPaymentService>(PaymentServiceDecoratorOrder.LoggingPaymentDecorator)] -public sealed class LoggingPaymentDecorator : IPaymentService, IDecorator<IPaymentService> { ... } -``` - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| DP016 | Error | 同一 contract 下重复 `Order` | `Decorator order '{0}' is already used for service contract '{1}'. Assign a unique order value or remove the duplicate [Decorator] attribute.` | -| DP017 | Error | 标注 `[Decorator]` 但未实现 service contract | `Type '{0}' does not implement service contract '{1}'. Implement the contract or fix the [Decorator] service argument.` | -| DP018 | Error | 标注 `[Decorator]` 但未实现 `IDecorator<TService>` | `Type '{0}' does not implement IDecorator<{1}>. Implement IDecorator<{1}> to participate in generated decorator stacks.` | -| DP019 | Error | 标注 `[Decorator]` 的装饰器缺少 public 无参构造 | `Type '{0}' must declare a public parameterless constructor for generated decorator stacks.` | -| DP042 | Error | async 装饰器 `DecorateAsync` 签名不匹配 | `Async decorator '{0}' must implement 'ValueTask<{1}> DecorateAsync({1} inner, CancellationToken cancellationToken = default)'. Fix the IAsyncDecorator<{1}> implementation.` | -| DP043 | Warning | 装饰器无法从 DI 容器解析(无 public 无参构造) | `Decorator '{0}' has no public parameterless constructor. Register it in the DI container via RegisterDi(), or add a parameterless constructor for Build() without IServiceProvider.` | - -CodeFix:DP017(加契约接口)、DP019(加无参构造)。DP018 需手写 `Decorate` 方法。 - -## 不变量 - -1. `Build(core)` 要求 `core` 非 `null`,否则抛 `ArgumentNullException`。 -2. `Order` 越小越靠外(先接到调用,再委托向内);先注册的装饰器包装在后注册的外层。 -3. 无装饰器时 `Build(core)` 返回同一 `core` 引用(不创建包装)。 -4. `DecoratorOrder` 常量名取装饰器**简单类型名**;同一 contract 下类型名须唯一。 -5. `Add(null)` / `Add(..., null)` 谓词抛 `ArgumentNullException`。 -6. 生成器产出的 `{Contract}DecoratorStack.Build(core)` 注册全部 `[Decorator]` 类型,条件开关(`Func<bool>`)仅适用于手动 `DecoratorStackBuilder` 组装。 - -## 兼容基线 - -- netstandard2.0 + net8.0(两者均须可用并随包分发) -- Roslyn 组件 4.8.0(`Microsoft.CodeAnalysis.CSharp` / Workspaces;Analyzers 3.3.4) - -## 不在范围内 - -- `DispatchProxy` / 透明代理 -- 装饰器 DAG 或按 key 选子集 -- 生成器为 `[Decorator]` 自动接线运行时谓词(条件装饰仅手动 builder) -- DI 自动注册 diff --git a/docs/spec/EventAggregator.md b/docs/spec/EventAggregator.md deleted file mode 100644 index 12d8c7e..0000000 --- a/docs/spec/EventAggregator.md +++ /dev/null @@ -1,134 +0,0 @@ -# Spec: Event Aggregator - -> **版本**:v0.2.2 -> **关联 Design Doc**:[docs/design/EventAggregator.md](../design/EventAggregator.md) -> **关联 ADR**:ADR-XXX(如有) - -## API 面 - -### 运行时接口 - -| 类型 | 职责 | -|------|------| -| `IEventHandler<TEvent>` | 处理指定事件类型 | -| `IEventAggregator` | 订阅、取消订阅、发布 | -| `EventAggregator` | 默认实现 | - -#### 事件处理器 - -```csharp -public interface IEventHandler<in TEvent> -{ - ValueTask HandleAsync(TEvent evt, CancellationToken cancellationToken = default); -} -``` - -任意类实现该接口即可作为处理器;**不要求**继承基类或注册特性。 - -#### 聚合器 - -```csharp -public interface IEventAggregator -{ - void Subscribe<TEvent>(IEventHandler<TEvent> handler); - void Unsubscribe<TEvent>(IEventHandler<TEvent> handler); - ValueTask PublishAsync<TEvent>(TEvent evt, CancellationToken cancellationToken = default); -} -``` - -事件类型 `TEvent` 可以是 `class`、`struct` 或 `record`;同一聚合器上可并存多种 `TEvent`。 - -#### 基本用法 - -```csharp -var aggregator = new EventAggregator(); - -aggregator.Subscribe(new EmailNotificationHandler()); -aggregator.Subscribe(new AuditLogHandler()); - -await aggregator.PublishAsync(new OrderPlacedEvent("ORD-001", 99.99m)); - -aggregator.Unsubscribe(auditHandler); -await aggregator.PublishAsync(new OrderPlacedEvent("ORD-002", 49.99m)); -``` - -### 特性(Attribute) - -| 特性 | 形态 | 适用 TFM | -|------|------|----------| -| `[RegisterEventHandler(typeof(FooEvent))]` | 非泛型 | 全部(netstandard2.0+) | -| `[RegisterEventHandler<FooEvent>]` | 泛型 | `#if NET7_0_OR_GREATER`(C# 11+ generic attributes) | - -与 `[RegisterStrategy]` / `[RegisterFactory]` 的双形态模式一致。 - -### 生成器产出 - -`RegisterEventHandlerGenerator` 在编译期扫描 `[RegisterEventHandler]` 特性,按 **event type 分组** 生成 `{Event}EventHandlerRegistry` 静态类(`Event` 后缀自动剥离,如 `OrderPlacedEvent` → `OrderPlacedEventHandlerRegistry`): - -| 成员 | 路径 | 条件 | -|------|------|------| -| `SubscribeAll(IEventAggregator)` | 静态:`new Handler()` + `Subscribe<TEvent>` | handler 有公共无参构造 | -| `RegisterDi(IServiceCollection, ServiceLifetime)` | DI:注册 handler 实现到容器 | `DesignPatterns_EnableDiIntegration=true` | -| `SubscribeAll(IEventAggregator, IServiceProvider)` | DI:从容器解析 handler + `Subscribe<TEvent>` | `DesignPatterns_EnableDiIntegration=true` | - -**静态路径**仅包含有公共无参构造的 handler;**DI 路径**包含全部有效 handler。这与其他生成器的无参构造约束一致(DP007/DP009/DP014/DP019/DP022 同源)。 - -#### 静态路径(无 DI 依赖) - -```csharp -public record OrderPlacedEvent(string OrderId); - -[RegisterEventHandler<OrderPlacedEvent>] -public sealed class LogOrderHandler : IEventHandler<OrderPlacedEvent> -{ - public ValueTask HandleAsync(OrderPlacedEvent evt, CancellationToken ct = default) => default; -} - -// 启动时 -var aggregator = new EventAggregator(); -OrderPlacedEventHandlerRegistry.SubscribeAll(aggregator); -await aggregator.PublishAsync(new OrderPlacedEvent("ORD-001")); -``` - -#### DI 路径(两步) - -```csharp -// 1. 注册聚合器 + handler 实现 -services.AddEventAggregator(); -OrderPlacedEventHandlerRegistry.RegisterDi(services); - -// 2. 启动时从容器解析并订阅 -var provider = services.BuildServiceProvider(); -var aggregator = provider.GetRequiredService<IEventAggregator>(); -OrderPlacedEventHandlerRegistry.SubscribeAll(aggregator, provider); -``` - -`RegisterDi` 默认 `implementationLifetime: ServiceLifetime.Transient`(每次解析新实例,因 handler 通常无状态)。 - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| DP044 | Info | 实现 `IEventHandler<T>` 但未标注 `[RegisterEventHandler]` | 提示未注册的 EventHandler | -| DP045 | Error | 同一 handler 类对同一 event type 重复标注 `[RegisterEventHandler]` | 报告重复标注 | -| DP046 | Error | 标注 `[RegisterEventHandler<T>]` 但类未实现 `IEventHandler<T>` | 报告契约不匹配 | - -## 不变量 - -1. 同一事件类型的处理器按 **订阅顺序** 依次调用。 -2. `PublishAsync` 在发布前对处理器列表做 **快照**,随后在锁外调用,避免在 `HandleAsync` 执行期间持有锁。 -3. 同一 `handler` 实例可重复 `Subscribe`(会多次出现在列表中)。 -4. `Unsubscribe` 移除**第一次**匹配项。 -5. 无处理器时 `PublishAsync` 立即完成(空订阅)。 - -## 兼容基线 - -- netstandard2.0 / net8.0(运行时核心,两者均须可用并随包分发) -- DI 集成:独立包 `DesignPatterns.Extensions.DependencyInjection`,`services.AddEventAggregator()` 注册 `IEventAggregator` → `EventAggregator`(默认 Singleton) - -## 不在范围内 - -- 不做弱事件 / 委托语法糖包装 -- 不做按名称或 topic 字符串路由(仅按 CLR 类型) -- 不在 Core 中引用 DI 容器 -- 不用反射扫描程序集自动订阅(改由 `[RegisterEventHandler]` 源生成器在编译期收集 handler) diff --git a/docs/spec/FactoryRegistry.md b/docs/spec/FactoryRegistry.md deleted file mode 100644 index 1436ffe..0000000 --- a/docs/spec/FactoryRegistry.md +++ /dev/null @@ -1,232 +0,0 @@ -# Spec: Factory Registry - -> **版本**:v0.2.2(与 NuGet 包版本对齐) -> **关联 Design Doc**:[docs/design/FactoryRegistry.md](../design/FactoryRegistry.md) -> **关联 ADR**:ADR-XXX(如有) - -## API 面 - -### 运行时接口 - -命名空间:`DesignPatterns.Creational` - -#### 注册表接口 - -```csharp -namespace DesignPatterns.Creational; - -/// <summary> -/// 按 key 解析工厂实现的只读注册表。每次 Create 调用对应 factory 委托得到新产品实例。 -/// </summary> -public interface IFactoryRegistry<TKey, TProduct> : IReadOnlyRegistry<TKey, TProduct> - where TKey : notnull -{ - bool TryCreate(TKey key, [MaybeNullWhen(false)] out TProduct product); - TProduct Create(TKey key); // 找不到抛 FactoryNotFoundException - IReadOnlyCollection<TKey> Keys { get; } -} -``` - -> **注意**:`IFactoryRegistry` 继承 `IReadOnlyRegistry<TKey, TProduct>`,与 `IStrategyRegistry` 共享该抽象。 - -#### 不可变实现 - -```csharp -public sealed class FactoryRegistry<TKey, TProduct> : IFactoryRegistry<TKey, TProduct> - where TKey : notnull -``` - -net8.0+ 上内部使用 `FrozenDictionary` 优化查找。 - -#### Builder(手动注册,无生成器时) - -```csharp -public sealed class FactoryRegistryBuilder<TKey, TProduct> - where TKey : notnull -{ - // 每次 Create(key) 调用该 factory - public FactoryRegistryBuilder<TKey, TProduct> Register(TKey key, Func<TProduct> factory); - - // factory 接收 key - public FactoryRegistryBuilder<TKey, TProduct> Register(TKey key, Func<TKey, TProduct> factory); - - public IFactoryRegistry<TKey, TProduct> Build(); -} -``` - -- 重复 key 在 `Register` 时抛 `ArgumentException`(**运行时**检测)。 - -#### 异常 - -```csharp -public sealed class FactoryNotFoundException : Exception -{ - public FactoryNotFoundException(); - public FactoryNotFoundException(string message); - public FactoryNotFoundException(string message, Exception innerException); - public static FactoryNotFoundException ForKey<TKey>(TKey key) where TKey : notnull; -} -``` - -`ForKey` 生成消息:`"No factory registered for key '{key}'."` - -#### 异步工厂(可选) - -```csharp -public interface IAsyncFactory<TProduct> -{ - ValueTask<TProduct> CreateAsync(CancellationToken cancellationToken = default); -} - -public interface IAsyncFactoryRegistry<TKey, TProduct> : IReadOnlyRegistry<TKey, TProduct> - where TKey : notnull -{ - ValueTask<(bool Success, TProduct? Product)> TryCreateAsync( - TKey key, CancellationToken cancellationToken = default); - ValueTask<TProduct> CreateAsync( - TKey key, CancellationToken cancellationToken = default); -} -``` - -`FactoryRegistryAsyncExtensions.AsAsync()` 可将同步 `IFactoryRegistry` 适配为 `IAsyncFactoryRegistry`。 - -### 特性(Attribute) - -命名空间:`DesignPatterns.Creational` - -#### 泛型版本(C# 11 / .NET 7+) - -```csharp -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class RegisterFactoryAttribute<TContract> : Attribute -{ - public string Key { get; } - public bool IsAsync { get; set; } - public int PoolSize { get; set; } - - public RegisterFactoryAttribute(string key); -} -``` - -#### 非泛型版本(netstandard2.0 / C# 7.3) - -```csharp -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class RegisterFactoryAttribute : Attribute -{ - public string Key { get; } - public Type Contract { get; } - public bool IsAsync { get; set; } - public int PoolSize { get; set; } - - public RegisterFactoryAttribute(string key, Type contract); -} -``` - -#### 属性说明 - -| 属性 | 类型 | 默认值 | 说明 | -|------|------|--------|------| -| `Key` | `string` | (构造函数必填) | 用于解析此工厂的 key | -| `Contract` | `Type` | (非泛型构造函数必填) | 工厂契约类型(接口或基类) | -| `IsAsync` | `bool` | `false` | 标记为异步工厂;`true` 时须实现 `IAsyncFactory<TProduct>`,生成器额外输出 `IAsyncFactoryRegistry`。`false` 时异步检测基于 `IAsyncFactory<TProduct>` 实现自动判断 | -| `PoolSize` | `int` | `0` | 启用对象池的最大池大小(per key)。`0` 禁用池化;正值使生成器输出 `IPooledFactoryRegistry`。仅异步工厂有效 | - -#### 用法示例 - -```csharp -// 泛型 Attribute(C# 11+) -[RegisterFactory<IProductFactory>("standard")] -public class StandardFactory : IProductFactory { ... } - -// 非泛型(netstandard2.0) -[RegisterFactory("standard", typeof(IProductFactory))] -public class StandardFactory : IProductFactory { ... } -``` - -工厂契约一般为**接口**(或基类);实现类需 **public 无参构造**,以便生成器注册 `() => new Implementation()`。 - -### 生成器产出 - -对每个契约 `TContract`(命名规则与 Strategy 相同:去掉前缀 `I` 等),生成器输出: - -#### 1. 强类型 Key 常量 - -```csharp -// ProductFactoryKeys.g.cs -public static partial class ProductFactoryKeys -{ - public const string Standard = "standard"; - public const string Premium = "premium"; -} -``` - -#### 2. 静态注册表(无 DI 场景) - -```csharp -// ProductFactoryRegistry.g.cs -public static partial class ProductFactoryRegistry -{ - public static IFactoryRegistry<string, IProductFactory> Create() { ... } -} -``` - -`Create()` 返回的注册表在每次 `Create(key)` 时**新建**产品实例(与 Strategy 注册表返回单例实例不同)。 - -#### 3. DI 集成(引用 `DesignPatterns.Extensions.DependencyInjection` 时) - -引用扩展包会自动 Import `build/DesignPatterns.Extensions.DependencyInjection.targets`,设置 `DesignPatterns_EnableDiIntegration=true`,生成器额外输出: - -```csharp -public static partial class ProductFactoryRegistry -{ - // Create() 仍保留(new() 静态实例,无 DI 生命周期) - - public static IFactoryRegistry<string, IProductFactory> Create(IServiceProvider serviceProvider) { ... } - - public static IServiceCollection RegisterDi( - IServiceCollection services, - ServiceLifetime implementationLifetime = ServiceLifetime.Transient, - ServiceLifetime registryLifetime = ServiceLifetime.Singleton); -} -``` - -> **注意**:`implementationLifetime` 默认值为 `Transient`(与 Strategy/Chain 等模式默认 `Singleton` 不同),因为工厂语义是每次 `Create` 返回新实例。 - -手动 Builder 仍可用:`services.AddFactoryRegistry<string, IProduct>(builder => { ... })`(扩展包),与生成器互不冲突。 - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| DP020 | Error | 同一 `TContract` 下 key 重复 | `Factory key '{0}' is already registered for contract '{1}'. Use a unique key or remove the duplicate [RegisterFactory] attribute.` | -| DP021 | Error | 标记的类未实现指定的 `TContract` | `Type '{0}' does not implement factory contract '{1}'. Implement the contract or fix the [RegisterFactory] contract argument.` | -| DP022 | Error | 标记的类缺少 public 无参构造 | `Type '{0}' must declare a public parameterless constructor for [RegisterFactory] static registration, or enable DI integration.` | -| DP023 | Info | 实现了某工厂契约但未加 `[RegisterFactory]` | `Type '{0}' implements factory contract '{1}' but has no [RegisterFactory] attribute. Add [RegisterFactory("key", typeof(...))] with a unique key and the contract type to register it.` | -| DP053 | Error | `IsAsync=true` 但未实现 `IAsyncFactory<TProduct>` | `Factory '{0}' is marked with IsAsync=true but does not implement IAsyncFactory<{1}>. Implement IAsyncFactory<{1}> or set IsAsync=false.` | -| DP054 | Error | `PoolSize` 为负数 | `Factory '{0}' has PoolSize={1}, which is negative. PoolSize must be >= 0 (0 disables pooling).` | -| DP055 | Warning | `PoolSize` 过大(可能导致内存过高) | `Factory '{0}' has PoolSize={1}, which may cause excessive memory usage. Consider a smaller value (recommended: 1-100).` | - -> **归属**:DP023 属 **Analyzer**(含 CodeFix,可一键添加 `[RegisterFactory("suggested-key", typeof(TContract))]`,与 DP006 对称);DP020 / DP021 / DP022 / DP053 / DP054 / DP055 属**生成器**。 - -## 不变量 - -1. **每次 Create 返回新实例**:`Create(key)` / `TryCreate(key, out _)` 每次调用均执行 factory 委托,得到新产品实例(与 Strategy 注册表返回同一实例不同)。 -2. **重复 key 抛 ArgumentException**:`FactoryRegistryBuilder.Register` 在运行时检测重复 key,抛 `ArgumentException`(`"A factory is already registered for key '{key}'."`)。编译期由 DP020 检测。 -3. **`IFactoryRegistry` 继承 `IReadOnlyRegistry`**:与 `IStrategyRegistry` 共享 `IReadOnlyRegistry<TKey, TValue>` 抽象(提供 `Keys` 和 `TryGet`)。 -4. **标记的类须实现指定的 `TContract`**(DP021)。 -5. **标记的类须有 public 无参构造**:生成器使用 `new()` 实例化(DP022),或启用 DI 集成由容器解析。 -6. **key 唯一性**:同一 `TContract` 下 key 不可重复(DP020 在编译期强制)。 - -## 兼容基线 - -- 运行时 TFM:`netstandard2.0` + `net8.0`(两者均须可用并随包分发)。 -- 泛型 Attribute(`RegisterFactoryAttribute<TContract>`)需要 C# 11+ / `net7.0+`;`netstandard2.0` 目标下用 `#if NET7_0_OR_GREATER` 条件编译。 -- 非泛型 `RegisterFactoryAttribute` 始终可用,功能等价。 -- net8.0 上 `FactoryRegistry<TKey, TProduct>` 内部使用 `FrozenDictionary` 优化查找。 - -## 不在范围内 - -- 不做抽象工厂族(多个产品类型族)的完整框架 -- 不在 Core 中引用 DI 容器 -- 不用反射扫描程序集注册 factory diff --git a/docs/spec/README.md b/docs/spec/README.md deleted file mode 100644 index 2fc1fb2..0000000 --- a/docs/spec/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Spec 索引 - -规范文档(Specification)— 定义模式的稳定契约:API 面、诊断 ID、不变量、兼容基线。 - -- **格式与变更门槛**:见 [DOCUMENTATION.md](../DOCUMENTATION.md#4-spec--规范文档) -- **模板**:[_template.md](_template.md) -- **与 Design Doc 的关系**:Spec 描述 **what**(契约),[Design Doc](../design/) 描述 **how** + **why**(实现) - -## 已有 Spec - -| 模式 | Spec | Design Doc | 关联 ADR | -|------|------|------------|----------| -| Strategy | [Strategy.md](Strategy.md) | [Strategy.md](../design/Strategy.md) | — | -| Chain of Responsibility | [ChainOfResponsibility.md](ChainOfResponsibility.md) | [ChainOfResponsibility.md](../design/ChainOfResponsibility.md) | — | -| Composite | [Composite.md](Composite.md) | [Composite.md](../design/Composite.md) | [ADR-006](../adr/ADR-006-composite-parallel-traversal.md)、[ADR-007](../adr/ADR-007-composite-tree-schema-validation.md) | -| Factory Registry | [FactoryRegistry.md](FactoryRegistry.md) | [FactoryRegistry.md](../design/FactoryRegistry.md) | — | -| Decorator | [Decorator.md](Decorator.md) | [Decorator.md](../design/Decorator.md) | — | -| Event Aggregator | [EventAggregator.md](EventAggregator.md) | [EventAggregator.md](../design/EventAggregator.md) | — | -| State Transition Table | [StateTransitionTable.md](StateTransitionTable.md) | [StateTransitionTable.md](../design/StateTransitionTable.md) | [ADR-005](../adr/ADR-005-state-transition-table.md) | - -## 迁移状态 - -所有 7 个模式文档已从 `docs/<PatternName>.md` 拆分迁移至 `docs/spec/` + `docs/design/`。旧文件已删除。 diff --git a/docs/spec/StateTransitionTable.md b/docs/spec/StateTransitionTable.md deleted file mode 100644 index aff79aa..0000000 --- a/docs/spec/StateTransitionTable.md +++ /dev/null @@ -1,393 +0,0 @@ -# Spec: State 转换表 - -> **版本**:v0.2.2(与 NuGet 包版本对齐) -> **关联 Design Doc**:[docs/design/StateTransitionTable.md](../design/StateTransitionTable.md) -> **关联 ADR**:[ADR-005](../adr/ADR-005-state-transition-table.md) - -## API 面 - -### 运行时接口 - -命名空间:`DesignPatterns.Behavioral` - -#### 转换表 - -```csharp -namespace DesignPatterns.Behavioral; - -/// <summary> -/// 只读转换表:(当前状态, 触发器) → 下一状态。 -/// </summary> -public interface ITransitionTable<TState, TTrigger> - where TState : struct, Enum - where TTrigger : struct, Enum -{ - bool TryTransition(TState from, TTrigger trigger, out TState to); - bool CanTransitionFrom(TState from, TTrigger trigger); - IReadOnlyCollection<TTrigger> GetAllowedTriggers(TState from); - TState InitialState { get; } -} -``` - -- `TryTransition`:命中边且 guard 通过时返回 `true` 并设置 `to`;否则返回 `false`。 -- `GetAllowedTriggers`:返回从 `from` 出发的所有合法触发器。 -- `CanTransitionFrom`:仅接收 state(无 trigger),判断是否存在以 `from` 为起点的边。 - -`TransitionTable<TState, TTrigger>` 为不可变实现;net8.0 上内部使用 `FrozenDictionary` 优化查找。 - -#### Builder(手动注册,无生成器时) - -```csharp -public sealed class TransitionTableBuilder<TState, TTrigger> - where TState : struct, Enum - where TTrigger : struct, Enum -{ - public TransitionTableBuilder<TState, TTrigger> WithInitial(TState initial); - public TransitionTableBuilder<TState, TTrigger> Add( - TState from, TTrigger trigger, TState to, - Func<TState, TTrigger, bool>? guard = null, - Action<TState, TState, TTrigger>? onEnterSync = null, - Action<TState, TState, TTrigger>? onExitSync = null, - Func<TState, TState, TTrigger, CancellationToken, ValueTask>? onEnterAsync = null, - Func<TState, TState, TTrigger, CancellationToken, ValueTask>? onExitAsync = null); - public ITransitionTable<TState, TTrigger> Build(); -} -``` - -> **v3.4 重载简化**:`Add` 的多个重载合并为单一签名,所有可选参数(`guard`、`onEnterSync`、`onExitSync`、`onEnterAsync`、`onExitAsync`)均有默认值 `null`,调用方可按命名参数传任意子集,无需为占位提供 `null`。 - -手动 Builder 用法: - -```csharp -using DesignPatterns.Behavioral; - -public enum OrderStatus { Draft, Submitted, Paid } -public enum OrderTrigger { Submit, Pay } - -var table = new TransitionTableBuilder<OrderStatus, OrderTrigger>() - .WithInitial(OrderStatus.Draft) - .Add(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted) - .Add(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid) - .Build(); - -if (table.TryTransition(OrderStatus.Draft, OrderTrigger.Submit, out var next)) -{ - // next == Submitted -} - -// 非法边返回 false;Transition() 扩展抛 InvalidTransitionException -``` - -#### Transition 扩展 - -```csharp -public static class TransitionTableExtensions -{ - // 非法边时抛 InvalidTransitionException(含当前态与触发器信息) - public static TState Transition<TState, TTrigger>( - this ITransitionTable<TState, TTrigger> table, TState from, TTrigger trigger) - where TState : struct, Enum - where TTrigger : struct, Enum; -} -``` - -#### Guard 委托 - -`TransitionTableBuilder.Add` 提供 `guard` 参数;`TransitionEdge<TState, TTrigger>.Guard` 存储委托。`TransitionTable.TryTransition` 在命中边后调用 guard;guard 返回 `false` 时该次转换视为不存在。 - -```csharp -var table = new TransitionTableBuilder<OrderStatus, OrderTrigger>() - .WithInitial(OrderStatus.Draft) - .Add(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted, - guard: (state, trigger) => !string.IsNullOrEmpty(orderId)) - .Build(); -``` - -#### Entry / Exit Actions - -`TransitionTableBuilder.Add` 提供 `onEnterSync` / `onExitSync` / `onEnterAsync` / `onExitAsync` 参数。Action 仅通过 async 路径(`TryTransitionAsync`)触发;同步 `TryTransition` 不调用 action。 - -```csharp -var table = new TransitionTableBuilder<OrderStatus, OrderTrigger>() - .WithInitial(OrderStatus.Draft) - .Add( - OrderStatus.Draft, - OrderTrigger.Submit, - OrderStatus.Submitted, - guard: null, - onEnterSync: (from, to, trigger) => Console.WriteLine($"Entering {to}"), - onExitSync: (from, to, trigger) => Console.WriteLine($"Exiting {from}")) - .Build(); -``` - -执行顺序(`TryTransitionAsync`):guard → OnExit(sync → async)→ OnEnter(sync → async)→ 返回结果。 - -> **部分执行**:若 OnExit 成功后 OnEnter 抛异常,异常传播给调用方,OnExit 副作用已发生。调用方需自行处理补偿。`TransitionResult<TState>` 不暴露执行进度。 - -#### IStateMachine 实例包装器 - -```csharp -public interface IStateMachine<TState, TTrigger> - where TState : struct, Enum - where TTrigger : struct, Enum -{ - TState CurrentState { get; } - bool TryTransition(TTrigger trigger, out TState to); - ValueTask<TransitionResult<TState>> TryTransitionAsync(TTrigger trigger, CancellationToken ct = default); -} - -public sealed class StateMachine<TState, TTrigger> : IStateMachine<TState, TTrigger> - where TState : struct, Enum - where TTrigger : struct, Enum -{ - public StateMachine(ITransitionTable<TState, TTrigger> table); - public TState CurrentState { get; } // 初始 == table.InitialState - public bool TryTransition(TTrigger trigger, out TState to); - public ValueTask<TransitionResult<TState>> TryTransitionAsync(TTrigger trigger, CancellationToken ct = default); -} -``` - -`TryTransition` 委托给底层 table;成功时更新 `CurrentState`。`TryTransitionAsync` 同理,并触发 entry/exit action。 - -```csharp -var machine = new StateMachine<OrderStatus, OrderTrigger>(table); -// machine.CurrentState == table.InitialState - -if (machine.TryTransition(OrderTrigger.Submit, out var next)) -{ - // machine.CurrentState == OrderStatus.Submitted -} -``` - -> **线程安全**:`StateMachine<TState,TTrigger>` 非线程安全,设计为单线程使用。多线程场景应在调用方同步,或每线程使用独立实例。 - -#### IStateHierarchy(v3 层次状态) - -```csharp -public interface IStateHierarchy<TState> - where TState : struct, Enum -{ - TState? GetParent(TState state); - bool IsInState(TState state, TState ancestor); - IReadOnlyList<TState> GetAncestors(TState state); -} -``` - -- `GetParent`:返回直接父状态,无父时返回 `null`。 -- `IsInState`:判断 `state` 是否为 `ancestor` 的后代(含自身)。 -- `GetAncestors`:从 `state` 向上到根的祖先链(不含 `state` 自身)。 - -### 特性(Attribute) - -命名空间:`DesignPatterns.Behavioral` - -#### [StateMachine] - -```csharp -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -public sealed class StateMachineAttribute : Attribute -{ - public Type State { get; } // state enum 类型 - public Type Trigger { get; } // trigger enum 类型 - public object? Initial { get; set; } // 初始状态(state enum 成员) - public bool Hierarchical { get; set; } // v3:启用层次状态模式 - public StateMachineAttribute(Type state, Type trigger); -} -``` - -#### [Transition] - -```csharp -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class TransitionAttribute : Attribute -{ - public object From { get; } // state enum 成员 - public object Trigger { get; } // trigger enum 成员 - public object To { get; } // state enum 成员 - public string? Guard { get; set; } // holder 类上的 static guard 方法名 - public string? OnEnter { get; set; } // holder 类上的 static entry action 方法名 - public string? OnExit { get; set; } // holder 类上的 static exit action 方法名 - public TransitionAttribute(object from, object trigger, object to); -} -``` - -#### [StateParent](v3 层次状态) - -```csharp -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class StateParentAttribute : Attribute -{ - public object Child { get; } // state enum 成员 - public object Parent { get; } // state enum 成员 - public StateParentAttribute(object child, object parent); -} -``` - -#### 用法示例 - -```csharp -[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft)] -[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted)] -[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid)] -public static partial class OrderMachine; - -// 生成:OrderStatusTransitionTable.Instance -// holder:OrderMachine.TryTransition(...)、OrderMachine.InitialState -``` - -Guard + Entry/Exit 示例: - -```csharp -[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft)] -[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted, Guard = nameof(CanSubmit))] -[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid, - OnEnter = nameof(OnSubmitted), OnExit = nameof(OnLeaveDraft))] -public static partial class OrderMachine -{ - public static bool CanSubmit(OrderStatus state, OrderTrigger trigger) => true; - public static void OnSubmitted(OrderStatus from, OrderStatus to, OrderTrigger trigger) { } - public static void OnLeaveDraft(OrderStatus from, OrderStatus to, OrderTrigger trigger) { } -} -``` - -层次状态示例(v3): - -```csharp -[StateMachine(typeof(OrderStatus), typeof(OrderTrigger), Initial = OrderStatus.Draft, Hierarchical = true)] -[StateParent(OrderStatus.Submitted, OrderStatus.Active)] -[StateParent(OrderStatus.Paid, OrderStatus.Active)] -[Transition(OrderStatus.Draft, OrderTrigger.Submit, OrderStatus.Submitted)] -[Transition(OrderStatus.Submitted, OrderTrigger.Pay, OrderStatus.Paid)] -[Transition(OrderStatus.Active, OrderTrigger.Cancel, OrderStatus.Cancelled, OnExit = nameof(OnExitActive))] -public static partial class OrderMachine -{ - public static void OnExitActive(OrderStatus from, OrderStatus to, OrderTrigger trigger) { } -} -``` - -### 生成器产出 - -对 state enum `OrderStatus`: - -| 生成物 | 名称 | -|--------|------| -| 转换表类型 | `OrderStatusTransitionTable` | -| 单例 | `OrderStatusTransitionTable.Instance` | -| Holder 便捷 API | `OrderMachine.TryTransition`、`InitialState` | - -#### {StateEnum}TransitionTable - -```csharp -public sealed partial class OrderStatusTransitionTable - : ITransitionTable<OrderStatus, OrderTrigger> - , IStateHierarchy<OrderStatus> // v3:层次模式时实现 -{ - public static OrderStatusTransitionTable Instance { get; } - public OrderStatus InitialState { get; } - public bool TryTransition(OrderStatus from, OrderTrigger trigger, out OrderStatus to); - public bool CanTransitionFrom(OrderStatus from, OrderTrigger trigger); - public IReadOnlyCollection<OrderTrigger> GetAllowedTriggers(OrderStatus from); - // v3 层次模式时额外实现 IStateHierarchy<OrderStatus> - public OrderStatus? GetParent(OrderStatus state); - public bool IsInState(OrderStatus state, OrderStatus ancestor); - public IReadOnlyList<OrderStatus> GetAncestors(OrderStatus state); -} -``` - -#### Holder 便捷方法 - -生成器在 holder partial 类上输出: - -- `TryTransition(...)` — 委托到 `Instance` -- `InitialState` — 委托到 `Instance.InitialState` - -#### RegisterDi(引用 `DesignPatterns.Extensions.DependencyInjection` 时) - -当消费项目引用 `DesignPatterns.Extensions.DependencyInjection`(自动 Import `build/DesignPatterns.Extensions.DependencyInjection.targets`)时,生成器在转换表类上额外输出: - -```csharp -public static IServiceCollection RegisterDi( - IServiceCollection services, - ServiceLifetime lifetime = ServiceLifetime.Singleton) -{ - services.TryAdd(new ServiceDescriptor( - typeof(ITransitionTable<OrderStatus, OrderTrigger>), - _ => Instance, - lifetime)); - // v3 层次模式时额外注册 IStateHierarchy<OrderStatus> - return services; -} -``` - -调用方式: - -```csharp -OrderStatusTransitionTable.RegisterDi(services); -// 或链式 -services.AddOtherStuff().RegisterDi<OrderStatus, OrderTrigger>(); -``` - -启用开关:MSBuild 属性 `DesignPatterns_EnableDiIntegration`(引用 DI 扩展包时自动为 `true`)。 - -#### 手动注册扩展 - -`DesignPatterns.Extensions.DependencyInjection.DesignPatternsServiceCollectionExtensions` 提供 `AddTransitionTable` 扩展方法,用于注册预构建的表实例: - -```csharp -services.AddTransitionTable(manualTable); -services.AddTransitionTable(OrderStatusTransitionTable.Instance, ServiceLifetime.Singleton); -``` - -使用 `TryAdd` 语义,重复注册不会覆盖。 - -v3 层次模式下使用 `AddStateHierarchy<TState, TTrigger>` 扩展方法(从容器解析 table 并转型)和 `AddStateMachine<TState, TTrigger>` 扩展方法。 - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| **DP026** | Error | 重复边 `(from, trigger)` | 重复的转换边 | -| **DP027** | Error | `[Transition]` 的 state 非 enum 成员 | state 非 enum 成员 | -| **DP028** | Error | trigger 非 enum 成员 | trigger 非 enum 成员 | -| **DP029** | Error | `Initial` 非 state enum 成员 | Initial 非 state enum 成员 | -| **DP030** | Error | holder 非 static partial class | holder 须为 static partial class | -| **DP031** | Info | state 从未作为 `from` 出现(终态提示) | state 从未作为 from 出现 | -| **DP032** | Error | guard 方法在 holder 类上未找到 | guard 方法未找到 | -| **DP034** | Error | guard 方法非 static | guard 方法须为 static | -| **DP035** | Error | guard 方法签名错误(须 `bool Method(TState, TTrigger)`) | guard 方法签名错误 | -| **DP036** | Info | `TryTransition` 字面量 (state, trigger) 对未声明(与 DP025 对称) | 字面量边未声明 | -| **DP037** | Error | entry/exit action 方法在 holder 类上未找到 | action 方法未找到 | -| **DP038** | Error | entry/exit action 方法非 static(防御性;CS0708 先于生成器拒绝) | action 方法须为 static | -| **DP039** | Error | entry/exit action 方法签名错误(须 `void Method(TState, TState, TTrigger)` 或 `ValueTask Method(TState, TState, TTrigger, CancellationToken)`) | action 方法签名错误 | -| **DP056** | Error | `[StateParent]` 父链存在循环 | 父链循环 | -| **DP057** | Error | `[StateParent]` 的 child/parent 非 state enum 成员 | 非法 enum 成员 | -| **DP058** | Error | `[StateParent]` 自引用(child == parent) | 自引用父 | -| **DP059** | Error | `[StateParent]` 的 parent 从未作为任何边的 state 出现(孤立父声明) | 孤立父声明 | - -常量与文案:[`DesignPatterns.Diagnostics/DiagnosticIds.cs`](../../DesignPatterns.Diagnostics/DiagnosticIds.cs)、[`DesignPatternsDiagnosticDescriptors.cs`](../../DesignPatterns.Diagnostics/DesignPatternsDiagnosticDescriptors.cs)。 - -用户向说明:[DesignPatterns.Docs — diagnostics](https://skymly.github.io/DesignPatterns.Docs/diagnostics#state-transition-table-dp026-dp031)。 - -## 不变量 - -1. `TState` 和 `TTrigger` 均须为 **enum**(`where TState : struct, Enum`)。 -2. Guard 方法签名须为 `bool Method(TState, TTrigger)`;须 **static**;须在 holder 类上声明。 -3. Entry/Exit action 方法签名须为 `void Method(TState from, TState to, TTrigger trigger)`(sync)或 `ValueTask Method(TState from, TState to, TTrigger trigger, CancellationToken)`(async);须 **static**;须在 holder 类上声明。 -4. `StateMachine<TState, TTrigger>` **非线程安全**,设计为单线程使用。多线程场景应在调用方同步,或每线程使用独立实例。 -5. 执行顺序(`TryTransitionAsync`):guard → OnExit(sync → async)→ OnEnter(sync → async)→ 返回结果。 -6. Entry/Exit action 仅通过 async 路径(`TryTransitionAsync`)触发;同步 `TryTransition` 不调用 action。 -7. 部分执行语义:若 OnExit 成功后 OnEnter 抛异常,异常传播给调用方,OnExit 副作用已发生;`TransitionResult<TState>` 不暴露执行进度。 -8. `TransitionTable<TState, TTrigger>` 不可变;`Build()` 后不可修改。 -9. DI 注册使用 `TryAdd` 语义,重复注册不会覆盖。 - -## 兼容基线 - -- netstandard2.0 + net8.0(两者均须可用并随包分发) -- Roslyn 组件 4.8.0(`Microsoft.CodeAnalysis.CSharp` / Workspaces;Analyzers 3.3.4) -- 生成器实现:`IIncrementalGenerator` + `ForAttributeWithMetadataName` - -## 不在范围内 - -- 并发状态(concurrent states)/ 历史状态(history states) -- `string` / `int` 状态键(仅支持 enum) -- Guard 的编译期表达式求值(v2 仅运行时委托 + 方法引用校验) diff --git a/docs/spec/Strategy.md b/docs/spec/Strategy.md deleted file mode 100644 index 3b26e57..0000000 --- a/docs/spec/Strategy.md +++ /dev/null @@ -1,294 +0,0 @@ -# Spec: Strategy - -> **版本**:v0.2.2(与 NuGet 包版本对齐) -> **关联 Design Doc**:[docs/design/Strategy.md](../design/Strategy.md) - -## API 面 - -### 运行时接口 - -命名空间:`DesignPatterns.Behavioral` - -#### 策略接口(可选标记接口) - -```csharp -namespace DesignPatterns.Behavioral; - -/// <summary> -/// 标记性策略接口。不强制实现,但配合源生成器时提供更好的类型约束。 -/// </summary> -public interface IStrategy<in TInput, out TOutput> -{ - TOutput Execute(TInput input); -} - -/// <summary> -/// 异步策略接口。 -/// </summary> -public interface IAsyncStrategy<in TInput, TOutput> -{ - ValueTask<TOutput> ExecuteAsync(TInput input, CancellationToken ct = default); -} -``` - -> **注意**:`[RegisterStrategy]` 不要求实现 `IStrategy<,>`。任何接口/基类均可作为策略契约。 -> -> `IStrategy<,>` / `IAsyncStrategy<,>` 为**可选标记接口**,便于表达同步/异步算法形状;注册表与生成器不依赖它们。用法见 `tests/DesignPatterns.Tests/Behavioral/StrategyMarkerInterfaceTests.cs`。 - -#### 注册表 - -```csharp -namespace DesignPatterns.Behavioral; - -/// <summary> -/// 按 key 解析策略实现的只读注册表。 -/// </summary> -public interface IStrategyRegistry<TKey, TStrategy> - where TKey : notnull -{ - bool TryGet(TKey key, [MaybeNullWhen(false)] out TStrategy strategy); - TStrategy Get(TKey key); // 找不到抛 StrategyNotFoundException - bool TryGetWithGuard(TKey key, [MaybeNullWhen(false)] out TStrategy strategy); - IReadOnlyCollection<TKey> Keys { get; } -} -``` - -> **注意**:`TStrategy` 为不变(invariant),非协变 `out`——`TryGetWithGuard` 的 `out TStrategy` 参数阻止协变。 - -提供不可变实现 `StrategyRegistry<TKey, TStrategy>`,net8.0 上内部使用 `FrozenDictionary` 优化查找。 - -#### Builder(手动注册,无生成器时) - -```csharp -public sealed class StrategyRegistryBuilder<TKey, TStrategy> where TKey : notnull -{ - public StrategyRegistryBuilder<TKey, TStrategy> Register(TKey key, TStrategy strategy); - public StrategyRegistryBuilder<TKey, TStrategy> Register(TKey key, Func<TStrategy> factory); - public IStrategyRegistry<TKey, TStrategy> Build(); -} -``` - -#### 异步策略解析 - -`IAsyncStrategy<TInput, TOutput>` 与 `[RegisterStrategy]` 使用**同一套** Keys / Registry / `RegisterDi` 路径;契约可以是继承 `IAsyncStrategy<,>` 的专用接口,无需额外 attribute 或并行注册表类型。 - -`StrategyRegistryExtensions` 提供按 key 解析并执行的便利方法: - -```csharp -// 注册表值为 IAsyncStrategy<TInput, TOutput> -var result = await registry.ExecuteAsync(PaymentAsyncStrategyKeys.Stripe, amount); - -// 注册表值为继承 IAsyncStrategy<,> 的契约(需显式指定 TContract / TOutput / TInput) -var length = await registry.ExecuteAsync<ITextProcessor, int, string>( - TextProcessorKeys.Length, "hello"); - -// 或等价写法 -var length = await registry.Get(TextProcessorKeys.Length).ExecuteAsync("hello"); -``` - -`TryExecuteAsync` 与 `ExecuteAsync` 对称,未命中 key 时返回 `false` 而不抛异常。 - -DI 场景:`{Contract}Registry.RegisterDi(services)` 后从容器解析 `IStrategyRegistry<string, TContract>`,再 `await registry.ExecuteAsync<...>(...)` 或 `Get(key).ExecuteAsync(...)`。 - -#### Guard 谓词 - -`TryGetWithGuard` 在解析策略时额外评估注册的 guard 谓词。guard 返回 `false` 时该策略视为未注册(返回 `false`)。 - -```csharp -var builder = new StrategyRegistryBuilder<string, IPaymentStrategy>() - .Register("alipay", new AlipayPayment(), guard: key => isEnabled("alipay")); -var registry = builder.Build(); - -// guard 通过时返回策略;guard 返回 false 时返回 false -if (registry.TryGetWithGuard("alipay", out var strategy)) { ... } -``` - -> **设计约束**:guard 签名为 `Func<TKey, bool>`(仅接收 key),非 `Func<TInput, bool>`。注册表层面不知道 `TInput`(`TStrategy` 不要求实现 `IStrategy<TInput, TOutput>`),因此无法基于输入判断。基于输入的动态路由是业务逻辑,不在此库范围。 - -源生成器支持:`[RegisterStrategy<TContract>("key", Guard = nameof(CanEnable))]`,生成器校验 guard 方法签名(DP047-DP049)。 - -### 特性(Attribute) - -命名空间:`DesignPatterns.Behavioral` - -#### 泛型版本(C# 11 / .NET 7+) - -```csharp -/// <summary> -/// 标记一个类为某策略接口的实现,并注册到编译期生成的策略注册表中。 -/// </summary> -/// <typeparam name="TContract">策略契约接口。</typeparam> -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class RegisterStrategyAttribute<TContract> : Attribute -{ - /// <summary> - /// 用于解析此策略的 key。 - /// </summary> - public string Key { get; } - - /// <summary> - /// 可选:实现类上的 static guard 方法名。设置后该方法须有签名 - /// <c>static bool Method(TKey key)</c>。guard 返回 false 时该策略视为未注册。 - /// </summary> - public string? Guard { get; set; } - - public RegisterStrategyAttribute(string key) - { - Key = key; - } -} -``` - -#### 非泛型版本(netstandard2.0 / C# 7.3) - -```csharp -/// <summary> -/// 非泛型版本,用于不支持泛型 Attribute 的目标框架。 -/// </summary> -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class RegisterStrategyAttribute : Attribute -{ - public string Key { get; } - - /// <summary> - /// 策略契约接口类型。 - /// </summary> - public Type For { get; } - - /// <summary> - /// 可选:实现类上的 static guard 方法名。签名须为 <c>static bool Method(TKey key)</c>。 - /// </summary> - public string? Guard { get; set; } - - public RegisterStrategyAttribute(string key, Type @for) - { - Key = key; - For = @for; - } -} -``` - -#### 用法示例 - -```csharp -// 泛型 Attribute(推荐,C# 11+) -[RegisterStrategy<IPaymentStrategy>("alipay")] -public class AlipayPayment : IPaymentStrategy { ... } - -[RegisterStrategy<IPaymentStrategy>("wechat")] -public class WechatPayment : IPaymentStrategy { ... } - -// 非泛型(netstandard2.0 / C# 7.3) -[RegisterStrategy("alipay", typeof(IPaymentStrategy))] -public class AlipayPayment : IPaymentStrategy { ... } -``` - -### 生成器产出 - -对于每个 `TContract`,生成器输出: - -#### 1. 强类型 Key 常量 - -```csharp -// PaymentStrategyKeys.g.cs -public static partial class PaymentStrategyKeys -{ - public const string Alipay = "alipay"; - public const string Wechat = "wechat"; -} -``` - -命名规则:`{接口名去掉前缀I和后缀Strategy}Keys`,可通过特性参数 override。 - -#### 2. 静态注册表(无 DI 场景) - -```csharp -// PaymentStrategyRegistry.g.cs -public static partial class PaymentStrategyRegistry -{ - private static readonly IStrategyRegistry<string, IPaymentStrategy> _instance = - new StrategyRegistry<string, IPaymentStrategy>( - new Dictionary<string, IPaymentStrategy> - { - ["alipay"] = new AlipayPayment(), - ["wechat"] = new WechatPayment(), - }); - - public static IStrategyRegistry<string, IPaymentStrategy> Instance => _instance; -} -``` - -#### 3. DI 集成(引用 `DesignPatterns.Extensions.DependencyInjection` 时) - -引用扩展包会自动 Import `build/DesignPatterns.Extensions.DependencyInjection.targets`,设置 `DesignPatterns_EnableDiIntegration=true`,生成器额外输出: - -```csharp -public static partial class PaymentStrategyRegistry -{ - // Instance 仍保留(new() 静态实例,无 DI 生命周期) - - public static IStrategyRegistry<string, IPaymentStrategy> Create(IServiceProvider serviceProvider) => - new ServiceProviderStrategyRegistry<string, IPaymentStrategy>(serviceProvider, _diEntries); - - public static IServiceCollection RegisterDi( - IServiceCollection services, - ServiceLifetime implementationLifetime = ServiceLifetime.Singleton, - ServiceLifetime registryLifetime = ServiceLifetime.Singleton); -} -``` - -推荐用法: - -```csharp -var services = new ServiceCollection(); -PaymentStrategyRegistry.RegisterDi(services); -var registry = services.BuildServiceProvider() - .GetRequiredService<IStrategyRegistry<string, IPaymentStrategy>>(); -``` - -| 生命周期组合 | 行为 | -|--------------|------| -| Singleton 实现 + Singleton 注册表 | 默认;`TryGet` 每次解析同一实现实例 | -| Transient 实现 + Singleton 注册表 | `TryGet` 每次从容器取新实例(推荐需要可变策略时) | -| Transient 注册表 | 每次解析注册表时重建 `Create(sp)` | - -手动 Builder 仍可用:`services.AddStrategyRegistry<string, IPaymentStrategy>(...)`(见扩展包),与生成器互不冲突。 - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| DP003 | Error | 同一 `TContract` 下 key 重复 | 编译期检测冲突 | -| DP004 | Error | 标记的类未实现指定的 `TContract` | 接口不匹配 | -| DP006 | Info | 实现了某策略契约但未加 `[RegisterStrategy]` | 建议添加特性 | -| DP007 | Error | 标记的类缺少 public 无参构造 | 无法 `new()` 实例化 | -| DP047 | Error | `Guard` 指定的方法在实现类上未找到 | 添加 static 方法或移除 Guard | -| DP048 | Error | `Guard` 指定的方法非 static | 改为 static | -| DP049 | Error | `Guard` 指定的方法签名错误(须 `static bool Method(TKey key)`) | 修正参数类型或返回类型 | - -> **注意**:DP005 属于 Handler(`[HandlerOrder]` 重复 Order),不属于 Strategy。 - -## 不变量 - -1. **key 唯一性**:同一 `TContract` 下 key 不可重复(DP003 在编译期强制)。 -2. **`TStrategy` 不变(invariant)**:`IStrategyRegistry<TKey, TStrategy>` 的 `TStrategy` 非协变 `out`——`TryGetWithGuard` 的 `out TStrategy` 参数阻止协变。 -3. **guard 签名固定为 `Func<TKey, bool>`**:guard 仅接收 key,不接收 `TInput`。注册表层面不知道 `TInput`(`TStrategy` 不要求实现 `IStrategy<TInput, TOutput>`)。 -4. **`[RegisterStrategy]` 不要求实现 `IStrategy<,>`**:任何接口/基类均可作为策略契约。 -5. **标记的类须有 public 无参构造**:生成器使用 `new()` 实例化(DP007)。 -6. **标记的类须实现指定的 `TContract`**(DP004)。 -7. **`IStrategy<,>` / `IAsyncStrategy<,>` 为可选标记接口**:注册表与生成器不依赖它们。 - -## 兼容基线 - -- 运行时 TFM:`netstandard2.0` + `net8.0`(两者均须可用并随包分发)。 -- 泛型 Attribute(`RegisterStrategyAttribute<TContract>`)需要 C# 11+ / `net7.0+`;`netstandard2.0` 目标下用 `#if NET7_0_OR_GREATER` 条件编译。 -- 非泛型 `RegisterStrategyAttribute` 始终可用,功能等价。 -- net8.0 上 `StrategyRegistry<TKey, TStrategy>` 内部使用 `FrozenDictionary` 优化查找。 - -## 不在范围内 - -- 不做策略选择/路由逻辑(这是业务代码) -- 不通过反射扫描程序集 -- 不在注册表里管理对象生命周期(那是 DI 的事) -- 不做 `Func<T1, T2, ..., TResult>` 的无限委托展开 -- 不强制所有策略实现 `IStrategy<,>` diff --git a/docs/spec/_template.md b/docs/spec/_template.md deleted file mode 100644 index 8005591..0000000 --- a/docs/spec/_template.md +++ /dev/null @@ -1,39 +0,0 @@ -# Spec: <模式名> - -> **版本**:vX.Y(与 NuGet 包版本对齐) -> **关联 Design Doc**:[docs/design/<PatternName>.md](../design/<PatternName>.md) -> **关联 ADR**:ADR-XXX(如有) - -## API 面 - -### 运行时接口 - -(接口定义、方法签名、泛型约束) - -### 特性(Attribute) - -(特性类、属性、构造函数签名) - -### 生成器产出 - -(生成的类名、方法签名、命名空间) - -## 诊断 ID - -| ID | 级别 | 触发条件 | 消息格式 | -|----|------|----------|----------| -| DPXXX | Warning | ... | ... | - -## 不变量 - -1. ... -2. ... - -## 兼容基线 - -- netstandard2.0 / net8.0 -- ... - -## 不在范围内 - -- ...