Modules and hooks in Swift


How do modules (plugins) work?

Would not be cool in case you may create objects that would work collectively with out figuring out about one another? Think about that you’re constructing a dynamic type. Based mostly on some inner situations, the fields are going to be composed utilizing the info coming from the enabled modules.

For instance you could have module A, B, C, the place A is offering you Discipline 1, 2, 3, the B module is taking good care of Discipline 4, 5 and C is the supplier of Discipline 6. Now in case you flip off B, it is best to solely be capable to see discipline 1, 2, 3 and 6. If every 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 functionalities by them. It is all primarily based on the idea I simply talked about above. That is a part of the event-driven structure 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 technique will likely be known as by the module supervisor to invoke the correct hook perform by title. We will move 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 below 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? {
        change 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? {
        change 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? {
        change 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 correct invocation technique on each single module and it will deal with the returned response in a type-safe method. We will implement two invoke technique variations instantly. One for merging the end result 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 end result = self.modules.map { module in
            module.invoke(title: title, params: params)
        }
        return end result.compactMap { $0 as? [T] }.flatMap { $0 }
    }

    func invokeHook<T>(_ title: String, sort: T.Sort, params: [String: Any] = [:]) -> T? {
        for module in self.modules {
            let end result = module.invoke(title: title, params: params)
            if end result != nil {
                return end result as? T
            }
        }
        return nil
    }
}

You should use the the invokeAllHooks technique to merge collectively an array of a generic sort. That is the one which we are able to use to assemble all he type 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 technique you’ll be able to obtain an analogous habits just like the chain of accountability design sample. The responder chain works very comparable comparable, Apple makes use of responders on virtually 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? {
        change 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 {
        "Howdy, that is module B."
    }
}

If we set off the brand new example_responder hook with the invokeHook technique on each managers we’ll see that the result is kind of completely 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 have now an implementation in certainly one of our modules for this hook, the return worth will likely be current, so we are able to print it. Within the second case there is no such thing as a module to deal with the occasion, so the block contained in the situation will not be executed. Informed ya’, it is like a responder chain. 😜

Conclusion

Utilizing modules or plugins is a strong method to decouple some elements of the code. I actually love hook capabilities since they will present extension factors for nearly something within the utility.

Combine this with a dynamic module loader and you’ve got a fully-extensible next-gen backend resolution on prime of Vapor. You’ll be able to have a compiled core system independently from the modules and afterward you’ll be able to improve just a few elements of the whole stuff with out touching the others. Whops… I simply made that occur and I believe (similar to Swift) it completely rulez. 🤘🏻

I am working onerous each on my upcoming Sensible server aspect Swift e book and the open-source weblog engine that is powering this web site for fairly some time now. I used this modular structure quite a bit through the creation of my engine. Cannot wait to launch every thing and present it to you. 😉

Recent Articles

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here

Stay on op - Ge the daily news in your inbox