[譯]如何將初始化程式碼從 AppDelegate 中移除

嘎嘣脆發表於2018-01-25

翻譯自:https://christiantietze.de/posts/2015/10/bootstrapping-appdelegate/

我為Word Counter開發了一個簡單的框架,用來在啟動時管理和初始元件的引導程式碼。通過使用這種方法,優化掉了 AppDelegate 中 60 行初始化程式碼。

  • 元件對它自己進行初始化。
  • 已經初始化過的元件為一個佇列,當有新的元件初始化成功,它會被放進這個佇列中。

通過這種方式,你可以構建一系列需要初始化的元件的列表,然後依次初始化他們。

這個框架非常簡單:

enum BootstrappingError: ErrorType {
    case ExpectedComponentNotFound(String)
}

protocol Bootstrapping {

    func bootstrap(bootstrapped: Bootstrapped) throws
}

struct Bootstrapped {

    private let bootstrappedComponents: [Bootstrapping]

    init() {
    
        self.bootstrappedComponents = []
    }

    init(components: [Bootstrapping]) {
    
        self.bootstrappedComponents = components
    }

    func bootstrap(component: Bootstrapping) throws -> Bootstrapped{

        try component.bootstrap(self)
    
        return Bootstrapped(components: bootstrappedComponents.add(component))
    }

    func component<T: Bootstrapping>(componentType: T.Type) throws -> T {
    
        guard let found = bootstrappedComponents.findFirst({ $0 is T }) as? T else {
            throw BootstrappingError.ExpectedComponentNotFound("\(T.self)")
        }
    
        return found
    }
}

複製程式碼

bootstrap(_:) 控制元件的初始化,如果成功,初始化佇列增長。

component(_:) 查詢已經被初始化的元件。如果找到,返回找到的元件。如果這個元件未被成功初始化或者沒有找到,丟擲.ExpectedComponentNotFound

注意,我沒有將 bootstrappedComponents 定義為一個變數。也沒有將bootstrap(:_)方法設計為mutate。通過reduce進行管道式呼叫:

// Bootstrap the sequence of components
func bootstrapped(components: [Bootstrapping]) throws -> Bootstrapped {

    return try components.reduce(Bootstrapped()) { bootstrapped, next in

        return try bootstrapped.bootstrap(next)
    }
}
複製程式碼

bootstrapped(_:) 最初執行的時候,持有一個空的元件序列,嘗試初始化所有的元件並最終返回初始化後的元件序列。這很像使用for-each來進行迭代,只是更加簡潔,程式碼:

var result = Bootstrapped()
for nextComponent in components {
    result = result.bootstrap(nextComponent)
}
return result

複製程式碼

為了保持示例足夠簡單,元件在初始化時並沒有接受引數,在正式的程式碼中,這塊兒可能有點不一樣。

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    // ...
    
    let components: [Bootstrapping] = [
        ErrorReportBootstrapping(),
        PersistenceBootstrapping(),
        DomainBootstrapping(),
        ApplicationBootstrapping()
    ]

    // Will be set during bootstrapping
    var showWindowService: ShowMainWindow!

    func applicationDidFinishLaunching(aNotification: NSNotification) {

        // Prepare everything
        bootstrap()

        // Then do something, like displaying the main window
        showWindow()
    }
    
    func bootstrap() {
        
        do {

            let bootstrapped = try bootstrapped(components)
            let application = try bootstrapped.component(ApplicationBootstrapping.self)
    
            showWindowService = application.showWindowService
        } catch let error as NSError {
            showLaunchError(error)
            fatalError("Application launch failed: \(error)")
        }
    }

    func bootstrapped(components: [Bootstrapping]) throws -> Bootstrapped {

        return try components.reduce(Bootstrapped()) { bootstrapped, next in
    
            return try bootstrapped.bootstrap(next)
        }
    }
    
    func showLaunchError(error: NSError) {

        let alert = NSAlert()
        alert.messageText = "Application launch failed. Please report to support@christiantietze.de"
        alert.informativeText = "\(error)"

        alert.runModal()
    }
    
    func showWindow() {

        showWindowService.showWindow()
    }
}

複製程式碼

錯誤報告元件,如例所示。非常簡單,也很基礎。

class ErrorReportBootstrapping: Bootstrapping {

    let errorHandler = ErrorHandler()
    let invalidDataHandler = InvalidDataHandler()

    func bootstrap(bootstrapped: Bootstrapped) throws {
    
        ErrorAlert.emailer = TextEmailer()
    
        // Add objects to a global registry of services used application-wide.
        ServiceLocator.sharedInstance.errorHandler = errorHandler
        ServiceLocator.sharedInstance.missingURLHandler = invalidDataHandler
        ServiceLocator.sharedInstance.fileNotFoundHandler = invalidDataHandler
    
    }
}
複製程式碼

元件初始化的時候可以呼叫其他已經初始化的元件:

import Foundation

class DomainBootstrapping: Bootstrapping {

    var bananaPeeler: BananaPeeler!

    func bootstrap(bootstrapped: Bootstrapped) throws {
      
        // may throw .ExpectedComponentNotFound("PersistenceBootstrapping")
        let persistence = try bootstrapped.component(PersistenceBootstrapping.self)
    
        bananaPeeler = BananaPeeler(bananaStore: persistence.bananaStore)
    }
}

複製程式碼

最後初始化的 ApplicationBootstrapping 可以使用已經初始化的DomainBootstrapping 元件,並通過 bananaPeeler,並把它用於視窗控制器的事件處理。

一些初始化元件為 VIPER 架構的 wireframes 做類似的工作,他們準備 interactor,presenter和檢視。全部的初始化工作包括設定 wireframes,然後構建 AppDenpendencies,接著在其中初始化 Core Data 還有一些其他的工作。

將初始化程式碼拆分成獨立部分,並有序的組織在一起,這就是這個框架所做的事。我認為它比把一切都堆在 AppDependencies 物件中要好。

相關文章