Skip to content

Latest commit

 

History

History
170 lines (130 loc) · 5.23 KB

Assembler.md

File metadata and controls

170 lines (130 loc) · 5.23 KB

Modularizing Service Registration

This feature provides your implementation the ability to group related service definitions together in an Assembly. This allows your application to:

  • Keep things organized by keeping like services in one place.
  • Provide a shared Container.
  • Allow registering different assembly configurations, which is useful for swapping in mock implementations.
  • To be notified when the container is fully configured.

This feature is an opinionated way to how your can register services in your Container and using it is not required.

There are several parts to this feature.

Assembly

The Assembly is a protocol that is provided a shared Container where service definitions can be registered. The shared Container will contain all service definitions from every Assembly registered to the Assembler. Let's look at an example:

class ServiceAssembly: Assembly {
    func assemble(container: Container) {
        container.register(FooServiceProtocol.self) { r in
           return FooService()
        }
        container.register(BarServiceProtocol.self) { r in
           return BarService()
        }
    }
}

class ManagerAssembly: Assembly {
    func assemble(container: Container) {
        container.register(FooManagerProtocol.self) { r in
           return FooManager(service: r.resolve(FooServiceProtocol.self)!)
        }
        container.register(BarManagerProtocol.self) { r in
           return BarManager(service: r.resolve(BarServiceProtocol.self)!)
        }
    }
}

Here we have created 2 assemblies: 1 for services and 1 for managers. As you can see the ManagerAssembly leverages service definitions registered in the ServiceAssembly. Using this pattern the ManagerAssembly doesn't care where the FooServiceProtocol and BarServiceProtocol are registered, it just requires them to be registered else where.

Load aware

The Assembly allows the assembly to be aware when the container has been fully loaded by the Assembler.

Let's imagine you have an simple Logger class that can be configured with different log handlers:

protocol LogHandler {
    func log(message: String)
}

class Logger {

    class var sharedInstance: Logger!

    var logHandlers = [LogHandler]()

    func addHandler(logHandler: LogHandler) {
        logHandlers.append(logHandler)
    }

    func log(message: String) {
        for logHandler in logHandlers {
            logHandler.log(message)
        }
    }
}

This singleton is accessed in global logging functions to make it easy to add logging anywhere without having to deal with injects:

func logDebug(message: String) {
    Logger.sharedInstance.log("DEBUG: \(message)")
}

In order to configure the Logger shared instance in the container we will need to resolve the Logger after the Container has been built. Using a Assembly you can keep this bootstrapping in the assembly:

class LoggerAssembly: Assembly {
    func assemble(container: Container) {
        container.register(LogHandler.self, name: "console") { r in
            return ConsoleLogHandler()
        }
        container.register(LogHandler.self, name: "file") { r in
            return FileLogHandler()
        }
    }

    func loaded(resolver: Resolver) {
        Logger.sharedInstance.addHandler(
            resolver.resolve(LogHandler.self, name: "console")!)
        Logger.sharedInstance.addHandler(
            resolver.resolve(LogHandler.self, name: "file")!)
    }
}

Assembler

The Assembler is responsible for managing the Assembly instances and the Container. Using the Assembler, the Container is only exposed to assemblies registered with the assembler and only provides your application access via the Resolver protocol which limits registration access strictly to the assemblies.

Using the ServiceAssembly and ManagerAssembly above we can create our assembler:

let assembler = Assembler([
    ServiceAssembly(),
    ManagerAssembly()
])

Now you can resolve any components from either assembly:

let fooManager = assembler.resolver.resolve(FooManagerProtocol.self)!

You can also lazy load assemblies:

assembler.applyAssembly(LoggerAssembly())

The assembler also supports managing your property files as well via construction or lazy loading:

let assembler = Assembler([
        ServiceAssembly(),
        ManagerAssembly()
    ], propertyLoaders: [
        JsonPropertyLoader(bundle: .mainBundle(), name: "properties")
    ])

  // or lazy load them
assembler.applyPropertyLoader(
    JsonPropertyLoader(bundle: .mainBundle(), name: "properties"))

IMPORTANT:

  • You MUST hold a strong reference to the Assembler otherwise the Container will be deallocated along with your assembler

  • If you are lazy loading your properties and assemblies you must load your properties first if you want your properties to be available to load aware assemblies when loaded is called

  • If you are lazy loading assemblies and you want your load aware assemblies to be invoked after all assemblies have been loaded then you must use addAssemblies and pass all lazy loaded assemblies at once

Next page: Thread Safety

Table of Contents