你有没有遇到过硬编码的困惑? 那种在你能够接触到的模块/代码中,放眼望去全是magic number/string。或许这是一个数据库表的名字,或者这是一个数据的状态值。在你试图想修改一个数据库名,或者更为普遍的,你发现两个状态值已不足以表达数据的状态了,你说了一句WTF.
我有。
近几个月来,我接触了几个项目,或大或小。我们的业务主要在于聚合视频资源。我们采用MongoDB做前端数据库,并使用JS/Python这些动态语言开发。NoSQL的好处在于你可以随时修改一个数据的结构(也就是任意添加一个字段并赋予其新的含义)。在编码初期,我们正急于搭建原型,MongoDB帮了我们大忙。
提需求了,需要对某个由于解码问题不能播放的视频禁用,那就添加一个status字段吧,就给它赋0/1吧,0表示启用,1表示禁用。
又提需求了,需要添加视频清晰度标识,那就添加一个definition字段吧,就给它赋1~5吧,分别是标清/高清/超清/蓝光/1080P。
今天的需求,最好下午就能看到效果并转测试,明天上线!
就是这样,动态语言以及NoSQL带给了我们自由扩展的能力。我们及时地满足了许多需求,直到有一天。
提需求了,在做视频解析扫描测试时,最好能判断视频的status,以便在检测到该视频能播放后,能够及时启用该视频。不是很好吗?让程序去完成这一自动化的过程。不,很不好。你忘了吗?我们只对视频进行启用/禁用操作。最初我们根据视频的播放与否来启用/禁用视频,但需求越长越大,在几个月以来,我们常常因为该视频的版权问题而禁用一个视频,并且我们同样的,也把它的状态置为了0.这有什么办法?这个问题在当时很急迫并且我们已经有了一套现成可用的启用/禁用接口。前人挖坑后人填,现在我们怎样才能判断出,一个视频状态为禁用的原因是因为它不能解码或是版权问题呢?我们不仅需要知道状态,还需要知道原因。
为什么不再添加几个字段,decodeAvaliable/copyrightDeny.等等,天呐,你还想把数据库搞得更乱吗?这种方案的结果是可预见的。
对视频状态的存取只是其中一个问题,我还遇到许多其他问题。判断一个新爬取的视频与数据库中的视频的相似度,与某一条相似/与所有都不相似/不确定,相似则合并,不相似则插入,不确定则归到待处理队列;判断一个数据的处理状态,待处理/已处理(已合并/未合并/暂不处理等)。
我被这些逻辑判断搞晕了,如果一个判断涉及多种逻辑,我该如何是好?继续在代码中硬编码吗?如果有一天需求有发生变化了呢?我要如何维护这份代码?
于是我试图归纳出共性:它们都是某个数据的某个状态。每条数据有各种预定义的状态。
哈哈!我可以定义出一个模块,用来表达/操作一条数据的状态值。
视频状态很复杂?那我们就从定义vidoeStatus模块开始吧。
首先定义已知的视频状态:
Stauts: {
ALLOW_PARSE_OK : 40, # 解析可用
ALLOW_COPYRIGHT_OK : 30, # 版权可用
ALLOW_MANUALLY : 20, # 手工启用
ALLOW : 10, # 节目为启用状态
ALLOW_ALIAS : 1, # 启用状态,别名
DENY_ALIAS : 0, # 禁用状态,别名
DENY : -10, # 节目为禁用状态
DENY_MANUALLY : -20, # 手工禁用
DENY_PARSE_FAIL : -30, # 该视频不能解析
DENY_COPYRIGHT_FAIL : -40, # 该视频涉及版权问题
}
再定义查询接口
- isAllowBy…()
- isAllow()
- isDeny()
- isDenyBy…()
- queryAllow() # 查询所有启用状态的视频
- queryDeny() # 查询所有禁用状态的视频
再定义操作接口
- setAllowBy…()
- setAllow()
- setDeny()
- setDenyBy…()
值得注意的是:
- 我们需要兼容以前的状态值0/1,所以我定义了ALLOW/ALLOW_ALIAS, DENY/DENY_ALIAS来进行过渡。
- 我用了值的大小来表明状态的优先级,越靠近1/0,优先级越高。被手工禁用的节目,即使是检测到可解析,依然不能置为启用状态,必须手动启动。在set*()系列中必须对状态的优先级进行检查。
- 为了能够充分扩展,将状态值设计成不连续的值。取个例子,鉴于我们目前所知的,视频清晰度的分级:标清/高清/超清/蓝光/1080P,是基本满足所有需求的。将对应值设计成1~5固然没错,但如果对应值10/20/30/40/50岂不更加优雅。我将状态值设计成-50/-40/-30/-20/-10/0/1/10/20/30/40/50。你能否体会这种间断值的美妙,如果你用各种理由来阐明你只能用1~5,那就想想HttpStatusCode吧,我强烈建议间断值的设计。
- 将接口设计的“自说明性”,普通的程序员只需要写出机器能运行的代码,但优秀的程序员还需要让所有人都能读懂的代码。
- 如果你使用的语言支持enum,那再好不过了;但如果没有,不用灰心,情况不会太糟糕。在支持dict/map的语言中,使用key-value结构足够优雅;最简单粗暴的方案,就是直接定义一系列常量。就像C程序的习惯,定义一系列返回值在头文件中。
再举个例子,我是如何应用这些状态处理模块的吧。
对于每天待进入数据库的数据,我们有时需要人工干预,因此有未处理/已处理(已合并/未合并/暂不处理等)。
首先,定义状态:
- UNHANDLED = 100
- HANDLED = 200
- ISNEW = 210
- ISSIMTO = 220
- ISUNKOWN = 230
再定义查询接口:
- isUnhandled()
- isHandled()
- isNew()
- isSimTo()
- isUnknown()
再定义设置接口:
- setUnhandled()
-
setHandled()
- setNew()
- setSimTo()
- setUnknown()
最后,在你的调用模块中完美的组合它们吧.
if unhandled:
do_unhandled()
else:
if isnew:
do_isnew()
elif issimto:
do_issimto()
elif isunkwn:
do_isunkown()
else:
do_exception()
或许很多人会辩解,你如何知道数据会有些什么状态,需求是不断变化的。没错,程序员都没有预见未来的能力,但我们有许多良好的设计可以借鉴。在可预见的范围内,满足尽可能的灵活性,没有借口。
讲到这里,我们已经可以总结出核心思想了:提取数据的共同特性,并对频繁操作或者逻辑复杂的状态进行封装。不必过于极端的将所有对状态的操作都封装成模块。
将这个想法写出来并非为了嘲笑或者是自夸这个想法有多好,但我认为这个方法在目前能够比较良好的解决我目前遇到的硬编码问题,并且提供了很好的模块扩展。如果你曾经碰到过硬编码,并且深陷其中,相信你能够体会。