耦合 V1

07/092 浏览
已分析当前 scripts/ 用户代码,未修改文件。整体看,项目是一个 main.lua 主入口 + 投资页面模块 + 纸上谈兵插件模块 + 网络/数据代理模块 的结构,耦合点比较集中。

1. 主入口如何耦合其他模块

主入口是:
  • scripts/main.lua
它分服务端和客户端两条启动路径。

服务端路径

main.lua 先判断服务端模式:
  • scripts/main.lua:7:判断 IsServerMode
  • scripts/main.lua:8:加载 Network.便签墙服务端
  • scripts/main.lua:10:调用 WallServer.Start()
服务端链路基本是:
text
main.lua └── Network.便签墙服务端 └── WallServer.Start()
虽然文件名叫“便签墙服务端”,但它实际上同时承载了:
  • 便签墙同步
  • 用户数据代理
  • 管理员导出
  • 纸上谈兵排行榜
  • 纸上谈兵安全屋公开页
所以服务端模块职责已经比较集中。

客户端路径

客户端直接 require 了主业务模块:
  • scripts/main.lua:19urhox-libs/UI
  • scripts/main.lua:20Data.Store
  • scripts/main.lua:21Network.数据代理
  • scripts/main.lua:22Pages.登录
  • scripts/main.lua:23Pages.持仓
  • scripts/main.lua:24Pages.记录
  • scripts/main.lua:25Pages.分析
  • scripts/main.lua:26Pages.怪物工坊
  • scripts/main.lua:27Pages.纸上状态
  • scripts/main.lua:28Pages.设置
  • scripts/main.lua:29Data.数据迁移
  • scripts/main.lua:30Audio.界面音效
另外:
  • scripts/main.lua:34 通过 pcall(require, "Network.便签墙客户端") 可选加载便签墙客户端。
也就是说,main.lua 是显式知道大部分业务模块的,它不是一个纯启动器,而是同时负责:
text
启动生命周期 UI 初始化 登录流程 页面路由 全局音效注入 全局鼠标样式 纸上谈兵入口 Store 同步驱动

2. UI 页面耦合方式

投资主界面:静态创建 + SetVisible 切换

主 UI 在 BuildUI() 中一次性创建四个页面:
  • scripts/main.lua:285BuildUI()
  • scripts/main.lua:287Holdings.Create()
  • scripts/main.lua:288Record.Create()
  • scripts/main.lua:289Analysis.Create()
  • scripts/main.lua:290Settings.Create()
  • scripts/main.lua:392SwitchToPage(pageId)
路由方式是:
text
main.lua ├── BuildUI() │ ├── 持仓页 │ ├── 记录页 │ ├── 分析页 │ └── 设置页 └── SwitchToPage() ├── 当前页 SetVisible(false) ├── 新页 SetVisible(true) └── 调用新页 OnActivate()
这是比较直接的 UI 耦合:主模块持有页面实例,并直接调用页面生命周期方法。

纸上谈兵:替换整个 UI Root

纸上谈兵不是普通 tab 页,而是通过全局函数重建 UI:
  • scripts/main.lua:467JumpToMonsterWorkshop(overlayPanel)
多个模块直接调用这个全局函数:
  • scripts/Pages/登录.lua:682
  • scripts/Pages/纸上排行.lua:102
  • scripts/Pages/纸上排行.lua:136
  • scripts/Pages/纸上排行.lua:224
  • scripts/Pages/纸上建家.lua:305
  • scripts/Pages/纸上建家.lua:346
  • scripts/Pages/纸上关卡.lua:306
  • scripts/Pages/怪物工坊/游戏循环.lua:273
这是一种强全局耦合:纸上谈兵子模块不通过参数或路由服务跳转,而是直接依赖 main.lua 暴露的全局函数。

3. 数据中心耦合

Data.Store 实际是一个 shim:
  • scripts/Data/Store.lua:3return require("Data.数据中心")
真正的数据中心在:
  • scripts/Data/数据中心/init.lua
它创建一个 Store 单例,然后由子模块向这个表安装方法:
  • scripts/Data/数据中心/init.lua:6:格式工具
  • scripts/Data/数据中心/init.lua:7:持仓数据
  • scripts/Data/数据中心/init.lua:8:账户数据
  • scripts/Data/数据中心/init.lua:9:云同步
  • scripts/Data/数据中心/init.lua:16:创建 Store
  • scripts/Data/数据中心/持仓数据.lua:17:安装 Store.AddHolding
  • scripts/Data/数据中心/账户数据.lua:16:安装账户相关方法
  • scripts/Data/数据中心/云同步.lua:77:安装 Store.SyncToCloud
