Files
gplan-2/design/dev/architecture/code-architecture-review-2026-09-13.md
T

133 lines
8.9 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.
# 代码架构审查报告 — 代码组织与封装
> 日期:2026-09-13
> 载体:`game/`(9707 行 GDScript:core 12 文件 / data 11 / presentation 28 / smoke 10)
> 基线:`docs/architecture/architecture.md` v1.0 + ADR-0001~0005 + `docs/code-conventions.md`
> 触发:用户在 code-review 之后要求补充「代码组织问题 + 封装问题」专项审查
> 审查方式:core/presentation 关键文件全读 + S1–S8 静态规则逐条 grep 验证 + 跨层引用面扫描
>
> **Verdict: CONCERNS** — 分层纪律实测扎实(S1–S7 零违例、仅 2 处已登记例外),但封装收口存在
> 一条系统性欠账:核心模块内部状态以公有字段暴露,呈现层已形成链式穿透的既成事实。
> 原型期无害;是 ADR-0009「切片机制正式化批」应收口的主题。
---
## 1. 正面观察(实测合规项)
| 规则 | 结论 | 证据 |
|---|---|---|
| S1/S2 零随机 | ✅ | core 内唯一 `RandomNumberGenerator.new` 在 rng_service.gd:76(所有者本体);`stream.randf()`(reward_supply_controller.gd:32)取号经 B4 注入封闭流 |
| S3 零表加载 | ✅ | core 内唯一 `load()` 在 config_db.gd:81(单点本体) |
| S4 core 零 UI | ✅ | core/ 无 Control/Node2D/.tscn 引用(grep 命中均为 `RewardSupplyController` 类名误报与注释) |
| S5 零裸引用 autoload | ✅ | core/ 内 ConfigDB/RngService 命中均为日志前缀字符串 |
| S6 core↔core 方法 Callable | ✅ | level_runner.gd:130-134 全部方法转发;lambda 仅用于 sort_custom(不捕获 self) |
| S7 编排唯一性 | ✅ | B2/B3/B4 调用面 core 内收口于 battle_orchestrator.gd;仅 level_runner.gd:112(grant_node_active,已登记接口漂移)与 :168(in_stock_count 只读)两处例外 |
| 呈现层零核心引用 | ✅ | 28 个视图/装配器文件中仅 main.gd(组合根)与 hud_presenter.gd(授权翻译点)触 core |
| 封装典范 | — | status_container / reward_supply_controller / b4.query_inventory():私有状态 + 投影查询 + 授予时快照;battle_orchestrator.diagnostic_snapshot()(:660)为「成文只读出口」正确先例 |
---
## 2. 封装问题(按根因 → 表征排序)
### E1(根因)编排器协作对象全公开,呈现层链式穿透已成既成事实 — P1
- **位置**:core/battle_orchestrator.gd:20-27(`var a1/a2/a3/b2/b3/b4` 全公有)
- **表征**:`b1.a2.board`、`b1.b2.hero.hp`、`b1.a3.phase` 式层层下钻——main.gd 18 处、hud_presenter.gd 15 处。
- **影响**:组合根与 presenter 是授权翻译点,但访问形态是「恰好字段公开」而非「B1 提供成文读取面」。
S7 只能做文件级扫描(防另写调用方),防不了呈现层意外调用写 API,也防不了读取路径随重构漂移。
- **建议**:B1 补聚合呈现快照/查询面(`hud_state()` 一类,先例 = `diagnostic_snapshot()`);
a2–b4 下划线化后经命名代理暴露只读操作。
### E2(表征)视图绕过 A2 查询面直读原始状态字典 — P1
- **位置**:core/minesweeper_rules.gd:18-20(`board/revealed/flags` 公开可变);
presentation/views/board_view.gd:278、:303 直接 `_a2.revealed.get(c, {})`
- **矛盾点**:board_view.gd:7 头注自称「只读 A2 查询面」——直读 `.revealed` 恰是绕过查询面。
B1 自身亦直读(battle_orchestrator.gd:565-569 `a2.board.width/height`)。
- **建议**:`revealed/flags` 私有化;补 `reveal_record(c) -> Dictionary`、`board_dimensions()` 查询面。
调用点仅 B1 + board_view + main 三处,成本很低。
### E3(表征)B2 裸字典句柄跨模块传递,API 缺「含死亡残位」查询面 — P2
- **位置**:core/unit_container.gd:13-14(`hero/enemies` 公开;句柄 = Dictionary 本体,任何持有者可
`u.hp = 999` 绕过 apply_damage)
- **现症**:hud_presenter.gd:78-79 为渲染死亡残位直读 `b1.b2.enemies` 原始数组——
B2 的 `query_alive` 不含死亡单位,API 缺口迫使呈现层越界(B2 头注已登记此痛点)。
- **建议**:短期补 `query_all_units()` 投影面把 presenter 需求收进 API;
正式化(ADR-0007 两 face 归属裁决时)再决定是否升级 Unit 类型。
### E4 `db.hero` 表值运行时改写——测试覆盖通道用共享可变单例传参 — P2
- **位置**:presentation/main.gd:327-329、core/level_runner.gd:60-61,75-76 直接写 `db.hero.atk/max_hp`;
B2.spawn 经读表间接获得加成值
- **前科**:双写叠加 6→18→54 教训、双基线同步义务(main.gd:187-189 注释)——纪律已成文但机制脆弱。
- **建议**:切片保留;将「运行时表可变」登记进 ADR-0009 候选;正式化改参数传递(`spawn(atk_override=...)`)。
### E5 「只读」字段仅靠纪律,无机制约束 — P2
- **位置**:core/action_economy.gd:17-20(`phase/ap/rap/last_round_end` 架构文档定义为只读,实际公开可变);
core/rng_service.gd:19(`master_seed` 可绕过 set_master_seed 的计数器全零断言)
- **现状**:当前调用方全部合规(A2 经 try_spend/request_dig,B1 经 notify_*,呈现层只读)。
- **建议**:下划线化 + 查询方法(`phase()/ap()/rap()`),趁调用方少时收口,成本极低。
### E6 小项 — P3
| 项 | 位置 | 说明 |
|---|---|---|
| 返回类型多态 | core/reward_economy.gd(grant/stock/use 失败返 `false`,query_* 返 `null`) | B1 消费处被迫 `if not (snap is Dictionary)`;统一 `null` 更符合 GDScript 惯例 |
| 泄漏原始表行 | core/reward_economy.gd:256-265 `equivalence_supply` 返回 `db.reward.rows` 行引用(可变) | 工具观测面,建议 `duplicate()` |
| BoardData 全公开 | core/board_data.gd:8-18「生成后不可变」仅靠注释;视图持板即可读全部雷位(当前未作弊) | 正式化时 duplicate 冻结或下划线化 + 访问器 |
| 成员声明位置 | core/action_economy.gd:121 `var _next_round_mode` 声明在中部、使用在前 | 集中到顶部 |
---
## 3. 代码组织问题
### O1 main.gd 回涨至 667 行,再越 ~400 行审视线(规范 1.1)— P1
- **位置**:presentation/main.gd(667 行);ui_assembler.gd:3 自记「回涨 761 行触发审视线——拆出」历史
- **增长主因**:调试模式(`_start_single/_start_debug_levels/_start_battle/_on_battle_finished` 的
`_level_mode` 分支约 100+ 行)糊在组合根。
- **建议**:照 tune_sim_controller.gd 先例把调试模式抽成独立控制器,组合根回到纯接线。
### O2 调试模式与 LevelRunner 平行实现,语义漂移风险 — P1
- **位置**:「库存跨战斗延续 + HP 携带 + 层级推进」存在两份实现——core/level_runner.gd(正式)与
main.gd 调试分支(历史入口);`level_clear()` 调用时点已各有细节(main.gd:367,376 vs level_runner.gd:167)。
- **建议**:调试两层模式直接复用 LevelRunner(其设计目标即此),或抽 `DebugBattleSession`。
### O3 B1 体量与纯函数沉淀 — P2
- **位置**:core/battle_orchestrator.gd 718 行(审视线超 300+);
伤害公式族 `_resolve_damage/_charge_damage_component/_armor_effectiveness/_clamped_pool`
(约 80 行)为零状态纯算术。
- **建议**:按规范 1.2 抽 `DamageMath` 静态宿主类;段序骨架不动、可回放承诺无风险。
- **顺带**:core/board_generator.gd:50-55 对同一张合法板调用两次 `measure_inferability`
(全库最贵求解器)——合并为一次。
### O4 呈现层反向依赖 smoke 工具域 — P2
- **位置**:presentation/tune_sim_controller.gd:23 `load("res://smoke/level_autoplay.gd")`——
UI 控制器依赖测试工具目录。
- **建议**:「关卡自动模拟循环」上移为共享组件(core 或 lib),smoke 与 tune_sim 共同消费。
- **顺带**:main.gd:106,188 与 tune_sim_controller.gd:23 用运行时字符串 `load()`,
与 main.gd:16-19 的 `preload` 常量风格混用——统一 `preload`(编译期路径校验)。
### O5 头注溯源缺失(规范 1.4)— P3
core/ 12 文件中 4 个缺标准 `# 实现 GDD:...` 行:
`board_data.gd`(有 A1 描述但非标准格式)、`config_db.gd`、`rng_service.gd`、`reward_supply_controller.gd`。
---
## 4. 建议行动顺序
1. **P1 三连(合计约 2 天)**:
E2(A2 状态私有化 + 查询面)→ E1(B1 呈现快照面)→ O1+O2(调试模式抽离/复用 runner)。
完成后 board_view / hud_presenter / main 与 core 的接触面全部变为命名 API,封装收口完成。
2. **P2 项并入 ADR-0009「切片机制正式化批」裁决**:E4 表值改写参数化、E3 单位句柄形态、
O3 DamageMath 拆分、O4 模拟循环上移。
3. **P3 顺手清**(头注补齐、返回值统一、声明位置、preload 统一)。
> 复审触发:P1 三连落地后重跑本审查核对接触面收口;ADR-0009 落定后正式工程骨架迁移前复核 E3/E4 裁决结果。