当我们测量一个房间的周长,从脱口而出,到肉眼估算,到用不同长度的直尺📏去量,得到的数值应该越来越收敛,逐步逼近真实的周长。这样的方法和结论可以同理扩展到衡量一个岛屿的周长么? 1961 年,英国学者刘易斯·理查森在注意到一问题并发表了论文,但并没引起多少注意。

1967年,数学家 曼德勃罗(B.B.Mandelbrot)在美国权威的《科学》杂志上发表了影响力更大的学术论文:《英国的海岸线有多长?统计自相似和分数维度》,得出的结论是:英国的海岸线长度是无法精确测量的!
为什么?因为真实世界中的海岸线不是一个光滑的几何体。尺子越小,量出的曲折就越多,累积的读数就越大。在大于原子和分子的可衡量尺度内,总长度并不会收敛,而是持续增加。
曼德勃罗发现,这类图形有一种奇异特性:自相似性。无论是在卫星图、地图还是脚下的岩滩上,海岸线的曲折形态在统计上都是相似的。放大任何一处,复杂度都不会降低。
他称这种图形为分形(fractal)。它的维度不再是整数,而是一个介于1和2之间的分数。比如英国西海岸大约是1.25维。正是这个分数维度,让长度成了随测量尺度变化的量,永远无法精确确定。
分形在自然界中随处可见——山脉、河流、血管、闪电——因为它们都是自然力量在不同尺度上反复迭代的产物,而非人为设计的光滑几何体。
这就是为什么,用测量房间的方法去测量海岸线,你面对的不是一个更庞大和复杂的房间,而是完全不同维度的事物。
《构建之法》中的软件的估算章节中也举了一个类似的例子:
一组学生决定要徒步绕行全国边界,大家估算一下,要多长时间?
很多软件项目就是这样雄心勃勃地开始的,大家觉得当年某某牛人在简陋的环境下也做出来世界级的软件,现在还有互联网,还有人精通设计模式,AI 也能写很多的代码了,我们应该能很快完成!
人物介绍:果冻:读书认真的初学者;小飞:直率,喜欢动手的工程师阿超:经历多种软件工程大坑的架构师
----
果冻推了推眼镜,指着白板上的估算表格:“经过刚才的系统二讨论,我们形成了不确定性圆锥的收敛,达成了共识——全国陆地边界长约 2.2 万公里。那剩下的不就是 WBS(工作分解结构)了吗?分而治之,把系统拆成子系统,子系统拆成模块,模块拆成类,最后落实到每个人每天在跑步机上跑多少步,乘以天数,完工!任务切到足够小,盲区不就没了吗?”
阿超靠在椅背上,揉了揉眉心,猛的一摆手,像是要挥走以前项目延期的噩梦。他扯了扯嘴角:“果冻,你以为 WBS 是一把切蛋糕的刀,切到最后能得到一块块实心的方块。但在软件工程里,WBS 切开的不是蛋糕,是海岸线。”
“海岸线?”果冻翻了翻手里的笔记本。
小飞噌地站起来,抓起记号笔在白板上画了一个树状图:“你看你的 WBS。你把‘用户登录’拆成前端页面、后端接口、数据库查询。你以为到底了?”

没等果冻搭话,小飞笔尖一转,在“前端页面”下又拉出一条线,画出一个新的树状图:“你拿着放大镜往‘前端页面’里看——里面有表单验证、状态管理、网络超时重试。你再往‘表单验证’里看——里面有正则匹配、防抖节流、错误提示国际化。”
小飞扔下笔,双手撑在白板上盯住果冻:“这叫什么?这叫自相似性(Self-similarity)!曼德勃罗在海岸线上看到的就是这个 -- 你拿卫星看,它是弯曲的;你站到悬崖边看,它还是那么弯曲;你趴在沙滩上拿放大镜看石头边缘,它的曲折度竟然跟卫星图上一模一样!”
阿超端起保温杯抿了一口,将杯子重重磕在桌面上:“这正是尺度的分形在 WBS 上的体现。你以为把大任务拆成小模块就收敛了?不,你每剥开一个所谓的‘小模块’,里面的工作结构和整个大项目是完全自相似的。它里面照样有需求扯皮、照样有架构设计、照样有脏数据清洗、照样有测试环境崩溃。你放大任何一个微观任务,它都包含了整个软件生命周期,是一个微型宇宙。细节之下,永远有细节,永不收敛。”