整体关系是:
text
Data.Store └── Data.数据中心 ├── 持仓数据.Install(Store) ├── 账户数据.Install(Store) └── 云同步.Install(Store)
页面模块普遍直接 require Store 并读写它,例如:
  • scripts/Pages/持仓/init.lua:8
  • scripts/Pages/记录/init.lua:9
  • scripts/Pages/记录/保存逻辑.lua:8
  • scripts/Pages/分析/指数对比.lua:8
  • scripts/Pages/设置.lua:8
典型写入点:
  • scripts/Pages/记录/保存逻辑.lua:79Store.AddCashFlow
  • scripts/Pages/记录/保存逻辑.lua:325Store.AddHolding
  • scripts/Pages/记录/保存逻辑.lua:367Store.AddTransaction
所以投资业务的数据耦合主要是:
text
页面模块 └── Store 单例 ├── 直接读状态 ├── 调业务方法 └── MarkDirty 后由 main.lua 定时同步
同步驱动在:
  • scripts/main.lua:866Store.UpdateSync(dt)

4. 网络模块耦合

网络事件统一定义在:
  • scripts/Network/共享定义.lua:12Shared.EVENTS
  • scripts/Network/共享定义.lua:116:注册服务端事件
  • scripts/Network/共享定义.lua:122:注册客户端事件

服务端集中处理

服务端模块:
  • scripts/Network/便签墙服务端.lua:63WallServer.Start()
  • scripts/Network/便签墙服务端.lua:67:注册服务端事件
  • scripts/Network/便签墙服务端.lua:70-88:订阅便签、用户数据、管理员、排行、安全屋事件
这意味着多个业务插件的后端逻辑都塞在同一个服务端模块里。

客户端数据代理

Network.数据代理 是多人模式下的用户数据代理:
  • scripts/Network/数据代理.lua:38UserDataProxy.Init()
  • scripts/Network/数据代理.lua:52:订阅 USER_DATA_RESPONSE
  • scripts/Network/数据代理.lua:53:订阅 USER_DATA_SAVE_ACK
  • scripts/Network/数据代理.lua:175BatchSave
  • scripts/Network/数据代理.lua:262:发送 USER_DATA_REQUEST
  • scripts/Network/数据代理.lua:375:发送 USER_DATA_SAVE
主入口调用:
  • scripts/main.lua:220:登录页显示前初始化 UserDataProxy
  • scripts/main.lua:857:未登录时 UserDataProxy.CheckTimeout(dt)
比较隐蔽的耦合是:UserDataProxy 自己不直接监听 ServerReady,而是由便签墙客户端通知:
  • scripts/Network/便签墙客户端.lua:41:订阅 ServerReady
  • scripts/Network/便签墙客户端.lua:53:调用 UserDataProxy.OnServerReady()
所以用户数据加载依赖便签墙客户端初始化流程。

5. 纸上谈兵插件耦合

这是当前耦合最密集的部分。
入口转发:
  • scripts/Pages/怪物工坊.lua:4return require("Pages.怪物工坊.init")
聚合模块:
  • scripts/Pages/怪物工坊/init.lua
它 require 了大量插件模块:
  • scripts/Pages/怪物工坊/init.lua:7Pages.纸上状态
  • scripts/Pages/怪物工坊/init.lua:8Pages.纸上战斗
  • scripts/Pages/怪物工坊/init.lua:9Pages.纸上关卡
  • scripts/Pages/怪物工坊/init.lua:10Pages.纸上背包
  • scripts/Pages/怪物工坊/init.lua:11Pages.纸上建家
  • scripts/Pages/怪物工坊/init.lua:12Pages.纸上排行
  • scripts/Pages/怪物工坊/init.lua:13Pages.纸上存档
  • scripts/Pages/怪物工坊/init.lua:23:合成面板
  • scripts/Pages/怪物工坊/init.lua:24:战斗机制
  • scripts/Pages/怪物工坊/init.lua:25:游戏循环
  • scripts/Pages/怪物工坊/init.lua:26:渲染模块,副作用定义全局 HandleBattleRender

核心状态单例

Pages.纸上状态 是所有纸上谈兵模块共享的状态表:
  • scripts/Pages/纸上状态.lua:6local State = {}
  • scripts/Pages/纸上状态.lua:13State.battleActive
  • scripts/Pages/纸上状态.lua:14State.modalOpen
  • scripts/Pages/纸上状态.lua:16State.soldier
  • scripts/Pages/纸上状态.lua:19-24:子弹、敌人、掉落、粒子、爆炸、障碍
  • scripts/Pages/纸上状态.lua:70-85:背包
  • scripts/Pages/纸上状态.lua:94-120:安全屋/建家状态
这部分耦合方式是:
text
纸上状态 State 单例 ├── 战斗模块直接读写 ├── 关卡模块直接读写 ├── 背包模块直接读写 ├── 建家模块直接读写 ├── 排行模块间接影响 UI 状态 └── 游戏循环按 State 驱动运行
这是典型“共享可变状态”耦合。

