下载 App

开发者日志 02 · 把一千行拆开

5 小时前1 浏览综合

警报:Simulation.lua 到 1000 行了

第 1 篇结尾提到,D6 结束时核心模拟文件已经超过一千行。当时的 Simulation.lua 什么都管:地块状态、水资源预算、天气推进、洪水、养殖试点、章节验收、存档校验——全塞在一个文件里。
这不是"能跑就行"的问题。真正的问题是:改任何一处都可能踩到别处。
举个当时真实发生过的 bug:我调整了水预算的计算顺序,结果章节验收的"全年非赤字"判定跟着变了,因为两者共用同一个 annualNet 字段但期望的口径不同。一个文件里,没有边界,就没有安全感。

拆法:按"谁依赖谁"切

我们没有按"功能"切,而是按依赖方向切:
Config.lua          纯数据,谁都能读,不依赖任何人
   ↓
Grid / Weather / Flood / Hatchery    独立规则模块,只依赖 Config
   ↓
Simulation.lua      业务状态机,组合上面的规则
   ↓
View / WaterPanel / ChapterGuide     只读展示层,不写业务
   ↓
Client.lua          装配与生命周期
关键约束只有一条:展示层只读,不写业务。 View.lua 可以调 sim:Work(),但不能自己改 sim.money。这条线一旦划清,UI 怎么改都不会污染模拟逻辑。
最终结构:
模块行数职责
Simulation.lua1024状态机、Tick、存档校验
View.lua981声明式 UI 树、交互
Art.lua665地图绘制、几何、命中判定
SettingsPanel.lua490设置弹窗
LocalProgress.lua424本地存档读写
ChapterGuide.lua347章节引导与回执
顺带一提:我们给自己定的红线是"单文件超过 1500 行必须拆"。Simulation.lua 停在 1024,是因为它的内聚性够高——所有字段都属于同一个状态机,硬拆反而会制造跨文件耦合。

测试:24 个文件替我们盯梢

拆完之后最怕的是"拆坏了没人知道"。所以我们把验收写成了 24 个测试文件,分两类:
Checks(纯逻辑) —— 不碰 UI,直接跑模拟:
  • OasisGridChecks 网格索引与坐标映射
  • OasisWaterChecks 水预算口径
  • OasisFloodChecks 浸水积分守恒
  • OasisSandChecks 覆沙压力
  • OasisHatcheryChecks 养殖水账
Visual(真实 UI) —— 用引擎真跑一遍,模拟真实指针事件:
  • OasisMapVisual 地图几何、命中、缩放、拖动
  • OasisGuideVisual 章节引导逐层揭示
  • OasisPlatformVisual 安全区、路线图、设置弹窗
每一条断言都是"可证伪"的具体事实,不是"看起来对":
lua复制
lua
V.assert(map:Hit(cx + x * scale, cy + y * scale) == i, "菱形地块中心命中 " .. i) V.assert(math.abs(card:GetAbsoluteLayout().y - side:GetAbsoluteLayout().y - scroll) < 2, "真实试点入口切水页并定位独立卡片")

一次真实的回归

拆解过程中,我们把"地块中心命中"这条断言加进了测试。它立刻抓到一个隐蔽 bug:等距菱形的命中判定在格子共边处会重复归属——点到两格交界线时,返回的是后画的格子,而不是深度更靠前的那格。
这个 bug 在手动点击时几乎不可能复现(要正好点在 1 像素宽的边界上),但测试能稳定触发。修完之后,这条断言至今跑了 6512 次全绿。

这一篇的教训

重构的安全感,来自你重构前就写好的测试。
如果没有那 24 个文件,我们不敢动一千行的核心文件;不敢动,代码就会持续腐化。测试不是负担,是让你敢于重构的许可证。