果冻挺直了脖子:“那这跟跑步机有什么关系?就算模块内部也是个小宇宙,总能分解到单人头上吧?”
阿超身体前倾,双手撑住桌面:“跑步机的隐喻,就是你把边界当成了直线,你觉得大家是可以独立接力的。果冻,你还记得 2016 年 npm 那个 left-pad 事件吗?”
果冻停下转笔的动作:“记得,一个只有 11 行代码的包,作者从 npm 下架了,结果全球几万个前端项目当场构建失败。”
2016年 npm left-pad 事件
- 事件描述:
2016年3月,开源开发者 Azer Koçulu 因与 Kik 公司的商标命名纠纷,决定将自己名下的所有模块从 npm 官方仓库中下架。其中包括一个仅有 11 行代码、功能为“在字符串左侧填充字符”的微型包 left-pad。- 事件影响:
尽管该模块极其简单,但它已被 Babel、React 等主流前端工具和数以万计的开源项目深度递归依赖。 left-pad的突然消失在瞬间引爆了全球范围内的构建链雪崩,导致无数科技企业和开发者的自动化部署当场中断,前端社区陷入短暂瘫痪。此事件彻底暴露出现代软件工程中“微依赖(micro-dependency)”网络交织的脆弱性,并最终迫使 npm 紧急修改了规则,禁止开发者随意下架已被广泛依赖的线上包。
“对,区区 11 行代码。”阿超敲击着桌面,“在跑步机上,一个人少跑了 11 步,后面的人多跑 11 步就补上了。但在软件依赖网络里,这 11 行代码不是缺在跑步机上,而是缺在所有人的心脏里!React 依赖它,Babel 依赖它,成千上万的项目在构建链的深处都指向同一个节点。它一消失,整个社区瘫痪。”
小飞两手在空中比划着网络拓扑的形状:“这叫依赖的分形。在跑步机上,步数是标量;在软件工程里,任务是带有向图的矢量。一个底层节点消失,所有指向它的连线断裂。局部故障无法隔离,它会沿着依赖树引发全局雪崩。用你徒步的比喻来说——大兴安岭的一块处塌方,让我们的步伐少了 11 步,这直接让你在杭州湾寸步难行,因为你要马上回到大兴安岭去修补!”
果冻愣住了,但他立刻翻回笔记本的第一页:“那也不对啊!面向对象编程不就是为了解决这个问题吗?把复杂的东西封装成黑盒,对外暴露接口,不就把细节屏蔽掉了吗?”
“CPU 压根不认你的抽象!”小飞单手劈向桌面。
阿超仰起头长吁一口气,指关节敲着桌子边缘:“果冻,抽象只是你们这些写上层逻辑的人的认知遮蔽,它从来没有在物理上消除过复杂性。你调了一个登录函数,以为只是一行代码。但在运行时,每一行被你藏起来的代码都在 CPU 里逐条执行。更要命的是第三方库。你引入一个图像处理库,以为是个黑盒。但它底层的 C 语言里藏着一个缓冲区溢出漏洞。黑客传一张恶意图片,数据溢出,覆盖了栈上的返回地址,系统沦陷。这行漏洞代码不是你写的,你甚至不知道它存在。你用抽象把它包起来了,但 CPU 执行的时候,它执行了所有指令,包括被破坏的指令栈上的随机指令。”
“这就是执行的分形。”小飞两手一摊,“抽象压缩了你的脑容量,但从未压缩过机器的计算。所有被你隐藏的复杂性,在代码跑起来的那一刻,都会复现。”
阿超站起身,在白板上的三条分形旁边划上重重的等号,马克笔快没有墨了,他反复画了多次:“尺度的分形,让 WBS 在拆解中展现出自相似的膨胀;依赖的分形,让局部故障全局化;执行的分形,让所有隐藏的风险在运行时引爆。这三者叠加,才是软件开发的真实地貌。你手里拿的是地图,脚下踩的却是海边的烂泥,东北的雪地,西南的草丛。”
... ...
会议室里没人说话。果冻盯着白板,喉结上下滚了滚:“所以,用 WBS 和跑步机去估算,唯一的产出物……就是一个用来忽悠老板和安慰自己的数字?”
阿超点了点头,把手里没有墨的马克笔抛进垃圾篓。
果冻双手搓了搓脸,再次抬头:“等一下。我们程序员有度量工具啊!比如圈复杂度(Cyclomatic Complexity)!用代码的分支数量来量化工作量,这总行了吧?”
小飞扯开嘴角哼了一声:“圈复杂度?那玩意儿是个好探针,但它只测出了第一重分形。”
“什么意思?”
小飞抓起黑笔,在白板上画出一个个交叉的路口:“圈复杂度本质上就是数一个函数里有多少个 if-else 和循环。一个圈复杂度为 50 的函数,里面就是 50 条路径交织的迷宫。这确实能帮你发现微观细节,也就是‘尺度的分形’。”
“那不就够了吗?”果冻反问。
阿超一把合上果冻的笔记本:“圈复杂度只能看见一个节点内部的烂摊子,但它看不见节点之间的网。刚才那个 left-pad,圈复杂度是多少?”
果冻卡壳了:“呃……它只有 11 行,没有几个 if,圈复杂度是个位数……”
“一路绿灯通过。”阿超撇了撇嘴,“但它的消失让全球断网。圈复杂度看不见几万个项目指向它的连线,这些静态和动态的连线体现了软件模块之间及其依赖关系”。
小飞紧接着补刀:“而且圈复杂度是静态扫描!你调了一个圈复杂度为 1 的 saveToDB(),绿灯通过。但底层数据库驱动里有个内存泄漏,或者是刚才超哥说的缓冲区溢出。代码一跑起来,服务器宕机。圈复杂度对‘执行的分形’一无所知。”
果冻瘫靠回椅背:“所以,圈复杂度只管这座山头平不平,不管它是不是跟地震带连着,也不管地下有没有活火山……”
小飞打了个响指。
几秒钟后,果冻突然坐直身体,双手拍在桌面上:“等一下!我好像抓到了一个更底层的逻辑。”
“不管是房间的墙壁还是海岸线,它们本身都是存在的。区别在于我们测量的尺度!”
果冻在半空中比划着步幅,“房间墙壁的粗糙度在微米级,我一步 0.7 米跨过去,那些微米级的粗糙被我直接踩在脚下,根本影响不了我。几十步我就能量完房间。但海岸线的曲折在米级到公里级,这恰好是人类步行的尺度!我必须绕开礁石、越过沟壑。不是海岸线变了,是我和它的相对尺度决定了分形是否生效!而且,我是赤脚走路,草丛里的小刺小虫会让我行走效率大大下降,甚至停止。”
阿超扬起眉毛,向果冻比了个大拇指。
果冻猛地站起身:“如果我开一辆越野车去绕海岸线呢?小礁石,人要绕过去,越野车一脚油门直接压过去!什么小刺小虫子,越野车的庞大尺度,把小尺度上的分形给抹掉了!”
“完全正确。”小飞拍了拍手,“越野车的轨迹比人走的轨迹平滑了至少一个数量级。”
果冻停下脚步,转过身:“那对应到软件里……我们调用一个函数,和 CPU 执行这个函数,也是两种相对尺度?”
“这就是软件工程最残酷的核心。”阿超拉开椅子坐下,“果冻,当你写下 auth.login()时,你的大脑就是那辆越野车,或者是搞规划的领导看沙盘。你用一个概念,一步跨过了几千行底层的泥泞。人脑擅长这种认知压缩。”
小飞收起脸上的笑意,身子往前一探:“但是,CPU 没有越野车的高级轮胎!不管你封装了多少层,CPU 都没有能力‘跨过去’。它只能在硬件层面——高低电平、时钟周期、寄存器状态——去走完那几百万个步子,而且是赤脚走过,每一个刺都不会饶恕 CPU 的赤脚。”

