夜雨聆风学习资料网

ARTICLE · 1029032

软件工程的第一性原理:从复杂度、变化与认知边界重新理解软件开发

软件工程的第一性原理:从复杂度、变化与认知边界重新理解软件开发

软件工程领域有大量方法论。

面向对象、函数式编程、领域驱动设计、微服务、Clean Architecture、SOLID、设计模式、敏捷开发、DevOps、TDD、CI/CD……

学习得越多,有时反而越容易产生一个问题:

为什么软件工程需要这么多原则、模式和工具?

这些东西之间究竟是什么关系?

如果有一天不再使用 Java、不再使用 C++,不再流行微服务,不再使用 Kubernetes,甚至大量代码都由 AI 自动生成,那么今天的软件工程知识还有多少仍然成立?

要回答这个问题,就必须越过具体语言、框架和工具,回到更加基础的问题:

软件工程究竟在解决什么问题?

这就是所谓的第一性原理思维。

所谓第一性原理,不是寻找一句听起来很深刻的口号,而是不断向下追问,直到找到那些无法再通过其他工程经验解释、却能够反过来解释大量工程现象的基本事实。

如果沿着这个方向推导,我认为软件工程最终可以归结为这样一个核心问题:

在有限的人类认知能力、有限资源和持续变化的需求之下,构造并长期维护一个具有足够正确性的复杂软件系统。

进一步压缩,可以得到两个关键词:

复杂度变化

而软件工程的大部分理论,本质上都是围绕这两个问题展开的。


一、先区分“编程”和“软件工程”

理解软件工程之前,首先必须把它与编程区分开。

编程解决的问题是:

如何让计算机执行某个任务?

例如:

defadd(a, b):return a + b

这段代码只需要考虑一个问题:

输入两个数执行加法返回结果

但是现实中的软件系统可能包含:

用户浏览器API Gateway身份认证业务服务消息系统数据库缓存搜索引擎对象存储监控系统

同时还存在:

多人开发多人修改版本演进需求变化线上故障并发访问网络失败权限问题数据迁移兼容性问题性能问题安全问题

这时候真正困难的已经不是:

“代码怎么写?”

而是:

“如何让一个复杂系统在多年演化之后仍然可以被人理解、修改、验证和运行?”

这就是软件工程出现的根本原因。

因此可以给出一个非常重要的判断:

编程主要解决计算问题,软件工程主要解决复杂度与演化问题。


二、第一条事实:软件本质上是在描述计算

所有软件最终都会落到一个非常朴素的过程:

输入状态计算状态变化输出

无论是操作系统、数据库、游戏引擎、机器人控制系统还是一个普通 Web 应用,本质上都逃不开这个结构。

例如电商系统:

用户提交订单读取商品和库存状态计算价格修改库存创建订单返回结果

机器人系统同样如此:

传感器数据环境状态感知算法规划算法控制算法执行器命令

因此从计算机本身的角度来看,软件没有那么神秘。

真正让软件变困难的,并不是 CPU 不知道如何执行程序。

而是:

人类无法轻松理解大型程序。

这引出了软件工程最重要的约束之一。


三、人类认知能力是软件工程真正的物理边界

计算机可以执行数十亿条指令。

但是一个程序员不可能同时理解数十亿条指令。

甚至一个由几十万个变量、函数和依赖关系构成的系统,人类也无法完整装进工作记忆。

这意味着大型软件系统存在一个不可回避的事实:

系统复杂度可以远远超过单个人类的认知能力。

于是软件工程就必须寻找一种方法:

巨大系统分解局部理解组合重新形成整体系统

这实际上解释了软件世界中大量概念为什么存在。

函数是为了什么?

为了把一段逻辑封装成:

输入 → 输出

调用者不需要理解内部实现。

模块是为了什么?

为了把大量函数组织成一个相对独立的功能单元。

类是为了什么?

为了把状态和行为组织到一个抽象边界中。

接口是为了什么?

为了让一个模块只暴露最小必要的信息。

服务是为了什么?

为了进一步建立系统级边界。

分层架构又是为了什么?

