
写业务系统久了,几乎每个后端开发者都或多或少接触过插件化架构。从IDE的插件生态,到后端服务的模块化插件,再到如今AI智能体里各式各样可动态加载的工具组件,动态组件的理念已经渗透进现代软件开发的方方面面。我们享受着插件化带来的灵活,不用重新编译整个工程,不用完整重启服务,就可以新增业务能力。可光鲜的表层之下,有一个埋藏了几十年的工程痛点始终没有被彻底解决。
不知道你有没有遇到过这样的场景,一个Java项目引入OSGi框架搭建插件体系,开发阶段一切顺利,插件可以正常安装运行。但运行一段时间之后,频繁的安装卸载插件,系统慢慢开始出现内存泄露,后台莫名触发已经卸载插件的定时器回调,事件总线还在分发事件给已经停用的模块。我们明明调用了插件的停止接口,写好了deactivate销毁方法,可总有一部分副作用偷偷残留下来。排查这类问题极其折磨人,因为故障不会立刻爆发,它潜伏在系统深处,随着运行时间慢慢累积,等到现象显现,很难定位到底是哪一处清理逻辑被遗漏。
同样的体验也出现在前端IDE,VS Code的扩展可以一键安装,但绝大部分包含可执行代码的扩展想要彻底禁用,必须重启整个扩展宿主进程。插件运行期间注册的事件,开启的连接,挂载的钩子,没有办法在运行时干净地抹除干净。我们习惯了接受这样的妥协,出现这类问题,大家普遍归因为开发者编码不仔细,销毁回调写的不够完备。可很少有人追问,这个问题是不是仅仅靠提升程序员的细心程度就能够解决,我们能不能换一套底层编程模型,从框架层面降低这类问题发生的概率。
北大与DeepSeek的那篇《一种时空可组合性编程范式》,核心就在直面这个行业老生常谈的痛点。很多人扫完论文的摘要,第一反应是,这不就是升级版的OSGi,不就是又一个IoC容器。表面看它做的事情和OSGi,Spring DM高度重合,支持组件动态加载卸载,管理组件之间的依赖关系。但剥开表层的功能表象,这套范式的核心创新,并不是又提供一套组件注册API,而是重新审视动态组件的两大根本矛盾,也就是论文提出的时间可组合性与空间可组合性。
很多技术文章讲解这两个概念,喜欢直接堆砌数学定义,读起来晦涩难懂。实际上,这两个名词翻译过来,描述的就是动态组件每天都在面对的两件事。时间可组合性,关注组件被移除之后,它过去产生的所有副作用能不能被完整撤销,环境恢复到组件加载之前的模样。空间可组合性,关注组件之间的依赖如何管理,当环境发生变化,依赖的提供者上线或者下线,组件的生命周期能不能自动做出合理响应。
传统OSGi,Spring这类框架,更多解决的是空间维度的依赖管理问题,对于时间维度的副作用清理,只是把销毁回调接口交给开发者,正确性完全依靠人工编码。而这篇论文把两者放在同等重要的地位,并且把效应和共效应这一套原本只用于编译期静态分析的理论,提升到程序运行时,衍生出可撤销效应,响应式共效应两大核心机制,再通过一套完整的动态组合演算,证明整套机制的完备性,最后落地实现为Cordis元框架。
这篇文章我不会照搬论文里大量的幺半群,λ演算公式,我想站在普通业务开发工程师的视角,聊聊动态组件领域长久以来的困境,对比传统组件框架的设计取舍,拆解这套时空可组合范式的底层逻辑,分析它的优势,现实约束,还有当我们尝试把这套思想迁移到Java这类传统静态语言的时候,会遇到哪些现实阻碍。
动态组件的现实困境,那些我们习以为常的折中方案
想要理解这套新范式的价值,首先要看清当下工程界处理动态组件的现状。为什么明明大家都想要细粒度热插拔,实际生产环境却大量使用粗粒度的方案。
当我们需要让软件具备动态变更能力,手上可选的工具链其实分为好几个层级。粒度最大的方案就是操作系统进程和容器编排。一个组件出问题,直接杀掉整个进程或者Pod,重新启动实例。重启的一瞬间,进程内部全部状态直接丢弃,不管是线程,监听,文件句柄,全部由操作系统回收。从结果看,这完美达成了时间可组合性,进程销毁,进程内一切副作用全部烟消云散。
但代价同样十分巨大。重启进程会丢失进程内所有缓存,长连接,未执行完毕的中间计算任务。为了保证服务可用性,我们不得不维护多副本集群,请求做负载均衡,容忍重启期间的抖动。很多时候我们仅仅想要关闭一个小小的插件,却要付出整个实例重启的高昂成本。
正是因为进程粒度太重,于是就有了JVM层面OSGi这类框架,在同一个进程内部,实现bundle级别的动态组件。OSGi可以做到不用重启JVM,就可以安装,更新,卸载bundle。它定义完整的组件生命周期,提供激活和停用回调,还支持声明式服务DS,组件可以声明对外提供的服务,也可以声明自己需要的外部依赖。当依赖的服务消失,组件会收到通知执行销毁逻辑。
看上去OSGi已经解决了动态组件的绝大多数诉求,但真实落地之后,很多工程师都会踩上销毁清理的大坑。OSGi仅仅管理bundle的Java对象和类加载器,它完全不知道这个bundle在运行过程中产生了哪些副作用。注册事件监听器,开启定时任务,创建网络连接,向全局管理器注册钩子,这些动作全部是业务代码执行的。框架只会通知你,现在bundle要停用了,调用deactivate方法,至于这个过程要清理哪些资源,全部要开发者自己回忆,自己手动写在回调内部。
这就带来一个天然的脆弱点。所有清理逻辑集中在deactivate回调,但是副作用却散落在整个业务代码的各个角落。开发过程中新增一处注册监听,新建一个定时器,开发者必须记得同步在deactivate增加对应的注销逻辑。人总会犯错,一旦某处清理逻辑被遗忘,就会产生幽灵副作用。哪怕bundle已经被标记停止,那些已经注册的回调还会继续运行,线程还在后台持续执行,内存悄悄泄露。这类bug很难复现,往往系统长时间运行之后才会逐步暴露。
Spring生态也是类似,普通Spring容器本身设计目标偏向静态装配。虽然也可以通过编程的方式动态注册销毁Bean,DisposableBean接口,@PreDestroy注解提供销毁回调。同样的逻辑,框架只是触发销毁方法,Bean在运行时向外产生的副作用,框架没有任何感知。Spring DM扩展把OSGi和Spring结合,也没有改变这套底层模型。
我们可以发现,OSGi,Spring这类传统组件框架,本质上是对象实例生命周期管理器。它管理的是对象本身,而不是对象执行过程中向外造成的环境修改。依赖通知机制做的很完善,但副作用清理这件事,完全委托给开发者的自律。
很多人会说,那我们写代码细心一点,写单元测试覆盖销毁逻辑,是不是就可以规避这类问题。理想是美好的,但现实业务代码复杂度会持续膨胀。业务迭代过程中,业务逻辑不断修改,新增分支,新增注册逻辑,隔几个版本之后,deactivate回调很容易和业务代码脱节。而且很多副作用出现在分支逻辑,异步回调内部,不是每次程序运行都会走到,单元测试很难做到全覆盖。
也正是因为细粒度动态组件存在这些棘手问题,所以很多业务项目即便有插件化需求,最后也会退而求其次,采用配置开关的模式。代码全部打包进程序,通过配置控制功能是否开启,并不做真正意义上的运行时卸载。功能关闭之后,相关代码逻辑不再执行,但是对象,监听器依然驻留在内存中,只是不再对外产生效果。这属于一种伪插件化,规避了卸载带来的清理难题,却没有真正释放资源。
论文的作者正是看到这些工程现实,才提出疑问,除了依赖通知加手动销毁回调这套模式,我们能不能重新设计抽象,让副作用的撤销不再集中在一个大的销毁函数中。这就引出这套范式最核心的创新,可撤销效应。
可撤销效应,把撤销逻辑留在副作用发生的现场
效应,通俗讲就是副作用,函数执行过程中修改外部环境的行为。注册事件监听,打开数据库连接,新增全局配置,启动定时任务,全部属于副作用。传统编程模式中,执行完副作用之后,正向动作和撤销动作是割裂的。执行注册写在A处,对应的注销代码,要千里迢迢写到另外一处的销毁回调函数。
而可撤销效应的核心思想非常直白。每执行一处副作用,就在发生的同一个位置,同步提供对应的逆操作,框架自动收集这些逆操作,组件卸载的时候统一回放执行。
我们可以对比两段伪代码,直观感受两种编程思维的差异。
传统OSGi风格,激活和销毁逻辑完全分离:
public class DemoBundle implements BundleActivator {
private long listenerId;
private ScheduledExecutorService timer;
@Override
public void start(BundleContext context) {
// 正向副作用,注册监听,启动定时器
listenerId = GlobalBus.register(this::handleEvent);
timer = Executors.newSingleThreadScheduledExecutor();
timer.scheduleAtFixedRate(this::tickTask, 0, 1, TimeUnit.SECONDS);
}
@Override
public void stop(BundleContext context) {
// 所有撤销逻辑全部集中在这里,很容易漏写
GlobalBus.unregister(listenerId);
timer.shutdownNow();
}
}
而在Cordis范式的思想下,代码会写成下面这个样子:
export function apply(ctx: Context) {
// 注册监听,在这里写正向逻辑,同时写撤销逻辑
ctx.effect(() => {
const id = eventBus.register((evt) => handleEvent(evt));
// 副作用发生的当场,返回对应的逆操作
return () => eventBus.unregister(id);
});
// 启动定时器,正向逻辑和撤销逻辑写在同一个代码块内
ctx.effect(() => {
const timer = setInterval(tickTask, 1000);
return () => clearInterval(timer);
});
// 不再需要单独编写deactivate销毁回调
}
这里的ctx.effect就是可撤销效应的原语。开发者传入一个回调函数,回调里面执行真正的副作用,同时返回撤销这个副作用的函数。框架拿到撤销函数,自动收集到属于当前组件实例的撤销栈中。当组件需要卸载的时候,框架自动逆序执行栈里面全部的撤销逻辑。
这里要澄清一个非常关键的误区,这套机制并不是拥有魔法,它不能自动推导生成撤销逻辑。开发者依然需要手动写出逆操作。它解决的不是“怎么撤销”,而是“撤销代码放在哪里”的问题。
传统模式,副作用散落在业务各个分支,撤销全部汇总到单独的销毁函数。业务迭代的时候,开发者需要跨文件记忆维护两份代码。而可撤销效应强制把正向操作和撤销操作写在同一个代码位置。读代码的时候,看到注册逻辑,立刻就能看到对应的清理逻辑,两者在同一个代码块,阅读和修改代码的时候不容易被遗忘。
论文里面一大半复杂的数学公式,就是围绕这套机制做形式化建模。定义扭曲复合,效应函数,累积器,证明一堆定理,本质上是回答一系列问题:多个可撤销效应先后执行,逆操作该如何组合保存。当多个组件的副作用互相穿插执行,能不能单独卸载其中某一个组件,而不干扰其他正在运行的组件。什么条件下,撤销顺序可以不用严格遵守后进先出。
这里就引出效应独立性这个概念。如果两个组件操作的资源互相之间互不干扰,比如一个组件注册键盘快捷键,另一个注册鼠标快捷键,即便两者的操作互相穿插执行,也可以单独卸载其中一个,另一个组件的运行完全不受影响。论文通过数学演算严格证明了这套性质成立的前提条件。
而传统OSGi没有任何对应的模型。Bundle卸载的时候,全靠deactivate回调里面业务代码写的是否足够严谨。如果A和B两个bundle操作互相穿插,卸载A的时候,如果A的销毁逻辑写的有问题,很容易直接把B搞崩。
我们还需要意识到这套机制存在边界。不是世界上所有操作都可以完美撤销。如果程序已经向外发送一条网络报文,报文已经抵达远端服务器,程序本地再怎么做回滚,也没办法把已经发出去的消息抹除。论文第六章专门讨论系统边界。只有完全处于程序内部,程序拥有完整控制权的资源,才适合做成可撤销效应。对于已经对外输出到外部世界的行为,没有办法真正撤销,只能做业务层面的补偿逻辑。
还有一条硬性约束,所有想要被自动追踪的副作用,都必须通过ctx.effect原语执行。如果开发者绕过框架,直接调用全局静态方法注册监听,框架完全感知不到这个副作用,自然也不会帮你收集撤销逻辑。这一点和OSGi一样,都需要开发者遵守编码契约,框架做不到完全的强制约束。
Fiber纤程,组件运行实例,不只是一个普通对象
讲完可撤销效应,就绕不开论文中反复出现的Fiber,很多人第一眼会把它和操作系统协程混淆,这是一个高频误解。操作系统的Fiber,协程,是用来调度执行代码的执行单元,拥有自己的调用栈。而Cordis的Fiber和代码调度,线程栈完全无关。
Component组件是静态模板,可以类比Java里面的Class,定义组件需要哪些依赖,启动的时候要执行什么样的逻辑,会对外提供哪些服务。而Fiber,就是这个模板实例化之后,驻留在内存中的运行实例。它远远不止保存一份业务对象,它是一个完整的状态管理容器。
每一个Fiber实例内部会维护很多关键信息。它拥有自己专属的上下文对象,维护属于这个实例的撤销累积栈,也就是收集全部可撤销效应逆操作的地方。它维护一套完整的状态机,状态包括未激活,正在启动,正常运行,正在卸载,故障。它记录自己声明的依赖集合,同时保存committed提交视图,快照记录当下究竟是哪些其他Fiber实例给自己提供依赖。它还有parent父指针,Fiber之间会形成一棵树形层级关系。
如果A组件内部,在apply函数执行过程中,调用原语加载B组件,生成B的Fiber实例,那么B的parent就指向A。这棵树形结构会带来很实用的级联卸载能力。当父Fiber被卸载,框架会自动标记所有子Fiber进入卸载流程,子组件全部自动清理。就像拔掉总电源插排,插排下面所有小插头全部断电。
Fiber的状态机是整套动态演算的核心。组件并不是简单的非开即关。启动过程中间有Reloading正在启动状态,卸载的时候会进入Unloading正在卸载状态。这里就涉及到空间可组合性,也就是响应式共效应要解决的问题。
响应式共效应,处理组件之间依赖的时序难题
共效应和效应是对偶的概念。效应描述组件会如何修改外部环境,共效应描述组件对外部环境有什么需求,也就是组件的依赖。
传统IoC容器,不管是Spring还是OSGi DS,都可以声明依赖。但是动态场景下会出现一个棘手的时序难题。假设组件C依赖组件P提供的服务。现在我们想要卸载P。
业务上的合理诉求是什么。不能直接立刻把P的全部资源关闭。因为C现在正在收到依赖消失的通知,进入自己的销毁流程,C的销毁逻辑执行过程中,还需要访问P提供的服务完成收尾工作。必须等待所有依赖P的消费者全部完整走完卸载流程之后,P才可以真正执行自己的撤销栈,释放资源。
这是工程实践中非常容易踩坑的地方。OSGi DS没有在框架层面内置这套等待守卫机制。P的deactivate回调和C的unbind回调之间的执行时序没有强保证。很容易发生P先把资源关闭,C的unbind回调执行的时候,调用已经销毁的P服务,直接抛出异常。很多项目只能靠业务代码增加大量判空防御逻辑来规避这个风险。
而Cordis的演算专门处理这个时序。当提供者P要被卸载,并不会立刻执行撤销栈。第一步,把P的Fiber标记为Unloading,这个标记一旦打上,P就不再对外提供服务,不会接收新的消费者。然后框架开始等待,遍历所有当前依赖P的Fiber实例,驱动这些消费者依次走完完整卸载生命周期。直到没有任何组件还依赖P之后,才真正执行P的撤销累积栈,回放全部逆操作,彻底清理P产生的副作用。
整套逻辑不是写在业务回调里面,而是写进Fiber状态机的运行规则,并且论文通过元理论证明这套机制具备进展性,不会发生死锁。
除了基础的依赖通知,响应式共效应还延伸出隔离与拦截两个很有意思的能力。
隔离能力,允许同一个依赖key,在不同Fiber上下文解析出完全不同的实例。同一个服务名字,测试环境的组件拿到测试实例,正式环境组件拿到正式实例。天然适配多租户,沙箱测试场景。传统OSGi服务注册表是扁平全局结构,想要实现同类能力,需要开发者自己在业务层做大量封装。
拦截能力,允许在不修改组件源码的前提下,对依赖访问增加横切逻辑。举个例子,某第三方插件要访问数据库服务,我们不想修改插件的一行代码,就可以通过拦截机制,给这个组件的数据库访问增加只读限制。拦截逻辑挂载在上下文,组件调用依赖服务的时候自动生效。
把可撤销效应代表的效应上下文,和响应式共效应代表的共效应上下文合并在一起,就得到论文提出的统一上下文范式。所有组件和外部环境的交互,全部经过一等上下文对象。组件修改环境,走上下文的effect原语登记逆操作。组件需要外部服务,向上下文声明自己的依赖。组件加载就是接入上下文,卸载就是从上下文摘除,自动回放撤销栈清理全部痕迹。
这套范式对比传统两大编程模式,走出一条中间路线。纯函数式编程,状态到处显式传递,推理安全,但是样板代码多,开发体验笨重。传统命令式面向对象编程,随便修改全局环境,依赖藏在代码深处,调试排错成本高。上下文范式保留显式追踪副作用和依赖的能力,同时兼顾开发的便捷性。
当然这套范式不是某一门编程语言的专属,它本质上是一套运行时模型。TypeScript实现的Cordis只是其中一个落地版本。理论上,Java,Rust都可以基于库复刻整套模型,不需要修改编译器,也不需要修改虚拟机。
在Java生态复刻这套范式,理想与现实的鸿沟
很多读到这里的Java开发者会思考,既然模型和语言无关,我能不能在Java项目里面自己造一套类似Cordis的组件框架,解决OSGi遇到的那些痛点。答案是逻辑模型完全可以复刻,但会遇到不少JVM语言特有的现实阻碍。
我们可以先用Java写一套极简的核心API感受一下:
@FunctionalInterface
public interface Disposer {
void dispose() throws Exception;
}
public interface Context {
// 执行可撤销效应,登记撤销逻辑
void effect(Supplier<Disposer> action);
// 对外提供服务key
<T> void set(Key<T> key, T value);
// 获取依赖服务
<T> T get(Key<T> key);
}
// 组件静态模板,声明依赖,组件启动逻辑
public interface Component {
List<Key<?>> inject();
void apply(Context ctx);
}
业务组件的编写风格会变成下面这样:
public class DemoPlugin implements Component {
@Override
public List<Key<?>> inject() {
return List.of(DbKey.INSTANCE);
}
@Override
public void apply(Context ctx) {
ctx.effect(() -> {
ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();
timer.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.SECONDS);
return timer::shutdownNow;
});
ctx.effect(() -> {
long id = globalEventBus.register(this::handleEvent);
return () -> globalEventBus.unregister(id);
});
}
}
可以看到,代码层面不再需要编写集中式的deactivate销毁回调。每一处副作用执行的同时,就提供对应的disposer撤销逻辑,框架自动收集到当前Fiber的撤销栈。卸载Fiber的时候,框架逆序执行栈内所有disposer。
但是Java环境下有几个绕不开的现实约束。
第一,框架只能保证会调用你提供的disposer函数,但是它没有能力校验disposer内部逻辑写的是否正确。如果开发者写的disposer存在bug,比如忘记从全局静态事件总线移除监听器,就算框架调用disposer,幽灵副作用依旧会残留。这套范式只是降低出错概率,不是消灭所有bug。而且Java中静态全局集合的强引用问题很棘手,很容易出现对象已经逻辑销毁,但还被静态集合持有引用的情况。Java Agent字节码插桩可以在测试阶段做扫描告警,提醒开发者哪里绕过context.effect直接调用注册逻辑,但是Agent不能在生产环境凭空生成撤销逻辑。字节码只记录方法调用,不会记录这个调用对应的逆操作语义。
第二,Java的检查异常会让disposer的异常处理变得繁琐。执行一整条撤销栈的时候,中间某一个disposer抛出异常,后续撤销动作该如何执行,失败该如何上报,都需要额外编写处理逻辑。
第三,热模块替换。Cordis的HMR能力建立在JS模块动态卸载之上。Java想要做到模块热替换,必须自定义ClassLoader。就算我们复刻完Fiber,撤销栈整套模型,类卸载还受Metaspace,对象强引用的制约。OSGi已经解决自定义类加载这件事,但OSGi没有处理副作用撤销栈。我们完全可以把复刻的Cordis模型跑在OSGi的类加载能力之上,取长补短。
很多人会联想到JDK21的StructuredTaskScope结构化并发,还有AutoCloseable,try‑with‑resources。这里要区分清楚。try‑with‑resources和结构化并发,生命周期绑定代码词法作用域,代码块执行完毕自动释放资源。而Fiber管理的组件生命周期和调用栈完全无关。插件加载之后可以运行几个小时,外部随时触发卸载,生命周期完全不受代码块约束。两者的应用场景有着本质区别。
所以在Java生态做这套东西,是一套库层面的编程契约。所有会产生副作用的操作,都必须走ctx.effect原语登记disposer。一旦业务代码绕过原语直接操作全局静态资源,框架就彻底失去管控。它可以大幅改善销毁清理的问题,但不是银弹。
原型落地Cordis与Koishi真实业务反馈
论文中的理论并不是纸上谈兵,作者实现了Cordis元框架。Cordis是元框架,它不绑定特定业务领域,只负责提供时空可组合的基础抽象。上层业务框架可以基于Cordis构建。Koishi聊天机器人框架就是最典型的案例。
Koishi是开源聊天机器人框架,社区沉淀了数千个第三方插件。每一个插件对应Cordis的Component组件。机器人控制台界面可以直接关闭任意插件,不需要重启整个机器人服务。插件注册的指令,事件监听,定时器,全部通过上下文effect原语登记撤销逻辑。关闭插件,所有副作用自动清理干净。开发阶段修改插件代码,热模块替换直接生效,不需要重启整个机器人进程。
传统HMR方案,比如Webpack,Vite的热更新,往往需要开发者手动编写HMR接收回调。而Cordis的HMR能力是原生得到的。替换模块的时候,旧组件对应的Fiber实例完整卸载,自动回放全部撤销栈,清理旧插件的全部副作用,之后实例化新的组件,生成新的Fiber运行。只要插件代码全部遵守框架契约,不需要额外写HMR相关业务代码。
当然论文第六章也客观的剖析这套范式现存的短板。
首先是依赖版本与接口兼容问题。原型实现中,依赖匹配靠key名字,没有内置接口结构校验。两个完全不同的组件,如果恰好使用同一个key字符串,就会发生key冲突。就算key是同一个,提供者升级接口之后,旧消费者没有同步修改,会出现静默错误。论文提出几个解决思路,命名空间隔离key,对等依赖,结构兼容性校验,但是都还属于理论探讨,没有做到完美的工程实现。
其次是循环依赖。A组件依赖B,B组件依赖A,两者的依赖规约永远无法被满足,两个组件永远不会被激活。框架可以检测出循环依赖并且报错,但是不会自动帮你消解循环依赖。工程上的解法,需要开发者做组件拆分,把双向交互拆分成多个单向依赖的细粒度组件,代价是组件数量膨胀,提升配置和理解成本。
沙箱安全也是现实挑战。范式内部提供拦截机制,可以做访问权限管控。但是如果组件是完全不可信的第三方代码,仅仅靠语言层面拦截远远不够,必须依靠外部沙箱,独立进程或者WebAssembly隔离。
站在更高视角看范式的价值与边界
读到这里,很多人会思考,既然OSGi也能做动态组件,那这套时空可组合范式,真正的价值究竟是什么。
OSGi是一套工程API规范,它定义组件生命周期,服务注册通知。但它没有对副作用,逆变换做形式化建模,正确性建立在开发者的编码习惯之上。而Cordis这套工作,是把原本编译期的效应和共效应静态类型理论,抬高到运行时一等抽象。它给出完整的演算,并且完成元理论证明,保证在满足前提条件下,动态增删组件,系统的各项性质可以维持。
很多工程框架是先解决现实业务问题,后面再补理论。而这篇论文的路径是反过来,先构建完整形式化模型,再做工程原型实现。这并不代表它可以解决所有现实世界问题,它同样受限于编码契约,受限于系统边界。
它的最大贡献,是改变动态组件的思考方式。传统思想,组件卸载等于通知组件对象执行销毁回调。而这套范式思想,组件卸载等于撤销这个组件曾经产生过的每一次副作用。副作用在哪里发生,撤销逻辑就写在哪里,运行时自动收集管理,配合响应式依赖状态机,处理好依赖之间的时序约束。
我们不能神化这套范式,它不是万能银弹。如果业务系统插件数量很少,组件生命周期简单,传统OSGi或者Spring动态Bean完全够用,引入整套范式会带来额外学习成本。但是当系统大量频繁动态加载卸载组件,插件来自不同开发者,生态庞大复杂,手动维护销毁回调的代价就会持续放大,这套范式的优势就会凸显出来。
放眼未来,这套上下文范式也给编程语言和操作系统设计提供新的启发。如果编程语言原生把上下文作为一等公民,编译器就可以做更多静态检查,在编译阶段校验组件依赖,检测潜在循环依赖。操作系统层面,如果把内存,文件描述符这类系统资源建模为共效应,内核层面就可以追踪资源归属,组件销毁的时候内核自动回收该组件申请的资源。当然这些都还停留在设想阶段。
动态组件的世纪难题,不会因为一篇论文彻底终结。但是可撤销效应加响应式共效应,为我们提供了一条和传统回调模式完全不同的解题思路。它告诉我们,插件卸载之后残留幽灵副作用,不一定只能归咎于程序员不够细心,我们可以修改底层抽象,把一部分正确性,从人的编码纪律,变成框架运行时的结构性保障。
291

被折叠的 条评论
为什么被折叠?



