jarlyyn 发表于 2024-4-24 14:30:16

杰哥瞎扯蛋之瞎扯MUD机器人的可维护性

大家好,杰哥又来瞎扯蛋了。
这次我来扯扯怎么保证机器人的可维护性。

当然,一如既往的,主打一个听起来很厉害,听完没啥用。

我口头假装自己很牛逼爽一爽,读者假装自己学到啥了爽一爽。

dtp 发表于 2024-4-24 14:33:38

好一个欲扬先抑!前排占座。

jarlyyn 发表于 2024-4-24 14:59:04

本帖最后由 jarlyyn 于 2024-4-24 03:00 PM 编辑

1.代码是有生命的。

扯蛋第一要义是有话题性,所以我先给一个结论

‘代码是有生命的,有记忆,有历史,有童年,有生长环境,有生老病死’

听上去很扯吧,好,看我怎么圆回来。

有一说一,一个一个的代码的确看上去的确是静态的无生命的东西,代码该怎么跑,自然由编译器/脚本引擎/语法来决定,给到A语句,自然有A回复,怎么可能有生命呢?

的确,对于单独一个语句,对于单独一个文件,的确如此。但是当我们把目光放到整个项目时,又不一样了。

先让我们看看正真的生命是怎么样的

扯到生命,不得不提到DNA

参考

在我的认识里,生物,比如人,都是通过DNA的遗传信息,通过细胞为单位,通过脂肪/肌肉/神经等基础的细胞组成器官,组成人体的。
哪怕人体,这个生命的奇迹,基因也不是完美的,留下了生物进化历史的痕迹。

代码之于生物。

每一个代码文件相当于一个细胞,每个代码的模块相当于一个器官。

一个个个的细胞(代码)和器官(模块)可以做科学研究。

但是所有的工程问题都会涉及到复杂度。

除了全知全能的上帝,或者亿里挑一的天才,能掌握的复杂度都是有限的,超过一定程度的复杂性,对于研究的人来说这就是一个类似于生物的混沌个体。只能从微观角度进行试验,宏观部分很可能是一个黑核。

维护代码很多时候和医生看病一样,只是根据现象,检测(问题还原)来做推测,并不一定是100%解决所有细节问题。

这就是代码项目和生命的联系点。

同时,代码有一个逐渐发展,迭代开发,根据外部需求(mud更新)调整的过程,整个框架存在妥协性,时效性,赶工期等各种发育/进化过程中的妥协,并成为地基影响之后所有的代码。

对于代码项目,完美只存在于理想之中,屎山才是常态。

别说代码。哪怕是生物体,也是如此。

比如,你是否知道人类其实是后口动物,老老老老祖宗腚眼一开始也是用来进食的?然后才进化到长个嘴出来?(注意这是口嗨)

扩展阅读

回到正题,代码生老病死是什么?

生好理解

病,很明显,就是修复bug。

老,就是屎山越叠越高,维护越来越难。

死,我们很容易知道,重写一个项目的代价是固定的,而维护代码的代价是越来越高的。当维护代码的代价远远超过重写一个可靠的代码后,老的代码,就等于死了。而新的代码,就是新生。

世界上大部分代码项目大体都是这样。

提高代码的可维护性,就是打好基础。

让代码不容易生病,得了病容易治,老的慢点,死的晚点?

dtp 发表于 2024-4-24 15:03:37

最后一行讲得很好,不过你漏了一句:生得早点!

都等不及了。快快快,1234567

jarlyyn 发表于 2024-4-24 15:10:22

2.善用版本管理。

任何和代码相关的地方,版本管理已经默认为基础设置了,这足以体现版本管理的重要性了。

现在版本管理的当红炸子鸡是linux的作者linus开发的git

参考

这也是我推荐使用的工具。

当然,我不建议非程序员去研究命令行,研究什么pull push clone rebase branch。

我们只需要用个合适的编辑器,使用内置的辅助工具就行。

毕竟,我们不是真正的要代码的合作管理。

我们只需要 一个 能查看历史版本,能对比历史版本和当前版本的区别,一个能方便的备份到网上的工具罢了。

我推荐git就是因为单纯的一点,git不依赖服务器,完全可以在本地完成所有的操作。你可以开一个github/gitee的账号,但完全不需要用各种辅助功能,只是单纯的在网上做个备份。

用版本管理工具,我们只是需要版本。因为版本,就是项目开发的历史。

而项目的历史,是维护代码的重要工具,甚至可以说是基石。

