最近在设计一个前端程序,基本上就是一个面向领域设计 DDD 的思路,主要思考了一下关于模块协作相关的内容,有点不成熟,也想听听各位大牛对此的看法。
程序设计主要就是模块之间如何相互合作,而又保持一个合理的耦合程度,每个模块只保留非常少的全局假设。
为了屏蔽一些细节,我大致总结了三种方式:动态注入,静态注入,观察者。
在一个复杂系统中,观察者模式能够有效地将模块解耦,几乎所有的大型系统都会使用该模式。
剩下两种注入模式:动态注入和静态注入,我想举两个例子。
首先是静态注入的方式,将依赖模块通过参数传递给函数
function moduleA(interfaceA) {
interfaceA.doSomething();
interfaceA.doOtherThing();
}
这种方式是 moduleA 对 interfaceA 做了某些假设,interfaceA 并不是专门为 moduleA 而设计,而是一个单独的合理抽象。这样 moduleA 在使用 interfaceA 时依然要小心翼翼,熟读 interfaceA 的文档。
这种方式是我一开始最常用的一种方式,但是它会导致很多问题,1. 就是理解成本增加,我需要知道 interfaceA 的设计理念。2. 参数爆炸。因为在一个复杂程序中我不可能只依赖了 interfaceA, 还可能依赖了其他好几个模块,如果都使用参数传递进来那 moduleA 的参数简直不可接受,特别是未来如果我想增加一个依赖还要调整参数顺序,维护起来不方便。
由于这一点在看 mxgraph 源码时发现开源项目中会动态注入依赖,就像 react-dom 注入到 react 中一样,并不是在其构造方法内注入,而是在构造方法外注入。
还是上面的代码片段,我们回归一下问题本质,本质是 moduleA 需要做一个操作,这个操作具体什么模块执行的我不关心,我只需要完成这个功能即可,由外部的组合者(暂且这么称呼它把) 负责调配不同的实现。这是问题的本质,那么原来的方法注重的是:可替换的实现。新的方法将会注重:完成某种功能。
function moduleA() {
}
moduleA.prototype.work = function() {
this.doSomeWork();
}
moduleA.prototype.doSomeWork = function () {
throw new Error('not implementation');
}
// outside
const a = new moduleA();
a.doSomeWork = function() {
interfaceA.doSomething();
interfaceA.doOtherThing();
}
使用这种方式 moduleA 甚至不需要知道中间协议 interfaceA 的存在,只需要知道有个东西需要实现,外面的组合者再将对应的依赖组装好。
这样也能达到关注点分离,甚至比通过参数的依赖注入更好,因为此时 moduleA 无需对外做什么假设,对应的成本就是 moduleA 的初始化有一定的成本,我觉得比较适合在确定位置初始化的对象使用这种方法比较合适。