Majsoul-QQBot icon indicating copy to clipboard operation
Majsoul-QQBot copied to clipboard

对重构提点建议

Open Sunshine40 opened this issue 1 year ago • 11 comments

@NekoRabi 听说你想重构项目,框架选型这方面我了解得不够,不多发表评论。

如果有条件,我建议你可以考虑把机器人命令的dispatcher逻辑解耦出来

dispatcher是说啥呢?就是看到一句话,决定要用什么功能模块去响应这句话的这段(分类)逻辑。

目前的设计,所有的命令处理程序各自为政,每个命令Handler都订阅了整个消息事件,然后自行判断是否要响应。

像这样的设计,如果没有更复杂的需求,那么简单的模式就是好的。但是像这样自由的处理模式,要在其上扩展就得要打一个接一个的补丁

举个最简单的例子:

你有一个help命令,展示所有命令一览。 同时,你又把命令所匹配的模式,从硬编码改为了可以通过command.yml来配置。 但是,如果在command.yml当中修改了命令的关键词(模式),那么help里的命令说明是不会有所反映的。

也就是说,虽然形式上比起硬编码的命令模式,似乎通过配置文件集中化地对命令模式进行自定义管理。 但实际上,和命令的调用条件相关的逻辑,还是分散在了各个响应程序中(基本全是正则),和help字段的手动配置(位置还和command.yml不在一处)

那么把help的配置也转移到command.yml当中怎样呢?

我觉得这要看你的野心。

如果你的想法是,能用就好,命令响应就按现在的模式,help的展示就意思意思,有空就改一改配置的位置,没空就放着它,改命令模式需要command.yml和help字段改两遍就改两遍。那维持现在的架构就好。

不知道你有没有了解过langchain?它的指导思想,就是把一个个功能模块,封装成叫做“工具”的接口。

“工具”不负责判断要不要响应输入,它只负责把输入处理好返回预期的输出。 大语言模型来负责决定,面对不同的发言,需要使用哪些“工具”。

而我的建议就是,对本项目的机器人,建立起一套“命令”的规范,其本质,就是上面说的这种“工具”。

这套规范的第一版实现,所使用的技术和现有版本可以一模一样。 但只要时机成熟,就可以把“监听到聊天发言”->“调用相关模块”中间的dispatching步骤,由现在的正则表达式匹配,改为由langchain来控制。

主要还是看你对这个项目有多少野心。如果想要做得更好,重构的时候顺便拔高一点架构设计,是利大于弊的。

一点点个人意见,仅供参考。

Sunshine40 avatar Jun 25 '23 15:06 Sunshine40

我想的是用nonebot2来重构,直接使用nb2的指令响应逻辑 我学艺不精,dispatch等逻辑最初设计时并没有考虑到,存在许多设计上缺陷。我只会想去实现设计,但如何实现设计总是缺乏考虑

NekoRabi avatar Jun 25 '23 16:06 NekoRabi

我想的是用nonebot2来重构,直接使用nb2的指令响应逻辑

你说的是Rule/RuleChecker模式吗?这的确是强制把指令的响应后具体操作和响应的判断分离开来了。其实你可以考虑写一个平凡一点的指令模式,这种模式可能支持的格式比正则所能指定的模式更加受限,但是能够自动通过你的配置来摘取出一个使用样例。这是一种改进方向。

另一种改进方向,就是把命令使用方式的样例,和命令的响应逻辑(判断是否要响应,也就是Rule)写在一起。这个代码组织方式好不好呢?至少Rust和Go是这样做的,文档直接以注释的形式紧挨在代码逻辑旁边,改到逻辑的时候,自然而然就想到把文档也改了,那么最后能向用户展示的文档(说明书)也就自动被修改了——可以作为一种参考。

(其实.NET、Python的文档生成器也有类似功能,如果你使用过它们来自动生成文档,并且从整理后的文档作为一个独立的产物的视角来考虑,就会知道,代码块写注释不仅是给读你代码的人看的,还是给不读你代码的人看的。这样你就可以理解你的help字段所处的地位了)