为了限制系统各部分之间可以发生的依赖。

它们看起来是完全不同的技术。

实际上都在做同一件事情:

降低理解一个系统所需要同时加载到大脑中的信息量。

这可以称为:认知复杂度压缩

它可能是理解软件工程最重要的概念之一。


四、软件工程的核心不是消灭复杂度,而是管理复杂度

很多工程原则都在谈“降低复杂度”。

但严格来说,这句话并不完全准确。

假设我们正在开发自动驾驶系统。

它本身就需要处理:

摄像头激光雷达定位地图目标检测目标跟踪路径规划运动规划车辆控制安全策略

这些复杂性不是程序员制造出来的。

它来自问题本身。

Fred Brooks 在软件工程领域提出过一个经典区分:

Essential Complexity本质复杂度Accidental Complexity偶然复杂度

本质复杂度来自问题本身。

例如银行系统天然需要:

账户交易余额结算风控权限审计

这些复杂性无法简单删除。

偶然复杂度则来自实现方式。

例如:

混乱的依赖重复代码不一致的数据结构隐式状态错误的抽象复杂的部署流程

软件工程真正应该做的是:

无法消除的本质复杂度        +尽可能减少的偶然复杂度

所以比“降低复杂度”更加准确的描述是:

软件工程的核心任务,是让复杂度保持在人类可控制的范围内。


五、复杂度真正危险的地方:关系数量增长得太快

假设系统只有三个模块:

ABC

模块之间可能存在:

A → BA → CB → C

还比较容易理解。

如果有十个模块,而且所有模块可以任意调用其他模块,潜在关系数量会快速增长。

如果有 (N) 个模块,理论上的两两关系数量约为:

也就是说,复杂度并不只是随模块数量线性增长。

真正危险的是:

模块之间的关系。

因此,一个系统拥有 100 个模块并不一定复杂。

如果它们结构清晰:

UIApplicationDomainInfrastructure

那么仍然可能很好理解。

但如果只有 20 个模块,却形成:

A → BA → HB → DD → AC → FF → BH → C...

系统可能已经非常难维护。

于是我们得到软件工程中的一个极其重要的结论:

大型系统的复杂度主要存在于关系,而不是节点。

这也解释了为什么“依赖管理”是软件架构的核心问题之一。


六、抽象的本质:压缩信息

为什么程序设计如此依赖抽象?

因为人脑不能处理无限信息。

假设你调用:

result = database.query(sql)

你并不需要同时理解:

TCP数据库协议连接池SQL ParserQuery PlannerB+ TreeBuffer PoolWAL磁盘驱动NVMePCIe

这些实现都被隐藏在:

database.query()

这个抽象后面。

因此抽象可以理解为:

把复杂实现压缩成一个认知符号。

这与数学非常类似。

例如:

这个符号可能代表极其复杂的计算。

但推理过程中,我们可以暂时把整个计算压缩成:

f

软件抽象也是一样。

复杂实现    ↓抽象    ↓简单接口

所以:

函数模块组件服务API协议

从本质上看都是一种:

信息压缩机制。


七、好的抽象为什么如此重要?

抽象并不是越多越好。

错误抽象甚至比没有抽象更加危险。

例如:

PaymentManagerUserManagerDataManagerSystemManagerCommonUtils

这些名字似乎进行了抽象,但它们实际上没有清晰表达任何边界。

真正好的抽象必须完成两件事情:

隐藏内部复杂度同时暴露稳定语义

例如:

FileSystem.open()Queue.push()Database.transaction()Robot.move_to()

调用者只需要理解行为的语义,而不是内部实现。

因此可以得到一个重要原则:

抽象的价值不在于减少代码,而在于减少理解代码所需要的信息。


八、封装的本质:控制信息传播

假设一个模块内部包含:

ABCDE

如果其他模块都可以直接访问:

ABCDE

那么模块内部任何变化都可能传播出去。

但如果只暴露:

Interface

系统就变成:

外部 ↓Interface ↓A B C D E

这时候内部实现可以自由变化,只要 Interface 不变。

所以封装真正解决的问题不是“把变量设成 private”。