git在使用时,主要是针对文本文件进行管理。

mush的mcl文件本质是一个xml文件,本身是可以管理的。

但由于各种变量的值,和触发器的激活状态都保存在mcl内,所以有点蛋疼。

我之前还在写mush机器人的时候是有个专门的update 脚本负责更新触发器的,不对mcl做版本管理。

当然,理论上mush也可以将触发单独管理进行管理。

我现在自己做客户端是直接讲用户触发和变量与脚本触发分开的,脚本不负责存储任何变量。

具体怎么做好,还是取决于你的开发模式,我只能做个提醒。

jarlyyn 发表于 2024-4-24 15:23:22

3.关于注释。

有一个很知名的段子:
作为程序员,我最讨厌两件事
1.别人的代码没有注释
2.让我写注释

这是个段子,但也体现出了注释的微妙状态。

在最完美的状态下,所有的代码都应该有完善的注释。

但实际开发中,合理注释乃至禁止注释才是比较主流的做法

参考

主要是两点

1.维护注释比维护代码要难很多
2.过时的注释危害比不写注释还大

总的来说,就是注释缺乏一个验证是否失效的有效手段。而保留失效的注释的话,等于拿着过时的设计图去维修房屋,命名地基5米图上只有3米,容易闯大祸。

我个人的建议是


[*]尽量少用注释,多分函数/方法,函数,方法和变量名足够长,让代码自己有自注释性
[*]大的模块/文件 可以放一个解释性的注释,因为这个不太会变,而且价值高
[*]算法类的加个注释解释下,因为一半不太会变动
[*]有过bug的修正可以加注释做个说明
[*]尽量通过注释生成文档(一半mud机器人不需要这个)
[*]注释已经失效但需要对照的代码


说人话就是尽量在不太变的地方加注释。经常变的地方,代码就写的易读点,不注释了。

维护代码时,测试>文档>测试

当然,我之前看到过某个mud机器人大神(不是北侠的),他有个习惯,不适合我的架构,但很推荐。

就是正则/触发器入口函数等地方,直接注释一段对应mud原文。

这样很容易知道正则和代码处理的是什么,以及和现在的更新做对比,更容易明白怎么改代码/触发。

jarlyyn 发表于 2024-4-24 15:36:47

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看是否有报错或者报失败就行。



jarlyyn 发表于 2024-4-24 15:44:55

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或直接报错,提示将空值作为函数处理。

当然,具体是否这样操作,取决于你的代码习惯和具体需求。

jarlyyn 发表于 2024-4-24 16:00:51

6.合适的代码组织结构

代码组织结构,这个是见仁见智的问题了。

我只能说说我的。

我一般会把代码分为几类

1.框架,用来组织其他代码的,没有实际作用
2.库,用来被引用的,一般是算法,比如中文数字转换,路径计算,时间处理
3.封装。将一部分常用流程抽取出来,比如我抽象了状态机,业务流,指令等。
3.业务代码,对应Mud就是对应的任务代码。

这几类的我使用的方式和注重点都有所不同。

1和2是最底层的,我一般都会有测试代码,甚至可能在不同的项目里复用。
3.属于业务的组织形式,业务种用来抽象归类的部分。由于和业务相关,未必适合测试,但一般也会偏向被多次复用来进行代码。
4.业务代码,是最脏的。基本无法通过代码进行测试,写的时候我会尽可能的往烂的写,让他容易崩溃。然后复用完全不考虑,直接ctrl+c然后ctrl+v。力求和其他代码没有任何关系,就算出bug,只会影响单独的一个包。

这样的优点是在有一定测试和复用的保证情况下,确保业务之间的隔离性,一个业务崩了,不应该影响到其他业务。

举个例子,

走路,战斗这种属于3这一类的,只要mud修正了,必须全局修改,测试,而且修改完毕后应该在所有业务包里都生效。
而具体的慕容任务,推车任务,这种属于业务包,崩了就崩了,不影响其他任务。甚至会 特地写的脆弱点,let it crash,以有问题就挂,确保能及时发现问题。

具体结构其实取决于程序员的习惯。

但作为一个有多年挖坑和踩坑经验的坑王,提醒下,代码里和业务的远近关系决定了代码的性质,是进行代码组织时的重要依据。

jarlyyn 发表于 2024-4-24 16:12:04

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
查看完整版本: 杰哥瞎扯蛋之瞎扯MUD机器人的可维护性