我只会想去实现设计,但如何实现设计总是缺乏考虑

这其实不完全是缺点,对于敏捷开发来说,你的行动很快,不像我总是会陷入过度设计,而实际的实现却迟迟不推进。

我也只是给你提供一些经过我个人过滤后认为值得提的点,至于最终设计,在开源项目里总是遵循谁出力多谁有发言权的简单原则。

Sunshine40 avatar Jun 25 '23 16:06 Sunshine40

谢谢你的支持与帮助,在重构的设计中我会认真考虑你的想法的,虽然我对你的建议与改进方法还很迷茫


我确实是指用nb2的 rule 和 permission 来实现指令响应,在新的重构中,目前只完成到重写部分代码,通过on_command( ) 进行响应,用 on_command( ).handle( ) 来完成整一个事务

NekoRabi avatar Jun 25 '23 17:06 NekoRabi

我对你的建议与改进方法还很迷茫

那我以这个具体的,规模不大的改动需求为例:

文档直接以注释的形式紧挨在代码逻辑旁边

具体以Python语言,nb2框架为例,我可以给你一种实现方案:

你可以规定一种形式,让使用范例的信息可以直接附带在Rule对象上。

你看每一个事件响应都要通过on_command()类似的形式来注册对不?你可以写一个自己派生的on_command函数,内部调用原有的on_command函数,但是顺便会检查一下这个rule是不是带有帮助信息——如果有的话,就记录进你的用户说明书。

通过这样,就可以做到把用户指引和实际的响应逻辑写到一块去,这个要求。

Sunshine40 avatar Jun 25 '23 17:06 Sunshine40

理解了,继承 on_command( ) ,在子类中重写方法,构造时检测到有类似的 "指令面板或者帮助文档" 时,自动添加到全局的说明书,指令说明书不需要写在 config 中。在响应 help 时,输出全部的指令说明书。 应该是这个意思吧

NekoRabi avatar Jun 25 '23 17:06 NekoRabi

理解了,继承 on_command( ) ,在子类中重写方法,构造时检测到有类似的 "指令面板或者帮助文档" 时,自动添加到全局的说明书,指令说明书不需要写在 config 中。在响应 help 时,输出全部的指令说明书。 应该是这个意思吧

对的,我就是这个意思,不过on_command好像不是类或方法,所以也称不上“继承”,总之你写一个函数将其包装了,然后在自己的项目中全部改用自己写的函数代替掉原有的on_command就可以。这算是开发中很常用的一种模式。

Sunshine40 avatar Jun 25 '23 17:06 Sunshine40

我会尝试的去实现的,这比手动写入config方便多了,也更便于修改

NekoRabi avatar Jun 25 '23 17:06 NekoRabi

顺便提醒一下,这也是我在试图修改你原有的代码时注意到的:

一方面你对于Python项目比较公认的命名规范不太了解,可以稍微查一查。 我倒不是说多管闲事,要在这种非原则性的问题上教你做事。主要是,如果你没有按照通行的命名规范,同时你也不制定一个针对本项目的,符合你自己风格的,全程保持一致规则的命名规范,那么就会导致,除了你以外的contributor(比如说我),变量/函数的命名风格也开始自作主张,最后代码风格明显看出色彩斑斓的补丁。

上面说的还是小事。


另外一个更重要的事,是我发现你对于数据结构的选用,非常漫不经心。

具体来说,如果你的函数所需要的参数,是一个字段名称固定的键值对结构,我发现你经常使用字典"dict",但实际上,这里更适合的是Class/Object。

其实最基础的用于表示数据结构的Class可以很简单,上面一个方法都没有也可以,直接定义几个字段就算完工了,例如:

class EligibleMatch:
    """指定玩家参与的比赛信息"""
    player_name: str
    match: Match

    def __init__(self, player_name: str, match: Match):
        self.player_name = player_name
        self.match = match

你可能觉得,啊,我打了7行代码,实际效果和原来一个dict不是差不多么?

