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.
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.
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")!)
}
}
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"))
-
You MUST hold a strong reference to the
Assembler
otherwise theContainer
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