而是:

限制变化和知识传播的范围。

从这个角度理解:

privatemodulepackagenamespaceAPIservice boundary

本质上都是在建立:

信息边界。


九、第二个根本事实:软件一定会变化

如果一个程序写完之后永远不修改,很多软件工程方法都没有存在的必要。

你甚至可以写一个 20 万行的:

main()

只要它:

运行正确永远不用改

理论上完全没有问题。

现实恰恰相反。

软件不断面对变化:

新需求Bug业务调整性能要求用户增长平台迁移安全漏洞组织变化人员变化依赖升级硬件变化

因此大型软件真正困难的事情不是:

把系统做出来。

而是:

让系统可以持续被修改。

这就是为什么“可维护性”如此重要。


十、软件架构真正管理的是变化

很多人第一次学习架构时,会认为架构就是:

MVC三层架构微服务DDD六边形架构Clean Architecture

但这些只是架构形式。

如果回到第一性原理,软件架构真正做的事情是:

设计变化边界。

假设:

UI业务逻辑数据库

如果业务逻辑大量直接依赖 SQL:

BusinessMySQL SQL

那么更换数据库时,变化可能传播整个系统。

如果建立:

BusinessRepository InterfaceMySQL Repository

那么数据库变化被限制在:

Infrastructure

这一层。

因此架构设计实际上是在回答:

哪些东西可能变化?

哪些东西应该一起变化?

哪些变化不应该影响其他部分?

我们如何建立边界阻止变化传播?

从这个角度看,架构可以定义为:

对变化传播路径的设计。


十一、耦合的本质:变化传播

软件工程经常强调:

低耦合高内聚

为什么?

因为耦合真正危险的地方不是“代码调用了另外一个模块”。

而是:

一个模块变化之后,另一个模块是否被迫变化。

假设:

A → B

如果 B 修改内部实现,A 完全不受影响,那么这种依赖并不危险。

如果 B 每次修改都导致:

A 修改C 修改D 修改E 修改

那么 B 就成为变化扩散中心。

可以把这种过程理解为:

局部变化依赖传播系统级修改回归测试未知副作用

优秀的软件设计希望实现:

局部变化局部影响

因此:

低耦合的真正含义,是降低变化传播范围。


十二、高内聚为什么重要?

高内聚意味着:

因为同一个原因变化的东西应该放在一起。

假设订单业务发生变化。

理想情况下修改:

Order Module

就够了。

如果订单逻辑分散在:

ControllerUtilsDatabasePaymentUserGlobalHelper

那么一次业务变化就必须跨越整个系统。

所以高内聚实际上是在做:

同一变化原因        ↓集中到一个边界

低耦合则是在做:

这个边界的变化        ↓不要向外扩散

二者结合就是:

高内聚=把变化聚集起来低耦合=阻止变化传播出去

这比简单背诵“高内聚低耦合”更接近它们存在的真正原因。


十三、SOLID 其实都在解决变化传播问题

如果继续向下推导,会发现很多经典设计原则都可以统一解释。

单一职责原则 SRP:

一个模块只有一个主要变化原因

否则不同方向的需求变化会不断修改同一模块。

开闭原则 OCP:

新增行为尽量通过扩展完成而不是修改大量已有代码

其本质仍然是在降低变化扩散。

依赖倒置原则 DIP:

高层策略抽象底层实现

目的就是避免高层业务逻辑绑定具体实现。

接口隔离原则 ISP,则是避免调用者依赖自己不需要的信息。

所以 SOLID 并不是五条互不相关的规则。

它们都可以视为一个更底层原则的不同表现:

让变化局部化。


十四、软件工程中的第三个事实:人一定会犯错

如果程序员永远不会犯错,也没有必要存在:

测试类型系统静态分析Code ReviewCI监控日志回滚

现实中:

需求理解会错设计会错代码会错配置会错部署会错依赖会错环境会错

甚至正确的程序也会因为:

网络异常磁盘损坏服务宕机内存不足并发竞争

而失败。

因此真正成熟的工程思想从来不是:

“如何保证程序员不犯错误。”