对于Python这款语言的执行过程确实是差不多的。但考虑到我们现在不是用记事本来写代码,而是使用IDE为主,如果采用类定义作为模板,那么IDE就能帮我们检查,到底什么字段是存在(可用)的,类型是什么,什么字段又是不存在(不可用)的。 鼠标悬停在有一个Class的类对象上时,也能看到IDE提取出来的更有意义的提示,而不仅仅是dict的信息。 IDE还能让我们方便的在类定义和它的某个字段的类定义之间快速跳转定位。

实际上,谁也没本事在一个延长的开发周期中,用脑子来记住所有这些数据结构。你自己也意识到这点,留下了这段注释:

# 以下为一个eligiblematch:
# {'playername': 'RAYMOK', 'match': {'url': '064E45DB', 'type': '1414', 'time': '15:10', 'numberX': '41',
# 'players': [{'playername': 'miku618', 'playerlevel': '15', 'playerrank': '2016.09'}, {'playername': 'RAYMOK',
# 'playerlevel': '14', 'playerrank': '1908.20'}, {'playername': '浦飯幽助', 'playerlevel': '14', 'playerrank':
# '1934.99'}, {'playername': '石丸電 化', 'playerlevel': '14', 'playerrank': '1906.34'}]}}

可惜的是,对于这种形式的注释,IDE是不会在应用过程中提供任何检查和提示的。


另一个场景是list,你经常会选用list这个数据结构之后,再发现需要从这个list集合中挑出某个元素,而且很多时候,是依据一个非常简单的检索规则——也就是元素的一个字段。

你以前的代码经常在这种场景中使用for来遍历list,然后判断指定字段和目标是否相等,相等的话就提前跳出/返回——但这种场景不适合使用list,反而适合使用dict——添加元素时,把用于检索比较的字段内容设为key即可。


在编程中,尤其是数据结构——面向对象的话叫做 类/对象,这部分的工夫往往是磨刀不误砍柴工的。

Python有一个优点就是它不强求你有意识定义数据结构——用基本类型+list/tuple+dict理论上就可以打天下了。 但实际上,这既是优点,也是缺点。它在让任何人都能轻松、零门槛入门的同时,如果入门之后形成了习惯而不去提高的话,那么写出来的代码,之后会在维护的阶段遗留下无尽的麻烦。

Sunshine40 avatar Jun 25 '23 18:06 Sunshine40

我是学Java的,对于面向对象编程还是比较了解的。最初的开发确实是受到了面向对象编程的影响,比如plugins/MajsoulInfo。但后来变得越来越懒,逐渐忽视许多规范,只追求结果不管过程,编写了许多有一大堆参数方法和构建一个类似对象的dict,如utils/Message ChainBuilder.messagechain_sender()和eligiblematch 许多这样的代码,后来看让人难以理解,这确实很大的影响了观感和限制了代码的修改,形如这样的代码我会改成class的。 使用list进行检索上,确实不如dict和set的效率高,设计初想过如何判断一个obj或dict是否在一个dict或set中,后来图省事就用list了

欢迎加入QQ群586468489和我畅聊

NekoRabi avatar Jun 25 '23 18:06 NekoRabi

后来变得越来越懒

其实我觉得你很勤快,就从你能够努力一个人把项目做到麻雀虽小五脏俱全的程度,这份行动力就很强。

我就是想提醒你一下,有些并不会增加太多复杂度的设计,从大局观角度,省下的时间是完全值得回一开始做准备工作的票价的。

Sunshine40 avatar Jun 25 '23 18:06 Sunshine40

设计初想过如何判断一个obj或dict是否在一个dict或set中,后来图省事就用list了

怎么说呢,其实以Python而言,反而是dict省事——除了一开始要指定 dict_name[obj.field_name] = obj 之外,检索的时候反而直接判断 if field_value in dictname: 就行了,我觉得这比打个循环+短路跳出省事直白。(另外,即使在使用list+循环代替dict的场景中,往往也会把这个过程封装起来,而不是暴露在需要使用这个检索逻辑的位置)

Sunshine40 avatar Jun 25 '23 19:06 Sunshine40