一些重构体会

最近在重构多媒体服务,包括爬虫和搜索两部分。原先的代码是一个实习生 Q 同学写的,用的 python。如果抽出其中的一块代码来看, Q 同学应该是一个重实践的同学,代码质量还是很不错的,但是之前看的时候我还是很难理解他的编程思路。直到这次彻底的重构,才发现代码中的一些问题。 这里记录下来,也是对自己的提醒。

命名

这其实是一个老生常谈的问题,而且大部分人其实都有这个意识,Q 同学在对待大部分的命名上也都在尽量选择合适词汇。但是,对于工具函数却没有一视同仁。 比如

def _l(x):
    r = list(map(_ft, x))
    if len(r) == 1:
        r = r[0]
    return r

这是一个内部函数,用来提取出爬取的元素,并根据获取的元素数量返回不同的数据结构,在爬取数据的时候多次用到。即使不用太高级的英语,直接用 get_result_as_list_or_string已经可以极大地缓解在阅读过程中返回去看这里干了什么的情况。同样的,用 xr之类的做变量名, 虽然我可以明白这个函数在做什么操作,但是我并没有办法把它和业务的某一部分联系起来。

这样的例子在整个源代码中还有很多,虽然最后还是可以看懂,但是读起来就不怎么舒畅。

保持程序的轻量

这个各人可能会有不同的习惯。我不喜欢在功能很简单的时候就引入太重量级的东西。比如,如果你整个程序只有五句 SQL,那为什么要引入一个 ORM。 多媒体服务中,一共不会用到超过10个 ES 访问,而且这些访问的语句是固定死的,也不会频繁改动。ES 的访问就是简单的 Http 请求, 所以自己写个文件来处理并不会很麻烦。但是一旦用上了 elasticsearch-dsl-py, 我除了要理解 ES,还要理解这个库的用法,就为了那不多的几个 ES 请求。

同样的,后台系统中是有开放可用的队列服务的,为什么一定要用 celery 来复杂化这个事情,还导致重建 ES 索引这样的任务必须要用 api 才能下发。

以业务逻辑而不是功能对代码分层

按照我看代码的情况,原先的设计中包括一个接口层,一个爬虫层,一个搜索引擎的实现层,最底下是支持各个层的 lib。 Q 同学做了很多的功能划分:一个 api 文件夹放所有的 api 实现,一个 engine 文件夹放各个源的搜索、爬取和索引的实现文件,一个源一个文件夹。 如果严格按照这样的方式来组织文件,到也不错,问题在于,Q 同学把 ES 相关的东西也放了进去,也许是觉得 ES 也是一种搜索实现吧, 但是 ES 的数据模型等其他信息也一并放在了这里,这就让刚读代码的我有些搞不清自己之前的阅读思路是否是对的。

实际上,整个业务逻辑是这样的

用户发起搜索 --> 社区程序向多媒体服务发起搜索请求 --> 调用搜索接口,使用 ES 来搜本地库 --> 使用用户的搜索词去爬取、更新数据

所以,ES 虽然也是搜索,但是在中业务的层次是和这里的 engine 不同的:同样是 search.py, ES 下是真的搜索,其他的其实是各个爬虫的组件; 同样是 job.py, 各个源下的是爬虫任务,ES 下的则爬虫的触发器;

另一个问题是,因为是按照爬取的目标不同而进行的划分,所以在努力地爬取功能下完成各个工作,导致许多重复的工作。如果按照

爬取->整理->入库->索引

流程,各个爬虫只要返回的多媒体数据结构一致, 入库和索引的步骤都可以用统一的实现。

最终重构的时候,我去掉了 api 层,把搜索放回了社区程序,因为总共只有2 ~ 3个 ES 查询,且是强业务的功能,没有必要放在这样以爬虫为主的程序上, 还需要一层 api, 用上 flask。爬取任务的触发就用 job 形式直接下发到队列。整个程序就按以下的流程来组织了。

任务监视器 --> 爬虫 --> 入库 --> 索引

又因为,去掉额外的爬取源,所以合并了爬虫和入库。

结语

代码是我写的,但是以后阅读和维护代码的人并不一定是我;我现在知道这里为什么这么写,一个月后未必知道。 所以写代码的时候要多想想应该如何保持简洁和逻辑的通顺,多问问我为什要这么写。


发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据