1. 什么是编程范式
1.1 定义
编程范式是构建程序结构与组织计算的思维方式,它规定了「如何分解问题、如何组合部件、如何表达状态与行为」。范式不是语法特性,而是世界观:
- 命令式问:怎么做(How)——一步步告诉机器如何改变状态。
- 声明式问:要什么(What)——描述目标,让运行时决定如何达成。
1.2 主要谱系
编程范式
┌──────────┼───────────┐
命令式 声明式 函数式
(状态+步骤)(描述结果)(函数组合)
│ │ │
结构化/过程 逻辑式/DSL 纯函数/不可变
│
面向对象(把状态与行为封装成对象)
现实中几乎没有「纯粹」的语言,主流语言都是多范式的混合体。
2. 命令式与结构化编程
2.1 命令式的核心
程序 = 一系列改变机器状态的指令。变量是可变的内存槽,赋值是最基本的操作,控制流(顺序、分支、循环)决定执行路径。
// 命令式求数组和:显式维护累加器与索引
int sum(int *a, int n) {
int s = 0;
for (int i = 0; i < n; i++) {
s += a[i];
}
return s;
}
贴近硬件、性能可控,是系统编程的默认范式。代价是可变状态多,随程序规模增长容易失控。
2.2 结构化编程的贡献
Dijkstra 提出结构化编程,用顺序、选择、循环三种结构替代任意 goto,使程序具备单入口单出口特性,从而可被推理与验证。这是工程上「可读性 > 技巧」的分水岭。
2.3 常见陷阱
- 深嵌套的条件与循环(圈复杂度爆炸)。
- 共享可变状态在多线程下引发竞态。
- 副作用散落各处,难以测试与并行化。
3. 面向对象编程
3.1 三大支柱
- 封装:把数据与操作数据的方法绑定,隐藏内部实现,只暴露稳定接口。
- 继承:复用与特化,子类继承父类的属性与行为。
- 多态:同一接口在不同类型上有不同实现,调用方无需关心具体类型。
interface Shape { double area(); }
class Circle implements Shape {
private final double r;
Circle(double r) { this.r = r; }
public double area() { return Math.PI * r * r; }
}
class Rect implements Shape {
private final double w, h;
Rect(double w, double h) { this.w = w; this.h = h; }
public double area() { return w * h; }
}
double total(Shape[] shapes) {
double s = 0;
for (Shape sh : shapes) s += sh.area(); // 多态分发
return s;
}
3.2 组合优于继承
继承是最强的耦合:父类改动会波及所有子类,多层继承还带来脆弱基类问题。工程共识:
- 优先组合:通过持有接口实例复用行为,运行时可变。
- 面向接口而非实现:依赖抽象,便于替换与测试。
- 里氏替换原则:子类必须能替换父类而不破坏语义。
3.3 SOLID 原则速览
| 原则 | 一句话 |
|---|---|
| 单一职责 SRP | 一个类只有一个变化的理由 |
| 开闭原则 OCP | 对扩展开放,对修改关闭 |
| 里氏替换 LSP | 子类可替换父类 |
| 接口隔离 ISP | 不强迫依赖不需要的接口 |
| 依赖倒置 DIP | 依赖抽象而非具体实现 |
4. 函数式编程
4.1 核心思想
- 纯函数:相同输入必得相同输出,且无副作用(不改外部状态、不做 IO)。
- 不可变性:数据一旦创建就不修改,变更通过产生新值表达。
- 一等函数:函数可作为参数传递、作为返回值返回。
- 函数组合:用
compose、pipe把小函数拼成复杂逻辑。
from functools import reduce
# 函数式风格:map / filter / reduce,无显式循环与累加器
def total(nums):
return reduce(lambda a, b: a + b,
map(lambda x: x * x, filter(lambda x: x % 2 == 0, nums)),
0)
4.2 不可变的价值
- 无数据竞争:不可变对象天然线程安全,可自由共享。
- 易于推理:值不会在你看不见的地方被改掉。
- 可缓存与可回放:纯函数结果可 memoize,状态可时间旅行调试。
4.3 副作用如何安放
纯函数不能做 IO,但程序必须读写世界。函数式语言的做法是把副作用显式化:
- Monad / IO 类型:把副作用描述成值,由运行时在边界执行。
- 函数式核心 + 命令式外壳:业务逻辑保持纯,只在边缘做 IO 编排。
4.4 常见函数式操作
// map 变换、filter 筛选、reduce 聚合
const nums = [1, 2, 3, 4, 5];
const result = nums
.filter(n => n % 2 === 0)
.map(n => n * n)
.reduce((a, b) => a + b, 0); // 20
注意:惰性求值(Haskell)可以把无限序列当作有限结构处理,take(5, repeat(1)) 之类表达力极强。
5. 声明式与逻辑式编程
5.1 声明式的本质
声明式只描述目标状态与约束,不描述执行步骤。典型代表:
- SQL:
SELECT name FROM users WHERE age > 18——只说要什么,优化器决定怎么扫。 - HTML/CSS:描述结构与样式,浏览器负责布局渲染。
- React JSX:
UI = f(state),声明 UI 与状态的关系。 - 构建工具:Makefile、Terraform 声明目标状态,工具计算差异。
-- 声明式:不关心索引、连接顺序、是否并行
SELECT u.name, COUNT(o.id) AS orders
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.name
HAVING COUNT(o.id) > 5;
5.2 逻辑式编程
Prolog 为代表,程序由事实与规则构成,通过合一(unification)与回溯求解:
parent(alice, bob).
parent(bob, carol).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
% 查询 ancestor(alice, carol) 会自动回溯搜索
逻辑式适合规则引擎、类型推导、约束求解等场景。
5.3 声明式 vs 命令式的取舍
| 维度 | 命令式 | 声明式 |
|---|---|---|
| 表达重心 | 步骤 | 结果 |
| 可读性 | 复杂逻辑更直观 | 意图更清晰 |
| 优化空间 | 依赖程序员 | 运行时/编译器可优化 |
| 调试 | 逐行可控 | 黑盒,难定位 |
| 性能控制 | 精细 | 受限于实现 |
6. 类型系统
6.1 静态 vs 动态
| 维度 | 静态类型 | 动态类型 |
|---|---|---|
| 检查时机 | 编译期 | 运行期 |
| 代表 | Java、Go、Rust、TypeScript | Python、Ruby、JS |
| 优势 | 早发现错误、IDE 支持、性能优化 | 灵活、开发快、少样板 |
| 代价 | 需写类型、编译期约束 | 运行时类型错误、重构难 |
渐进类型(Python 的 type hints、TypeScript)试图兼得:默认动态,关键路径加注解。
6.2 强类型 vs 弱类型
强类型不允许隐式转换("1" + 1 报错),弱类型允许(JS 得 "11")。强类型 + 静态检查是现代大型项目的稳妥选择。
6.3 类型推导
let x = 5; // 推导为 i32
let v = vec![1, 2, 3]; // 推导为 Vec<i32>
let f = |a: i32| a + 1; // 闭包类型自动推导
Hindley-Milner 类型系统能在不写类型注解的情况下推导出最一般类型,兼顾安全与简洁(Haskell、ML 家族)。
6.4 代数和类型与模式匹配
- 积类型(Product):
struct、元组,字段同时存在。 - 和类型(Sum):
enum,多个变体互斥。
// 和类型 + 模式匹配,编译期强制处理所有分支
enum Shape {
Circle(f64),
Rect(f64, f64),
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect(w, h) => w * h,
}
}
用类型表达「不可能的状态」(如 Option 替代 null、Result 替代异常码)能消灭大量运行时错误,是类型系统最实用的价值。
6.5 泛型与多态
- 参数化多态:泛型,
List<T>对任意 T 都成立。 - 特设多态:重载、类型类(Haskell 的 typeclass、Rust 的 trait)。
- 子类型多态:继承体系下的替换。
7. 范式融合与多范式语言
7.1 现代语言都在融合
- Java 8+:引入 Lambda 与 Stream,吸收函数式。
- Python:支持 OOP、函数式、过程式,可混用。
- JavaScript:原型 OOP + 一等函数 + 声明式 React。
- Rust:命令式 + 函数式迭代器 + 代数和类型 + trait 泛型。
- Scala / Kotlin:OOP 与 FP 深度统一。
7.2 融合的动机
不同问题适合不同范式:状态机适合命令式,数据变换适合函数式,UI 适合声明式,领域建模适合 OOP + 类型系统。强行用一种范式解决所有问题往往事倍功半。
8. 如何选择范式
8.1 决策清单
- 性能敏感、贴近硬件 → 命令式(C/C++、Rust 的 unsafe 部分)。
- 并发与并行密集 → 函数式 + 不可变,规避数据竞争。
- 复杂业务领域建模 → OOP + 强类型 + 代数和类型。
- 数据查询与转换 → 声明式(SQL、DataFrame)。
- UI 与配置 → 声明式,让框架处理渲染与协调。
8.2 实践建议
- 团队熟悉度优先:范式的收益要扣掉学习成本。
- 渐进采用:先在局部引入(如用不可变值对象),而非全盘重写。
- 以可测试性为标尺:纯函数、依赖注入、接口隔离都能显著提升可测性。
- 警惕教条主义:函数式不等于不用循环,OOP 不等于到处继承,声明式不等于放弃性能调优。
9. 常见陷阱
- 把继承当复用银弹:深继承树脆弱,优先组合。
- 误以为函数式就是不用循环:核心是纯函数与不可变,不是语法糖。
- 忽视可变状态的共享:多线程下共享可变对象是 bug 温床。
- 类型注解写成摆设:
Any满天飞等于没有静态检查。 - 用 null 表示缺失:应使用
Option/Optional显式建模。 - 声明式黑盒过度优化:SQL 慢查询、React 重渲染都需理解执行模型才能调优。
- 范式混用无边界:一个文件里 OOP、FP、过程式风格混杂,可读性崩塌,应约定分层边界。
参考文章
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。