而是:

假设错误一定会发生,然后构造能够发现、隔离和恢复错误的系统。


十五、测试的本质:建立可验证性

为什么需要测试?

最浅层的回答是:

检查代码有没有 Bug。

但更深层的意义是:

建立对系统行为的机器可验证描述。

例如:

assert add(12) == 3

这实际上表达了一条系统约束:

当输入为 1 和 2系统输出必须为 3

因此测试不仅是在“找 Bug”。

测试还在建立:

系统行为可验证约束

当系统发生变化时:

修改代码执行测试验证旧约束是否仍然成立

这就是测试为什么会显著提高大型系统的可修改能力。

所以测试真正保护的是:

演化过程中系统行为的稳定性。


十六、类型系统的本质:把错误提前

假设一个函数:

defprocess(x):    ...

x 可以是什么?

理论上什么都可能。

于是错误可能直到运行时才暴露。

而:

fnprocess(user: User) -> Result<Order, Error>

通过类型系统提前表达:

输入必须是 User输出可能是 Order也可能是 Error

于是大量不合法状态在编译阶段就可以被排除。

这揭示了一个非常普遍的软件工程思想:

越晚发现错误修复成本越高

理想流程是:

设计阶段发现    ↓编译阶段发现    ↓测试阶段发现    ↓集成阶段发现    ↓生产环境发现

越靠上越好。

因此:

类型系统静态分析Lint编译器检查测试CICode Review

虽然形式完全不同,但本质上都在做一件事:

让错误尽可能早地暴露。


十七、可观测性的本质:降低未知状态

生产系统还有一个特殊问题。

当程序运行在:

服务器容器云平台机器人嵌入式设备

中时,开发者无法直接看到系统内部。

系统可能表现为:

请求很慢机器人停止运动订单没有创建CPU 突然升高

但是原因未知。

于是需要:

LogsMetricsTracingProfilingDebugging

这些工具的本质是:

把系统内部不可见状态转化为人类可观察信息。

因此可观测性并不是运维附属功能。

它实际上是在降低:

运行时认知复杂度。


十八、软件工程的第四个事实:大型软件是多人协作系统

单人项目和大型工程存在根本区别。

如果只有一个开发者:

所有上下文都在一个人的脑中

但如果有 100 个工程师:

100 个大脑+不同时间+不同经验+不同目标

那么软件工程就必须解决另一个问题:

如何让很多人能够在不理解整个系统的情况下共同开发同一个系统?

这也是模块化如此重要的原因。

理想的软件系统应该允许:

Team A → Module ATeam B → Module BTeam C → Module C

而不是:

任何团队修改任何地方

因此 Conway 定律之所以长期受到重视,是因为:

软件结构

和:

组织沟通结构

往往会相互影响。

软件架构不仅是机器运行结构。

它同时还是:

人类协作结构。


十九、接口实际上是一种协作契约

假设两个团队分别负责:

订单系统支付系统

如果订单团队必须理解支付系统所有内部实现才能调用它,团队就无法真正独立工作。

所以需要:

Payment API

例如:

POST /payments

接口背后的系统可能非常复杂。

但是订单团队只需要理解:

输入输出错误语义行为约束

于是:

内部实现知识

被压缩成:

接口契约

这就是为什么 API、IDL、Schema 和协议在大型系统中如此重要。

它们既是:

机器之间的边界

也是:

团队之间的边界

二十、为什么“简单”是软件工程中的高级能力?

工程师经常说:

Keep It Simple.

但什么叫简单?

代码少不一定简单。

例如一行:

result = reduce(lambda x,y: ..., map(...))

可能比十行清晰代码更难理解。

真正的软件简单性应该衡量:

理解系统需要建立多少心理模型。

一个设计如果需要你同时理解:

17 个类8 个设计模式6 层抽象4 个事件系统3 个生命周期

那么即使代码写得非常“高级”,认知成本仍然很高。

因此:

软件中的简单,不是代码数量少,而是认知模型少。

这也是为什么优秀工程师经常倾向于删除不必要的抽象。