游戏循环回调注入

怪物工坊/init.lua 给游戏循环注入回调:
  • scripts/Pages/怪物工坊/init.lua:294游戏循环.Install({ ... })
游戏循环再给战斗机制注入:
  • scripts/Pages/怪物工坊/游戏循环.lua:32M.Install(workshop)
  • scripts/Pages/怪物工坊/游戏循环.lua:35战斗机制.Install(...)
  • scripts/Pages/怪物工坊/游戏循环.lua:36战斗机制.SetGameOverCallback(...)
这是比全局函数稍好的耦合方式:通过 Install 注入依赖,但仍然依赖共享 State。

6. 全局变量和全局函数耦合

这是当前最需要注意的部分。

CursorStyle

定义在:
  • scripts/main.lua:104CursorStyle = "pointer"
纸上谈兵多个模块直接修改它:
  • scripts/Pages/怪物工坊/init.lua:186
  • scripts/Pages/怪物工坊/init.lua:201
  • scripts/Pages/怪物工坊/init.lua:243
  • scripts/Pages/怪物工坊/init.lua:250
  • scripts/Pages/纸上关卡.lua:305
  • scripts/Pages/纸上建家.lua:304
  • scripts/Pages/纸上排行.lua:109
  • scripts/Pages/怪物工坊/游戏循环.lua:679
这使鼠标样式成为跨模块隐式状态。

JumpToMonsterWorkshop

定义在:
  • scripts/main.lua:467
被多个纸上谈兵模块直接调用。这个函数实际承担了“纸上谈兵路由器”的角色,但目前是全局函数。

BackToMainMenu

定义在:
  • scripts/main.lua:427
设置页调用:
  • scripts/Pages/设置.lua:128

全局事件 handler

由于事件订阅使用字符串函数名,项目里存在不少全局 handler:
  • scripts/main.lua:107Start
  • scripts/main.lua:123HandleGlobalCursor
  • scripts/main.lua:838HandleUpdate
  • scripts/Pages/怪物工坊/渲染.lua:390HandleBattleRender
  • scripts/Network/便签墙客户端.lua:48HandleServerReady
  • scripts/Network/数据代理.lua:267HandleUserDataResponse
  • scripts/Network/纸上安全屋客户端.lua:76HandlePaperWarHomeClaimResult
  • scripts/Pages/设置.lua:1137HandleAdminPlayersList
  • scripts/Pages/设置.lua:1262HandleAdminExportResult
这类耦合风险是:函数名冲突、重复订阅、不易追踪调用来源。

7. 主模块与插件模块的耦合方式总结

可以分成 6 类:
耦合方式说明代表位置
require 显式依赖主入口直接加载页面、数据、网络模块scripts/main.lua:19-30
单例数据中心所有投资页面共享 Storescripts/Data/Store.lua:3
共享可变状态纸上谈兵共享 Pages.纸上状态scripts/Pages/纸上状态.lua:6
全局函数插件直接调用主模块函数scripts/main.lua:467
全局变量多模块读写 CursorStylescripts/main.lua:104
事件总线引擎事件和 RemoteEvent 通过字符串 handler 连接scripts/Network/共享定义.lua:12

8. 主要风险点

  1. main.lua 职责过重
    同时负责启动、登录、路由、全局音效、光标、Store 同步、纸上谈兵入口。
  2. 纸上谈兵依赖主模块全局函数
    JumpToMonsterWorkshop() 是事实上的插件路由器,但不是模块化接口。
  3. CursorStyle 是隐式全局状态
    多个模块直接写,后续新增弹窗/模式时容易互相覆盖。
  4. 服务端模块职责过宽
    Network.便签墙服务端.lua 同时处理便签、用户数据、排行、安全屋、管理员功能。
  5. WallClientUserDataProxy 存在隐式启动顺序依赖
    用户数据代理依赖便签墙客户端收到 ServerReady 后转发。
  6. 事件 handler 全局命名有碰撞风险
    尤其是 HandleServerReadyHandleUserDataResponse 这类通用名称。
  7. Store 和 State 都是大单例
    好处是简单,坏处是副作用传播广,测试和局部重构成本高。

9. 如果后续要解耦,优先级建议

建议先从低风险处开始:
  1. JumpToMonsterWorkshop 封装成 Pages.纸上路由WorkshopRouter 模块,纸上谈兵模块 require 它,而不是调用全局函数。
  2. CursorStyle 封装成 CursorService.SetStyle()/GetStyle()
  3. 拆分 Network.便签墙服务端.lua:便签墙、用户数据代理、排行、安全屋、管理员导出分别独立模块。
  4. UserDataProxy 自己监听或显式接收 ServerReady,避免依赖 WallClient
  5. main.lua 只保留启动和顶层路由,登录流程、主菜单、纸上谈兵入口可下沉到独立控制器。
4