· 约 6 分钟

【开发第一性原理】02 · 复杂问题,要先学会「切」

配图:要先学会切

系列说明:这个系列,我想把那些开发里反复应验的规律写下来。不追新概念,只讲那些”早晚会再次撞上”的道理。

TL;DR:拆分这事,关键不在行数,在职责。

几百行的对账单,到底怎么拆才不崩?这篇给你三个判断标准(能不能单独测、能不能单独搬过来、能不能一句话讲清楚),再加三条红线(按行数硬切、按时间顺序切、伪拆分)。看完你下次写代码前,就能问对那个问题——“它是不是其实在做好几件事?“


一、现象:一次让我印象深刻的”切”

我之前接过一个对账单报表,跑了好几年了。

打开一看,好家伙,几千行代码全堆在一个文件里。几十个 CASE 分支,一个 FORM 五百多行——读数据、格式化、计算、汇总、输出,全揉在一块儿。变量几十个,工作区大家共用,临时内表满天飞。

你想动它?盯上半天,最后基本都默默关掉 SE38,假装没看见。

更扎心的是,业务每次提个小需求——“对账单上的客户地址要按主数据走”——我都不敢直接改。原代码那逻辑,简直就是一团毛线,你随手抽一根,谁知道会带出什么来。

最早期我以为,这事儿的难点是”行数太多”。于是试过硬切——100 行一段,每段塞一个 FORM,完事。

结果呢?切完之后,FORM 之间全靠全局变量勾着。改一段,另一段就崩。表面上看是切开了,骨子里还是一坨。

后来有一次,看一个老同事改这个程序——他把那五百行,按”它在做几件事”重新切了一刀。读数据归读数据,格式化归格式化,计算归计算,输出归输出。每一段都是一个独立的 FORM,输入是什么、输出是什么,直接写在注释第一行。

*&---------------------------------------------------------------------*
*& Form FRM_GET_VALUE
*&---------------------------------------------------------------------*
*& 取字段中的值
*&---------------------------------------------------------------------*
*&      --> LT_DATA  输入数据
*&      <-- LV_VALUE 输出值
*&      <-- ES_MESSAGE 输出消息
*&---------------------------------------------------------------------*
FORM frm_get_value  TABLES pt_data
                    CHANGING p_value es_message  TYPE zs_message.
  ...
ENDFORM.

改完,那团毛线一下变成一条链。每个 FORM 都能单独读、单独测、单独替换。

那一刻我才反应过来:拆分的关键不是行数,是职责。 行数只是表面,职责才是真正的边界。

从那以后,我写任何超过五十行的程序之前,都会先停下来,画个大框架:把一个复杂需求拆成几个小需求。每个小需求,我都会问自己一句——“它在做几件事?能不能再拆?“


二、原理:把每个难题分成”可以解决的小块儿”

说真的,这不是我悟出来的,是三百年前笛卡尔在《方法论》里就写过的:

把我所考察的每一个难题,都尽可能地分成细小的部分,直到可以而且适于加以圆满解决的程度为止。

后人把这条原则叫做”分而治之”(Divide and Conquer)。它可不只是算法里的归并排序、快排,它是几乎所有复杂问题能被解开的根本方式。

你可能会问:为什么”按职责”就比”按行数”更本质?

我跟你说——

按行数拆,是按”篇幅”切。看上去模块化了,但每段还是在跟别的段共享状态、纠缠逻辑。它只是把一团毛线剪成几段毛线,根本没解开。

按职责拆,是按”它在做几件事”切。每个模块只承担一个明确的责任——只读数据、只做计算、只做输出。它的输入是什么、输出是什么、对外承诺什么,都得写清楚。

这就像咱们工作里,每个人分工明确,互不干扰,各干各的。

按职责拆,三个判断标准我反复用:

  • 能不能单独测:只给它一组输入,能不能验证输出是对的?
  • 能不能单独搬:别的程序里也出现这个逻辑,能不能直接拷过去用?
  • 能不能一句话讲清:能不能用一句话说清楚这模块是干嘛的?

三条里面只要有一条不满足,就说明拆得还不够——要么该拆的没拆,要么不该拆的硬拆了。

还有一个隐藏好处,我特别喜欢:模块的边界,其实就是思考的边界。 一个程序被按职责切完之后,每个模块的接口就是它对外的全部承诺。你写接口的过程,其实是在逼自己想清楚”我到底要什么、我能给什么”。这一步的收获,常常比代码本身还大。

这思路其实在 SAP 的面向对象开发里早就在用了——先定义类,再定义方法,每个方法都有明确的输入输出,最后才动手写具体逻辑。


三、呼应:这道理在开发之外,早就被反复验证过

医学是”分而治之”做到极致的一个例子。

医院不是”一个医生看所有病”的吧?它先分内外妇儿,再分亚专科,亚专科里再分专病。一个复杂病例,会被会诊制度拆给不同专科——心脏的问题归心内、感染的问题归感染科、免疫的问题归风湿免疫科。没人会想做一个”全能医生”,因为没人能同时精通所有亚专科的细节。

分科不是为了”专业化”这种虚名。分科的真正意义是:让每一类问题有清晰的归属,让每一种知识有积累的容器。 这跟软件模块化,其实是同一回事。

军事上的”分进合击”,也是这个路子。先分,是因为一整团兵力没法对每个局部都做精细判断;合,是因为分完之后还得协同。毛泽东在《论持久战》里讲的”化整为零、化零为整”,本质也是分而治之的运用。

侦探推理也是这样。福尔摩斯不会一上来就把所有线索混在一起想,他会按维度切——时间线、空间线、动机线、人物关系线——每一维单独推,再交叉验证。

医学、军事、侦探,三个八竿子打不着的领域,用着同一个底层动作:把一团混沌,按维度切成几块独立的子问题。


四、实践:把原理变成你桌上的清单

落到日常,我把”按职责拆分”做成了一个小清单,每次写之前翻一翻。

写程序前(拆分原则)

  • 按”它在做几件事”切,别按”它有多少行”切
  • 每个模块都写明输入输出,写在注释第一行
  • 命名就是契约:方法名要说清它做什么,别说它怎么做
  • 接口稳定,内部可改:外部调用的人,不该因为你改实现就崩

拆分时(红线与判断)

  • 三问:
    • 只给它一组输入,能不能验证它的输出是对的?(能单独测)
    • 这逻辑将来会不会在别处出现,能不能直接搬过去?(能单独复用)
    • 我能不能用一句话讲清这模块是干嘛的?(能一句话讲清)
  • 红线:
    • ❌ 按行数硬切(500 行一段,3 段完事)
    • ❌ 按时间顺序切(第一步、第二步、第三步——这是流程,不是模块)
    • ❌ 共用全局变量传数据(这是伪拆分)

写完后(回归检查)

  • 找个从没看过这段代码的同事,让他读三分钟,然后问他”这段在干啥”
    • 如果他讲不清楚 → 模块边界不对,回去重切
  • 找一段逻辑,假装需求要变;改完它,其他段不动能不能跑通
    • 如果改它必须连带改其他段 → 耦合太高,职责没分干净

五、小结

拆分的关键不是行数,是职责。 行数只是表面,职责才是边界。

说到底,它就教了我们一个朴素的判断:遇到一团毛线,先问”它在做好几件事”,再决定怎么切——而不是先问”它有多少行”。

下一篇,我想聊”为什么一个数据只能有一个源头”。

评论