代码堆砌是如何形成的以及如何解决
量度而行,不失矣。
通常来说,我们追求代码清晰性和可维护性,追求代码放置整洁有序,放在它该呆的地方,这样阅读起来逻辑井井有条,自然通畅,节省脑力和精力。
代码堆砌,是指只顾实现功能,在原有代码上不断堆砌新的代码,使得整个实现过程逐渐变得杂乱无章。代码堆砌累积足够多后,整个流程会变得难以理解,难以修改或者修改很容易出错。
本文用一个代码实验小例子,来揭示代码堆砌如何形成,以及有什么办法来应对解决。
先说结论:
- 代码堆砌是因为”差异化处理不足“累积而成。
- 解决之法: 分组、策略模式、整体可扩展设计。
代码堆砌形成过程
初始状态
让我们从最简单的情形开始。
假设有一张文件表,里面有文件信息:
table file:
create table file (
id
file_path // 文件路径
file_hash // 文件 hash 值
file_type // 文件类型
file_size // 文件大小
)
需求一:
从这张表,查询 id = E1 的文件信息。
很简单,代码实现如下(这里均采用伪代码实现,读者可脑补为具体实现):
File f = find(id)
func find(id):
fetch("select * from file where id = " + id)
需求二:
从这张表,查询 id 为 E1, E2, E3 的信息。
嗯,也好做,无非是 等于 换成范围操作。
代码依然看上去美好。
File fs = find(ids)
func find(ids):
fetch("select * from file where id in " + ids)
例子看上去很简单,但说明了一个很重要又值得利用的启示:
- 同类型元素的处理,不会导致代码堆砌。
- 可复用的代码处理,不会导致代码堆砌。
- 往往从最简单的示例中,可以得到很有用的规律。
加点码
需求三:
现在,又来了一种进程类型 process 的信息,有 pid, pname, cmd。要根据 id 查进程信息,怎么办呢?
总不能每来一种类型的信息,就加一张表吧?好,咱们在原来的表进行扩展。
file 表扩展成 element 表。字段也要改造下。不同的元素类型有不同的信息,可以用一个基类字段 detail 来表示。
table element:
create table element (
id
element_id // 元素ID
type // 元素类型,目前有 file, process
detail // 元素详情
)
detail 现在有两种:
file_detail (
file_path // 文件路径
file_hash // 文件 hash 值
file_type // 文件类型
file_size // 文件大小
)
process_detail (
pid
pname
cmd
)
获取元素信息:
func get_elements(elem_ids, type):
elements = fetch("select * from element where element_id in " + elem_ids + " and type = " + type)
list = []
for e in elements:
if e.detail instanceof file_detail {
file = e.cast(File)
list.add(file)
} else if e.detail instanceof process_detail {
process = e.cast(Process)
list.add(process)
}
return list
哈, if-else 出现了。
这里说明了一个重要的启示:
- 差异化特征,会导致 if-else 的出现,从而产生代码堆砌的火星。
再加点码
需求四:
根据不同的元素类型,需要生成不同的指令参数。比如,进程类型的,指令参数为 op = kill_process, params = [{"pid":xxx}], 文件类型的,指令参数为 op = isolate_file, params = [{"file_path":"xxx", "file_hash":"yyy"}]。
这时候,我们需要根据不同类型的元素,写不同的参数,然后组合起来。
func generate_ins(processes, files):
all_ins = []
all_ins.addAll(get_file_ins(files))
all_ins.addAll(get_process_ins(processes))
return all_ins
func get_file_ins(files):
return {"op":"isolate_file", "params": map(files, get_file_ins)}
func get_process_ins(processes):
return {"op":"kill_process", "params": map(processes, get_process_ins)}
func get_process_ins(process):
return {"pid": (process_detail)(process.detail).pid}
func get_file_ins(file):
file_detail = (file_detail)file.detail
return {"file_path": file_detail.file_path, "file_hash": file_detail.file_hash}
这里使用了一个重要技巧:
- 分组:是应对差异化的有效之法。
- 策略模式: 是分组的扩展方法,能够应对多种不同类型的元素的处理,将 if-else 或 switch 里的东西分离到多个同类型的元素的处理,然后再加以组合。
再加点码
需求六:
假设有主机类型和容器类型的文件操作,需要不同的 op(底层接口不同),比如主机文件操作指令为 op_file, 容器文件操作指令为 ctn_op_file。op 可能为不同的值。我们必须根据是否主机生成不同的操作指令。
func get_file_ins(files, operation, contaierId):
op = containerId != null ? "ctn_" + operation + "_file"
ft1_ins = {"op": op, "params": map(files, get_file_ins)}
return ft1_ins
这里,我们发现了一个启示:
- 底层接口的不同,是上层需要差异化处理的一个来源。
需求七:
假设有不同的多个 agent,每个 agent 都有不同的进程信息和文件信息,需要生成不同的指令,然后下发给这些 agent。 你得在上层再做一层循环。
func generate_ins(agents):
for agent in agents:
files = get_elements(agent.file_elemids, "file")
processes = get_elements(agent.process_elemids, "process")
ins = generate_ins(processes, files)
send(agent, ins)
需求八:
文件不仅有 iso_file 还有 delete_file。 delete_file 根据是否已经执行过 isolate_file,分为 delete_file 和 delete_isolate_file。
这里需要加判断,如果已经有过 isolate_file,则对应指令为 delete_isolate_file。哈,指令的产生,需要先去查一遍已有操作信息,才能确定。
需求九:
对于已经加白的文件,不能隔离。
emm...,你得加个判断。虽然判断是如此的微不足道,可也给流程加了一个 if-else。
实际的判断可没这么简单。
需求十:
又来了一种文件类型,而这种文件类型的某些信息,不能从表里获取,而要通过 API 从外部获取。这样,除了从表获取数据,你需要再加一种从 API 获取信息的方法,同时添加一个 if-else 来处理获取这种特殊信息。
需求十一:
进程联动隔离的文件,在 element 表里没有信息,需要存起来,以备后用。
需求十二:
又来了一个 IP 类型。需要进行 IP 封停。又需要一种不同的处理。
需求十三:
需要对容器进行操作。而 容器ID 在 element 表里的字段不是 element_id, 而是 container_id 。又需要做一些特殊处理。
需求十四:
发送指令之前或之后,需要做点其它的事情。这个事情同样也可能因为各种原因需要加各种 if-else。
不难想象:
- 当差异越来越多,内部的 if-else 就会越来越多;
- 当流程越来越长,容纳的 if-else 也会越来越多;
整个业务流程的实现,本质上就是各种差异化处理的组合。要想保持不乱,就需要时时刻刻运用分组和策略模式,把差异化处理分开,把同类型的处理放在一起。同时,还需要处理好参数的传递。
差异化处理不足的影响因素
前述可知,要应对如此多的差异,同时保证代码始终整洁不乱,需要付出一定的努力才能达成。必须时时刻刻注意运用分组和策略模式,把差异化处理分开。堆积木的时候,必时时注意维持稳定,不然可能一下子就全部崩塌了。
什么时候会难以处理好差异化呢?
- 工期紧张。可能没有充分时间去思考,而是先实现功能再说;
- 线上问题修复。这时候,同样没有充分时间去考虑如何优雅处理,而是先修复问题再说;
- 初入职场,不擅长把差异化处理分开。直接在内部 if-else 。
- 刚刚接手一个系统,还不太清楚系统的整体设计,但是又要完成需求,怎么办呢? 先加 if-else 再说。
- 特殊处理。遇到特殊场景,需要加特殊处理。
- 性能优化。性能优化往往需要加一些特殊逻辑,形成代码堆砌。
- 多处 if-else 遥相呼应。不是改一处,而是要改多处。
当流程越来越长时,还会发生一种现象:你不得不同时在多处做相关的 if-else 处理。这些地方相隔比较远,但又存在紧密联系。随着时间的累积,这种现象开始只是缓慢地发生,随后发生得越来越快,进一步导致这种现象越来越多,形成恶性循环。最终,在系统多处累积的 if-else 越来越多,代码堆砌现象越来越严重,逐渐就形成了所谓的”屎山“,—— 程序员深恶痛绝的代码现象。
“屎山”是差异化处理不足经年累月逐渐累积而成。它并不是因为技术难度高,也不是因为程序员编程水平不足,只是微小的难以察觉的不足逐渐累积成明显而巨大的问题,从而压垮了人们的应对能力。正如社会的痼疾经年累月逐渐缠结而难以革除一样。事常起于微末,而成于不堪。
代码堆砌如何解决
要解决代码堆砌问题,本质上就是解决差异化逻辑问题。
有两个层面:
- 微观层面:要持续做好差异化分离处理的小事。勿以善小而不为,勿以恶小而为之。不要觉得加一个if-else 没啥事。持续小幅重构,把差异化分离做好。
- 宏观层面:在整体设计上要更有容纳性,事先想到变化扩展的部分,尽可能预留扩展方式。
当然,很难很完美地在一开始就把整体设计做得无限可扩展,但这事儿就是一个度的问题。如果你的设计能够支撑盖到 16 楼,那么就只能盖到 16 楼,业务发展到 25 楼,就只得以修修补补的方式来往上盖了;如果你的设计能够支撑盖到 33 楼,而业务只发展到了 25 楼,那么也够用了。未必有必要非得作出能支撑盖到 100 楼的设计,但是设计扩展太有限也不行。这事还得看经验和直觉,对事物的预测和设计能力。这个就考验一个人的大智慧了。
万事万物皆有度。量度而行,不失矣。
小结
代码堆砌的形成,主要是差异化处理不足的累积产生的。
差异化处理的来源:
- 新增不同类型需求;
- 原有处理不可复用;
- 底层接口不同。
差异化处理的方法:
- 分组与策略模式;
- 整体可扩展设计。
差异化处理不足的影响因素:
- 工期紧张;紧急修复;
- 特殊处理;性能优化;
- 差异化处理的技能不足;
- 整体设计的可扩展能力不足;
- 差异化不足的累积遵循先慢后快规律,不易察觉。