350 lines
10 KiB
Markdown
350 lines
10 KiB
Markdown
# UI 五层架构设计规范(UseCase / RawData / Controller / Context / View)
|
||
|
||
## 1. 文档目标
|
||
|
||
本文定义一套可长期复用的 UI 分层设计方案,用于约束 UI 模块的职责边界、依赖方向、通信方式和测试策略。
|
||
|
||
- 本文描述的是规范,不是对某个项目现状的总结
|
||
- 项目内目录或基类只作为落地示例,不改变本文的抽象约束
|
||
- 本文重点约束 UIForm 级模块;子组件(`Item`、`Area` 等)可只实现 `Context + View`
|
||
- Unity GameFramework 的底层细节不在本文展开,本文只约束项目内 UI 代码组织
|
||
|
||
## 2. 核心原则
|
||
|
||
### 2.1 单向数据职责
|
||
|
||
UI 的职责链路固定为:
|
||
|
||
```text
|
||
外部流程
|
||
-> Controller
|
||
-> UseCase
|
||
-> RawData / Result
|
||
-> BuildContext
|
||
-> View
|
||
|
||
View
|
||
--(UI 专用事件)--> Controller
|
||
```
|
||
|
||
说明:
|
||
|
||
- `UseCase` 只负责业务规则、状态推进和纯业务数据生成
|
||
- `Controller` 是 UI 编排中心,也是唯一允许构建 `Context` 的层
|
||
- `View` 只负责渲染和抛出交互,不直接处理业务状态
|
||
|
||
### 2.2 严格分离业务数据与展示数据
|
||
|
||
- `RawData` 是纯业务数据,不承载展示模型
|
||
- `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 展示行为
|
||
- 可以包含领域对象、配置对象、标识符、枚举、数值和纯数据集合
|
||
- 不允许依赖 `Context`、`View`、`Sprite`、`TMP_Text` 等展示相关类型
|
||
- 不允许直接使用 `XXXItemContext`、`XXXFormContext` 作为字段类型
|
||
|
||
说明:
|
||
|
||
- `RawData` 的目标是表达“业务上发生了什么”
|
||
- `Context` 的目标是表达“界面应该怎么显示”
|
||
- 两者不能混用
|
||
|
||
### 3.3 Controller 层
|
||
|
||
职责:UI 编排层,负责连接外部流程、`UseCase`、`View`,并统一管理 UI 生命周期与展示状态。
|
||
|
||
约束:
|
||
|
||
- UIForm 级模块默认必须有 `Controller`
|
||
- 命名:`XXXFormController`
|
||
- 可基于 `UIFormControllerCommonBase<TContext, TForm>` 实现
|
||
- 通过 `BindUseCase(IUIUseCase)` 注入用例并做类型校验
|
||
- `OpenUI(object userData = null)` 负责接受外部参数、准备数据并打开 UI
|
||
- 负责 `RawData / Result -> Context` 的转换,常见形式为 `BuildContext`
|
||
- 负责事件订阅与解除订阅,且必须成对出现
|
||
- 负责全量刷新与局部刷新策略
|
||
- 负责过滤 UI 专用事件的 `sender`,确保事件只作用于当前 UI 实例
|
||
|
||
允许职责:
|
||
|
||
- 将业务数据转换为展示友好的文本、图标、颜色、列表状态
|
||
- 在必要时查询本地化、资源映射或展示适配逻辑
|
||
|
||
禁止职责:
|
||
|
||
- 在 `Controller` 中堆叠大段领域业务规则
|
||
- 绕过 `Context` 直接把业务对象塞给 `View`
|
||
- 直接修改其他 UI 的内部 `View`
|
||
|
||
### 3.4 Context 层
|
||
|
||
职责:承载“可直接驱动 UI 展示”的上下文数据。
|
||
|
||
约束:
|
||
|
||
- 继承 `UIContext`
|
||
- 命名:`XXXFormContext`、`XXXItemContext`、`XXXAreaContext`
|
||
- 只能由 `Controller` 构建和更新
|
||
- 字段以展示友好为目标,例如标题、描述、图标、颜色、状态、列表、按钮文案
|
||
- 允许组合子 `Context`
|
||
- 不进入 `UseCase`
|
||
|
||
说明:
|
||
|
||
- `Context` 可以包含展示层需要的最终数据
|
||
- `Context` 可以是“已格式化”的显示数据
|
||
- 但这些数据必须由 `Controller` 负责准备,而不是 `UseCase`
|
||
|
||
### 3.5 View 层
|
||
|
||
职责:纯表现层,负责控件绑定、渲染刷新、动画触发和交互事件抛出。
|
||
|
||
约束:
|
||
|
||
- Form 类继承 `UGuiForm`,子组件通常继承 `MonoBehaviour`
|
||
- 命名:`XXXForm`、`XXXItem`、`XXXArea`
|
||
- 提供 `RefreshUI(Context)`、`OnInit(Context)`、`OnReset()` 等渲染入口
|
||
- 只消费 `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 -> 领域状态修改`
|
||
|
||
## 6. 事件通信规范
|
||
|
||
### 6.1 UI 与 Controller 的通信方式
|
||
|
||
`View` 与 `Controller` 的通信通过当前 UI 模块专用事件完成。
|
||
|
||
约束:
|
||
|
||
- UI 专用事件只服务于当前 UI 模块
|
||
- 这些事件不是业务公共事件
|
||
- 业务模块、流程模块、领域模块不应复用这些事件
|
||
- 如果底层使用全局事件总线实现,也只能把它当作“传输通道”,不能把事件语义扩散成全局契约
|
||
|
||
### 6.2 事件边界
|
||
|
||
- `View -> Controller`:使用 UI 专用事件
|
||
- `Controller -> View`:通过刷新 `Context` 或调用 `View` 的渲染接口
|
||
- `Controller -> UseCase`:直接方法调用
|
||
- `UseCase -> Controller`:通过返回 `RawData / Result`,不通过 UI 事件反推界面
|
||
|
||
### 6.3 事件安全要求
|
||
|
||
- `Controller` 必须校验事件 `sender`
|
||
- 同类 UI 可同时存在时,事件必须只作用于当前窗体实例
|
||
- UI 专用事件命名应体现模块归属,避免语义过宽
|
||
|
||
## 7. 标准交互流程
|
||
|
||
### 7.1 有 UseCase 的标准流程
|
||
|
||
```text
|
||
Procedure / GameState
|
||
-> 创建 UseCase
|
||
-> BindUseCase
|
||
-> OpenUI
|
||
|
||
Controller
|
||
-> UseCase.CreateInitialModel()
|
||
-> BuildContext(rawData)
|
||
-> View.RefreshUI(context)
|
||
|
||
View
|
||
--(UI 专用事件)--> Controller
|
||
|
||
Controller
|
||
-> UseCase.Action(...)
|
||
-> Result / RawData
|
||
-> BuildContext / PartialRefresh
|
||
-> View.RefreshUI(...)
|
||
```
|
||
|
||
### 7.2 无 UseCase 的轻量流程
|
||
|
||
```text
|
||
外部流程
|
||
-> OpenUI(userData)
|
||
|
||
Controller
|
||
-> BuildContext(userData)
|
||
-> View.RefreshUI(context)
|
||
|
||
View
|
||
--(UI 专用事件)--> Controller
|
||
|
||
Controller
|
||
-> 处理轻量逻辑或路由动作
|
||
-> 更新 Context / 打开其他 UI / 关闭当前 UI
|
||
```
|
||
|
||
### 7.3 关闭流程
|
||
|
||
1. 外部流程或 `Controller` 调用关闭逻辑
|
||
2. `Controller` 解除事件订阅
|
||
3. `View.OnClose` 清理本地视觉状态
|
||
4. 下次打开时重新按 `Context` 初始化
|
||
|
||
## 8. 目录与命名规范
|
||
|
||
目录示例以 `Assets/GameMain/Scripts/UI` 为例。
|
||
|
||
- 目录:`UI/<SceneDomain>/UseCase|RawData|Controller|Context|View`
|
||
- 五层同名前缀保持一致,例如 `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. 如果存在业务规则或状态推进,必须引入 `UseCase`
|
||
3. `RawData` 中不得出现 `Context` 类型
|
||
4. `Context` 只能在 `Controller` 中构建
|
||
5. `View` 不得订阅全局业务事件
|
||
6. `View` 的交互只能通过 UI 专用事件上报
|
||
7. `Controller` 必须成对管理事件订阅与解除订阅
|
||
8. 有 `UseCase` 且需要补自动化测试时,测试写入 EditMode
|
||
|
||
## 11. 非目标说明
|
||
|
||
- 本文不讨论历史实现,也不为历史写法背书
|
||
- 本文不要求所有 UI 必须强制引入 `UseCase`
|
||
- 本文不展开底层 UI 框架或事件系统实现细节
|