【开发第一性原理】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 段完事)
- ❌ 按时间顺序切(第一步、第二步、第三步——这是流程,不是模块)
- ❌ 共用全局变量传数据(这是伪拆分)
写完后(回归检查)
- 找个从没看过这段代码的同事,让他读三分钟,然后问他”这段在干啥”
- 如果他讲不清楚 → 模块边界不对,回去重切
- 找一段逻辑,假装需求要变;改完它,其他段不动能不能跑通
- 如果改它必须连带改其他段 → 耦合太高,职责没分干净
五、小结
拆分的关键不是行数,是职责。 行数只是表面,职责才是边界。
说到底,它就教了我们一个朴素的判断:遇到一团毛线,先问”它在做好几件事”,再决定怎么切——而不是先问”它有多少行”。
下一篇,我想聊”为什么一个数据只能有一个源头”。