“这也正是为什么,软件工程里永远没有银弹(No Silver Bullet)。”
果冻抬起头:“没有银弹?”
“对。从上世纪开始,无数人宣称自己找到了杀死软件复杂性怪兽的银弹——新的面向对象语言、敏捷开发、低代码框架,乃至现在的 AI 辅助编程。这些东西有用吗?有。但它们充其量,只是给人类的大脑造了一辆更大、更先进的认知越野车,让我们能更快地在宏观地图上勾勒路线,就算现在的 AI 工具能帮你一键生成很多程序,它充其量也就是给这辆越野车换上了 100 倍马力的发动机,同时还附带了不少幻觉。”
“但是,它们依然改变不了底层海岸线的分形本质,也代替不了机器必须去踩过每一个水坑的物理现实。”
阿超站直了身体,双手抱胸:“所以,软件估算为什么不准?因为估算的时候,是人脑在用认知尺度丈量地图;但项目交付的时候,是客户端、网络节点和服务器的各种 CPU 在用微观的执行尺度一步步走完所有地貌。抽象,只压缩了人类的理解,从未压缩过机器的执行。这两种尺度之间无法弥合的落差,就是软件工程这一行的宿命,当然,这也正是我们人类工程师的价值所在——永远要有人去面对那些泥泞的真实地形。”
附录:白板上的代码示例:
小飞在白板上顺手写下了这段对比极其强烈的 JavaScript 代码。它非常适合作为阿超反驳果冻时的视觉道具,用来揭示为什么圈复杂度只能看到“第一重分形(尺度)”,而对网状的依赖和底层的执行完全无能为力。
// =================================================================// 场景 A:圈复杂度 = 1// 【阿超:一条直线走到底。内部没有迷宫,但它是一切灾难的潜伏地。】// =================================================================functionsaveToDB(userData) {// 静态扫描看起来完美:0个if,0个loop,圈复杂度 = 1 db.rawInsert(userData); returntrue;}// =================================================================// 场景 B:圈复杂度 = 5// 【小飞:这就是微观迷宫。你放大看,第一重“尺度的分形”开始显现。】// =================================================================functionauthLogin(request) {let user = request.user;if (!user) { // +1 分支路径return"Invalid Request"; }if (user.isBanned) { // +1 分支路径return"User Banned"; }if (user.age < 18) { // +1 分支路径if (!user.hasParentalConsent) { // +1 分支路径return"Need Consent"; } }returnsaveToDB(user); // 调用场景A的函数(圈复杂度无法透视此处依赖)}这段代码呼应了对话中的三个核心痛点:
- 尺度的分形(WBS的漏洞):
authLogin内部有 4 个if条件,加上主逻辑,圈复杂度为 5。它就像海岸线上的一个小湾汊。你以为它很简单,但如果把user.isBanned再拆开,里面可能又是一层自相似的权限校验迷宫(需要查缓存、查角色、判断过期)。 - 依赖的分形(工具的盲区):
authLogin的最后一行调用了saveToDB(user)。在 SonarQube 等静态工具眼中,这两者是独立的。saveToDB的圈复杂度是完美的 1。但工具看不见那根致命的“连线”——如果saveToDB被下架(类似 left-pad),或者底层数据库挂了,圈复杂度为 5 的顶级函数和圈复杂度为 1 的底层函数会同时崩溃。 - 执行的分形(抽象的幻象):
即使 saveToDB只有一行代码,圈复杂度为 1,但当 CPU 执行db.rawInsert时,它会剥开高级语言的封装,展开成底层驱动中几万条涉及内存分配、字节对齐、内核态切换的机器指令。如果有漏洞,就在这几万步的执行尺度里被引爆。
夜雨聆风