Design pattern - Command Pattern & mediator pattern & composite pattern ~ AppDelegate decoupling

preface

Hi Coder, I'm CoderStar!

This week, I will mainly share with you three design modes (command mode, mediator mode and combination mode) and their applications in the AppDelegate decoupling scenario, especially the combination mode, which precipitates the corresponding wheels for you to share.

At the same time, let me tell you about the plan of the following articles on Design Patterns Series. Because the articles related to design patterns will be sorted out in combination with the scenes we will actually encounter in development, the documents may be discontinuous. I hope you can understand that I will sort out most code examples of design patterns into the designpatterns demo [1] warehouse in the form of Playground, Therefore, there may be some cases of manually calling system functions in the code example.

At the same time, I recommend a good website for learning design patterns - in-depth design patterns [2]. Some UML diagrams involved in this article are also from this website.

scene

AppDelegate is the root object of the application, that is, the only proxy, and can be considered the core of every iOS project.

  • It provides exposure to application lifecycle events;

  • It ensures that the application interacts correctly with the system and other applications;

  • It usually assumes many responsibilities, which makes it difficult to change, expand and test.

With the iterative upgrading of business, new functions and businesses are added, and the amount of code in AppDelegate is also growing, resulting in its Massive. Common businesses in AppDelegate include:

  • Event handling and dissemination in the life cycle;

  • Manage UI stack configuration: select the initial view controller and perform root view controller conversion;

  • Manage background tasks;

  • Management notice;

  • Third party library initialization;

  • Manage equipment direction;

  • Set UIAppearance;

  • ...

And because AppDelegate will affect the whole APP, we will be careful when facing complex AppDelegate for fear that our changes will affect other functions. Therefore, the simplicity and clarity of AppDelegate is very important for a healthy iOS architecture.

Next, we use the above three design patterns to decouple AppDelegate and make it elegant.

Command mode

Command pattern is a behavior design pattern, which can transform a request into a separate object containing all the information related to the request. This transformation allows you to parameterize the method according to different requests, delay the execution of requests or put them in the queue, and implement revocable operations.

UML

Command mode URL graph

Implementation mode

  • Declare a command interface with only one execution method.

  • Extract the request and make it a specific command class that implements the command interface. Each class must have a set of member variables to hold request parameters and references to the actual recipient object. The values of all these variables must be initialized through the command constructor.

  • Find the class that is responsible for the sender. Add member variables to these classes that hold commands. The sender can only interact with its commands through the command interface. The sender itself usually does not create a command object, but obtains it through client code.

  • Modify the sender to execute the command instead of sending the request directly to the receiver.

  • The client must initialize objects in the following order:

  • Create recipients.

  • Create a command and associate it with the recipient if necessary.

  • Create a sender and associate it with a specific command.

Code example

import UIKit

//  MARK:  -  Command interface
protocol AppDelegateDidFinishLaunchingCommand {
    func execute()
}

//  MARK:  -  Initialize third-party commands
struct InitializeThirdPartiesCommand: AppDelegateDidFinishLaunchingCommand {
    func execute() {
        print("InitializeThirdPartiesCommand trigger")
    }
}

//  MARK:  -  Initialize rootViewController
struct InitialViewControllerCommand: AppDelegateDidFinishLaunchingCommand {
    let keyWindow: UIWindow

    func execute() {
        print("InitialViewControllerCommand trigger")
        keyWindow.rootViewController = UIViewController()
    }
}

//  MARK:  -  Command constructor
final class AppDelegateCommandsBuilder {
    private var window: UIWindow!

    func setKeyWindow(_ window: UIWindow) -> AppDelegateCommandsBuilder {
        self.window = window
        return self
    }

    func build() -> [AppDelegateDidFinishLaunchingCommand] {
        return [
            InitializeThirdPartiesCommand(),
            InitialViewControllerCommand(keyWindow: window),
        ]
    }
}

// MARK: - AppDelegate
///   Act as sender and client
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow()

        AppDelegateCommandsBuilder()
            .setKeyWindow(window!)
            .build()
            .forEach { $0.execute() }
        return true
    }
}


//  MARK:  -  Manual call
AppDelegate().application(UIApplication.shared, didFinishLaunchingWithOptions: nil)

