了解如何使用以 Swift
编写的松耦合模块插件系统通过新功能扩展应用程序。
模块(插件)如何工作?
如果你可以创建可以在不知彼此的情况下一起工作的对象,那会很酷吗? 想象一下,你正在构建一个动态表单。 根据一些内部条件,将使用来自启用模块的数据来构成字段。
例如,你拥有模块 A
,B
,C
,其中 A
为您提供字段 1、2、3
,B
模块负责字段4、5,而 C
是字段6的提供者。现在,如果您关闭 B
, 您应该只能看到字段1、2、3和6。如果已打开所有内容,则应该看到从1到6的所有字段。
我们可以将完全相同的模式应用于许多事物。 试想一下最大的插件生态系统之一。 WordPress
使用 钩子(英文:hooks)
来扩展核心功能。 这些都是基于我上面刚刚提到的概念。 这是 事件驱动的体系结构设计模式(英文:Event-driven architecture)
的一部分。 现在的问题是,我们如何使用 Swift
实现类似的东西? 🤔
钩子系统的实现
首先,我们从带有调用点的协议开始。 模块管理器将调用此方法,以按名称调用正确的钩子函数。 我们将传递参数字典,因此我们的钩子可以有参数。 我们在这里使用 Any
类型作为值,因此你可以在给定键下将任何内容作为参数发送。
1 2 3 4 5 6 7
| protocol Module { func invoke(name: String, params: [String: Any]) -> Any? }
extension Module { func invoke(name: String, params: [String: Any]) -> Any? { nil } }
|
现在,让我们使用基于表单示例的简化版本来实现我们的模块。 🤓
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| class A: Module {
func invoke(name: String, params: [String: Any]) -> Any? { switch name { case "example_form": return self.exampleFormHook() default: return nil } }
private func exampleFormHook() -> [String] { ["Field 1", "Field 2", "Field 3"] } }
class B: Module { func invoke(name: String, params: [String: Any]) -> Any? { switch name { case "example_form": return self.exampleFormHook() default: return nil } }
private func exampleFormHook() -> [String] { ["Field 4", "Field 5"] } }
class C: Module { func invoke(name: String, params: [String: Any]) -> Any? { switch name { case "example_form": return self.exampleFormHook() default: return nil } }
private func exampleFormHook() -> [String] { ["Field 6"] } }
|
接下来,我们需要一个模块管理器,可以使用模块数组对其进行初始化。 该管理器将负责在每个模块上调用正确的调用方法,并将以类型安全的方式处理返回的响应。 我们将立即实现两个 invoke
方法版本。 一个用于合并结果,另一个用于返回挂钩的第一个结果。
你可以尝试实现一个可以使用 &&
运算符合并 Bool
值的版本。
这是我们使用两种通用方法的模块管理器实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| struct ModuleManager {
let modules: [Module] func invokeAllHooks<T>(_ name: String, type: T.Type, params: [String: Any] = [:]) -> [T] { let result = self.modules.map { module in module.invoke(name: name, params: params) } return result.compactMap { $0 as? [T] }.flatMap { $0 } }
func invokeHook<T>(_ name: String, type: T.Type, params: [String: Any] = [:]) -> T? { for module in self.modules { let result = module.invoke(name: name, params: params) if result != nil { return result as? T } } return nil } }
|
你可以使用 invokeAllHooks
方法将通用类型的数组合并在一起。 这是我们可以使用基础钩子方法收集他所有表单字段的方法。
1 2 3 4 5 6 7
| let manager1 = ModuleManager(modules: [A(), B(), C()]) let form1 = manager1.invokeAllHooks("example_form", type: String.self) print(form1)
let manager2 = ModuleManager(modules: [A(), C()]) let form2 = manager2.invokeAllHooks("example_form", type: String.self) print(form2)
|
使用 invokeHook
方法,你可以实现类似的行为,例如责任链设计模式。 响应程序链的工作方式非常相似,Apple
几乎在每个平台上都使用响应程序来处理 UI
事件。 让我通过更新模块 B
向你展示它的工作方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class B: Module { func invoke(name: String, params: [String: Any]) -> Any? { switch name { case "example_form": return self.exampleFormHook() case "example_responder": return self.exampleResponderHook() default: return nil } }
private func exampleFormHook() -> [String] { ["Field 4", "Field 5"] } private func exampleResponderHook() -> String { "Hello, this is module B." } }
|
如果我们在两个管理器上使用 invokeHook
方法触发新的 example_responder
挂钩,我们将看到结果完全不同。
1 2 3 4 5 6 7
| if let value = manager1.invokeHook("example_responder", type: String.self) { print(value) }
if let value = manager2.invokeHook("example_responder", type: String.self) { print(value) }
|
在第一种情况下,由于我们在其中一个模块中为此钩子实现了一个实现,因此将显示返回值,因此可以进行打印。 在第二种情况下,没有模块可以处理该事件,因此不会执行条件内的块。 告诉你,就像一个响应链。 😜
总结
使用模块或插件是将代码的某些部分解耦的有效方法。 我真的很喜欢钩子函数,因为它们可以为应用程序中的几乎所有内容提供扩展点。
将其与动态模块加载器混合使用,你将在 Vapor
之上拥有一个完全可扩展的下一代后端解决方案。 你可以独立于模块使用已编译的核心系统,以后可以仅升级整个组件的某些部分而无需接触其他部分。