Patterns: Plug-ins

Patterns: Plug-ins

ยท

9 min read

Heyyy ๐Ÿ‘‹

Long time no see ๐Ÿ˜„

And last I checked ๐Ÿง, I have an unfinished series...

CleanShot 2022-11-29 at 12.26.41@2x.png

But, truth be told, it's a complex topic with lots of details, but I also intend on finishing it, so don't worry, all in due time ๐Ÿ˜„

Problem

So, what's up today? Today is a very good day to discuss one of the bloaters we all know very well, The big guy, AppDelegate, it starts small, and a couple of integrations later, it's almost unreadable anymore

I know I had some AppDelegates that I really didn't like at all, sometimes it was refactored to a Facade, and sometimes it was left with comments and MARKs all over the file to make it easier to navigate. However, there is still this kind of background pain from going into this file, and then... it clicked!

image.png

What if I told you that patterns can be combined?

What are we trying to achieve?

Refactoring is always about asking the right questions. What am I trying to get after being done with this part of the code?

Something that is easier to read, and maintain, so I need to separate some stuff

So how to separate it?

But... How?

Let's break down the AppDelegate for a second

The AppDelegate is made for a couple of things... like notifying us when a lifecycle event occurred, a notification was received, a deep link triggered our app, and so on.

And unfortunately for us, these methods get bloated with code. Surely we can add private methods to ease our burden, but what if we broke down the app delegate itself? Cut it into different reusable parts, that instead of "it" handling everything for us, it provides its APIs, and different implementors get called, as if it were... a Plug-in?

Abstracting the AppDelegate

Since we're trying to break it down, it makes sense that each implementer adheres to the same protocol. So, let's say ->

public protocol ApplicationPlugin {
    func application(
        _ application: UIApplication,
        willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
    func application(
        _ application: UIApplication,
        continue userActivity: NSUserActivity,
        restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool

    func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication)
    func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication)

    func applicationWillTerminate(_ application: UIApplication)
    func applicationDidReceiveMemoryWarning(_ application: UIApplication)

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any])
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error)
}

This move allowed us to have multiple implementations, each to their own concern of that event, but they have to adhere to the way the AppDelegate works. In other words, if we have to support Firebase, Deeplinks, Push Notifications, and Google Maps

Instead of having one gigantic AppDelegate, we would have mini App Delegates but specific to these, something like this...

class AppearancePlugin: ApplicationPlugin { ... }
class UtilsPlugin: ApplicationPlugin { ... }
class FirebasePlugin: ApplicationPlugin { ... }
class NotificationPlugin: ApplicationPlugin { ... }
class GMSPlugin: ApplicationPlugin { ... }

Starting to make sense, eh? ๐Ÿ˜

Gluing parts together

Ok, Ramy, the concept is ok, but how do you tell the AppDelegate to call your plugins or break itself?

Well, that's the neat part... you don't

You actually change the perception of AppDelegate with our own

Huh?

See, any iOS App (and any other language or framework that has the concept of an entry-point) needs to see this annotation to know which class is the delegate that it needs to send lifecycle updates

CleanShot 2022-11-29 at 12.55.48@2x.png

And that's what we're going to do, so...

image.png

Creating the Plugins host

Since the vanilla AppDelegate is incompetent in hosting our Plugins, we'll give it some superpowers.

open class ApplicationPluggableDelegate: UIResponder, UIApplicationDelegate {
    public var window: UIWindow?

    /// List of application plugins for binding to `AppDelegate` events
    public private(set) lazy var pluginInstances: [ApplicationPlugin] = plugins()

    override public init() {
        super.init()

        // Load lazy property early
        _ = pluginInstances
    }

    /// List of application plugins for binding to `AppDelegate` events
    open func plugins() -> [ApplicationPlugin] { [] } // Override
}

And now that the PluggableDelegate is defined, we're going to implement each method and make it call the plugins so they can do their work.

extension ApplicationPluggableDelegate {
    open func application(
        _ application: UIApplication,
        willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil)
        -> Bool {
        // Ensure all delegates called even if condition fails early
        pluginInstances.reduce(true) {
            $0 && $1.application(application, willFinishLaunchingWithOptions: launchOptions)
        }
    }

    public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        pluginInstances.reduce(true) {
            $0 && $1.application(app, open: url, options: options)
        }
    }

// ... (they're kind of a lot, so feel free to copy the approach from the Github link)
}

Building the Plugins

Now, for the fun part, to not barrage the post with lots of code, let's focus on a tiny piece that is easy to digest, the part where you define the Appearances for your core UIKit/SwiftUI parts, like Tabbars & NavigationBars (...etc.)

import UIKit.UIApplication

// MARK: - AppearancePlugin

struct AppearancePlugin { }

// MARK: ApplicationPlugin

extension AppearancePlugin: ApplicationPlugin {
    func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        addTabBarAppearances()
        addNavigationBarAppearance()
        return true
    }
}

private extension AppearancePlugin {
    func addTabBarAppearances() {
        UITabBar.appearance().unselectedItemTintColor = .greysSpaceGrey
        UITabBar.appearance().tintColor = .primaryBluberry
        UITabBar.appearance().shadowImage = UIImage()
        UITabBar.appearance().backgroundImage = UIImage()
    }

