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

8.9 KiB
Raw Blame History

代码架构审查报告 — 代码组织与封装

日期: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 裁决结果。