In fact, the above transformation is not a command mode strictly followed. For example, there is no receiver role, and the sender and client are not completely separated. At the same time, AppDelegateCommandsBuilder is actually a builder mode, which is also commonly used. This mode will be explained separately later. If you want to see the complete command mode code example of the role, see the command code example [3].

After transforming AppDelegate using the Command mode, when we need to add processing logic to the callback, we do not need to modify AppDelegate, but directly add the corresponding Command class and add it in AppDelegateCommandsBuilder.

The disadvantages of this method must be obvious to you. The above code example only understands and couples the didFinishLaunch method, and does not transform other methods. If other methods are transformed, the above set also needs to be implemented, which will be somewhat redundant.

Intermediary model

Mediator pattern is a behavior design pattern that allows you to reduce chaotic dependencies between objects. This pattern restricts the direct interaction between objects, forcing them to cooperate through a mediator object.

In fact, developers should be very familiar with the mediator pattern, because in the MVC pattern, C is a typical mediator, which limits the direct interaction between M and V.

UML

Mediator pattern UML diagram

Code example

import UIKit

//  MARK:  -  Lifecycle event interface
protocol AppLifecycleListener {
    func onAppWillEnterForeground()
    func onAppDidEnterBackground()
    func onAppDidFinishLaunching()
}

//  MARK:  -  Interface is implemented by default, so that the implementation class can optionally implement methods
extension AppLifecycleListener {
    func onAppWillEnterForeground() {}
    func onAppDidEnterBackground() {}
    func onAppDidFinishLaunching() {}
}

//  MARK:  -  Implementation class
class AppLifecycleListenerImp1: AppLifecycleListener {
    func onAppDidEnterBackground() {

    }
}

class AppLifecycleListenerImp2: AppLifecycleListener {
    func onAppDidEnterBackground() {

    }
}

//  MARK:  -  tertium quid
class AppLifecycleMediator: NSObject {
    private let listeners: [AppLifecycleListener]

    init(listeners: [AppLifecycleListener]) {
        self.listeners = listeners
        super.init()
        subscribe()
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    ///   Subscribe to lifecycle events
    private func subscribe() {
        NotificationCenter.default.addObserver(self, selector: #selector(onAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(onAppDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(onAppDidFinishLaunching), name: UIApplication.didFinishLaunchingNotification, object: nil)
    }

    @objc private func onAppWillEnterForeground() {
        listeners.forEach { $0.onAppWillEnterForeground() }
    }

    @objc private func onAppDidEnterBackground() {
        listeners.forEach { $0.onAppDidEnterBackground() }
    }

    @objc private func onAppDidFinishLaunching() {
        listeners.forEach { $0.onAppDidFinishLaunching() }
    }

    //  MARK:  -  To add a new Listener, you can modify it here
    public static func makeDefaultMediator() -> AppLifecycleMediator {
        let listener1 = AppLifecycleListenerImp1()
        let listener2 = AppLifecycleListenerImp2()
        return AppLifecycleMediator(listeners: [listener1, listener2])
    }
}

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    ///   Build listeners and automatically subscribe to lifecycle notifications internally
    let mediator = AppLifecycleMediator.makeDefaultMediator()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }
}

As you can see above, applifecycle mediator is obviously a mediator through which lifecycle events can be propagated to specific users.

In fact, the mediator mode is also commonly used in component communication schemes. I'll introduce it to you later. If you are interested, you can learn about it yourself, that is, the CTMediator scheme we often call.

Combination mode

Composite mode is a structural design mode. You can use it to combine objects into a tree structure and use them like independent objects.

UML

Combined mode URL graph

In the AppDelegate scenario, AppDelegate is a root Composite role, and each business is a Leaf role. If it is applied to componentization, each component is a Leaf role or Composite role (components can be redistributed to each business Leaf).

Code example

//  MARK:  -  Interface, directly inheriting UIApplicationDelegate,   UNUserNotificationCenterDelegate two protocols.

///   Empty protocol, and each component module implements the protocol
public protocol ApplicationService: UIApplicationDelegate, UNUserNotificationCenterDelegate {}

///   It is convenient to obtain window in the component
extension ApplicationService {
    /// window
    public var window: UIWindow? {
        // swiftlint:disable:next redundant_nil_coalescing
        return UIApplication.shared.delegate?.window ?? nil
    }
}


