vampire-like/docs/UI-5层架构设计规范.md

428 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UI 五层架构设计规范UseCase / RawData / Controller / Context / View
## 1. 文档目标
本文定义一套可长期复用的 UI 分层设计方案,用于约束 UI 模块的职责边界、依赖方向、通信方式和测试策略。
- 本文描述的是规范,不是对某个项目现状的总结
- 项目内目录或基类只作为落地示例,不改变本文的抽象约束
- 本文重点约束 UIForm 级模块;子组件(`Item`、`Area` 等)可只实现 `Context + View`
- Unity GameFramework 的底层细节不在本文展开,本文只约束项目内 UI 代码组织
补充说明(插件化落地):
- 本规范通过 `Assets/Plugins/UIModule/` 以**可选插件**方式落地
- 插件无独立程序集,通过 `.asmref` 融入原项目各程序集:
- `SepCore.Base.UIModule.asmref``Base/` → `SepCore.Base`
- `SepCore.Runtime.UIModule.asmref``Runtime/` → `SepCore.Runtime`
- `SepCore.Presentation.UIModule.asmref``UI/` → `SepCore.Presentation`
- `Runtime/``SepCore.UI` 命名空间放置基础抽象、UseCase、RawData`SepCore.CustomComponent` 放置 `UIRouterComponent`
- `UI/``SepCore.UIModule` 命名空间放置 Controller / Context / View
- 不启用插件时,项目可继续使用原有基座 UI 路线
- 启用插件时,必须通过 `UIRouterComponent` 统一管理 `Controller / UseCase`
- 通过 `UIRouterComponent` 打开的 UIForm 必须配置对应 Controller缺少绑定视为接入错误不回退到原始 UI 打开路径
- 当前插件对外 UI 生命周期接口为 async-first`OpenUIAsync / CloseUIAsync`
## 2. 核心原则
### 2.1 单向数据职责
UI 的职责链路固定为:
```text
外部流程
-> Controller
-> UseCase
-> RawData / Result
-> BuildContext
-> View
View
--(UI 专用事件)--> Controller
```
说明:
- `UseCase` 只负责业务规则、状态推进和纯业务数据生成
- `Controller` 是 UI 编排中心,也是唯一允许构建 `Context` 的层
- `View` 只负责渲染和抛出交互,不直接处理业务状态
### 2.2 严格分离业务数据与展示数据
- `RawData` 默认是业务传输模型,不承载展示模型。
- 轻量 UI 可携带回调委托,但回调只允许由 Controller 注册和触发,不得进入 Context / View。
- `Context` 是纯展示数据,不进入 `UseCase`
- `UseCase` 不能返回 `Context`
- `View` 只能消费 `Context`
### 2.3 Controller 是 UI 唯一外部入口
对于 UIForm 级模块,外部流程只能通过 `Controller` 驱动 UI
- 异步打开 UI
- 异步关闭 UI
- 绑定 `UseCase`
- 刷新 UI
- 响应 UI 专用事件
`View` 不作为外部流程的直接依赖对象。
## 3. 五层职责定义
### 3.1 UseCase 层
职责:封装 UI 对应的业务用例,负责业务规则、状态推进、校验和纯业务结果输出。
约束:
- 实现 `IUIUseCase`
- 命名:`XXXFormUseCase`
- 对外提供语义化方法,例如 `CreateInitialModel`、`TryRefresh`、`Select`、`Confirm`
- 返回值只能是 `RawData` 或纯业务结果对象,例如 `XXXResult`、`XXXActionResult`
- 不依赖 `Context`、`View`、`UGuiForm`、`MonoBehaviour` 等 UI 类型
- 不负责 UI 资源加载、文本拼装、颜色选择、图标转换等展示处理
- 不发布 UI 专用事件
适用场景:
- UI 会读写领域状态
- UI 存在明确业务规则、条件分支、校验或状态推进
- UI 的交互结果需要被测试和复用
### 3.2 RawData 层
职责:承载 `UseCase -> Controller` 的纯业务传输模型。
约束:
- 命名:`XXXFormRawData`
- 只描述业务数据,不包含 UI 展示行为
- 可以包含领域对象、配置对象、标识符、枚举、数值和纯数据集合
- **轻量场景下可携带回调委托**,由 Controller 在构建 Context 前完成注册
- 不允许依赖 `Context`、`View`、`Sprite`、`TMP_Text` 等展示相关类型
- 不允许直接使用 `XXXItemContext`、`XXXFormContext` 作为字段类型
说明:
- `RawData` 的目标是表达“业务上发生了什么”
- `Context` 的目标是表达“界面应该怎么显示”
- `RawData` 可以携带行为(如回调),`Context` 只承载展示数据,不允许携带回调
### 3.3 Controller 层
职责UI 编排层,负责连接外部流程、`UseCase`、`View`,并统一管理 UI 生命周期与展示状态。
约束:
- 对启用 `UIRouterComponent` 管理的 UIForm必须存在可实例化的 Controller 绑定;未绑定时 Router 应直接失败并输出 Error不允许 fallback 到 `GameEntry.UI.OpenUIForm(...)`
- 命名:`XXXFormController`
- 可基于 `UIFormControllerBase<TContext, TForm>` 实现
- 通过 `BindUseCase(IUIUseCase)` 注入用例并做类型校验
- 当前对外入口为 `OpenUIAsync(object userData = null, float timeout = 30f)``CloseUIAsync(...)`
- `OpenUIAsync` 只允许外部传入 `RawData` 或可转换为 `RawData` 的参数,不接受外部传入 `Context`
-`userData == null` 且 UI 绑定了 UseCase 时Controller 应通过 UseCase 构造初始 RawData再转换为 Context
-`userData == null` 且 UI 没有 UseCase 时Controller 可按 UI 类型决定构造默认 RawData / 默认 Context或返回失败并输出 warning
- 负责 `RawData / Result -> Context` 的转换,常见形式为 `BuildContext`
- 负责事件订阅与解除订阅,且必须成对出现
- 负责全量刷新与局部刷新策略
- 在同类 UI 可能多实例时,负责通过 `sender`、`serialId` 或其他等价标识限制事件只作用于当前 UI 实例
- **推荐子类显式组织业务时序**参数校验、Context 构造、临时状态缓存、关闭后续动作等都直接写在具体 Controller 中;基类只提供共用开关窗机制,不隐藏子类业务时机
允许职责:
- 将业务数据转换为展示友好的文本、图标、颜色、列表状态
- 在必要时查询本地化、资源映射或展示适配逻辑
禁止职责:
-`Controller` 中堆叠大段领域业务规则
- 绕过 `Context` 直接把业务对象塞给 `View`
- 直接修改其他 UI 的内部 `View`
### 3.4 Context 层
职责:承载“可直接驱动 UI 展示”的上下文数据。
约束:
- 继承 `UIContext`
- 命名:`XXXFormContext`、`XXXItemContext`、`XXXAreaContext`
- 只能由 `Controller` 构建和更新
- 字段以展示友好为目标,例如标题、描述、图标、颜色、状态、列表、按钮文案
- **不允许携带回调委托或行为**,交互行为由 Controller 注册View 通过 UI 专用事件通知 Controller
- 允许组合子 `Context`
- 不进入 `UseCase`
说明:
- `Context` 可以包含展示层需要的最终数据
- `Context` 可以是“已格式化”的显示数据
- 但这些数据必须由 `Controller` 负责准备,而不是 `UseCase`
### 3.5 View 层
职责:纯表现层,负责控件绑定、渲染刷新、动画触发和交互事件抛出。
约束:
- Form 类继承 `UGuiForm`,子组件通常继承 `MonoBehaviour`
- 命名:`XXXForm`、`XXXItem`、`XXXArea`
- 提供打开、关闭、刷新入口:
- `UGuiForm``OnOpen(object userData)`、`OnClose(bool isShutdown, object userData)`、`RefreshUI(Context)`
- `MonoBehaviour` 子组件:可统一设计为 `OnOpen(Context)`、`OnClose()`、`RefreshUI(Context)`
- 只消费 `Context`
- 用户交互通过 UI 专用事件通知 `Controller`
- 不承载业务规则、流程推进、数据筛选和领域状态修改
- 不订阅全局业务事件
允许职责:
- 本地控件显隐
- 动画播放
- 一次性的纯视觉状态缓存
禁止职责:
- 直接调用 `UseCase`
- 直接修改领域状态
- 订阅或处理全局业务事件
- 将自己作为业务逻辑的入口
## 4. UI 类型分级
### 4.1 标准五层 UI
组成:`UseCase + RawData + Controller + Context + View`
适用条件:
- UI 需要读写领域状态
- UI 存在明确业务规则或分支
- UI 交互会改变游戏流程、角色状态、背包、战斗结果等业务对象
- UI 行为需要被自动化测试覆盖
默认规则:
- 新增业务型 UI优先使用完整五层
### 4.2 轻量 UI
组成:`Controller + Context + View`
说明:
- `UseCase` 对轻量 UI 不强制
- `RawData` 也不是强制层,可按需要补充
- 轻量 UI 仍然必须通过 `Controller` 驱动,不能让 `View` 直接承担外部入口
适用条件:
- 只承担展示、导航、确认、提示等轻量职责
- 没有独立的业务规则或状态推进
- 只需要把已有参数转换成界面展示
升级规则:
- 一旦轻量 UI 开始承载业务规则、校验或状态推进,应升级为标准五层 UI
## 5. 依赖方向约束
允许依赖:
- `UseCase -> 领域对象 / 纯业务服务 / RawData / Result`
- `Controller -> UseCase + RawData + Result + Context + View + UI 专用事件`
- `Context -> 子 Context / 纯展示值对象`
- `View -> Context + UI 专用事件`
禁止依赖:
- `UseCase -> Context / View / Unity 具体展示组件`
- `RawData / Result -> Context / View`
- `Context -> View / UseCase`
- `View -> UseCase`
- `View -> 全局业务事件`
- `View -> 领域状态修改`
### 5.1 插件化分层建议
建议分层如下:
- `SepCore.Base`UI 专用事件(`SepCore.Event`
- `SepCore.Runtime`:基座与业务流程层,同时融入插件 `Runtime/` 中的基础抽象、UseCase、RawData 与 `UIRouterComponent`
- `SepCore.Presentation`Controller、Context、View`SepCore.UIModule`
- Editor`UIModule.Editor`,代码命名空间当前为 `SepCore.UIModule.Editor`
插件目录与程序集映射:
- `Base/``SepCore.Base`
- `Runtime/``SepCore.Runtime`
- `UI/``SepCore.Presentation`
建议依赖方向:
```text
SepCore.BaseUI 专用事件)
SepCore.Runtime基础抽象 + UseCase + RawData + Router
SepCore.PresentationController + Context + View
```
约束:
- `SepCore.Base` / `SepCore.Runtime` 不反向依赖 Presentation 层
- UI 专用事件虽在 `SepCore.Event` 命名空间,但语义上仍归属对应 UI 模块,禁止被业务模块复用
- 业务流程层通过插件入口(如 Router调用五层 UI 能力
- 插件内无独立程序集,通过 `.asmref` 文件融入原项目编译
## 6. 事件通信规范
### 6.1 UI 与 Controller 的通信方式
`View``Controller` 的通信通过当前 UI 模块专用事件完成。
约束:
- UI 专用事件只服务于当前 UI 模块
- 这些事件不是业务公共事件
- 业务模块、流程模块、领域模块不应复用这些事件
- UI 专用事件可统一放在 `SepCore.Event` 命名空间下,但事件语义仍归属对应 UI 模块,禁止被业务模块复用
- 如果底层使用全局事件总线实现,也只能把它当作“传输通道”,不能把事件语义扩散成全局契约
### 6.2 事件边界
- `View -> Controller`:使用 UI 专用事件
- `Controller -> View`:通过刷新 `Context` 或调用 `View` 的渲染接口
- `Controller -> UseCase`:直接方法调用
- `UseCase -> Controller`:通过返回 `RawData / Result`,不通过 UI 事件反推界面
- `外部流程 -> Controller`:只传入 `RawData` 或可转换为 `RawData` 的参数,不传入 `Context`
### 6.3 事件安全要求
- 同类 UI 可同时存在时,事件必须只作用于当前窗体实例;可通过 `sender`、`serialId` 或其他等价标识实现
- UI 专用事件命名应体现模块归属,避免语义过宽
- 同一 UI 可以按需要使用“多个精细事件”或“单一事件 + 子类型/按钮编号”的方式建模;例如当前 Dialog 使用 `DialogEventArgs + ButtonId`
## 7. 标准交互流程
### 7.1 有 UseCase 的标准流程
```text
Procedure / GameState
-> BindUseCase
-> await OpenUIAsync(rawData or null)
Controller
-> 如果外部传入 RawData直接使用该 RawData
-> 如果外部未传入 RawDataUseCase.BuildRawData() / CreateInitialModel()
-> BuildContext(rawData)
-> View.RefreshUI(context)
View
--(UI 专用事件)--> Controller
Controller
-> UseCase.Action(...)
-> Result / RawData
-> BuildContext / PartialRefresh
-> View.RefreshUI(...)
```
说明:
- 外部流程不传入 `Context`
- `Context` 只在 Presentation 层内由 Controller 构建
- `OpenUIAsync(null)` 对有 UseCase 的 UI 是合法入口,表示由 UseCase 构造初始 RawData
### 7.2 无 UseCase 的轻量流程
```text
外部流程
-> await OpenUIAsync(rawData)
Controller
-> BuildContext(rawData)
-> View.RefreshUI(context)
View
--(UI 专用事件)--> Controller
Controller
-> 处理轻量逻辑或路由动作
-> 更新 Context / 打开其他 UI / 关闭当前 UI
```
说明:
- 轻量 UI 如果没有默认数据来源,`OpenUIAsync(null)` 可以失败并输出 warning
- Dialog 这类提示型 UI 属于必须由外部传入 RawData 的轻量 UI
- 即使是轻量 UI外部也不传入 `Context`Context 仍由 Controller 构建
### 7.3 关闭流程
1. 外部流程或 `Controller` 调用 `CloseUIAsync(...)`
2. `Controller` 解除事件订阅
3. `Controller` 清理本次交互缓存,例如回调、临时 `UserData` 和局部状态
4. `View.OnClose` 清理本地视觉状态
5. 下次打开时重新按 `Context` 初始化
## 8. 目录与命名规范
目录示例以插件目录为例(`Assets/Plugins/UIModule`)。
- 插件 `Base/Event/<UIName>/`UI 专用事件(命名空间 `SepCore.Event`
- 插件 `Runtime/Base/`:五层 UI 基础抽象(命名空间 `SepCore.UI`
- 插件 `Runtime/<UIName>/`UseCase、RawData命名空间 `SepCore.UI`
- 插件 `Runtime/` 根目录:`UIRouterComponent`(命名空间 `SepCore.CustomComponent`
- 插件 `UI/<UIName>/`Controller、Context、View命名空间 `SepCore.UIModule`
- 五层同名前缀保持一致,例如 `ShopForm*`、`LevelUpForm*`
- 子组件上下文命名:`RoleItemContext`、`RewardItemContext`、`DisplayAreaContext`
- 结果对象命名:`XXXResult`、`XXXActionResult`
- 轻量 UI 可以省略 `UseCase``RawData`,但不省略 `Controller`、`Context`、`View`
## 9. 测试规范
### 9.1 自动化测试范围
如果一个 UI 具备 `UseCase`,并且需要补自动化测试,则统一使用 EditMode 测试。
优先覆盖:
- 初始化模型生成
- 业务分支与校验
- 用户动作对应的结果对象
- 边界条件和非法输入
### 9.2 Controller / View 的验证策略
`Controller``View` 以人工验收为主,重点验证:
- 首次打开
- 交互刷新
- 局部刷新
- 关闭重开
- 非法参数或空数据输入时的表现
## 10. 落地检查清单
新增 UI 时,至少检查以下事项:
1. 先判断该 UI 属于标准五层还是轻量 UI
2. 使用 `UIRouterComponent` 管理的 UIForm 必须配置可实例化的 Controller 绑定
3. 如果存在业务规则或状态推进,必须引入 `UseCase`
4. 外部打开 UI 时只传入 `RawData` 或可转换为 `RawData` 的参数,不传入 `Context`
5. `OpenUIAsync(null)` 对有 UseCase 的 UI 应通过 UseCase 构造初始 RawData
6. `RawData` 中不得出现 `Context` 类型
7. `Context` 只能在 `Controller` 中构建
8. `View` 不得订阅全局业务事件
9. `View` 的交互只能通过 UI 专用事件上报
10. `Controller` 必须成对管理事件订阅与解除订阅
11. `Controller` 必须在关闭时清理本次交互缓存
12.`UseCase` 且需要补自动化测试时,测试写入 EditMode
## 11. 非目标说明
- 本文不讨论历史实现,也不为历史写法背书
- 本文不要求所有 UI 必须强制引入 `UseCase`
- 本文不展开底层 UI 框架或事件系统实现细节