二十一、YAGNI 的第一性解释

YAGNI:

You Aren't Gonna Need It.

它常被解释为:

不要提前开发没有需求的功能。

但从复杂度角度看,含义更加深刻。

每新增一个功能都会增加:

代码状态测试接口依赖文档部署维护

真正危险的还不是这些节点本身。

而是新的关系。

因此每增加一个功能,都可能增加未来系统的组合复杂度。

所以:

没有价值的功能并不是免费的。

即使它运行正确,也会长期占用系统的:

复杂度预算。


二十二、DRY 为什么不能被机械执行?

DRY:

Don't Repeat Yourself.

很多人将其简单理解为:

发现两段相似代码就抽象。

结果经常制造出大量错误抽象。

真正应该避免的是:

同一知识在系统中存在多个独立来源。

例如:

税率规则

如果分别写在五个模块里:

ABCDE

规则变化时就必须同时修改五处。

这才是 DRY 真正危险的问题:

一个变化原因多个修改点

因此 DRY 的本质仍然是:

减少变化传播和知识分裂。


二十三、状态为什么是软件复杂度的重要来源?

如果:

y = f(x)

那么只要知道 x,就能理解 y。

但如果:

y = f(x, state)

就必须知道系统当前状态。

如果 state 又依赖:

历史请求数据库缓存线程时间配置网络

问题就复杂得多。

所以状态越多:

可能状态空间

就越大。

假设系统有 10 个布尔状态:

如果有 100 个:

潜在状态空间已经是天文数字。

虽然现实系统并不会遍历所有组合,但它揭示了为什么:

全局变量共享可变状态复杂生命周期并发修改

特别容易产生 Bug。

因此减少共享状态、明确所有权、限制状态变化范围,是非常底层的工程原则。

Rust 的所有权系统为什么重要,也可以从这里理解:

它试图在语言层面对状态、资源和并发访问建立更强的约束。


二十四、分布式系统为什么如此困难?

单机系统至少共享:

CPU内存时钟进程状态

分布式系统把这些统一假设打破了。

服务之间通过网络通信,而网络可能:

延迟丢包重复乱序断开超时

不同机器甚至无法拥有完全一致的时间。

于是原本简单的:

A 调用 B

变成:

请求发送了吗?B 收到了吗?B 执行了吗?响应丢了吗?应该重试吗?重试会不会重复执行?

所以微服务不是单纯把代码拆成很多进程。

它实际上把:

函数调用复杂度

升级成了:

分布式系统复杂度

因此架构选择必须遵循一个原则:

复杂度必须有收益作为交换。

如果一个单体应用可以解决问题,就没有必要为了“架构先进”引入分布式系统。


二十五、性能优化的第一性原理

性能问题也可以从软件工程基本事实推导。

所有计算都需要消耗真实资源:

CPU内存磁盘网络时间能量

所以性能优化本质上是在减少:

计算量数据移动等待资源竞争

最终大部分性能优化都可以追溯到这些方向。

例如:

算法优化→ 减少计算量缓存→ 避免重复计算或慢速访问SIMD→ 提高单位时间计算量并发→ 利用多个执行资源批处理→ 降低固定开销零拷贝→ 减少数据移动

因此真正的性能工程不是“背优化技巧”。

而是研究:

资源究竟花在哪里?

这也是 profiling 比凭感觉优化更重要的原因。


二十六、软件工程最终是在做风险管理

进一步抽象,会发现工程决策几乎都在权衡风险。

例如选择成熟数据库:

降低技术风险

写测试:

降低修改风险

使用类型系统:

降低运行时错误风险

Code Review:

降低认知盲区风险

灰度发布:

降低上线风险

备份:

降低数据丢失风险

限流和熔断:

降低故障扩散风险

所以工程并不是追求:

绝对正确绝对性能绝对优雅

而是在现实约束下寻找:

成本时间复杂度性能可靠性可维护性

之间可以接受的平衡。

因此:

软件工程从来不是追求理论最优,而是在约束条件下进行风险优化。


二十七、为什么不存在“完美架构”?

因为架构永远服务于约束。

