How do modules (plugins) work?
Would not be cool in case you may create objects that would work collectively with out realizing about one another? Think about that you’re constructing a dynamic kind. Primarily based on some inner circumstances, the fields are going to be composed utilizing the info coming from the enabled modules.
For instance you have got module A, B, C, the place A is offering you Area 1, 2, 3, the B module is taking good care of Area 4, 5 and C is the supplier of Area 6. Now in case you flip off B, it is best to solely have the ability to see area 1, 2, 3 and 6. If every little thing is turned on it is best to see all of the fields from 1 to six.
We will apply this very same sample to many issues. Simply take into consideration one of many largest plugin ecosystem. WordPress is utilizing hooks to increase the core functinalities via them. It is all primarily based on the idea I simply talked about above. That is a part of the event-driven architecture design sample. Now the query is how can we implement one thing comparable utilizing Swift? π€
A hook system implementation
First we begin with a protocol with a degree of invocation. This methodology will likely be known as by the module supervisor to invoke the right hook perform by title. We’ll cross round a dictionary of parameters, so our hooks can have arguments. We’re utilizing the Any
sort right here as a price, so you’ll be able to ship something as a parameter beneath a given key.
protocol Module
func invoke(title: String, params: [String: Any]) -> Any?
extension Module
func invoke(title: String, params: [String: Any]) -> Any? nil
Now let’s implement our modules utilizing a simplified model primarily based on the shape instance. π€
class A: Module
func invoke(title: String, params: [String: Any]) -> Any?
swap title
case "example_form":
return self.exampleFormHook()
default:
return nil
non-public func exampleFormHook() -> [String]
["Field 1", "Field 2", "Field 3"]
class B: Module
func invoke(title: String, params: [String: Any]) -> Any?
swap title
case "example_form":
return self.exampleFormHook()
default:
return nil
non-public func exampleFormHook() -> [String]
["Field 4", "Field 5"]
class C: Module
func invoke(title: String, params: [String: Any]) -> Any?
swap title
case "example_form":
return self.exampleFormHook()
default:
return nil
non-public func exampleFormHook() -> [String]
["Field 6"]
Subsequent we want a module supervisor that may be initialized with an array of modules. This supervisor will likely be answerable for calling the appropriate invocation methodology on each single module and it will deal with the returned response in a type-safe method. We’ll implement two invoke methodology variations instantly. One for merging the outcome and the opposite to return the primary results of a hook.
You’ll be able to attempt to implement a model that may merge Bool
values utilizing the &&
operator
Right here is our module supervisor implementation with the 2 generic strategies:
struct ModuleManager
let modules: [Module]
func invokeAllHooks<T>(_ title: String, sort: T.Sort, params: [String: Any] = [:]) -> [T]
let outcome = self.modules.map module in
module.invoke(title: title, params: params)
return outcome.compactMap $0 as? [T] .flatMap $0
func invokeHook<T>(_ title: String, sort: T.Sort, params: [String: Any] = [:]) -> T?
for module in self.modules
let outcome = module.invoke(title: title, params: params)
if outcome != nil
return outcome as? T
return nil
You need to use the the invokeAllHooks
methodology to merge collectively an array of a generic sort. That is the one which we are able to use to collect all he kind fields utilizing the underlying hook strategies.
let manager1 = ModuleManager(modules: [A(), B(), C()])
let form1 = manager1.invokeAllHooks("example_form", sort: String.self)
print(form1)
let manager2 = ModuleManager(modules: [A(), C()])
let form2 = manager2.invokeAllHooks("example_form", sort: String.self)
print(form2)
Utilizing the invokeHook
methodology you’ll be able to obtain the same conduct just like the chain of accountability design sample. The responder chain works very comparable similiar, Apple makes use of responders on nearly each platform to deal with UI occasions. Let me present you the way it works by updating module B
. π
class B: Module
func invoke(title: String, params: [String: Any]) -> Any?
swap title
case "example_form":
return self.exampleFormHook()
case "example_responder":
return self.exampleResponderHook()
default:
return nil
non-public func exampleFormHook() -> [String]
["Field 4", "Field 5"]
non-public func exampleResponderHook() -> String
"Hey, that is module B."
If we set off the brand new example_responder
hook with the invokeHook
methodology on each managers we’ll see that the result is kind of totally different.
if let worth = manager1.invokeHook("example_responder", sort: String.self)
print(worth)
if let worth = manager2.invokeHook("example_responder", sort: String.self)
print(worth)
Within the first case, since we now have an implementation in one among our modules for this hook, the return worth will likely be current, so we are able to print it. Within the second case there isn’t a module to deal with the occasion, so the block contained in the situation will not be executed. Instructed ya’, it is like a responder chain. π