代码堆砌是如何形成的以及如何解决

量度而行,不失矣。


通常来说,我们追求代码清晰性和可维护性,追求代码放置整洁有序,放在它该呆的地方,这样阅读起来逻辑井井有条,自然通畅,节省脑力和精力。

代码堆砌,是指只顾实现功能,在原有代码上不断堆砌新的代码,使得整个实现过程逐渐变得杂乱无章。代码堆砌累积足够多后,整个流程会变得难以理解,难以修改或者修改很容易出错。

本文用一个代码实验小例子,来揭示代码堆砌如何形成,以及有什么办法来应对解决。

先说结论:

  • 代码堆砌是因为”差异化处理不足“累积而成。
  • 解决之法: 分组、策略模式、整体可扩展设计。

代码堆砌形成过程

初始状态

让我们从最简单的情形开始。

假设有一张文件表,里面有文件信息:

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 楼的设计,但是设计扩展太有限也不行。这事还得看经验和直觉,对事物的预测和设计能力。这个就考验一个人的大智慧了。

万事万物皆有度。量度而行,不失矣。

小结

代码堆砌的形成,主要是差异化处理不足的累积产生的。

差异化处理的来源:

  • 新增不同类型需求;
  • 原有处理不可复用;
  • 底层接口不同。

差异化处理的方法:

  • 分组与策略模式;
  • 整体可扩展设计。

差异化处理不足的影响因素:

  • 工期紧张;紧急修复;
  • 特殊处理;性能优化;
  • 差异化处理的技能不足;
  • 整体设计的可扩展能力不足;
  • 差异化不足的累积遵循先慢后快规律,不易察觉。

posted @ 2023-12-23 20:39  琴水玉  阅读(63)  评论(0编辑  收藏  举报