实现依赖于其他插件的插件的设计模式
Design pattern to implement plugins that depend on other plugins
我正在编写一个工具,每 10 分钟获取用户手动输入的一些数据,运行对每个输入进行一组计算。插件 A 提供一种计算,插件 B 提供另一种,依此类推。大多数这些插件是相互独立的,即顺序无关紧要,因为每个插件的计算return是一个与其他插件的整数相加的整数。
但假设现在,我确实有一个插件 C,它取决于插件 A 的 return 是否为非零。在数据方面,假设我知道如何使插件 A 的状态对插件 C 可用。(例如,如果是 C++,我会让插件 A 成为插件 C 的 friend
。但是,我正在写这在 Javascript 中,所以我可能会采取更宽松的方法。)我的问题更多是关于排序/依赖的模式。 如何确保插件 A 的计算在插件 C 之前 运行?
当然,最简单的方法就是简单地"install"按照需要的顺序插入插件运行,即把插件插入到右边order 到一个数组中,这样循环遍历所述数组的循环就不需要考虑了。
但是随着我添加越来越多的插件(超过 20 个,也许 30 个,视情况而定),这可能会变得脆弱。我想要更结实的东西。
我现在最好的想法是:
- 在 "installing" 一个插件上,我提供了它所依赖的一组插件。
- 每个插件都有一个静态成员,比如
_complete
,表明它是否 运行,并在每次新迭代(用户输入)时重置。
- 当我遍历每个插件时,我检查每个插件的依赖项的
_complete
状态;如果一个不完整,那么我还没有 运行 计算;该循环将是一个 while
循环,在尝试所有其他插件后返回重试该插件。我还将有一个最大重试次数保护来防止无限循环。
如何改进?
正如 Gothdo 所建议的那样,Promises 可以很好地解决这样的问题。但是,您的代码不需要是异步的。使用 promises 时,混合和匹配异步和同步代码没有任何限制。如果您 运行 加载小型同步函数,您最终将付出性能开销,但对于数十个函数的用例,开销可以忽略不计。
Promises 为未来发生的事情提供控制流抽象:运行 this, when that completes。主要用于异步代码。可能有人会说这是用迷你枪打鸭子。我用关于重新发明轮子(或从 GitHub 深处找到一个不起眼的六角轮)的论点来证明选择的合理性,如果需要的话,支持异步就绪,而且大多数 JS 程序员已经熟悉这一事实使用 Promises 和库得到很好的支持。最重要的一点是:对 promises 进行必要的包装非常简单。
我快速画了一个这样的包装器可能是什么样子的草图。该代码使用有点丑陋的延迟模式来启用以任何顺序添加任务。随意添加错误处理,或以其他方式修改以满足您的需要。
function TaskRunner () {
this.tasks = {};
// name: String
// taskFn: fn(dep_1_result, dep_1_result...) -> result or Promise(result),
// deps: Optional, array of names or a name.
// Return: Promise over added task
TaskRunner.prototype.add = function (name, taskFn, deps) {
var self = this;
deps = (deps === undefined ? [] : (deps instanceof Array ? deps : [deps]));
name = name.toString();
self.tasks[name] = self.tasks[name] || {};
if(self.tasks[name].fn)
throw "Task " + name + " exists."
deps = deps.map(function (d) {
// Create result handler for deps, if none exist
self.tasks[d] = self.tasks[d] || {};
self.tasks[d].result = self.tasks[d].result || defer();
return self.tasks[d].result.promise;
});
// Create result handler for this task if none was created when
// handling deps of formely created tasks
self.tasks[name].result = self.tasks[name].result || defer();
// Excecute when all deps are done
self.tasks[name].fn = Promise.all(deps).spread(taskFn)
.then(function(res, err) {
// Trigger own result handler
if(err) {
self.tasks[name].result.reject(err);
throw err;
}
else {
self.tasks[name].result.resolve(res);
return res;
}
});
return self.tasks[name].fn;
}
}
用法示例:https://jsfiddle.net/3uL9chnd/4/
Bluebird 承诺库:http://bluebirdjs.com/docs/api-reference.html
编辑,免责声明:考虑开销时还有另一点:重置效率。如果您 运行 在一个紧密的时间间隔内进行轻度计算,则在每个周期为每个任务创建一个 promise 对象会使这种方法不尽如人意。
我正在编写一个工具,每 10 分钟获取用户手动输入的一些数据,运行对每个输入进行一组计算。插件 A 提供一种计算,插件 B 提供另一种,依此类推。大多数这些插件是相互独立的,即顺序无关紧要,因为每个插件的计算return是一个与其他插件的整数相加的整数。
但假设现在,我确实有一个插件 C,它取决于插件 A 的 return 是否为非零。在数据方面,假设我知道如何使插件 A 的状态对插件 C 可用。(例如,如果是 C++,我会让插件 A 成为插件 C 的 friend
。但是,我正在写这在 Javascript 中,所以我可能会采取更宽松的方法。)我的问题更多是关于排序/依赖的模式。 如何确保插件 A 的计算在插件 C 之前 运行?
当然,最简单的方法就是简单地"install"按照需要的顺序插入插件运行,即把插件插入到右边order 到一个数组中,这样循环遍历所述数组的循环就不需要考虑了。
但是随着我添加越来越多的插件(超过 20 个,也许 30 个,视情况而定),这可能会变得脆弱。我想要更结实的东西。
我现在最好的想法是:
- 在 "installing" 一个插件上,我提供了它所依赖的一组插件。
- 每个插件都有一个静态成员,比如
_complete
,表明它是否 运行,并在每次新迭代(用户输入)时重置。 - 当我遍历每个插件时,我检查每个插件的依赖项的
_complete
状态;如果一个不完整,那么我还没有 运行 计算;该循环将是一个while
循环,在尝试所有其他插件后返回重试该插件。我还将有一个最大重试次数保护来防止无限循环。
如何改进?
正如 Gothdo 所建议的那样,Promises 可以很好地解决这样的问题。但是,您的代码不需要是异步的。使用 promises 时,混合和匹配异步和同步代码没有任何限制。如果您 运行 加载小型同步函数,您最终将付出性能开销,但对于数十个函数的用例,开销可以忽略不计。
Promises 为未来发生的事情提供控制流抽象:运行 this, when that completes。主要用于异步代码。可能有人会说这是用迷你枪打鸭子。我用关于重新发明轮子(或从 GitHub 深处找到一个不起眼的六角轮)的论点来证明选择的合理性,如果需要的话,支持异步就绪,而且大多数 JS 程序员已经熟悉这一事实使用 Promises 和库得到很好的支持。最重要的一点是:对 promises 进行必要的包装非常简单。
我快速画了一个这样的包装器可能是什么样子的草图。该代码使用有点丑陋的延迟模式来启用以任何顺序添加任务。随意添加错误处理,或以其他方式修改以满足您的需要。
function TaskRunner () {
this.tasks = {};
// name: String
// taskFn: fn(dep_1_result, dep_1_result...) -> result or Promise(result),
// deps: Optional, array of names or a name.
// Return: Promise over added task
TaskRunner.prototype.add = function (name, taskFn, deps) {
var self = this;
deps = (deps === undefined ? [] : (deps instanceof Array ? deps : [deps]));
name = name.toString();
self.tasks[name] = self.tasks[name] || {};
if(self.tasks[name].fn)
throw "Task " + name + " exists."
deps = deps.map(function (d) {
// Create result handler for deps, if none exist
self.tasks[d] = self.tasks[d] || {};
self.tasks[d].result = self.tasks[d].result || defer();
return self.tasks[d].result.promise;
});
// Create result handler for this task if none was created when
// handling deps of formely created tasks
self.tasks[name].result = self.tasks[name].result || defer();
// Excecute when all deps are done
self.tasks[name].fn = Promise.all(deps).spread(taskFn)
.then(function(res, err) {
// Trigger own result handler
if(err) {
self.tasks[name].result.reject(err);
throw err;
}
else {
self.tasks[name].result.resolve(res);
return res;
}
});
return self.tasks[name].fn;
}
}
用法示例:https://jsfiddle.net/3uL9chnd/4/
Bluebird 承诺库:http://bluebirdjs.com/docs/api-reference.html
编辑,免责声明:考虑开销时还有另一点:重置效率。如果您 运行 在一个紧密的时间间隔内进行轻度计算,则在每个周期为每个任务创建一个 promise 对象会使这种方法不尽如人意。