//  MARK:  -  AppDelegate inheritance
open class ApplicationServiceManagerDelegate: UIResponder, UIApplicationDelegate {
    ///   Subclasses need to be assigned in the constructor
    public var window: UIWindow?

    ///   It is rewritten by the subclass and returns the plist file address containing the class name of each module implementing ApplicationService
    ///   plist file needs to be of type NSArray
    open var plistPath: String? { return nil }

    ///   It is rewritten by subclasses to return the classes that implement ApplicationService in each module
    open var services: [ApplicationService] {
        guard let path = plistPath else {
            return []
        }
        guard let applicationServiceNameArr = NSArray(contentsOfFile: path) else {
            return []
        }
        var applicationServiceArr = [ApplicationService]( "ApplicationService")
        for applicationServiceName in applicationServiceNameArr {
            if let applicationServiceNameStr = applicationServiceName as? String, let applicationService = NSClassFromString(applicationServiceNameStr), let module = applicationService as? NSObject.Type {
                let service = module.init()
                if let result = service as? ApplicationService {
                    applicationServiceArr.append(result)
                }
            }
        }
        return applicationServiceArr
    }

    public func getService(by type: ApplicationService.Type) -> ApplicationService? {
        for service in applicationServices where service.isMember(of: type) {
            return service
        }
        return nil
    }

    ///   Lazy load gets the calculation property services so that it is calculated only once
    private lazy var applicationServices: [ApplicationService] = {
        self.services
    }()
}

//  MARK:  -  The protocol is implemented by default and events are distributed to each Leaf
extension ApplicationServiceManagerDelegate {
    @available(iOS 3.0, *)
    open func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        var result = false
        for service in applicationServices {
            if service.application?(application, didFinishLaunchingWithOptions: launchOptions) ?? false {
                result = true
            }
        }
        return result
    }

    /**
    Implement the protocol methods one by one, and distribute the events to each Leaf
    */
}

//  MARK:  -  Mode of use

final class AppThemeApplicationService: NSObject, ApplicationService {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        /// setup AppTheme
        return true
    }
}

final class AppConfigApplicationService: NSObject, ApplicationService {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        /// setup AppConfig
        return true
    }
}

@UIApplicationMain
class AppDelegate: ApplicationServiceManagerDelegate {
     override var services: [ApplicationService] {
         return [
           AppConfigApplicationService(),
           AppThemeApplicationService(),
         ]
     }

     override init() {
         super.init()
         if window == nil {
             window = UIWindow()
         }
     }
}

From the above code example, we can see that each Leaf implements the ApplicationService protocol, which can get all callbacks that AppDelegate can get.

For AppDelegate, there will be no business logic inside. Because of the default implementation of the protocol, the tasks have been distributed to each Leaf by default. The remaining tasks are only to provide the list of leaves. Considering the use in the component environment, it does not directly reference each Leaf and provides the form of plist configuration file.

The decoupling scheme is improved, and the precipitated wheel address is application service manager [4]. The function is relatively lightweight, welcome to use.

In fact, Alibaba's BeeHive[5] is about the decoupling of AppDelegate, but it is a comprehensive component scheme, and the event distribution of AppDelegate is only a part of it.

last

The above three design modes can be selected or combined according to the actual situation of their respective projects. For example, the composite mode can be selected for shell engineering to distribute events to each component, and the command or mediator mode can be selected for event distribution within the component.

We should work harder!

Let's be CoderStar!

reference material

  • Refactoring Massive App Delegate[6]

reference material

[1]

DesignPatternsDemo: https://github.com/Coder-Star/DesignPatternsDemo

[2]

In depth design mode: https://refactoringguru.cn/design-patterns

[3]

Command code example: https://refactoringguru.cn/design-patterns/command/swift/example#example-0

[4]

ApplicationServiceManager: https://github.com/Coder-Star/LTXiOSUtils/blob/master/LTXiOSUtils/Classes/Util/ApplicationServiceManager.swift

[5]

BeeHive: https://github.com/alibaba/BeeHive

[6]

Refactoring Massive App Delegate: https://www.vadimbulavin.com/refactoring-massive-app-delegate/

It is very important to have a technical circle official account with a group of like-minded friends. Here is my technical public address. Dry cargo is only a technical topic here.

WeChat official account: CoderStar

Tags: iOS

Posted on Sat, 20 Nov 2021 09:24:24 -0500 by Prismatic