大工程中的代码结构

在游戏行业做过几年客户端和服务器端程序, 维护过又大又火的项目, 也做过新项目, 对代码结构的思想有过几次变化, 回想起来很有意思.

阶段一: 对代码组织结构的盲目崇拜

刚毕业我进组, 是一个已经上线很多年的项目. 当时我做客户端程序, 是一个 c++ 的引擎加上 lua 的脚本, 大多数情况下我们都是用 lua 开发. 我当时还没学过 lua 语言, 我的第一反应就是怎么搞类, 对象, 整个继承结构是怎样的, 数据和视图是怎么分离的等等.

结果就是我发现这个代码风格, 就是一般大家喜欢吐槽的那种, 一个文件四五千行, 一个函数两三百行; 然后除了框架用到一点类的思想, 平时做活动开发是根本不会用到的. 然后 UI 的逻辑也是没有什么视图数据分离, 更没有 vue.js 那种双向绑定技术.

于是我大惑不解, 我觉得不应该是这样, 我觉得从我开始这个项目中就要有一些更"优雅"的代码出现. 我要一个函数二十行, 一个文件两百行, 一个活动单独一个文件夹.

这么坚持了一段时间之后, 我发现我写的代码确实都做到了自己定的那些要求, 但是面临三个问题:

  • 开发效率太慢, 总要思考这个结构怎么弄, 才符合书上说的那种优雅
  • 别人维护起来太麻烦, 甚至过了一段时间我自己再来维护都觉得麻烦
  • 数据隐藏太深, 游戏中经常会碰到的热更新, 难以进行

阶段二: 对设计模式的盲目崇拜

游戏客户端中有很多 UI 界面, 做 UI 的开发与 Web 前端类似, 就是从服务器拿到数据之后, 在合适的地方渲染出来, 配以按钮, 滚动面板等组件. 同时也要考虑一些多窗口层级的问题.

我们组做这个的方式很简单, 服务器把数据一发过来, 客户端就做动作(打开相应窗口, 把数据填进窗口相应位置). 客户端响应不同的网络包也不是靠什么显式的注册, 而是利用函数名, 只要你以某一个 scheme 命名一个函数, 那么他就会响应某一种网络包.

当时我就觉得不应该是这样, 每个活动模块, 都应该有一个小控制器来分发网络包, 然后数据们要组织成一个状态机的样子, 数据一更新, 状态机的状态就发生变化, 状态变化的时候就会 emit event, 然后再自己注册一些 listener 来处理不同的状态变化, 更新视图.

于是我就开始要求自己要这么做, 最后结果就是, 有的时候觉得很棒, 但是很多时候觉得很没用, 很浪费时间, 还搞的维护起来很麻烦.

阶段三: 体验项目原本的风格和模式

由于在代码组织风格和设计模式的选用上, 我都进行了一番大胆的尝试, 最后发现效果不是那么理想, 甚至有点添堵的样子, 于是我打算试一下我本来觉得很不优雅的写法.

我也一个文件写他几千行, 一个函数几百行.

我也不搞什么状态机, 直接网络包A过来我就打开窗口填数据, 网络包B过来我就关闭窗口, 网络包A又过来就又重新打开窗口重新填一遍数据.

我发现还真的很数据, 代码结构也很清晰, 维护起来也很容易找到需要修改的地方, 热更新内存数据也很方便.

有点真香的感觉啊.

阶段四: 有点恍然大悟

最近有新人入组, 而且还有个同学跟我当年一样, 非要追求 web 开发中那种设计模式和代码组织, 更有缘分的是, 他开发的系统过了几个月要我来迭代新功能. 我看他的代码, 整了两周终于想通了很多事情.

大部分的设计模式应该由项目框架提供

项目框架的结构和模式, 组内所有的程序都很熟悉, 基于他的结构和模式, 开发效率最高, 维护他人的代码最快. 如果是他没有提供而需求确实需要, 可以自己进行局部补充, 但是要撇开项目的架构而自己引入一套 (比如引入一套mvc, 引入一套ecs等), 是错误的.

扁平与多层封装的平衡

只有考虑清楚为什么要封装和什么时候应该封装, 才能写出有用的封装. 在一个维护起来必须要阅读你的所有细节的情况, 你封装越多越难阅读 (封装的初衷是为了别人维护起来只用阅读上层代码, 而不用阅读底层).

所以平时的开发很多都是逻辑, 表达清楚逻辑是第一要素, 封装大多没有什么用, 因为这些逻辑都极少在别的地方引用. 大部分情况会过来修改你的各处代码.

如果你要修改别人的各处纯逻辑的代码, 当然不喜欢他搞很多层的封装, 找一个功能都找的头晕.

而且封装会限制操作的自由, 导致不停的迭代会增加很多的 hack 代码.

引入潮流技术很可能是个灾难

这个我想游戏客户端跟 web 前端是一样的, 在一个纯 jQuery 的项目, 你自己在某个页面的某个 frame 下搞一套数据双向绑定的框架和代码进来, 大家都会不开心.

这跟整个项目一开始就用双向绑定框架是很大区别的.