一个系统可能优先:

快速开发

另一个系统优先:

高可靠

另一个系统优先:

极低延迟

另一个系统则优先:

长期可扩展

例如:

创业公司的早期产品可能适合:

Monolith+PostgreSQL+Redis

而一个全球大型平台可能需要:

Microservices+Message Queue+Distributed Cache+Sharding+Multi-region

不是因为后者“更高级”。

而是因为约束不同。

所以一个成熟的架构问题不应该是:

“微服务是不是比单体先进?”

而应该是:

“当前系统的主要约束是什么?”


二十八、重新理解“可维护性”

所谓可维护性,其实并不是一个模糊概念。

它可以被拆解成一个非常具体的问题:

当需求发生变化时,我们需要理解多少代码、修改多少地方、承担多少未知风险?

优秀的软件通常具有这样的变化路径:

需求变化定位模块修改局部实现运行自动测试验证接口契约部署

糟糕的软件则可能是:

需求变化不知道改哪里搜索几十个文件修改多个模块担心隐藏依赖人工测试上线等待用户发现 Bug

这两种系统最根本的差距就是:

变化成本。

因此可以给软件质量一个非常实用的定义:

高质量软件,是能够以较低风险和较低认知成本持续变化的软件。


二十九、技术债务到底是什么?

技术债务经常被理解成:

代码写得不好

其实并不准确。

技术债务真正的问题是:

今天为了降低成本而做出的选择,提高了未来修改系统的成本。

例如:

复制代码硬编码绕过抽象缺少测试隐藏依赖临时接口

短期可能更快。

但之后每一次变化都需要支付额外成本。

因此技术债务与金融债务非常相似:

当前收益+未来利息

如果未来根本不会修改这段代码,那么某些“债务”甚至可能永远不需要偿还。

所以技术债务管理的关键不是:

“消灭所有不完美代码。”

而是:

识别哪些债务正在持续产生高额变化成本。


三十、AI 编程时代,这些原则会不会失效?

今天 AI 已经能够:

生成函数生成模块写测试写 SQL创建前端页面重构代码解释代码辅助设计架构

于是一个自然的问题出现:

如果写代码越来越便宜,软件工程还重要吗?

答案可能恰恰相反。

AI 降低的是:

代码生成成本

但它不会自动消除:

需求复杂度状态复杂度系统依赖架构约束历史包袱业务变化运行风险

甚至当代码生成成本接近于零之后,一个新的问题会出现:

生成代码越来越容易代码数量快速增长系统复杂度快速增长理解和维护更加困难

因此 AI 时代最稀缺的能力可能逐渐从:

如何写出代码

转向:

应该构造什么系统系统应该如何分解边界应该放在哪里哪些复杂度是不必要的哪些约束必须建立如何验证系统行为如何让系统持续演化

也就是说:

AI 会降低编码成本,却可能进一步提高复杂度管理的重要性。


三十一、软件工程师真正的核心能力

如果只从语言和框架角度定义程序员:

Java 工程师Python 工程师React 工程师C++ 工程师

那么技术变化确实很容易带来焦虑。

但如果从第一性原理看,优秀软件工程师真正解决的问题是:

理解问题建立模型拆分复杂度设计边界定义接口约束状态管理依赖验证行为观察系统控制变化

编程语言只是完成这些工作的工具。

这也是为什么一个真正优秀的软件工程师换一种语言之后,通常仍然能够很快恢复生产力。

因为他的核心能力并不储存在:

API 记忆

里。

而储存在:

系统模型工程判断复杂度意识边界意识风险意识

之中。


三十二、把所有软件工程原则统一起来

现在我们可以重新审视大量熟悉的软件工程概念。

工程概念
更底层的目的
函数
压缩局部逻辑
模块
建立认知边界
封装
限制信息传播
接口
建立稳定契约
抽象
压缩复杂实现
高内聚
聚集同一变化原因
低耦合
限制变化传播
分层
限制依赖方向
架构
组织复杂度和变化边界
类型系统
提前排除非法状态
测试
建立可验证约束
CI
自动执行工程约束
Code Review
降低认知盲区
日志
暴露运行状态
Metrics
量化系统行为
Tracing
恢复跨模块因果链
DDD
对齐软件模型和问题模型
微服务
建立独立部署与组织边界
DevOps
缩短开发到生产反馈循环
重构
降低未来变化成本

