1. 为什么“编程尚未被解决”是个值得认真对待的判断
第一次听到“编程尚未被解决”这个说法,很多人下意识会觉得这是一句博眼球的暴论。毕竟我们身边跑着操作系统、数据库、编译器、浏览器,每天有海量代码被提交、被部署、被调用,怎么看编程都已经被“解决”得相当彻底了。但如果你在一线写过足够多的项目,尤其是维护过那种跑了三五年、经手过七八个人的老系统,你大概会有另一种感受:我们解决的只是“让机器执行指令”这件事,而“让人可靠地表达意图并持续演进”这件事,离解决还差得远。
这个判断的核心,不是说编程语言不够多、框架不够强,而是说编程这件事的本质困难一直没被真正消除。本质困难是什么?是把一个模糊的、变化的、充满隐含前提的人类需求,翻译成一份精确的、可执行的、能被机器和后来者同时理解的指令集合。这个翻译过程里,机器那一端我们做得越来越好,人这一端几乎原地踏步。标题里的“尚未被解决”,指向的正是人这一端的鸿沟。
我写这篇文章,不是要唱衰编程行业,恰恰相反,是想把这道鸿沟拆开来看清楚。适合谁来读?如果你是被各种“低代码将取代程序员”“AI 让编程消失”之类说法搞得很焦虑的开发者,或者是刚入行、想知道自己到底在跟什么困难搏斗的新人,再或者你是技术管理者、想搞清楚团队效率瓶颈到底卡在哪,这篇内容都值得你花时间。我会从问题本质、现有方案的局限、真实项目里的表现、以及我个人踩过的坑几个角度,把“编程尚未被解决”这件事讲透,并且给出一些可落地的应对思路。
需要先说明一点:下面涉及的具体案例、项目、人物,都是我在行业里见到的普遍现象的抽象化表达,用虚构代称呈现,不指向任何真实主体。重点在于现象和规律,而不是某个具体故事。
2. 拆解“尚未被解决”的三层含义
2.1 第一层:意图到实现的翻译损耗
编程最原始的形态,是人把想法写成指令。问题在于,人脑里的想法从来不是以指令形式存在的。一个产品经理说“用户下单后如果库存不足就提示等待”,这句话里藏着多少没说的东西?库存是实时查还是缓存查?并发下单时怎么保证不超卖?提示是弹窗还是页面跳转?等待是排队还是直接失败?这些细节在需求描述里全是空白,而程序员必须把它们全部补全,补错一个,线上就出问题。
这种翻译损耗,我习惯叫它“语义鸿沟”。它不是靠更高级的语法能填平的。你换成函数式、换成声明式、换成任何范式,鸿沟依然在,因为鸿沟的根源在于自然语言的模糊性和形式语言的精确性之间没有直通车。我见过太多项目,需求评审时大家点头如捣蒜,开发到一半发现双方理解的根本不是同一件事。这不是沟通能力问题,是语言本身的粒度不匹配。
2.2 第二层:复杂度增长与人的认知上限
第二层困难更隐蔽,也更致命。软件系统的复杂度不是线性增长的,是超线性增长的。你加一个功能,它可能和现有五个模块产生交互,每个交互又可能引入新的边界情况。一个中型系统跑三年,代码量翻三倍,但理解成本可能翻十倍。为什么?因为理解一个系统不仅要理解每个部分,还要理解部分之间的所有连接。
人的工作记忆容量是有限的,这是生理事实,不是靠“多加班多学习”能突破的。当系统复杂度超过某个阈值,任何单个开发者都无法在脑子里完整建模整个系统。于是出现一种典型现象:改 A 处崩 B 处,修 B 处又影响 C 处。这不是程序员水平差,是系统复杂度已经超出了单点认知的承载能力。所谓“编程尚未被解决”,很大程度上是指我们还没有一套真正有效的手段,让复杂度增长和人的认知能力保持同步。
2.3 第三层:演进中的系统与静态知识的矛盾
第三层是最容易被忽视的。代码是静态的文本,但系统是动态演进的。今天写下的注释、文档、架构决策,半年后可能就过时了,但代码还在跑,后来者只能对着过时的说明猜意图。更麻烦的是,很多关键决策根本没有被记录下来,它们只存在于当初那个开发者的脑子里,而那个人可能已经离职了。
我管这个叫“知识蒸发”。一个系统运行越久,蒸发越严重,最后变成一座没人敢动的黑箱。你问为什么这里要加个特殊判断,没人知道;你问能不能删掉这段逻辑,没人敢删。这种状态下,编程与其说是创造,不如说是考古。而考古式的维护,效率极低,风险极高。这同样是“尚未被解决”的体现:我们还没有让系统在演进过程中自动保留可理解性的机制。
3. 现有方案为什么没能真正解决它
3.1 编程语言与范式的演进:改善了表达,没消除鸿沟
从汇编到 C,从 C 到 Java,从 Java 到 Python,再到各种函数式、声明式语言,编程语言的演进确实让表达越来越接近人的思维。但要注意,它改善的是“写”的体验,不是“翻译”的准确性。你用 Python 写一行filter,比用汇编写循环舒服多了,但filter背后的语义边界、异常处理、性能特征,依然需要人来判断。
换句话说,语言解决的是“怎么说”,没解决“说什么”。而编程的核心困难恰恰在“说什么”这一端。我见过用最时髦语言写出来的最难维护的系统,也见过用很朴素语言写出来的极其清晰的系统。语言是工具,工具再好,也替代不了对问题的理解。把编程的困难归因于语言不够高级,是一种常见的误判。
3.2 框架与抽象:把复杂度藏起来,不等于消除
框架的思路是“别重复造轮子”,把通用问题封装好,你只管业务逻辑。这个思路本身没错,但它有个副作用:复杂度被藏起来了,而不是被消除了。当你用框架顺风顺水时,一切都很美好;一旦遇到框架没覆盖的边界情况,你就得掀开盖子往里看,而这时候你面对的是一个你并不熟悉的、由别人设计的复杂系统。
更麻烦的是抽象泄漏。所有非平凡的抽象,在某个层面都会泄漏。你以为调个 API 就完事了,结果性能问题、并发问题、内存问题从底层渗上来,逼着你去理解那些被抽象掉的细节。这时候你会发现,抽象没有降低你需要的知识总量,只是改变了知识出现的时机。平时不学,出事现学,成本更高。
3.3 低代码与可视化:降低了门槛,抬高了天花板
低代码平台这几年很热,逻辑是让不懂编程的人也能搭应用。它确实能解决一类问题:表单、流程、简单 CRUD。但它的边界非常清晰,一旦业务逻辑复杂到需要自定义算法、需要精细控制性能、需要处理复杂状态,低代码就力不从心了。而且低代码平台往往有自己的锁定效应,你搭得越深,迁移成本越高。
我个人的观察是,低代码把编程的门槛降低了,但同时把能达到的上限也压低了。它解决的是“让更多人能编程”,没解决“让编程本身变得可靠”。对于简单场景它是好工具,对于复杂场景它只是把困难推迟了。把它当成万能解药,最后一定会失望。
3.4 AI 辅助编程:加速了写,没解决想
AI 辅助编程是当下最热的方向,自动补全、代码生成、甚至根据注释生成函数,确实能显著提升写代码的速度。但这里有个关键区分:AI 加速的是“把已经想清楚的东西写出来”,它没有解决“想清楚”这件事。而编程的困难,大头在想清楚,小头在写出来。
你给 AI 一个模糊的需求,它生成的代码看起来像模像样,但边界条件、异常路径、性能特征往往经不起推敲。因为它是在模仿代码的表面模式,不是在理解问题的实质约束。AI 能帮你写得更快,但不能帮你判断“这个设计对不对”“这个边界要不要处理”“这个取舍合不合理”。这些判断,依然是人来做的。所以 AI 让编程的“写”这一环变快了,但“想”这一环的困难原封不动。
4. 真实项目里,“尚未被解决”是怎么表现的
4.1 需求反复:不是需求方善变,是翻译没对齐
我参与过一个内部管理系统,需求文档写了三十页,评审开了四轮,大家都觉得清楚了。结果开发到第三周,产品经理看到第一个可交互版本,说“这不是我要的”。问题出在哪?文档里写“列表支持筛选”,但没写筛选是前端过滤还是后端查询,没写筛选条件能不能组合,没写筛选后分页怎么处理。这些空白在文档阶段看不出来,一旦变成可点的界面,分歧立刻暴露。
这类反复,表面看是需求变更,实质是翻译损耗在开发过程中的延迟暴露。越晚暴露,返工成本越高。我后来养成的习惯是,任何需求在动手前,先用最笨的方式画一遍数据流和状态变化,把隐含假设显式写出来,让需求方确认。这一步多花两小时,能省两天返工。
4.2 技术债累积:每一笔都是当时的最优解
技术债这个词被用烂了,但它的本质值得说清楚。技术债不是“程序员偷懒”,绝大多数技术债在产生的那一刻,都是当时约束下的最优解。 deadline 紧、人手少、需求不确定,你选择先跑通再优化,这是理性决策。问题在于,这些决策的上下文没有被记录下来,后来者只看到“这段代码很烂”,看不到“为什么当时要这么写”。
我维护过一个跑了四年的服务,里面有一段极其别扭的兼容逻辑。我花了半天才搞明白,是因为早期某个上游系统的字段格式不统一,当时为了快速上线做了特殊处理。后来上游系统早就升级了,这段逻辑成了纯粹的负担,但没人敢删,因为不知道删了会不会影响什么。这就是技术债的典型形态:不是代码本身有问题,是代码背后的决策上下文丢失了。
4.3 人员流动:知识随人走,系统留成谜
人员流动是常态,但很多团队没有应对知识流失的机制。一个核心开发者离职,带走的不只是他的代码能力,还有他对系统的大量隐性理解:为什么这个模块要这么拆分、为什么这个参数要设成这个值、为什么这个流程要绕一下。这些理解不在文档里,不在注释里,只在他脑子里。
我见过最极端的例子,是一个关键服务只有一个人能改,因为只有他知道那些看似随意的判断条件背后的业务规则。这个人一休假,相关需求全部卡住。这不是个人英雄主义,是系统设计没有把知识从个人转移到系统里。所谓“编程尚未被解决”,在团队层面就表现为:我们还没有一套可靠的方法,让系统的可理解性不依赖于特定个人的在场。
5. 面对“尚未被解决”,一线开发者能做什么
5.1 把隐含假设显式化,是最低成本的保险
既然翻译损耗是核心困难,那最直接的应对就是:在翻译过程中,主动把隐含假设写出来。具体怎么做?我常用的方法是“三问法”:这个功能的输入有哪些可能?输出有哪些可能?中间有哪些状态?每问一次,就逼自己把没说的东西说出来。比如“用户登录”,输入可能是手机号、邮箱、第三方凭证;输出可能是成功、失败、需要验证;中间状态可能是已锁定、已过期、首次登录。把这些列出来,和需求方逐条确认,能挡掉大量后期返工。
这个动作看起来笨,但极其有效。它的本质是把自然语言的模糊性,在早期就压缩成形式化的清单。你不需要写代码,只需要列清单,成本极低,收益极高。我带的项目里,坚持做这一步的,需求返工率明显低于不做的。
5.2 为决策留痕,而不是为代码留痕
传统注释是解释“代码在做什么”,但代码本身已经说明了它在做什么。真正有价值的是解释“为什么这么做”。所以我现在写注释,重点写决策理由:为什么选这个方案而不是那个、当时有什么约束、如果未来要改需要注意什么。这种注释,才是后来者真正需要的。
更进一步,我会在关键模块维护一个简短的决策日志,记录每次重大改动的背景、选项、取舍。不需要很正式,几行字就行。它的作用是让后来者能重建当时的上下文,而不是对着一堆代码猜。这个习惯坚持下来,团队里“这段代码能不能删”的讨论会少很多,因为答案往往就在日志里。
5.3 控制复杂度增长,主动做减法
复杂度是编程困难的放大器,所以控制复杂度就是直接对抗困难。控制的手段不是“写得更优雅”,而是“主动做减法”。具体来说,我会定期审视系统,问三个问题:这个模块还有用吗?这个抽象还有必要吗?这个依赖能去掉吗?很多复杂度是历史堆积的,没人主动清理,它就一直在那。
做减法最难的是心理关:删代码比写代码更需要勇气,因为你要承担删错的风险。但如果不做减法,系统只会越来越重,最后谁都动不了。我的经验是,删之前先确认没有调用方,再灰度观察一段时间,确认无影响再彻底移除。这个过程慢,但比让系统烂掉快。
5.4 把可理解性当成一等公民
很多团队把“能跑”当成唯一标准,能跑就行,看不懂无所谓。这是短视的。一个系统如果只有原作者能看懂,那它的生命周期就绑定在原作者身上,这是巨大的风险。所以我会把可理解性当成和功能、性能同等重要的指标。具体表现是:命名要达意、结构要清晰、关键路径要有说明、新人能在一周内理解核心流程。
怎么检验可理解性?最直接的办法是让一个没参与过的人来读,看他能不能讲清楚这个模块在干什么、为什么这么干。如果讲不清楚,说明可理解性不达标,需要补说明或重构。这个检验很残酷,但很有效。我见过太多“作者觉得很清楚、别人完全看不懂”的代码,问题就出在缺少这个检验环节。
6. 常见问题与排查技巧实录
6.1 需求理解偏差,怎么尽早发现
需求偏差最怕的是“开发完了才发现”。要尽早发现,核心是缩短反馈环。我的做法是,任何需求在动手前,先用伪代码或流程图把核心逻辑走一遍,拿给需求方看。注意,不是拿最终界面,是拿逻辑。因为界面容易让人关注样式,逻辑才能暴露理解偏差。如果需求方看到逻辑说“不对,这里应该是那样”,恭喜你,你在写代码前就发现了问题,省下的返工时间是以天计的。
还有一个技巧是“反向复述”:让需求方把你要做的事复述一遍。如果他的复述和你的理解一致,说明对齐了;如果不一致,立刻暴露分歧。这个动作只要五分钟,但能挡掉大量后期扯皮。
6.2 代码越写越乱,怎么判断该重构还是该重写
这是高频问题。我的判断标准是看“理解成本”和“修改成本”。如果理解成本高但修改成本低,说明结构乱但耦合低,可以逐步重构;如果理解成本和修改成本都高,说明耦合严重,重构可能比重写还慢,这时候要慎重考虑重写。但重写有个大坑:你以为重写能解决复杂度,其实只是把复杂度从旧系统转移到新系统,而且新系统还会引入新问题。
我的经验是,除非旧系统已经彻底无法维护,否则优先重构。重构的关键是“小步走”,每次只改一个点,改完立刻验证,确保系统始终可运行。重写则是一次性替换,风险集中,一旦出问题很难回退。所以能重构就别重写,这是用血换来的教训。
6.3 关键人员离职,知识怎么交接
知识交接不能等到离职那天才开始,那时候已经晚了。正确做法是平时就做“知识分散”:任何关键模块,至少两个人能改。怎么做到?靠代码评审和结对。代码评审不只是找 bug,更是知识传递的过程。评审的人通过读别人的代码,理解系统;被评审的人通过解释自己的代码,梳理思路。结对则是更直接的知识同步。
如果已经面临离职交接,我的建议是:不要试图交接所有细节,优先交接“决策上下文”和“高风险区域”。具体来说,让离职者讲清楚哪些地方容易出问题、哪些地方有历史包袱、哪些地方改的时候要特别小心。这些信息比代码本身更重要,因为代码可以读,上下文读不出来。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 应对建议 |
|---|---|---|---|
| 需求反复变更 | 隐含假设未对齐 | 检查需求文档是否有未定义的状态和边界 | 动手前用伪代码对齐逻辑 |
| 改一处崩多处 | 模块耦合过重 | 梳理调用关系,找隐藏依赖 | 小步重构,逐步解耦 |
| 代码没人敢动 | 决策上下文丢失 | 查注释和文档是否解释“为什么” | 补决策日志,标注风险区域 |
| 新人上手慢 | 可理解性不足 | 让新人复述核心流程 | 补关键路径说明,优化命名 |
| 技术债越积越多 | 缺少主动清理机制 | 定期审视无用模块和依赖 | 灰度删除,确认无影响再移除 |
7. 我对“编程尚未被解决”的真实体会
写了这么多,最后说点个人感受。我入行这些年,最大的认知转变是:编程的困难不在技术,在表达和理解的传递。技术会更新,语言会换代,但“把想法准确传递成可执行、可维护的指令”这件事,一直没变,也一直没被真正解决。AI 来了,它加速了写,但没解决想;低代码来了,它降低了门槛,但没解决复杂场景;新框架来了,它封装了通用问题,但没消除边界情况。
所以我的态度是:接受“尚未被解决”这个现实,然后在这个前提下,把能做的做好。把隐含假设显式化、为决策留痕、控制复杂度、把可理解性当回事,这些动作不炫酷,但实实在在能降低困难。我试过,也见过别人试过,有效。
如果你也在跟这些困难搏斗,我的建议是别追求“彻底解决”,追求“持续改善”。每次项目结束,问自己一句:这次有没有比上次更清楚一点?如果有,就是在进步。编程这件事,可能永远不会有“解决”的那一天,但我们可以让它一天比一天不那么难。这大概就是从业者能做的、也值得做的事。