From 8c3d3b304edaf85771be7644bcb61ed77e633f19 Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Thu, 5 Nov 2015 19:15:36 +0100 Subject: [PATCH] multiple-injection --- CHANGELOG.md | 4 +- Dip/Dip/Dip.swift | 82 ++++++++++++++++++- Dip/Dip/RuntimeArguments.swift | 34 +++++++- Dip/DipTests/DipTests.swift | 69 +++++++++++++++- .../Contents.swift | 2 +- .../Contents.swift | 48 +++++++++++ .../timeline.xctimeline | 6 ++ .../Testing.xcplaygroundpage/Contents.swift | 2 +- .../contents.xcplayground | 3 +- README.md | 11 +++ 10 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 DipPlayground.playground/Pages/Multi-injection.xcplaygroundpage/Contents.swift create mode 100644 DipPlayground.playground/Pages/Multi-injection.xcplaygroundpage/timeline.xctimeline diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b4e9f..1f30074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ * Added auto-injection feature. [#13](https://github.com/AliSoftware/Dip/pull/13), [@ilyapuchka](https://github.com/ilyapuchka) * Factories and `resolveDependencies` blocks of `DefinitionOf` are now allowed to `throw`. - [#32](https://github.com/AliSoftware/Dip/pull/13), [@ilyapuchka](https://github.com/ilyapuchka) + [#32](https://github.com/AliSoftware/Dip/pull/32), [@ilyapuchka](https://github.com/ilyapuchka) * Thread safety reimplemented with support for recursive methods calls. [#31](https://github.com/AliSoftware/Dip/pull/31), [@mwoollard](https://github.com/mwoollard) +* Multi-injection to resolve an array of all the instances registere for the protocol. + [#38](https://github.com/AliSoftware/Dip/pull/38), [@ilyapuchka](https://github.com/ilyapuchka) ## 4.0.0 diff --git a/Dip/Dip/Dip.swift b/Dip/Dip/Dip.swift index e473ea4..b0c7d19 100644 --- a/Dip/Dip/Dip.swift +++ b/Dip/Dip/Dip.swift @@ -35,7 +35,7 @@ public final class DependencyContainer { with the same protocol, to differentiate them. Tags can be either String or Int, to your convenience. */ - public enum Tag: Equatable { + public enum Tag: Equatable, Comparable { case String(StringLiteralType) case Int(IntegerLiteralType) } @@ -120,7 +120,7 @@ public final class DependencyContainer { container.register { ClientImp(service: try! container.resolve() as Service) as Client } ``` */ - public func register(tag tag: Tag? = nil, _ scope: ComponentScope = .Prototype, factory: () throws -> T) -> DefinitionOfT > { + public func register(tag tag: Tag? = nil, _ scope: ComponentScope = .Prototype, factory: () throws -> T) -> DefinitionOf T> { return registerFactory(tag: tag, scope: scope, factory: factory) } @@ -233,6 +233,51 @@ public final class DependencyContainer { } } + /** + Resolve dependency as array. Resulting array will contain instances of resolved type created by resolving all registered definitions for resolved type with matching factory. + + - note: resolved instances will be ordered according to tags order. + Tags are compared by strings made of their associated values. + + - returns: array of resolved instances + + **Example**: + ```swift + container.register() { ServiceImp0() as Service } + container.register(tag: "service1") { ServiceImp1() as Service } + container.register(tag: "service2") { ServiceImp2() as Service } + + let allServices = try! container.resolveAll() as [Service] + + allServices[0] // is ServiceImp0() + allServices[1] // is ServiceImp1() + allServices[2] // is ServiceImp2() + ``` + + */ + public func resolveAll() throws -> [T] { + return try resolveAll { (factory: () throws -> T) -> T in try factory() } + } + + /** + Resolves all components registered for the type. + + - note: You should use this method _only_ to resolve dependency with more runtime arguments than _Dip_ supports. + + - seealso: `resolve(tag:builder:)` + */ + public func resolveAll(builder: F throws -> T) rethrows -> [T] { + return try threadSafe { + return try self.definitions + .sort({ $0.0.associatedTag < $1.0.associatedTag }) + .flatMap { (key, definition) in + guard let definition = definition as? DefinitionOf else { return nil } + let usingKey: DefinitionKey? = definition.scope == .ObjectGraph ? key : nil + return try self._resolve(key: usingKey, definition: definition, builder: builder) + } + } + } + /// Actually resolve dependency private func _resolve(tag: Tag? = nil, key: DefinitionKey?, definition: DefinitionOf, builder: F throws -> T) rethrows -> T { @@ -340,14 +385,43 @@ extension DependencyContainer.Tag: StringLiteralConvertible { } } +/* +.String("a") == .Stirng("a") +.Int(1) == .Int(1) +.String("1") == .Int(1) +*/ public func ==(lhs: DependencyContainer.Tag, rhs: DependencyContainer.Tag) -> Bool { switch (lhs, rhs) { case let (.String(lhsString), .String(rhsString)): return lhsString == rhsString case let (.Int(lhsInt), .Int(rhsInt)): return lhsInt == rhsInt - default: - return false + case let (.String(lhsString), .Int(rhsInt)): + return lhsString == String(rhsInt) + case let (.Int(lhsInt), .String(rhsString)): + return String(lhsInt) == rhsString + } +} + +/** + Tags are compared by comparing strings created from their assiciated values. + + .String("a") < .String("b") + .Int(0) < .Int(1) + .String("0") < .Int(1) + .Int(0) < .String("1") + + */ +public func <(lhs: DependencyContainer.Tag, rhs: DependencyContainer.Tag) -> Bool { + switch (lhs, rhs) { + case let (.String(lhsString), .String(rhsString)): + return lhsString < rhsString + case let (.Int(lhsInt), .Int(rhsInt)): + return lhsInt < rhsInt + case let (.String(lhsString), .Int(rhsInt)): + return lhsString < String(rhsInt) + case let (.Int(lhsInt), .String(rhsString)): + return String(lhsInt) < rhsString } } diff --git a/Dip/Dip/RuntimeArguments.swift b/Dip/Dip/RuntimeArguments.swift index 58e957f..9863536 100644 --- a/Dip/Dip/RuntimeArguments.swift +++ b/Dip/Dip/RuntimeArguments.swift @@ -58,6 +58,18 @@ extension DependencyContainer { return try resolve(tag: tag) { (factory: (Arg1) throws -> T) in try factory(arg1) } } + /** + Resolve dependency with runtime argument as array. Resulting array will contain instances of resolved type created by resolving all registered definitions for resolved type with matching factory. + + - parameter arg1: Argument that will be used to resolve each instance + - returns: array of resolved instances + + - seealso: `resolveAll()`, `resolve(tag:withArguments:)` + */ + public func resolveAll(arg1: Arg1) throws -> [T] { + return try resolveAll { (factory: (Arg1) throws -> T) in try factory(arg1) } + } + // MARK: 2 Runtime Arguments /// - seealso: `register(:factory:scope:)` @@ -65,11 +77,15 @@ extension DependencyContainer { return registerFactory(tag: tag, scope: scope, factory: factory) } - /// - seealso: `resolve(tag:_:)` + /// - seealso: `resolve(tag:withArguments:)` public func resolve(tag tag: Tag? = nil, withArguments arg1: Arg1, _ arg2: Arg2) throws -> T { return try resolve(tag: tag) { (factory: (Arg1, Arg2) throws -> T) in try factory(arg1, arg2) } } + public func resolveAll(arg1: Arg1, _ arg2: Arg2) throws -> [T] { + return try resolveAll { (factory: (Arg1, Arg2) throws -> T) in try factory(arg1, arg2) } + } + // MARK: 3 Runtime Arguments public func register(tag tag: Tag? = nil, _ scope: ComponentScope = .Prototype, factory: (Arg1, Arg2, Arg3) throws -> T) -> DefinitionOf T> { @@ -81,6 +97,10 @@ extension DependencyContainer { return try resolve(tag: tag) { (factory: (Arg1, Arg2, Arg3) throws -> T) in try factory(arg1, arg2, arg3) } } + public func resolveAll(arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3) throws -> [T] { + return try resolveAll { (factory: (Arg1, Arg2, Arg3) throws -> T) in try factory(arg1, arg2, arg3) } + } + // MARK: 4 Runtime Arguments public func register(tag tag: Tag? = nil, _ scope: ComponentScope = .Prototype, factory: (Arg1, Arg2, Arg3, Arg4) throws -> T) -> DefinitionOf T> { @@ -92,6 +112,10 @@ extension DependencyContainer { return try resolve(tag: tag) { (factory: (Arg1, Arg2, Arg3, Arg4) throws -> T) in try factory(arg1, arg2, arg3, arg4) } } + public func resolveAll(arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4) throws -> [T] { + return try resolveAll { (factory: (Arg1, Arg2, Arg3, Arg4) throws -> T) in try factory(arg1, arg2, arg3, arg4) } + } + // MARK: 5 Runtime Arguments public func register(tag tag: Tag? = nil, _ scope: ComponentScope = .Prototype, factory: (Arg1, Arg2, Arg3, Arg4, Arg5) throws -> T) -> DefinitionOf T> { @@ -103,6 +127,10 @@ extension DependencyContainer { return try resolve(tag: tag) { (factory: (Arg1, Arg2, Arg3, Arg4, Arg5) throws -> T) in try factory(arg1, arg2, arg3, arg4, arg5) } } + public func resolveAll(arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5) throws -> [T] { + return try resolveAll { (factory: (Arg1, Arg2, Arg3, Arg4, Arg5) throws -> T) in try factory(arg1, arg2, arg3, arg4, arg5) } + } + // MARK: 6 Runtime Arguments public func register(tag tag: Tag? = nil, _ scope: ComponentScope = .Prototype, factory: (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) throws -> T) -> DefinitionOf T> { @@ -114,4 +142,8 @@ extension DependencyContainer { return try resolve(tag: tag) { (factory: (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) throws -> T) in try factory(arg1, arg2, arg3, arg4, arg5, arg6) } } + public func resolveAll(arg1: Arg1, _ arg2: Arg2, _ arg3: Arg3, _ arg4: Arg4, _ arg5: Arg5, _ arg6: Arg6) throws -> [T] { + return try resolveAll { (factory: (Arg1, Arg2, Arg3, Arg4, Arg5, Arg6) throws -> T) in try factory(arg1, arg2, arg3, arg4, arg5, arg6) } + } + } diff --git a/Dip/DipTests/DipTests.swift b/Dip/DipTests/DipTests.swift index 180d48f..e29fe79 100644 --- a/Dip/DipTests/DipTests.swift +++ b/Dip/DipTests/DipTests.swift @@ -25,7 +25,7 @@ import XCTest @testable import Dip -protocol Service { +protocol Service: class { func getServiceName() -> String } @@ -182,4 +182,71 @@ class DipTests: XCTestCase { } } + func testThatItCanResolveAllImplementationsAsArray() { + container.register() { ServiceImp1() as Service } + container.register(tag: "service1") { ServiceImp1() as Service } + container.register(tag: "service2") { ServiceImp2() as Service } + + let allServices = try! container.resolveAll() as [Service] + + XCTAssertEqual(allServices.count, 3) + XCTAssertTrue(allServices[0] is ServiceImp1) + XCTAssertTrue(allServices[1] is ServiceImp1) + XCTAssertTrue(allServices[2] is ServiceImp2) + + XCTAssertFalse(allServices[2] === allServices[1]) + } + + func testThatItDoesNotReusesInstancesInObjectGraphScopeWhenResolvingAsArray() { + container.register(.ObjectGraph) { ServiceImp1() as Service } + container.register(tag: "service1", .ObjectGraph) { ServiceImp1() as Service } + container.register(tag: "service1_1") { ServiceImp1() as Service } + + let allServices = try! container.resolveAll() as [Service] + + XCTAssertEqual(allServices.count, 3) + + XCTAssertFalse(allServices[2] === allServices[1]) + XCTAssertFalse(allServices[0] === allServices[1]) + } + + func testThatItCanRemoveAndReAddDefinition() { + let def1 = container.register() { ServiceImp1() as Service } + container.register(tag: "service2") { ServiceImp2() as Service } + + container.remove(def1) + + let allServices = try! container.resolveAll() as [Service] + XCTAssertEqual(allServices.count, 1) + XCTAssertTrue(allServices.last is ServiceImp2) + + container.register() { ServiceImp1() as Service } + + let newAllServices = try! container.resolveAll() as [Service] + XCTAssertEqual(newAllServices.count, 2) + XCTAssertTrue(newAllServices.first is ServiceImp1) + XCTAssertTrue(newAllServices.last is ServiceImp2) + } + + func testTagsEquality() { + XCTAssertEqual(DependencyContainer.Tag.String("a"), DependencyContainer.Tag.String("a")) + XCTAssertNotEqual(DependencyContainer.Tag.String("a"), DependencyContainer.Tag.String("b")) + + XCTAssertEqual(DependencyContainer.Tag.Int(0), DependencyContainer.Tag.Int(0)) + XCTAssertNotEqual(DependencyContainer.Tag.Int(0), DependencyContainer.Tag.Int(1)) + + XCTAssertEqual(DependencyContainer.Tag.String("0"), DependencyContainer.Tag.Int(0)) + XCTAssertEqual(DependencyContainer.Tag.Int(0), DependencyContainer.Tag.String("0")) + + XCTAssertNotEqual(DependencyContainer.Tag.String("0"), DependencyContainer.Tag.Int(1)) + XCTAssertNotEqual(DependencyContainer.Tag.Int(1), DependencyContainer.Tag.String("0")) + } + + func testTagsComparison() { + XCTAssertLessThan(DependencyContainer.Tag.String("a"), DependencyContainer.Tag.String("b")) + XCTAssertLessThan(DependencyContainer.Tag.Int(0), DependencyContainer.Tag.Int(1)) + XCTAssertLessThan(DependencyContainer.Tag.Int(0), DependencyContainer.Tag.String("1")) + XCTAssertLessThan(DependencyContainer.Tag.String("0"), DependencyContainer.Tag.Int(1)) + } + } diff --git a/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/Contents.swift b/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/Contents.swift index 5f8f3f7..2d4fefd 100644 --- a/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/Contents.swift +++ b/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/Contents.swift @@ -180,4 +180,4 @@ So as you can see there are certain advantages and disadvatages of using auto-in So you should decide for yourself whether you prefer to use auto-injection or "the standard" way. At the end they let you achieve the same result. */ -//: [Next: Testing](@next) +//: [Next: Multi-injection](@next) diff --git a/DipPlayground.playground/Pages/Multi-injection.xcplaygroundpage/Contents.swift b/DipPlayground.playground/Pages/Multi-injection.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..a395f59 --- /dev/null +++ b/DipPlayground.playground/Pages/Multi-injection.xcplaygroundpage/Contents.swift @@ -0,0 +1,48 @@ +//: [Previous: Auto-injection](@previous) + +import Dip + +let container = DependencyContainer() + +/*: +Let's say you are developing a killer e-mail client. Surely you want your users to feel comfortable using your service so you want to let them to use their existing accounts on other e-mail services like Gmail, Yahoo, Outlook. For that you will need some kind of screen where you will display the list of all 3'rd party services that you support. And of course you want to be able to easily add new services. Also you wish to split the work on different services between your teammates so that you ship this feature faster. + +To enable all that you will need to define some common protocol that all 3'rd party services implementations will conform to. +Then you start to add concrete implementations: +*/ + +protocol ThirdPartyEmailService { /* … */ } + +class GmailService: ThirdPartyEmailService { /* … */ init(){} } +class YahooService: ThirdPartyEmailService { /* … */ init(){} } +class OutlookService: ThirdPartyEmailService { /* … */ init(){} } + +/*: +When ready you register them in the container. +*/ + +container.register(tag: "gmail") { GmailService() as ThirdPartyEmailService } +container.register(tag: "yahoo") { YahooService() as ThirdPartyEmailService } +container.register(tag: "outlook") { OutlookService() as ThirdPartyEmailService } + +/*: +Then when you need to display all of them you can get them from the container just with one call. +*/ + +var thirdPartyServices = try! container.resolveAll() as [ThirdPartyEmailService] + +/*: +When you realize that you need to support one more service you will only need to drop in it's implementation and register it in the container. It will appear in the list of your services without any other changes in your code. +*/ + +class YandexService: ThirdPartyEmailService { /* … */ init(){} } + +container.register(tag: "yandex") { YandexService() as ThirdPartyEmailService } + +thirdPartyServices = try! container.resolveAll() as [ThirdPartyEmailService] + +/*: +Sharing to different services, providing different payment or goods delivery services, logging to different sources, fetching data at once from different kinds of content providers are some of other possible use cases for that feature. +*/ + +//: [Next: Testing](@next) diff --git a/DipPlayground.playground/Pages/Multi-injection.xcplaygroundpage/timeline.xctimeline b/DipPlayground.playground/Pages/Multi-injection.xcplaygroundpage/timeline.xctimeline new file mode 100644 index 0000000..bf468af --- /dev/null +++ b/DipPlayground.playground/Pages/Multi-injection.xcplaygroundpage/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/DipPlayground.playground/Pages/Testing.xcplaygroundpage/Contents.swift b/DipPlayground.playground/Pages/Testing.xcplaygroundpage/Contents.swift index 5b18226..bcf2e3b 100644 --- a/DipPlayground.playground/Pages/Testing.xcplaygroundpage/Contents.swift +++ b/DipPlayground.playground/Pages/Testing.xcplaygroundpage/Contents.swift @@ -1,4 +1,4 @@ -//: [Previous: Shared Instances](@previous) +//: [Previous: Multi-injection](@previous) /*: diff --git a/DipPlayground.playground/contents.xcplayground b/DipPlayground.playground/contents.xcplayground index 948f5fe..e325d5f 100644 --- a/DipPlayground.playground/contents.xcplayground +++ b/DipPlayground.playground/contents.xcplayground @@ -1,5 +1,5 @@ - + @@ -10,6 +10,7 @@ + \ No newline at end of file diff --git a/README.md b/README.md index a366121..7edee67 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,17 @@ let client = try! container.resolve() as Client ``` You can find more use cases for auto-injection in the Playground available in this repository. +### Multi-injection + +If you have several implementations of the same protocol and you wish to get them all as an array you can do that using `resolveAll` method: + +```swift +container.register(tag: "facebook") { FacebookShare() as SharingService } +container.register(tag: "twitter") { TwitterShare() as SharingService } + +let sharingServices = try! container.resolveAll() as [SharingService] +``` + ### Thread safety `DependencyContainer` is thread safe, you can register and resolve components from different threads.