From 525b6f8b0bb788811e6338527b73df765df2c37f Mon Sep 17 00:00:00 2001 From: "mike.owens" Date: Wed, 9 Dec 2015 08:05:44 -0800 Subject: [PATCH 1/3] Adding Assembler feature for modular components --- Documentation/Assembler.md | 148 ++++++++++++++++ Documentation/Properties.md | 2 +- Documentation/README.md | 1 + README.md | 1 + Swinject.xcodeproj/project.pbxproj | 44 +++++ Swinject/Assembler.swift | 99 +++++++++++ Swinject/AssemblyType.swift | 34 ++++ Swinject/Container.swift | 2 +- SwinjectTests/AssemblerSpec.swift | 232 ++++++++++++++++++++++++++ SwinjectTests/BasicAssembly.swift | 47 ++++++ SwinjectTests/LoadAwareAssembly.swift | 32 ++++ 11 files changed, 640 insertions(+), 2 deletions(-) create mode 100644 Documentation/Assembler.md create mode 100644 Swinject/Assembler.swift create mode 100644 Swinject/AssemblyType.swift create mode 100644 SwinjectTests/AssemblerSpec.swift create mode 100644 SwinjectTests/BasicAssembly.swift create mode 100644 SwinjectTests/LoadAwareAssembly.swift diff --git a/Documentation/Assembler.md b/Documentation/Assembler.md new file mode 100644 index 00000000..9619bc7a --- /dev/null +++ b/Documentation/Assembler.md @@ -0,0 +1,148 @@ +# Modularizing Service Registration +This feature provides your implementation the ability to group related service definitions together +in an `AssemblyType`. This allows your application to: + + - keep things organized by keeping like services in 1 place + - provided a shared `Container` + - allows registering different assembly configurations which is useful for swapping out mock implementations + - can be "load aware" when the container is fully configured + +This feature is an opinionated way to how your can register services in your `Container`. There are +parts to this feature: + +## AssemblyType +The `AssemblyType` 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 +`AssemblyType` registered to the `Assembler`. Let's look at an example: + + class ServiceAssembly: AssemblyType { + func assemble(container: Container) { + container.register(FooServiceType.self) { r in + return FooService() + } + container.register(BarServiceType.self) { r in + return BarService() + } + } + } + + class ManagerAssembly: AssemblyType { + func assemble(container: Container) { + container.register(FooManagerType.self) { r in + return FooManager(service: r.resolve(FooServiceType.self)!) + } + container.register(BarManagerType.self) { r in + return BarManager(service: r.resolve(BarServiceType.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 `FooServiceType` and `BarServiceType` are registered, it just requires them to +be registered else where. + +### AssemblyLoadAwareType +The `AssemblyLoadAwareType` supports 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 LogHandlerType { + func log(message: String) + } + + class Logger { + + class var sharedInstance: Logger! + + var logHandlers = [LogHandlerType]() + + func addHandler(logHandler: LogHandlerType) { + 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 `AssemblyLoadAwareType` you can keep this +bootstrapping in the assembly: + + class LoggerAssembly: AssemblyLoadAwareType { + func assemble(container: Container) { + container.register(LogHandlerType.self, name: "console") { r in + return ConsoleLogHandler() + } + container.register(LogHandlerType.self, name: "file") { r in + return FileLogHandler() + } + } + + func loaded(resolver: ResolverType) { + Logger.sharedInstance.addHandler( + resolver.resolve(LogHandlerType.self, name: "console")!) + Logger.sharedInstance.addHandler( + resolver.resolve(LogHandlerType.self, name: "file")!) + } + } + +## Assembler +The `Assembler` is responsible for managing the `AssemblyType` 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 `ResolverType` protocol which limits registration +access strictly to the assemblies. + +Using the `ServiceAssembly` and `ManagerAssembly` above we can create our assembler: + + let assembler = try! Assembler(assemblies: [ + ServiceAssembly(), + ManagerAssembly() + ]) + +Now you can resolve any components from either assembly: + + let fooManager = assembler.resolver.resolve(FooManagerType.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 = try! Assembler(assemblies: [ + 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 + +_[Table of Contents](README.md)_ diff --git a/Documentation/Properties.md b/Documentation/Properties.md index 0a76c651..ceb002f8 100644 --- a/Documentation/Properties.md +++ b/Documentation/Properties.md @@ -111,6 +111,6 @@ And: The resulting value for `items` would be: `[ "hello from B" ]` -_[Next page: Thread Safety](ThreadSafety.md)_ +_[Next page: Modularizing Service Registration](Assembler.md)_ _[Table of Contents](README.md)_ diff --git a/Documentation/README.md b/Documentation/README.md index 45394a71..cbd22c38 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -17,6 +17,7 @@ Swinject is a lightweight dependency injection framework for Swift apps. It help 1. [Container Hierarchy](ContainerHierarchy.md) 2. [Properties](Properties.md) 3. [Thread Safety](ThreadSafety.md) +4. [Modularizing Service Registration](Assembler.md) ### UI Features diff --git a/README.md b/README.md index 187f28cc..5cb18dd2 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Dependency injection (DI) is a software design pattern that implements Inversion - [x] [Container Hierarchy](./Documentation/ContainerHierarchy.md) - [x] [Property Injection from Resource files](./Documentation/Properties.md) - [x] [Thread Safety](./Documentation/ThreadSafety.md) +- [x] [Modular Components](./Documentation/Assembler.md) - [x] [Storyboard](./Documentation/Storyboard.md) ## Requirements diff --git a/Swinject.xcodeproj/project.pbxproj b/Swinject.xcodeproj/project.pbxproj index 47f4a7e0..98c2aa77 100644 --- a/Swinject.xcodeproj/project.pbxproj +++ b/Swinject.xcodeproj/project.pbxproj @@ -7,6 +7,23 @@ objects = { /* Begin PBXBuildFile section */ + 90B029751C18599200A6A521 /* AssemblyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029741C18599200A6A521 /* AssemblyType.swift */; }; + 90B029761C18599200A6A521 /* AssemblyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029741C18599200A6A521 /* AssemblyType.swift */; }; + 90B029771C18599200A6A521 /* AssemblyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029741C18599200A6A521 /* AssemblyType.swift */; }; + 90B029791C185B1600A6A521 /* Assembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029781C185B1600A6A521 /* Assembler.swift */; }; + 90B0297A1C185B1600A6A521 /* Assembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029781C185B1600A6A521 /* Assembler.swift */; }; + 90B0297B1C185B1600A6A521 /* Assembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029781C185B1600A6A521 /* Assembler.swift */; }; + 90B0297C1C185B1600A6A521 /* Assembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029781C185B1600A6A521 /* Assembler.swift */; }; + 90B0297D1C185B2200A6A521 /* AssemblyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029741C18599200A6A521 /* AssemblyType.swift */; }; + 90B0297F1C18666200A6A521 /* AssemblerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B0297E1C18666200A6A521 /* AssemblerSpec.swift */; }; + 90B029801C18666200A6A521 /* AssemblerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B0297E1C18666200A6A521 /* AssemblerSpec.swift */; }; + 90B029811C18666200A6A521 /* AssemblerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B0297E1C18666200A6A521 /* AssemblerSpec.swift */; }; + 90B029831C18670000A6A521 /* BasicAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029821C18670000A6A521 /* BasicAssembly.swift */; }; + 90B029841C18670000A6A521 /* BasicAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029821C18670000A6A521 /* BasicAssembly.swift */; }; + 90B029851C18670000A6A521 /* BasicAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B029821C18670000A6A521 /* BasicAssembly.swift */; }; + 90B0298B1C186D5300A6A521 /* LoadAwareAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B0298A1C186D5300A6A521 /* LoadAwareAssembly.swift */; }; + 90B0298C1C186D5300A6A521 /* LoadAwareAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B0298A1C186D5300A6A521 /* LoadAwareAssembly.swift */; }; + 90B0298D1C186D5300A6A521 /* LoadAwareAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90B0298A1C186D5300A6A521 /* LoadAwareAssembly.swift */; }; 90D409F41C14E5F9009DF1B1 /* PropertyLoaderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90D409F31C14E5F9009DF1B1 /* PropertyLoaderType.swift */; }; 90D409F61C14E69F009DF1B1 /* JsonPropertyLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90D409F51C14E69F009DF1B1 /* JsonPropertyLoader.swift */; }; 90E70A201C14FADE00F12C2A /* first.json in Resources */ = {isa = PBXBuildFile; fileRef = 90E70A1B1C14F77D00F12C2A /* first.json */; }; @@ -281,6 +298,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 90B029741C18599200A6A521 /* AssemblyType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssemblyType.swift; sourceTree = ""; }; + 90B029781C185B1600A6A521 /* Assembler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assembler.swift; sourceTree = ""; }; + 90B0297E1C18666200A6A521 /* AssemblerSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssemblerSpec.swift; sourceTree = ""; }; + 90B029821C18670000A6A521 /* BasicAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicAssembly.swift; sourceTree = ""; }; + 90B0298A1C186D5300A6A521 /* LoadAwareAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadAwareAssembly.swift; sourceTree = ""; }; 90D409F31C14E5F9009DF1B1 /* PropertyLoaderType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PropertyLoaderType.swift; sourceTree = ""; }; 90D409F51C14E69F009DF1B1 /* JsonPropertyLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JsonPropertyLoader.swift; sourceTree = ""; }; 90E70A1B1C14F77D00F12C2A /* first.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = first.json; sourceTree = ""; }; @@ -550,6 +572,8 @@ 981ABE831B5FC9DF00294975 /* Swinject */ = { isa = PBXGroup; children = ( + 90B029781C185B1600A6A521 /* Assembler.swift */, + 90B029741C18599200A6A521 /* AssemblyType.swift */, 981899E31B5FFE5800C702D0 /* Container.swift */, 98B012BA1B82D6A400053A32 /* Container.Arguments.erb */, 984774EF1C02F25D0092A757 /* SynchronizedResolver.swift */, @@ -584,6 +608,7 @@ children = ( 90E70A1D1C14F78600F12C2A /* Resources */, 9878C63A1B65CA0600CBEFEF /* Fakes */, + 90B0297E1C18666200A6A521 /* AssemblerSpec.swift */, 981899E01B5FF88800C702D0 /* ContainerSpec.swift */, 9848611B1B6F9B7000C07072 /* ContainerSpec.Arguments.swift */, 9855C5BE1B686F5900DADB0B /* ContainerSpec.Circularity.swift */, @@ -666,6 +691,8 @@ 9855C5C41B689B7B00DADB0B /* FoodType.swift */, 981577F01B675B7F00BF686B /* Circularity.swift */, 90E70A1E1C14F95700F12C2A /* Properties.swift */, + 90B029821C18670000A6A521 /* BasicAssembly.swift */, + 90B0298A1C186D5300A6A521 /* LoadAwareAssembly.swift */, ); name = Fakes; sourceTree = ""; @@ -1139,6 +1166,8 @@ 9880E70F1C09EE2900ED5293 /* FunctionType.swift in Sources */, 981650351B6D0C0E00BC4222 /* _SwinjectStoryboardBase.m in Sources */, 98B012C01B82F68E00053A32 /* Resolvable.swift in Sources */, + 90B0297A1C185B1600A6A521 /* Assembler.swift in Sources */, + 90B029761C18599200A6A521 /* AssemblyType.swift in Sources */, 986A58281B6CCC9600FE710F /* Container+Typealias.swift in Sources */, 98D06D4E1B6CC7C00081AFE0 /* SwinjectStoryboard.swift in Sources */, 984774F11C02F25D0092A757 /* SynchronizedResolver.swift in Sources */, @@ -1156,6 +1185,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 90B029841C18670000A6A521 /* BasicAssembly.swift in Sources */, 90E70A3E1C15458B00F12C2A /* Properties.swift in Sources */, 9855C5C61B689B7B00DADB0B /* FoodType.swift in Sources */, 9855C5C91B689D9000DADB0B /* ServiceEntrySpec.swift in Sources */, @@ -1165,8 +1195,10 @@ 9878C63D1B65CA8700CBEFEF /* PersonType.swift in Sources */, 98D462781BE76D500006D45A /* ViewController1.swift in Sources */, 9884E2AB1B60C51C00120259 /* ServiceKeySpec.swift in Sources */, + 90B0298C1C186D5300A6A521 /* LoadAwareAssembly.swift in Sources */, 9855C5C31B68721800DADB0B /* ResolutionPoolSpec.swift in Sources */, 986A58331B6D002D00FE710F /* NSViewController+SwinjectSpec.swift in Sources */, + 90B029801C18666200A6A521 /* AssemblerSpec.swift in Sources */, 9847BF861BC93D36004FE09D /* NSStoryboard+SwizzlingSpec.swift in Sources */, 90E70A4A1C154A7D00F12C2A /* PlistPropertyLoaderSpec.swift in Sources */, 90E70A461C1549A000F12C2A /* JsonPropertyLoaderSpec.swift in Sources */, @@ -1188,6 +1220,7 @@ 90D409F41C14E5F9009DF1B1 /* PropertyLoaderType.swift in Sources */, 986A582F1B6CFA3400FE710F /* RegistrationNameAssociatable.swift in Sources */, 983B98311C06ECB2006A23D4 /* SpinLock.swift in Sources */, + 90B029791C185B1600A6A521 /* Assembler.swift in Sources */, 9880E70E1C09EE2900ED5293 /* FunctionType.swift in Sources */, 985BAFA91B625E0F0055F998 /* ServiceEntry.swift in Sources */, 98D06D3F1B6BA7B60081AFE0 /* UIViewController+Swinject.swift in Sources */, @@ -1207,6 +1240,7 @@ 9819701F1B6145D600EEB942 /* ObjectScope.swift in Sources */, 989377C61BCBA125008F4B0F /* SwinjectStoryboardType.swift in Sources */, 984774FA1C02F5A50092A757 /* SynchronizedResolver.Arguments.swift in Sources */, + 90B029751C18599200A6A521 /* AssemblyType.swift in Sources */, 98C7D25A1C09B5A1005C7184 /* Container+SwinjectStoryboard.swift in Sources */, 90E70A5A1C17279300F12C2A /* PropertyLoaderError.swift in Sources */, ); @@ -1222,13 +1256,16 @@ 9809A75F1B6B5288005E037E /* SwinjectStoryboardSpec.swift in Sources */, 9878C63C1B65CA8700CBEFEF /* PersonType.swift in Sources */, 98D06D411B6BCDA20081AFE0 /* UIViewController+SwinjectSpec.swift in Sources */, + 90B0298B1C186D5300A6A521 /* LoadAwareAssembly.swift in Sources */, 9848611C1B6F9B7000C07072 /* ContainerSpec.Arguments.swift in Sources */, + 90B0297F1C18666200A6A521 /* AssemblerSpec.swift in Sources */, 90E70A491C154A7D00F12C2A /* PlistPropertyLoaderSpec.swift in Sources */, 9884E2AA1B60C51C00120259 /* ServiceKeySpec.swift in Sources */, 9855C5C21B68721800DADB0B /* ResolutionPoolSpec.swift in Sources */, 984774FF1C034C5C0092A757 /* SynchronizedContainerSpec.swift in Sources */, 98D06D3D1B6B816C0081AFE0 /* AnimalViewController.swift in Sources */, 9878C6381B65C9E000CBEFEF /* AnimalType.swift in Sources */, + 90B029831C18670000A6A521 /* BasicAssembly.swift in Sources */, 90E70A451C1549A000F12C2A /* JsonPropertyLoaderSpec.swift in Sources */, 98E9469C1BC930A100FA6B37 /* UIStoryboard+SwizzlingSpec.swift in Sources */, 981899E11B5FF88800C702D0 /* ContainerSpec.swift in Sources */, @@ -1258,9 +1295,11 @@ 9850111F1BBE7E8900A2CCFC /* ServiceKey.swift in Sources */, 90E70A5C1C17279300F12C2A /* PropertyLoaderError.swift in Sources */, 98C7D25C1C09B5A1005C7184 /* Container+SwinjectStoryboard.swift in Sources */, + 90B0297B1C185B1600A6A521 /* Assembler.swift in Sources */, 985011201BBE7E8900A2CCFC /* ServiceEntry.swift in Sources */, 985011241BBE7E8900A2CCFC /* Container.Arguments.swift in Sources */, 984774F21C02F25D0092A757 /* SynchronizedResolver.swift in Sources */, + 90B029771C18599200A6A521 /* AssemblyType.swift in Sources */, 984774FC1C02F5A50092A757 /* SynchronizedResolver.Arguments.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1269,6 +1308,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 90B0297D1C185B2200A6A521 /* AssemblyType.swift in Sources */, 90E70A581C16885800F12C2A /* PropertyRetrievable.swift in Sources */, 90E70A331C15429600F12C2A /* PropertyLoaderType.swift in Sources */, 90E70A341C15429600F12C2A /* JsonPropertyLoader.swift in Sources */, @@ -1287,6 +1327,7 @@ 98689C9C1BBFC7EB0005C6D3 /* ResolutionPool.swift in Sources */, 98689CA31BBFC7F20005C6D3 /* UIViewController+Swinject.swift in Sources */, 98689C9A1BBFC7EB0005C6D3 /* ServiceKey.swift in Sources */, + 90B0297C1C185B1600A6A521 /* Assembler.swift in Sources */, 98689C9B1BBFC7EB0005C6D3 /* ServiceEntry.swift in Sources */, 98689CA51BBFC8080005C6D3 /* _SwinjectStoryboardBase.m in Sources */, 98689C9F1BBFC7EB0005C6D3 /* Container.Arguments.swift in Sources */, @@ -1307,13 +1348,16 @@ 98689CC91BBFD5B90005C6D3 /* SwinjectStoryboardSpec.swift in Sources */, 98689CBC1BBFD5110005C6D3 /* ContainerSpec.Circularity.swift in Sources */, 98689CB61BBFD5110005C6D3 /* PersonType.swift in Sources */, + 90B0298D1C186D5300A6A521 /* LoadAwareAssembly.swift in Sources */, 98689CBA1BBFD5110005C6D3 /* ContainerSpec.swift in Sources */, + 90B029811C18666200A6A521 /* AssemblerSpec.swift in Sources */, 90E70A4B1C154A7D00F12C2A /* PlistPropertyLoaderSpec.swift in Sources */, 98689CBD1BBFD5110005C6D3 /* ServiceKeySpec.swift in Sources */, 98689CCA1BBFD5B90005C6D3 /* UIViewController+SwinjectSpec.swift in Sources */, 984775011C034C5C0092A757 /* SynchronizedContainerSpec.swift in Sources */, 98689CC81BBFD5B30005C6D3 /* AnimalViewController.swift in Sources */, 98689CB81BBFD5110005C6D3 /* FoodType.swift in Sources */, + 90B029851C18670000A6A521 /* BasicAssembly.swift in Sources */, 90E70A471C1549A000F12C2A /* JsonPropertyLoaderSpec.swift in Sources */, 98A430721BCE2E5800B6B588 /* UIStoryboard+SwizzlingSpec.swift in Sources */, 98689CB71BBFD5110005C6D3 /* AnimalType.swift in Sources */, diff --git a/Swinject/Assembler.swift b/Swinject/Assembler.swift new file mode 100644 index 00000000..ba0afce1 --- /dev/null +++ b/Swinject/Assembler.swift @@ -0,0 +1,99 @@ +// +// Assembler.swift +// Swinject +// +// Created by mike.owens on 12/9/15. +// Copyright © 2015 Swinject Contributors. All rights reserved. +// + + +/// The `Assembler` provides a means to build a container via `AssemblyType` instances. +public class Assembler { + + /// the container that each assembly will build its `Service` definitions into + private let container: Container + + /// expose the container as a resolver so `Service` registration only happens within an assembly + public var resolver: ResolverType { + return container + } + + /// Will create an empty `Assembler` + /// + /// - parameter container: the baseline container + /// + public init(container: Container? = Container()) { + self.container = container! + } + + /// Will create a new `Assembler` with the given `AssemblyType` instances to build a `Container` + /// + /// - parameter assemblies: the list of assemblies to build the container from + /// - parameter propertyLoaders: a list of property loaders to apply to the container + /// - parameter container: the baseline container + /// + public init(assemblies: [AssemblyType], propertyLoaders: [PropertyLoaderType]? = nil, container: Container? = Container()) throws { + self.container = container! + if let propertyLoaders = propertyLoaders { + for propertyLoader in propertyLoaders { + try self.container.applyPropertyLoader(propertyLoader) + } + } + runAssemblies(assemblies) + } + + /// Will apply the assembly to the container. This is useful if you want to lazy load an assembly into the assembler's + /// container. + /// + /// If this assembly type is load aware, the loaded hook will be invoked right after the container has assembled + /// since after each call to `addAssembly` the container is fully loaded in its current state. If you wish to + /// lazy load several assemblies that have interdependencies between each other use `appyAssemblies` + /// + /// - parameter assembly: the assembly to apply to the container + /// + public func applyAssembly(assembly: AssemblyType) { + assembly.assemble(container) + if let assemblyLoadAware = assembly as? AssemblyLoadAwareType { + assemblyLoadAware.loaded(resolver) + } + } + + /// Will apply the assemblies to the container. This is useful if you want to lazy load several assemblies into the assembler's + /// container + /// + /// If this assembly type is load aware, the loaded hook will be invoked right after the container has assembled + /// since after each call to `addAssembly` the container is fully loaded in its current state. + /// + /// - parameter assemblies: the assemblies to apply to the container + /// + public func applyAssemblies(assemblies: [AssemblyType]) { + runAssemblies(assemblies) + } + + /// Will apply a property loader to the container. This is useful if you want to lazy load your assemblies or build + /// your assembler manually + /// + /// - parameter propertyLoader: the property loader to apply to the assembler's container + /// + /// - throws: PropertyLoaderError + /// + public func applyPropertyLoader(propertyLoader: PropertyLoaderType) throws { + try self.container.applyPropertyLoader(propertyLoader) + } + + // MARK: Private + + private func runAssemblies(assemblies: [AssemblyType]) { + // build the container from each assembly + for assembly in assemblies { + assembly.assemble(self.container) + } + + // inform all of the assemblies that the container is loaded + for assembly in assemblies { + if let assemblyLoadAware = assembly as? AssemblyLoadAwareType { + assemblyLoadAware.loaded(self.resolver) + } + } + } +} diff --git a/Swinject/AssemblyType.swift b/Swinject/AssemblyType.swift new file mode 100644 index 00000000..067310ff --- /dev/null +++ b/Swinject/AssemblyType.swift @@ -0,0 +1,34 @@ +// +// AssemblyType.swift +// Swinject +// +// Created by mike.owens on 12/9/15. +// Copyright © 2015 Swinject Contributors. All rights reserved. +// + + +/// The `AssemblyType` provides a means to organize your `Service` registration in logic groups which allows +/// the user to swap out different implementations of `Services` by providing different `AssemblyType` instances +/// to the `Assembler` +public protocol AssemblyType { + + /// Provide hook for `Assembler` to load Services into the provided container + /// + /// - parameter container: the container provided by the `Assembler` + /// + func assemble(container: Container) +} + +/// The `AssemblyLoadAwareType` provies a means for the `Assembler` to inform the `AssemblyType` that the container +/// has been loaded. This hook is useful for when an `AssemblyType` needs to run some code after the +/// container has registered all `Services` from all `AssemblyType` instances passed to the `Assembler`. For +/// example, one might want to setup some 3rd party services or load some `Services` into a singleton +public protocol AssemblyLoadAwareType: AssemblyType { + + /// Provides a hook to the `AssemblyLoadAwareType` that will be called once the `Assembler` has loaded all `AssemblyType` + /// instances into the container. + /// + /// - parameter resolver: the resolver that can resolve instances from the built container + /// + func loaded(resolver: ResolverType) +} diff --git a/Swinject/Container.swift b/Swinject/Container.swift index 30a442a7..95fe6f78 100644 --- a/Swinject/Container.swift +++ b/Swinject/Container.swift @@ -100,7 +100,7 @@ public final class Container { /// /// - parameter loader: the loader to load properties into the container /// - public func applyPropertyLoader(loader: T) throws { + public func applyPropertyLoader(loader: PropertyLoaderType) throws { let props = try loader.load() for (key, value) in props { properties[key] = value diff --git a/SwinjectTests/AssemblerSpec.swift b/SwinjectTests/AssemblerSpec.swift new file mode 100644 index 00000000..bf888349 --- /dev/null +++ b/SwinjectTests/AssemblerSpec.swift @@ -0,0 +1,232 @@ +// +// AssemblerSpec.swift +// Swinject +// +// Created by mike.owens on 12/9/15. +// Copyright © 2015 Swinject Contributors. All rights reserved. +// + +import Foundation +import Quick +import Nimble +import Swinject + +class AssemblerSpec: QuickSpec { + override func spec() { + + describe("Assembler basic init") { + it("can assembly a single container") { + let assembler = try! Assembler(assemblies: [ + AnimalAssembly() + ]) + let cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Whiskers" + + let sushi = assembler.resolver.resolve(FoodType.self) + expect(sushi).to(beNil()) + } + + it("can assembly a multiple container") { + let assembler = try! Assembler(assemblies: [ + AnimalAssembly(), + FoodAssembly() + ]) + let cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Whiskers" + + let sushi = assembler.resolver.resolve(FoodType.self) + expect(sushi).toNot(beNil()) + expect(sushi is Sushi) == true + } + + it("can assembly a multiple container with inter dependencies") { + let assembler = try! Assembler(assemblies: [ + AnimalAssembly(), + FoodAssembly(), + PersonAssembly() + ]) + let petOwner = assembler.resolver.resolve(PetOwner.self) + expect(petOwner).toNot(beNil()) + + let cat = petOwner!.pet + expect(cat).toNot(beNil()) + expect(cat!.name) == "Whiskers" + + let sushi = petOwner!.favoriteFood + expect(sushi).toNot(beNil()) + expect(sushi is Sushi) == true + } + + it("can assembly a multiple container with inter dependencies in any order") { + let assembler = try! Assembler(assemblies: [ + PersonAssembly(), + AnimalAssembly(), + FoodAssembly(), + ]) + let petOwner = assembler.resolver.resolve(PetOwner.self) + expect(petOwner).toNot(beNil()) + + let cat = petOwner!.pet + expect(cat).toNot(beNil()) + expect(cat!.name) == "Whiskers" + + let sushi = petOwner!.favoriteFood + expect(sushi).toNot(beNil()) + expect(sushi is Sushi) == true + } + } + + describe("Assembler lazy build") { + it("can assembly a single container") { + let assembler = try! Assembler(assemblies: []) + var cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).to(beNil()) + + assembler.applyAssembly(AnimalAssembly()) + + cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Whiskers" + } + + it("can assembly a multiple containers 1 by 1") { + let assembler = try! Assembler(assemblies: []) + var cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).to(beNil()) + + var sushi = assembler.resolver.resolve(FoodType.self) + expect(sushi).to(beNil()) + + assembler.applyAssembly(AnimalAssembly()) + + cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Whiskers" + + sushi = assembler.resolver.resolve(FoodType.self) + expect(sushi).to(beNil()) + + assembler.applyAssembly(FoodAssembly()) + + sushi = assembler.resolver.resolve(FoodType.self) + expect(sushi).toNot(beNil()) + } + + it("can assembly a multiple containers at once") { + let assembler = try! Assembler(assemblies: []) + var cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).to(beNil()) + + var sushi = assembler.resolver.resolve(FoodType.self) + expect(sushi).to(beNil()) + + assembler.applyAssemblies([ + AnimalAssembly(), + FoodAssembly() + ]) + + cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Whiskers" + + sushi = assembler.resolver.resolve(FoodType.self) + expect(sushi).toNot(beNil()) + } + } + + describe("Assembler load aware") { + it("can assembly a single container") { + let loadAwareAssembly = LoadAwareAssembly() { resolver in + let cat = resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Bojangles" + } + + expect(loadAwareAssembly.loaded) == false + let assembler = try! Assembler(assemblies: [ + loadAwareAssembly + ]) + expect(loadAwareAssembly.loaded) == true + + let cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Bojangles" + } + + it("can assembly a multiple container") { + let loadAwareAssembly = LoadAwareAssembly() { resolver in + let cat = resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Bojangles" + + let sushi = resolver.resolve(FoodType.self) + expect(sushi).toNot(beNil()) + } + + expect(loadAwareAssembly.loaded) == false + let assembler = try! Assembler(assemblies: [ + loadAwareAssembly, + FoodAssembly() + ]) + expect(loadAwareAssembly.loaded) == true + + let cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Bojangles" + + let sushi = assembler.resolver.resolve(FoodType.self) + expect(sushi).toNot(beNil()) + } + } + + describe("Assembler with properties") { + it("can assembly with properties") { + let assembler = try! Assembler(assemblies: [ + PropertyAsssembly() + ], propertyLoaders: [ + PlistPropertyLoader(bundle: NSBundle(forClass: self.dynamicType.self), name: "first") + ]) + + let cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "first" + } + + it("can't assembly with missing properties") { + expect { + try Assembler(assemblies: [ + PropertyAsssembly() + ], propertyLoaders: [ + PlistPropertyLoader(bundle: NSBundle(forClass: self.dynamicType.self), name: "noexist") + ]) + }.to(throwError(errorType: PropertyLoaderError.self)) + } + } + + describe("Empty Assembler") { + it("can create an empty assembler and build it") { + let assembler = Assembler() + + var cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).to(beNil()) + + assembler.applyAssembly(AnimalAssembly()) + + cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Whiskers" + + let loader = PlistPropertyLoader(bundle: NSBundle(forClass: self.dynamicType.self), name: "first") + try! assembler.applyPropertyLoader(loader) + + assembler.applyAssembly(PropertyAsssembly()) + + cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "first" + } + } + } +} diff --git a/SwinjectTests/BasicAssembly.swift b/SwinjectTests/BasicAssembly.swift new file mode 100644 index 00000000..f9632b1e --- /dev/null +++ b/SwinjectTests/BasicAssembly.swift @@ -0,0 +1,47 @@ +// +// BasicAssembly.swift +// Swinject +// +// Created by mike.owens on 12/9/15. +// Copyright © 2015 Swinject Contributors. All rights reserved. +// + +import Swinject + +class AnimalAssembly: AssemblyType { + + func assemble(container: Container) { + container.register(AnimalType.self) { r in + return Cat(name: "Whiskers") + } + } +} + +class FoodAssembly: AssemblyType { + + func assemble(container: Container) { + container.register(FoodType.self) { r in + return Sushi() + } + } +} + +class PersonAssembly: AssemblyType { + + func assemble(container: Container) { + container.register(PetOwner.self) { r in + let definition = PetOwner() + definition.favoriteFood = r.resolve(FoodType.self) + definition.pet = r.resolve(AnimalType.self) + return definition + } + } +} + +class PropertyAsssembly: AssemblyType { + func assemble(container: Container) { + container.register(AnimalType.self) { r in + return Cat(name: r.property("test.string")!) + } + } +} \ No newline at end of file diff --git a/SwinjectTests/LoadAwareAssembly.swift b/SwinjectTests/LoadAwareAssembly.swift new file mode 100644 index 00000000..163459e7 --- /dev/null +++ b/SwinjectTests/LoadAwareAssembly.swift @@ -0,0 +1,32 @@ +// +// LoadAwareAssembly.swift +// Swinject +// +// Created by mike.owens on 12/9/15. +// Copyright © 2015 Swinject Contributors. All rights reserved. +// + +import Swinject + + +class LoadAwareAssembly: AssemblyLoadAwareType { + var onLoad: (ResolverType) -> Void + var loaded = false + + init(onLoad: (ResolverType) -> Void) { + self.onLoad = onLoad + } + + func assemble(container: Container) { + container.register(AnimalType.self) { r in + return Cat(name: "Bojangles") + } + } + + func loaded(resolver: ResolverType) { + onLoad(resolver) + loaded = true + } +} + + From e83447e959631daa5148efcd0965e8550f0e1a56 Mon Sep 17 00:00:00 2001 From: "mike.owens" Date: Wed, 9 Dec 2015 08:16:53 -0800 Subject: [PATCH 2/3] Fixing swiftlint errors --- SwinjectTests/BasicAssembly.swift | 2 +- SwinjectTests/LoadAwareAssembly.swift | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/SwinjectTests/BasicAssembly.swift b/SwinjectTests/BasicAssembly.swift index f9632b1e..d9852f95 100644 --- a/SwinjectTests/BasicAssembly.swift +++ b/SwinjectTests/BasicAssembly.swift @@ -44,4 +44,4 @@ class PropertyAsssembly: AssemblyType { return Cat(name: r.property("test.string")!) } } -} \ No newline at end of file +} diff --git a/SwinjectTests/LoadAwareAssembly.swift b/SwinjectTests/LoadAwareAssembly.swift index 163459e7..3fcb89ca 100644 --- a/SwinjectTests/LoadAwareAssembly.swift +++ b/SwinjectTests/LoadAwareAssembly.swift @@ -28,5 +28,3 @@ class LoadAwareAssembly: AssemblyLoadAwareType { loaded = true } } - - From 3f9b365e7e2b0fba3d2b98dae925ec10dbf5177d Mon Sep 17 00:00:00 2001 From: "mike.owens" Date: Thu, 10 Dec 2015 08:52:39 -0800 Subject: [PATCH 3/3] Updating assemblies feature with PR feedback - Removed LoadAwareAssemblyType in favor of protocol extension with no-op function - Added missing test for load aware assembly for lazy loading - Updated documentation with master - Dedupped assembly loading code to call the same code path in Assembler --- Documentation/Assembler.md | 10 ++++++---- Documentation/README.md | 4 ++-- Swinject/Assembler.swift | 9 ++------- Swinject/AssemblyType.swift | 15 +++++++-------- SwinjectTests/AssemblerSpec.swift | 20 ++++++++++++++++++++ SwinjectTests/LoadAwareAssembly.swift | 2 +- 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/Documentation/Assembler.md b/Documentation/Assembler.md index 9619bc7a..523ccaf2 100644 --- a/Documentation/Assembler.md +++ b/Documentation/Assembler.md @@ -42,8 +42,8 @@ leverages service definitions registered in the `ServiceAssembly`. Using this pa doesn't care where the `FooServiceType` and `BarServiceType` are registered, it just requires them to be registered else where. -### AssemblyLoadAwareType -The `AssemblyLoadAwareType` supports the assembly to be aware when the container has been fully loaded +### Load aware +The `AssemblyType` 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: @@ -77,10 +77,10 @@ without having to deal with injects: } 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 `AssemblyLoadAwareType` you can keep this +`Logger` after the `Container` has been built. Using a `AssemblyType` you can keep this bootstrapping in the assembly: - class LoggerAssembly: AssemblyLoadAwareType { + class LoggerAssembly: AssemblyType { func assemble(container: Container) { container.register(LogHandlerType.self, name: "console") { r in return ConsoleLogHandler() @@ -145,4 +145,6 @@ The assembler also supports managing your property files as well via constructio all assemblies have been loaded then you must use `addAssemblies` and pass all lazy loaded assemblies at once +_[Next page: Thread Safety](ThreadSafety.md)_ + _[Table of Contents](README.md)_ diff --git a/Documentation/README.md b/Documentation/README.md index cbd22c38..693a5383 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -16,8 +16,8 @@ Swinject is a lightweight dependency injection framework for Swift apps. It help 1. [Container Hierarchy](ContainerHierarchy.md) 2. [Properties](Properties.md) -3. [Thread Safety](ThreadSafety.md) -4. [Modularizing Service Registration](Assembler.md) +3. [Modularizing Service Registration](Assembler.md) +4. [Thread Safety](ThreadSafety.md) ### UI Features diff --git a/Swinject/Assembler.swift b/Swinject/Assembler.swift index ba0afce1..0310785f 100644 --- a/Swinject/Assembler.swift +++ b/Swinject/Assembler.swift @@ -52,10 +52,7 @@ public class Assembler { /// - parameter assembly: the assembly to apply to the container /// public func applyAssembly(assembly: AssemblyType) { - assembly.assemble(container) - if let assemblyLoadAware = assembly as? AssemblyLoadAwareType { - assemblyLoadAware.loaded(resolver) - } + runAssemblies([assembly]) } /// Will apply the assemblies to the container. This is useful if you want to lazy load several assemblies into the assembler's @@ -91,9 +88,7 @@ public class Assembler { // inform all of the assemblies that the container is loaded for assembly in assemblies { - if let assemblyLoadAware = assembly as? AssemblyLoadAwareType { - assemblyLoadAware.loaded(self.resolver) - } + assembly.loaded(self.resolver) } } } diff --git a/Swinject/AssemblyType.swift b/Swinject/AssemblyType.swift index 067310ff..4a041c5b 100644 --- a/Swinject/AssemblyType.swift +++ b/Swinject/AssemblyType.swift @@ -17,18 +17,17 @@ public protocol AssemblyType { /// - parameter container: the container provided by the `Assembler` /// func assemble(container: Container) -} - -/// The `AssemblyLoadAwareType` provies a means for the `Assembler` to inform the `AssemblyType` that the container -/// has been loaded. This hook is useful for when an `AssemblyType` needs to run some code after the -/// container has registered all `Services` from all `AssemblyType` instances passed to the `Assembler`. For -/// example, one might want to setup some 3rd party services or load some `Services` into a singleton -public protocol AssemblyLoadAwareType: AssemblyType { - /// Provides a hook to the `AssemblyLoadAwareType` that will be called once the `Assembler` has loaded all `AssemblyType` + /// Provides a hook to the `AssemblyType` that will be called once the `Assembler` has loaded all `AssemblyType` /// instances into the container. /// /// - parameter resolver: the resolver that can resolve instances from the built container /// func loaded(resolver: ResolverType) } + +public extension AssemblyType { + func loaded(resolver: ResolverType) { + // no-op + } +} diff --git a/SwinjectTests/AssemblerSpec.swift b/SwinjectTests/AssemblerSpec.swift index bf888349..9ee7a3d1 100644 --- a/SwinjectTests/AssemblerSpec.swift +++ b/SwinjectTests/AssemblerSpec.swift @@ -91,6 +91,26 @@ class AssemblerSpec: QuickSpec { expect(cat!.name) == "Whiskers" } + it("can assembly a single load aware container") { + let assembler = try! Assembler(assemblies: []) + var cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).to(beNil()) + + let loadAwareAssembly = LoadAwareAssembly() { resolver in + let cat = resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Bojangles" + } + + expect(loadAwareAssembly.loaded) == false + assembler.applyAssembly(loadAwareAssembly) + expect(loadAwareAssembly.loaded) == true + + cat = assembler.resolver.resolve(AnimalType.self) + expect(cat).toNot(beNil()) + expect(cat!.name) == "Bojangles" + } + it("can assembly a multiple containers 1 by 1") { let assembler = try! Assembler(assemblies: []) var cat = assembler.resolver.resolve(AnimalType.self) diff --git a/SwinjectTests/LoadAwareAssembly.swift b/SwinjectTests/LoadAwareAssembly.swift index 3fcb89ca..6ffc7dba 100644 --- a/SwinjectTests/LoadAwareAssembly.swift +++ b/SwinjectTests/LoadAwareAssembly.swift @@ -9,7 +9,7 @@ import Swinject -class LoadAwareAssembly: AssemblyLoadAwareType { +class LoadAwareAssembly: AssemblyType { var onLoad: (ResolverType) -> Void var loaded = false