大话编程思想:OOP、AOP、FP 和 FRP 到底在想什么
城南有家“有间客栈”,掌柜想把纸账本换成一套程序。
他找来四位程序员:欧阳公子擅长 OOP,阿婆擅长 AOP,方先生信奉 FP,风姑娘研究 FRP。四个人看的是同一家客栈,却各自盯着不同的问题。
掌柜问:“你们谁的武功最高?”
四个人都摇头:“我们不是四套互相取代的武功,而是四种看问题的角度。”
OOP:先看江湖里有哪些人和东西
欧阳公子在大堂转了一圈,说:“这里有客人、菜品、订单和账单。每样东西都有自己的状态,也有自己能做的事。”
这就是 OOP(Object-Oriented Programming,面向对象编程)的基本视角:把程序看成一群对象,让数据和操作数据的行为待在一起。
class Order {
private items: { name: string; price: number }[] = []
addItem(name: string, price: number) {
if (price < 0) throw new Error('菜价不能为负数')
this.items.push({ name, price })
}
total() {
return this.items.reduce((sum, item) => sum + item.price, 0)
}
}掌柜看懂了:“订单自己保管菜品,也知道怎样算总价。外人不能随手把菜价改成负数。”
欧阳公子点头:“这叫封装。对象把规则守在自己的门内。继承和多态也属于 OOP,但不是每个对象都要认祖归宗。很多时候,几个职责清楚的对象已经够了。”
OOP 适合描述身份明确、状态持续变化的事物,例如订单从“待付款”变成“已付款”,玩家拥有等级和装备,文档经历创建、审核和发布。
它的麻烦也来自这里:对象一多,互相调用、互相修改,状态就可能像客栈里层层转交的口信,最后没人说得清是谁改的。
AOP:有些规矩,每个门派都得遵守
订单写好了,掌柜又提要求:“每次下单都要记日志,贵客才能操作,失败还要上报。以后退款、进货也一样。”
欧阳公子准备在每个方法里加三段代码。阿婆拦住他:“这些不是下单本身,却散落在每一门生意里。让我在它们经过时统一查验。”
这就是 AOP(Aspect-Oriented Programming,面向切面编程)的视角:把日志、鉴权、事务、监控这类横跨多个业务流程的共同关注点集中处理。
在支持装饰器或拦截器的框架里,它可能写成注解;不用框架,一个高阶函数也能表达同样的思想:
function withLog<T extends unknown[], R>(
name: string,
action: (...args: T) => R,
) {
return (...args: T) => {
console.log(`${name} 开始`)
try {
return action(...args)
} finally {
console.log(`${name} 结束`)
}
}
}
const submitOrder = withLog('下单', (order: Order) => order.total())掌柜说:“原来 AOP 不是另一种业务写法,而是从业务的横截面下刀。”
阿婆提醒他:“刀也不能乱挥。若切面偷偷改变业务结果,代码会变得像暗器,明面上找不到调用,背后却处处生效。AOP 最适合边界清楚、规则统一的基础能力。”
FP:别问谁变了,问输入会得到什么
午市一到,优惠规则把众人绕晕了:满 100 减 20,会员再打九折,还不能改坏原订单。
方先生拿出一张白纸:“给我订单和规则,我还你一个结果。相同输入永远得到相同输出,中途不碰账本,也不改别人的东西。”
这就是 FP(Functional Programming,函数式编程)的核心倾向:用函数组合计算,偏爱纯函数、不可变数据和显式的数据流。
type Bill = Readonly<{
amount: number
isMember: boolean
}>
const subtractThresholdDiscount = (bill: Bill): Bill => ({
...bill,
amount: bill.amount >= 100 ? bill.amount - 20 : bill.amount,
})
const applyMemberDiscount = (bill: Bill): Bill => ({
...bill,
amount: bill.isMember ? bill.amount * 0.9 : bill.amount,
})
const settle = (bill: Bill) =>
applyMemberDiscount(subtractThresholdDiscount(bill))这里每个函数都只做一次转换:旧账单进去,新账单出来。它们不依赖藏在角落里的变量,所以容易单独验证,也容易换顺序、重新组合。
掌柜问:“那程序就完全不能改状态了?”
方先生答:“当然不是。点单、写数据库、发消息都有效果。FP 只是把副作用赶到边界,把中间的计算尽量保持纯净。厨房必须生火,但不必让整间客栈都冒烟。”
FP 特别适合规则计算、数据转换和并发任务。它的代价是团队需要适应不可变数据与函数组合;若为了“纯”而把简单流程写成层层术语,也只是换了一种复杂。
FRP:把变化当成一条河
傍晚,掌柜又想要一块实时看板:新订单来了,待做数量要变;菜做好了,数量要减;超过十单,招牌要变红。
若每次变化都手动通知看板、招牌和伙计,很快就会漏掉一个。风姑娘说:“不要盯着某个时刻的数字,把订单看成一条随时间流动的河。我们只描述河水怎样汇合、过滤和转向。”
FRP(Functional Reactive Programming,函数响应式编程)把 FP 的函数组合用于随时间变化的值和事件流。开发者声明关系,数据变化后,下游结果自动更新。
const pendingCount = orderEvents
.scan((count, event) => {
if (event.type === 'created') return count + 1
if (event.type === 'completed') return count - 1
return count
}, 0)
const signColor = pendingCount.map((count) =>
count > 10 ? 'red' : 'green',
)这段代码不关心“第几秒手动刷新招牌”。它只说明:待做数量由订单事件累积而来,招牌颜色又由待做数量派生而来。
表格公式也是一个朴素类比:C1 = A1 + B1,当 A1 改变,C1 随之更新。前端事件、实时搜索、WebSocket 消息和传感器数据,都适合用这种视角理解。
不过 FRP 并不等于“用了响应式框架”。严格说,FRP 强调连续或离散的时间变化值、声明式依赖和函数组合。不同前端框架常借用了其中一部分思想,不必因为有状态更新就都叫 FRP。
事件流若没有清楚的创建与结束边界,也会带来订阅泄漏、重复触发和难以追踪的问题。河道越多,越要标清源头和出口。
四种思想如何坐在同一张桌上
夜里打烊,四个人把方案拼在一起:
- OOP 管住订单、会员、菜品这些有身份和生命周期的对象。
- AOP 在业务入口统一处理鉴权、日志和事务。
- FP 负责优惠、计价和数据转换,让计算可预测。
- FRP 把订单事件变成实时看板和通知。
它们并不处于同一层面。OOP 和 FP 更多是在组织业务逻辑与状态;AOP 处理跨越多处的公共规则;FRP 处理随时间传播的变化。一个系统完全可以同时使用四者。
真正需要避免的,是先选门派,再逼问题迁就招式。看到长期存在、不断变化的实体,可以先想 OOP;看到确定的输入输出,先想 FP;看到到处重复的非业务逻辑,考虑 AOP;看到连续事件及其派生关系,考虑 FRP。
最后只记四句话
掌柜合上账本,给每位客人留下一句话:
- OOP:谁拥有这些数据,谁负责它的行为?
- AOP:哪些规则横跨了许多业务流程?
- FP:能否把过程写成可组合、可预测的数据转换?
- FRP:当值随时间变化,下游关系能否自动传播?
编程思想不是必须宣誓效忠的门派,而是一盒工具。好代码不在于用了多少术语,而在于问题被放进了最合适的模型里,并且下一位接手的人仍然看得懂。