杰哥瞎扯蛋之瞎扯MUD机器人的可维护性
大家好,杰哥又来瞎扯蛋了。这次我来扯扯怎么保证机器人的可维护性。
当然,一如既往的,主打一个听起来很厉害,听完没啥用。
我口头假装自己很牛逼爽一爽,读者假装自己学到啥了爽一爽。
好一个欲扬先抑!前排占座。 本帖最后由 jarlyyn 于 2024-4-24 03:00 PM 编辑
1.代码是有生命的。
扯蛋第一要义是有话题性,所以我先给一个结论
‘代码是有生命的,有记忆,有历史,有童年,有生长环境,有生老病死’
听上去很扯吧,好,看我怎么圆回来。
有一说一,一个一个的代码的确看上去的确是静态的无生命的东西,代码该怎么跑,自然由编译器/脚本引擎/语法来决定,给到A语句,自然有A回复,怎么可能有生命呢?
的确,对于单独一个语句,对于单独一个文件,的确如此。但是当我们把目光放到整个项目时,又不一样了。
先让我们看看正真的生命是怎么样的
扯到生命,不得不提到DNA
参考
在我的认识里,生物,比如人,都是通过DNA的遗传信息,通过细胞为单位,通过脂肪/肌肉/神经等基础的细胞组成器官,组成人体的。
哪怕人体,这个生命的奇迹,基因也不是完美的,留下了生物进化历史的痕迹。
代码之于生物。
每一个代码文件相当于一个细胞,每个代码的模块相当于一个器官。
一个个个的细胞(代码)和器官(模块)可以做科学研究。
但是所有的工程问题都会涉及到复杂度。
除了全知全能的上帝,或者亿里挑一的天才,能掌握的复杂度都是有限的,超过一定程度的复杂性,对于研究的人来说这就是一个类似于生物的混沌个体。只能从微观角度进行试验,宏观部分很可能是一个黑核。
维护代码很多时候和医生看病一样,只是根据现象,检测(问题还原)来做推测,并不一定是100%解决所有细节问题。
这就是代码项目和生命的联系点。
同时,代码有一个逐渐发展,迭代开发,根据外部需求(mud更新)调整的过程,整个框架存在妥协性,时效性,赶工期等各种发育/进化过程中的妥协,并成为地基影响之后所有的代码。
对于代码项目,完美只存在于理想之中,屎山才是常态。
别说代码。哪怕是生物体,也是如此。
比如,你是否知道人类其实是后口动物,老老老老祖宗腚眼一开始也是用来进食的?然后才进化到长个嘴出来?(注意这是口嗨)
扩展阅读
回到正题,代码生老病死是什么?
生好理解
病,很明显,就是修复bug。
老,就是屎山越叠越高,维护越来越难。
死,我们很容易知道,重写一个项目的代价是固定的,而维护代码的代价是越来越高的。当维护代码的代价远远超过重写一个可靠的代码后,老的代码,就等于死了。而新的代码,就是新生。
世界上大部分代码项目大体都是这样。
提高代码的可维护性,就是打好基础。
让代码不容易生病,得了病容易治,老的慢点,死的晚点?
最后一行讲得很好,不过你漏了一句:生得早点!
都等不及了。快快快,1234567 2.善用版本管理。
任何和代码相关的地方,版本管理已经默认为基础设置了,这足以体现版本管理的重要性了。
现在版本管理的当红炸子鸡是linux的作者linus开发的git
参考
这也是我推荐使用的工具。
当然,我不建议非程序员去研究命令行,研究什么pull push clone rebase branch。
我们只需要用个合适的编辑器,使用内置的辅助工具就行。
毕竟,我们不是真正的要代码的合作管理。
我们只需要 一个 能查看历史版本,能对比历史版本和当前版本的区别,一个能方便的备份到网上的工具罢了。
我推荐git就是因为单纯的一点,git不依赖服务器,完全可以在本地完成所有的操作。你可以开一个github/gitee的账号,但完全不需要用各种辅助功能,只是单纯的在网上做个备份。
用版本管理工具,我们只是需要版本。因为版本,就是项目开发的历史。
而项目的历史,是维护代码的重要工具,甚至可以说是基石。
git在使用时,主要是针对文本文件进行管理。
mush的mcl文件本质是一个xml文件,本身是可以管理的。
但由于各种变量的值,和触发器的激活状态都保存在mcl内,所以有点蛋疼。
我之前还在写mush机器人的时候是有个专门的update 脚本负责更新触发器的,不对mcl做版本管理。
当然,理论上mush也可以将触发单独管理进行管理。
我现在自己做客户端是直接讲用户触发和变量与脚本触发分开的,脚本不负责存储任何变量。
具体怎么做好,还是取决于你的开发模式,我只能做个提醒。
3.关于注释。
有一个很知名的段子:
作为程序员,我最讨厌两件事
1.别人的代码没有注释
2.让我写注释
这是个段子,但也体现出了注释的微妙状态。
在最完美的状态下,所有的代码都应该有完善的注释。
但实际开发中,合理注释乃至禁止注释才是比较主流的做法
参考
主要是两点
1.维护注释比维护代码要难很多
2.过时的注释危害比不写注释还大
总的来说,就是注释缺乏一个验证是否失效的有效手段。而保留失效的注释的话,等于拿着过时的设计图去维修房屋,命名地基5米图上只有3米,容易闯大祸。
我个人的建议是
[*]尽量少用注释,多分函数/方法,函数,方法和变量名足够长,让代码自己有自注释性
[*]大的模块/文件 可以放一个解释性的注释,因为这个不太会变,而且价值高
[*]算法类的加个注释解释下,因为一半不太会变动
[*]有过bug的修正可以加注释做个说明
[*]尽量通过注释生成文档(一半mud机器人不需要这个)
[*]注释已经失效但需要对照的代码
说人话就是尽量在不太变的地方加注释。经常变的地方,代码就写的易读点,不注释了。
维护代码时,测试>文档>测试
当然,我之前看到过某个mud机器人大神(不是北侠的),他有个习惯,不适合我的架构,但很推荐。
就是正则/触发器入口函数等地方,直接注释一段对应mud原文。
这样很容易知道正则和代码处理的是什么,以及和现在的更新做对比,更容易明白怎么改代码/触发。
4.单元测试
mud机器人,特别是北侠的mud机器人有特殊性,应对的时经常变化的场景,所以大部分情况瞎不需要单元测试。
但我个人还是建议,在有需要的地方,比如各种库/文字数字转化/地图路径计算时加入单元测试。
关于单元测试,本质就是将代码分为最小单元,根据涉及意图设计多个测试用例,通过程序自动进行测试,确定功能实现成却,以及更新和维护代码后功能依旧可用。
参考
单元测试的意义,实际中主要体现在几个方面
[*]确保代码符合自己的意图(测试用例)
[*]确保更新对代码没有影响
[*]一个使用代码的约定(我的代码应该按我的测试的方式来使用)
其中个人觉得最有用的是第三点
lua的话,我目前选择的是luunit这个库
https://luaunit.readthedocs.io/en/latest/#
使用起来其实比较简单
参考这个测试
先引入luunit
local lu = dofile('../../src/hclua/vendor/luaunit/luaunit.lua')
然后建立Test或test开头的函数
function TestList()
local l = list.new()
checkListPointers(l, {})
local e = l:pushFront('a')
checkListPointers(l, { e })
l:moveToFront(e)
checkListPointers(l, { e })
里面是一个一个用例
在需要判断的地方加入诸如
lu.assertEquals(n, N)
lu.assertNotIsTrue(sum >= 0 and s ~= sum)
这样的判断代码
然后lua结尾加上
os.exit(lu.LuaUnit.run())的代码就行了。
使用时,就是lua -v 你的测试代码.lua
比如
$ lua5.1 test.lua
..............
Ran 14 tests in 0.010 seconds, 14 successes, 0 failures
OK看是否有报错或者报失败就行。
5.善用ide
老话说的好,公欲利其事必先善其器。
写代码还是用个合适的ide/编辑器比较好。
这里我比较推荐vscode加lua扩展。
下载地址
开源,免费,跨平台,中文支持良好,自带编码(gbk<=>utf8)转换,大厂(微软出品)。
对git的支持也不错,还有自己的文件保存管理方案。
用了vscode+lua后,对于很多语法错误能不执行直接指出,未使用变量也能提示,也支持代码补全,能很好的提高效率。
当然,编辑器也不是万能的,特别是对脚本语言。
比如lua项目,跨文件不全基本是很难的。
这里面也可以做一些保护性编程。
比如,代码种一个常见错误就是打错属性名。
比如 quest.name打成了quest.nane.
不小心很难发现,lua也不会报错,只会得到一个nil值。
我这里会设置一个quest.setName(name)和quest.name()方法,实际数据放在quest._name里。
这样如果调用quest.nane()的话,lua或直接报错,提示将空值作为函数处理。
当然,具体是否这样操作,取决于你的代码习惯和具体需求。
6.合适的代码组织结构
代码组织结构,这个是见仁见智的问题了。
我只能说说我的。
我一般会把代码分为几类
1.框架,用来组织其他代码的,没有实际作用
2.库,用来被引用的,一般是算法,比如中文数字转换,路径计算,时间处理
3.封装。将一部分常用流程抽取出来,比如我抽象了状态机,业务流,指令等。
3.业务代码,对应Mud就是对应的任务代码。
这几类的我使用的方式和注重点都有所不同。
1和2是最底层的,我一般都会有测试代码,甚至可能在不同的项目里复用。
3.属于业务的组织形式,业务种用来抽象归类的部分。由于和业务相关,未必适合测试,但一般也会偏向被多次复用来进行代码。
4.业务代码,是最脏的。基本无法通过代码进行测试,写的时候我会尽可能的往烂的写,让他容易崩溃。然后复用完全不考虑,直接ctrl+c然后ctrl+v。力求和其他代码没有任何关系,就算出bug,只会影响单独的一个包。
这样的优点是在有一定测试和复用的保证情况下,确保业务之间的隔离性,一个业务崩了,不应该影响到其他业务。
举个例子,
走路,战斗这种属于3这一类的,只要mud修正了,必须全局修改,测试,而且修改完毕后应该在所有业务包里都生效。
而具体的慕容任务,推车任务,这种属于业务包,崩了就崩了,不影响其他任务。甚至会 特地写的脆弱点,let it crash,以有问题就挂,确保能及时发现问题。
具体结构其实取决于程序员的习惯。
但作为一个有多年挖坑和踩坑经验的坑王,提醒下,代码里和业务的远近关系决定了代码的性质,是进行代码组织时的重要依据。
7.命名空间及其他
首先,虽然我觉得mudlet的wiki比Mush的文档质量上还是稍差一点的,但关于写机器的最佳实践可以参考下
https://wiki.mudlet.org/w/Manual:Best_Practices
这个帖子主要说的是其中的
Group your globals together in a table
以我的代码为例
我的pkuxkx.noob机器是一个js机器,按照js惯例,代码打都放在一个全局的app变量中。
而且虽然有这个全局变量,实际使用时,也不强依赖这个全局变量,而是加载模块时传进去,比如
(function (App) {
})(App)这个样子的,这样能避免很多重复和冲突,在lua这个默认作用域时global的更能解决很多问题。
在这个变量种还有个两个特殊的子表
[*]App.Data 所有业务的数据当放在这个变量里(比如角色数据App.Data.Player,房间信息App.Data.Room),避免到处丢垃圾。
[*]App.Core,里面放的所有核心模块,与涉及具体业务的扩展模块区分开,避免冲突
lua的话和js会有点区别。
但我开始整理的lua框架里,还是做了一个简单的模块加载系统,有类似于我js框架的加载模式
return function(runtime)
local M = {}
...
return M
end
这个基本也属于个人开发习惯,只是举个例子。
页:
[1]
2