去年冬天有个学生把我问住了。他说,老师,分解问题我懂啊,我写议论文也是先列三个分论点再往里填内容,那学编程思维和学写作文到底有啥区别?
我当时随口糊弄过去了,回家越想越觉得他说得对。我讲了三年的那套东西——分解、模式识别、抽象、算法这四要素——单拎出来没有一样是编程独有的。分解是项目管理在做的事,模式识别是医生看片子在做的事,抽象是写小说的人在塑造典型人物,算法这个词小学数学就开始教流程图了。把四样通用技能打包盖上“编程思维”的章,然后声称学会这四样就会写代码,这个推理链其实是循环的:先找一群会写代码的人,观察他们的共同点,再把共同点命名为编程思维。等于说,会写代码的人都有这些特点,所以有这些特点的人就会写代码。
这套框架本身不是错的,只是用错了地方。它最早是英国 BBC Bitesize 提出来、后来被 Google for Education 打包推广的教学脚手架,面向的场景是“一节课 45 分钟,怎么让十一二岁的小孩明白什么叫抽象”。它是给老师设计课堂活动用的,不是给工程师用的方法论。就像小学语文的“五感描写法”——从视觉听觉嗅觉味觉触觉去写一个苹果,这个方法本身是好的、也是必需的,但你见过哪个职业作家拿它当创作理论?小孩子需要脚手架,因为他们的脑子里还什么都没有。一个成年人的问题是:没有人再给你发作业本了。
这三年我真正从写代码里长出来的东西,跟那四要素关系不大。先说命名这件事吧,听上去特别不重要,甚至不像个“思维”。但你随手翻一段别人留下的代码:data = get_list(),然后 for i in data: if i.status == 3: handle(i)。data、i、3、handle,四个名字的信息量全部为零。读这段代码的时候,你脑子里得同时维持四个猜测:data 是什么数据,i 是什么东西,3 是哪个状态,handle 又要 handle 成什么样。
把它们改一遍:pending_orders = fetch_orders_by_status(OrderStatus.PENDING),for order in pending_orders: refund(order)。改完你会发现,命名根本不是给代码贴个好看的标签。你把 data 换成 pending_orders 的那一瞬间,其实是给自己上了一条约束——待支付订单,不许出现已付款的,不许出现已取消的。编译器管不着这条约束,但你下一个读代码的同事管得着,三个月后的你自己也管得着。我一直觉得命名是一种廉价的类型系统,而且它比类型系统更早生效,因为它在你想清楚之前就在逼你做选择:这个变量到底能装什么,不能装什么。想不明白这件事,你就起不出这个名字。
另一件事更隐蔽,因为它通常没有名字,只会在某一天突然炸掉。2021 年我参与过一个订单系统,created_at 用的是 Python 的 datetime.now(),就是服务器的本地时间,不带时区。项目在国内跑了一年多,什么事都没有。后来服务器迁到新加坡,两边差几个小时,跨月的对账报表全错了。我们花了两个下午排查,一直以为是 ORM 的问题,最后发现根本不是代码错,是从来没有人定义过 created_at 里的“时间”到底指什么——是物理上那个瞬间,还是北京办公室里挂钟上的读数。这两个定义在服务器不出国的时候是完全重合的,所以没有人会想到要区分它们。
那之后我把 datetime.now() 改成了 datetime.now(timezone.utc)。但更重要的变化是,我开始在写每一行代码之前问自己一个问题:这个东西的边界在哪,它进入这段逻辑的时候,世界是什么状态。这个问题在教科书里不叫“编程思维”,它被拆散了,一部分叫输入校验,一部分叫数据建模,一部分叫领域语义,散在好几本书的角落里,从来没有被集中包装成一个能卖课的概念。但它才是真正区分新手和熟手的东西。
说到这里可以对比一下数学思维,因为这两个经常被放在一起说。数学要求闭合:一个命题要么真要么假,一个证明要么完成要么没完成,你不能交上去一份“证到一半”的东西。编程恰好相反,代码是允许开口的。你可以写 # TODO: 这里先这样,等接口上线再改,你可以先返回一个写死的假数据让前端跑起来,你可以明知道这个算法在数据量超过十万的时候会崩,先把量级卡在一万。一个数学证明里出现 TODO 是灾难,一个项目里出现五十个 TODO 是常态。新手往往带着数学那套习惯进来,想把每一行都写“对”,结果在第一个模块上卡了三星期;老手的动作是先用最丑的方式把整个骨架打通,跑起来,看见数据流了,再回头一格一格抠。
那这套东西怎么练?我自己的答案不是刷题,是读代码。2018 年 IEEE Transactions on Software Engineering 上有篇论文,研究者跟踪了 78 名职业开发者,结论是他们一半以上的工作时间花在“看懂代码”上,而不是写代码——如果我记错了具体数字,你也可以在 GitHub 上随便找个中等规模的项目打开,看看自己读三十分钟能读懂多少。练的方法是:挑一个你熟悉的库,只读它的测试文件。测试文件是作者写给自己的说明书,它告诉你每个函数在什么边界下会怎么样。读三十个测试用例,比闷头写三百行代码更能长记性,因为写代码的时候你只是在重复自己已经会的东西。
回到那个学生的问题。写作文和写代码确实有共同点,两者都是在跟一个“读者”打交道,只不过作文的读者是人,代码的读者首先是编译器,然后才是人。而编译器可能是这个世界上最好糊弄也最难糊弄的读者——你写的一切它照单全收,你脑子里想的它一概不知。所以真正要练的,可能不是怎么把大问题拆成小问题,那是谁都会的事。要练的是把你脑子里那些模糊的、你自己都没意识到的前提,一个字一个字地抠出来,写进名字里,写进边界上,写进注释里。这个过程非常不浪漫,经常是痛苦的,而且它看上去一点都不像“思维”。