观察者模式的六大顽疾
核心摘要
交互式软件因观察者模式的六大顽疾(顺序不可控、首事件丢失、状态混乱、线程死锁、回调泄漏、意外递归)必然撞上复杂性墙;FRP 通过“声明依赖、引擎拓扑排序、流与胞两大不可变容器、十大纯函数原语、原子事务合并”彻底消除副作用,实现真正的组合性。
干货提炼
主题一:观察者模式的六大顽疾
- 顺序不可控:监听器触发顺序取决于注册时的内存顺序,导致仪表盘类场景出现“笑点在梗前”的乱序。
- 首事件丢失:初始化事件在监听器注册前触发,系统“对着空房间喊状态”,监听器永远收不到首帧。
- 状态混乱:回调是孤立反应,迫使开发者手写庞大状态机,人脑无法追踪海量状态迁移。
- 线程死锁:为让重叠监听器线程安全,引入锁后极易陷入相互等待的死锁,整个应用冻结。
- 回调泄漏:组件关闭时忘记注销监听器,监听器长驻内存,随时间累积导致 OOM 崩溃。
- 意外递归:更新局部变量与通知其他监听器的先后顺序稍错,即触发无限回调循环。
主题二:从命令式“操作序列”到声明式“依赖图”
- 范式转换:不再写“热油→炒洋葱→加肉→炖二十分钟”的操作步骤,而是写“千层面 = 芝士覆盖肉酱覆盖面皮烘烤;肉酱 = 洋葱+肉+番茄炒制”的依赖定义。
- 引擎托底:FRP 引擎在内存中构建有向无环图(DAG),自动拓扑排序得出唯一合法执行顺序,顺序 Bug 在数学上不可能出现。
- 心智减负:程序员只管“是什么(依赖)”,引擎管“怎么跑(时序)”,彻底切断对时间线的微观管理。
主题三:两大不可变容器——流与胞
- 流:离散瞬时事件,仅在触发毫秒存在(如点击“请多指教”按钮发送一条聊天消息)。
- 胞:随时间变化但始终有值的状态,任意微秒可采样(如聊天输入框,初始为空串,输入时逐字更新,从不“不存在”)。
- 潘多拉魔盒:可变状态被切碎锁入不可变的流/胞容器;容器本身不可变,内部承载的是“值的不可变时间线”,从而兼得函数式可靠性与交互式状态性。
主题四:零作弊纪律与十大原语
- 引用透明强制:传入引擎的每个函数不得读写全局变量、文件、网络;一旦作弊,DAG 破裂,六大顽疾全数复发。
- 十大原语足矣:map、merge、hold、snapshot、lift 等十个纯函数原语可组合出任意复杂度系统。
- snapshot 解耦时序:按钮流触发时,精准快照输入框胞的当前值,完成“事件+状态”无竞态组合(如拉丁语翻译器:点击翻译按钮→快照输入框→加后缀 us)。
- merge 强制冲突解析:同一鼠标点击同时触发“取消选中三角形”与“选中八边形”,merge 要求开发者显式提供合并函数,编译期即消除竞态,原子事务保证“要么全选中、要么全取消,绝无中间态”。
主题五:加油机模拟器的工业级验证
- 建模映射:三把油枪提枪/放枪→流;油价→胞;流量计脉冲→流;预设金额→胞;累计金额/升数→胞循环。
- 胞循环打破递归:累计金额 = 上一事务累计金额 + 本事务新增升数 × 当前油价;引擎按离散事务帧原子更新,前向引用合法,无限循环被帧边界天然切断。
- 需求变更即加边:产品经理要求“预设金额临近自动减速”,只需在依赖图新增一条“剩余金额→流速”的边,零重构、零回归测试。
高光金句
- ❝ In the operational recipe, the sequence is paramount. If you tell the computer to add the cheese before it heats the oil, you ruin the dish entirely. ❞
- ❝ You aren't mutating a variable in place like you do in traditional code. You are defining an immutable timeline of values. ❞
- ❝ If the small components work, the massive system built from them is mathematically guaranteed to work just as reliably. ❞
提及资源
- 书籍/文章/内容:《Functional Reactive Programming》 - Steven Blackheath 和 Anthony Jones 著,全书核心理论与案例(六大顽疾、流/胞、十大原语、加油机模拟器)均源自此书。