    func addNavigationBarAppearance() {
        UINavigationBarAppearance().configureWithTransparentBackground()
    }
}

See, in this part, usually, the appearance logic would be encapsulated inside private methods in a larger file (which is a common way of implementing the AppDelegate), but here, we're more focused on the appearance, which makes it easier to read and understand mentally.

AppDelegate: Hosting the Plugins

Now for the wrap-up and the part which looks like a Command pattern combined with a flavor of Composite pattern...

@main
class AppDelegate: ApplicationPluggableDelegate {
    override func plugins() -> [ApplicationPlugin] {
        [
            ServicesPlugin(),
            AppearancesPlugin(),
            UtilsPlugin(),
            FirebasePlugin(),
            GoogleMapsPlugin(),
            NotificationsPlugin()
        ]
    }
}

This is the actual interface of what the AppDelegate does. Any logic involved is separated across two parts, the PluggableDelegate & the Plugin itself.

So, in a nutshell, this is what is happening from a higher-level perspective.

Improvement: #2 Interface Segregation

So, you may think that we have reached an acceptable level of improvement, which is true, btw. Our goal from the start was to separate things using a couple of patterns to the degree that makes dealing with the logic even easier, but there is indeed one improvement we can make, segregation if I may say

Categorizing is a good rule of thumb for segregation

Let's clear things out first. Let's agree that an AppDelegate like we said above, is a receptor of any events, but since events can be categorized, so does our protocol. For example, our protocol handles 3 parts, Lifecycle events, Deeplinking Events, and Push Notifications Events

Which, in the end, gives us conformation to the ISP (Interface Segregation Principle)

Good Rule of ๐Ÿ‘ is, whenever there's some methods to categorize, is an indicator that you can apply the ISP.

And if I were to use Firebase Messaging. In that case, I'd only be interested in just the Remote Notifications delegate methods, similarly for Deeplinking Events which are not "necessarily" coupled to Lifecycle, so segregating the delegate methods in our tiny protocol gives us the ability to be more flexible with our 3rd party integrations and allow ourselves to write more clean and concise code, now doesn't that sound "cool"?

My manager would disagree since we hit the goal already and should focus on something more important to the business, and we can iterate when needed (be wiser than me ๐Ÿ˜‚)

Segregating Notification Methods

So, let's see this on diagrams first, so it paves the way for easier understanding.

So, this is how this segregation made our diagram look like this โ˜๏ธ

Disclaimer, I am not very adept at UML diagraming, barely have a grasp over all the arrow types and such, but I figured this may be simpler and easier to explain with, so let me know if you think so, or should I really learn UML arrow types & legends if that would help ๐Ÿ™

As you can see, now we have our ApplicationPlugin Protocol broken and segregated into 3 different protocols, and now we can compose them, even more to make for a flexible design

Implementation time

Let's start by defining and default implementing our RemoteNotification Protocols

public protocol ApplicationNotificationPlugin: AnyObject, ApplicationPlugin {
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any])
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error)
}

// Then we define a default implementation so Plugins later on can pick which method to implement according to its needs
public extension ApplicationNotificationPlugin {
    func application(_: UIApplication, didReceiveRemoteNotification _: [AnyHashable: Any]) { }
    func application(_: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken _: Data) { }
    func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) { }
}

Then, back to our backend of the AppDelegate, let's instruct it on how to utilize the NotificationCenter's delegate methods

extension ApplicationPluggableDelegate: UNUserNotificationCenterDelegate {
    public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        // Hmmm, how to call the plugins?
    }

    public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // Hmmm, how to call the plugins?
    }

    public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        // Hmmm, how to call the plugins?
    }
}

Now that the PluggableDelegate knows about the UNUserNotificationCenterDelegate, it can channel those events to its children's plugins. A good way of doing this is .compactMap { ... }

A neat functional trick where u can filter & typecast a sequence without much hassle, and in 1 shot

public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        pluginInstances.compactMap { $0 as? ApplicationNotificationPlugin }.forEach {
            $0.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
        }
    }

Now, similarly, we can follow the same approach with other types of segregation

But, where is the fun in that, when you, my dear reader, can do that yourself, and make sure you understood the above concepts, and I can get back to GOW: Ragnarok?

Pin on ฯŸ|ANIME ICONS|ฯŸ

Conclusion

Quick Recap

So, this article, was a tough one, ngl. This is because you're not seeing a specific Design Pattern in action. If I counted correctly, you're seeing around 5 patterns combined, each taking an idea from the other until we reach the part where we end up with something easy to read, edit and extend, and probably that's what you'd want to gain from design patterns in general.

Quick Breakdown

But, to make it easier to grasp, some of the very obvious patterns (apart from the Delegate, of course) are the Command & Composite Patterns, which are the secret sauce of this recipe; however, more patterns were also inspired, like Builder patterns and the Factory, which you can find in the array of plugins, and the abstraction layer between each type; lastly, the strategy itself of which Plugin to run, which was the final segregation we've made.

hai | 60's Spider-Man | Know Your Meme

ย