看起来几十种完全不同的技术,最终实际上集中到了极少数根本目标:

控制复杂度限制变化传播降低认知成本降低错误概率提高可验证性提高反馈速度

三十三、一个更完整的软件工程第一性原理模型

如果要构造一个统一模型,可以把软件工程理解成下面这条链:

现实问题    ↓需求    ↓领域模型    ↓软件模型    ↓模块    ↓接口    ↓实现    ↓运行系统    ↓反馈    ↓新的需求

但是整个过程中受到四种基本约束:

认知有限资源有限错误不可避免变化持续发生

于是软件工程的方法就是:

通过抽象压缩复杂度通过模块化拆分复杂度通过封装隐藏复杂度通过接口稳定边界通过架构控制依赖通过高内聚聚集变化通过低耦合隔离变化通过类型和测试约束错误通过监控暴露未知状态通过自动化减少人工操作通过反馈持续修正系统

这样,大量看似分散的软件工程知识就获得了统一解释。


三十四、最终可以浓缩成四条第一性原理

如果一定要把整篇文章压缩到最少,我会保留四条。

第一条:

人的认知能力有限。

因此需要抽象、模块化、封装、分层和架构。

第二条:

软件复杂度主要来自状态、依赖和关系。

因此需要控制状态、依赖方向和模块关系。

第三条:

软件必然持续变化。

因此设计的重点不是“现在能运行”,而是“未来能否低成本修改”。

第四条:

错误不可避免。

因此系统必须能够发现错误、限制错误、观察错误并从错误中恢复。

这四条事实共同决定了软件工程的大部分方法。


三十五、如果只保留一条原则

如果继续压缩,我认为软件工程最值得记住的一句话是:

让复杂度被分解,让变化被隔离,让错误被尽早发现,让系统始终保持在人类可以理解和修改的范围内。

而如果还要继续压缩:

软件工程的本质,是管理复杂度。

但这里的“复杂度”并不仅仅是代码复杂度。

它包括:

问题复杂度状态复杂度依赖复杂度变化复杂度协作复杂度运行复杂度认知复杂度

真正优秀的软件工程,并不是构造最复杂、最先进的技术体系。

恰恰相反。

它是在一个本来极其复杂的问题中,不断寻找:

更少的概念更清晰的边界更稳定的接口更短的依赖链更局部的变化更快速的反馈

最终让一个原本超过单个人认知能力的巨大系统,仍然能够被一群有限的人类持续理解、修改和演化。

这可能就是软件工程真正存在的原因。


结语:代码不是软件工程最终的产品

程序员每天看到的是:

代码

所以很容易产生错觉:

软件工程的核心就是把代码写好。

但代码其实只是软件系统的一种表达形式。

真正的软件工程关注的是:

知识如何被表达复杂度如何被组织状态如何被约束模块如何被分解依赖如何被控制变化如何被隔离行为如何被验证系统如何被观察团队如何进行协作系统如何长期演化

因此,一个软件工程师能力不断提高的过程,往往会经历这样的变化:

初级工程师关注:代码怎么写中级工程师关注:模块怎么设计高级工程师关注:系统如何组织架构师关注:依赖和变化如何管理技术负责人关注:技术系统、业务系统和组织系统如何共同演化

越往上走,真正重要的就越不是“会多少 API”。

而是能不能看见:

复杂度从哪里产生,又应该在哪里被截断。

当你开始用这个视角重新审视函数、类、模块、微服务、DDD、SOLID、测试、CI/CD、DevOps,甚至 AI 编程时,会发现它们不再是一堆孤立的知识点。

它们只是在不同尺度上,回答着同一个古老的问题:

面对一个不断变化并且越来越复杂的系统,我们怎样才能始终保持对它的控制?

这就是软件工程。

相关学习资料

返回首页浏览学习资料