How to Move Bootstrapping Code Out of AppDelegate
The test project I work on to develop a component of the Word Counter faster sets up a lot of service objects during launch. I shaved off 60 lines of AppDelegate
by introducing bootstrapping as a thing to my code.
- A
Bootstrapping
component sets up itself as it pleases. - A
Bootstrapped
collection tries to bootstrap new components. If the set-up succeeds, the collection grows.
This way you can prepare a list of Bootstrapping
components and iterate over them to set up one after another.
The framework is very simple:
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(_:)
in a command which potentially grows the collection – as long as no error is thrown.
component(_:)
is a query to obtain an already bootstrapped component, indicating a dependency. If the sequence is out of order, this query method will throw an .ExpectedComponentNotFound
error and the bootstrapping part of the app’s launch sequence will not succeed.
Note I haven’t declared bootstrappedComponents
as var
. Neither have I made bootstrap(:_)
mutating. That caused trouble when piping the bootstrapping with reduce
like this:
// 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(_:)
starts with an empty collection and sequentially tries to bootstrap every component, returning the complete collection in the end. This is like a for-each loop to add up numbers, only more terse:
var result = Bootstrapped()
for nextComponent in components {
result = result.bootstrap(nextComponent)
}
return result
To keep the following example simple, none of the components take any arguments during initialization, though in production I pass a few preset values in.
@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()
}
}
Error reporting, for example, is very simple. That’s by design since it’s so fundamental.
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
}
}
Objects can declare dependencies by querying the Bootstrapped
collection:
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)
}
}
And then ApplicationBootstrapping
will be able to fetch bananaPeeler
from the previously set-up DomainBootstrapping
and use it to delegate from the event handler for a window controller it sets up, say.
Some of the Bootstrapping
components do a similar job to VIPER’s wireframes which prepare interactor, presenter, and view. The whole Bootstrapped
collection wraps the set-up of wireframes, building what some (including me, usually) call an AppDependencies
object, holding the Core Data stack and other things at once, sequentially.
I like that the Bootstrapping
components work in isolation and group similar concepts. That works better than // MARK
‘ing groups of concepts in a single AppDependencies
container.
Photo Credit: Please Clean Up Your Mess by Allen Goldblatt. License: CC-BY-2.0.