From 932b27c6d53cfeeea982b23419e822ac6ee09ddc Mon Sep 17 00:00:00 2001 From: Ilya Puchka Date: Sat, 9 Jan 2016 13:14:01 +0100 Subject: [PATCH] auto injection playground page and updated documentation --- CHANGELOG.md | 5 + Dip/Dip/AutoInjection.swift | 6 +- .../Contents.swift | 183 ++++++++++++++++++ .../timeline.xctimeline | 6 + .../Contents.swift | 4 +- DipPlayground.playground/Sources/Models.swift | 23 +++ .../contents.xcplayground | 3 +- README.md | 32 +++ 8 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/Contents.swift create mode 100644 DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/timeline.xctimeline diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c39c9..eb89dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## Develop + +* Added auto-injection feature + [#13](https://github.com/AliSoftware/Dip/pull/13), [@ilyapuchka](https://github.com/ilyapuchka) + ## 4.0.0 #### New Features diff --git a/Dip/Dip/AutoInjection.swift b/Dip/Dip/AutoInjection.swift index a5755bd..3991d21 100644 --- a/Dip/Dip/AutoInjection.swift +++ b/Dip/Dip/AutoInjection.swift @@ -121,7 +121,9 @@ extension DependencyContainer { - Note: Use `InjectedWeak` to define one of two circular dependecies if another dependency is defined as `Injected`. - This will prevent retain cycle between resolved instances. + This will prevent a retain cycle between resolved instances. + + - Warning: If you resolve dependencies of the object created not by container and it has auto-injected circular dependency, container will be not able to resolve it correctly because container does not have this object in it's resolved instances stack. Thus it will create another instance of that type to satisfy circular dependency. **Example**: ```swift @@ -134,7 +136,7 @@ extension DependencyContainer { } //when resolved client will have service injected - let client = container.resolve() as Client + let client = try! container.resolve() as Client ``` diff --git a/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/Contents.swift b/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..c4434e7 --- /dev/null +++ b/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/Contents.swift @@ -0,0 +1,183 @@ +//: [Previous: Shared Instances](@previous) + +import UIKit +import Dip + +let container = DependencyContainer() +/*: + +### Auto-Injection + +If you follow Single Responsibility Principle chances are very high that you will end up with more than two collaborating components in your system. Let's say you have a component that depends on few others. Using _Dip_ you can register all of the dependencies in a container as well as that component itself and register a factory that will create that component and feed it with the dependencies resolving them with a container: +*/ + +protocol Service: class { + var logger: Logger? { get set } + var tracker: Tracker? { get set } +} + +class ServiceImp: Service { + var logger: Logger? + var tracker: Tracker? +} + +container.register() { TrackerImp() as Tracker } +container.register() { LoggerImp() as Logger } + +container.register() { ServiceImp() as Service } + .resolveDependencies { container, service in + service.logger = try! container.resolve() as Logger + service.tracker = try! container.resolve() as Tracker +} + +let service = try! container.resolve() as Service +service.logger +service.tracker + +/*: +Not bad so far. Though that `resolveDependencies` block looks heavy. It would be cool if we can get rid of it. Alternatively you can use _constructor injection_ here, which is actually more prefereable by default but not always possible (see [circular dependencies](Circular%20dependencies)). +Now let's say that you have a bunch of components in your app that require `Logger` or `Tracker` too. You will need to resolve them in a factory for each component again and again. That can be a lot of boilerplate code, simple but still duplicated. + +That is one of the scenarios when auto-injection can be usefull. It works with property injection and with it the previous code will transform to this: +*/ + +class AutoInjectedServiceImp: Service { + private var injectedLogger = Injected() + var logger: Logger? { get { return injectedLogger.value } set { injectedLogger.value = newValue } } + + private var injectedTracker = Injected() + var tracker: Tracker? { get { return injectedTracker.value } set { injectedTracker.value = newValue } } +} + +container.register() { AutoInjectedServiceImp() as Service } + +let autoInjectedService = try! container.resolve() as Service +autoInjectedService.logger +autoInjectedService.tracker + +/*: +The same you can do if you already have an instance of service and just want to resolve its dependencies: +*/ + +let providedService = AutoInjectedServiceImp() +container.resolveDependencies(providedService) +providedService.logger +providedService.tracker + +/*: +As you can see we added two private properties to our implementation of `Service` - `injectedLogger` and `injectedTracker`. Their types are `Injeceted` and `Injected` respectively. Note that we've not just defined them as properties of those types, but defined them with some initial value. `Injected` is a simple _wrapper class_ that wraps value of generic type and provides read-write access to it with `value` property. This property is defined as optional, so that when we create instance of `Injected` it will have `nil` in its value. There is also another wrapper - `InjectedWeak` - which in contrast to `Injected` holds a week reference to its wrapped object, thus requiring it to be a _reference type_ (or `AnyObject`), when `Injected` can also wrap value types (or `Any`). + +What is happening under the hood is that after concrete instance of resolved type is created (`Service` in that case), container will iterate through its properties using `Mirror`. For each of the properties wrapped with `Injected` or `InjectedWeak` it will search a definition that can be used to create an instance of wrapped type and use it to create and inject a concrete instance in a `value` property of a wrapper. The fact that wrappers are _classes_ or _reference types_ makes it possible at runtime to inject dependency in instance of resolved type. + +Another example of using auto-injection is circular dependencies. Let's say you have a `Server` and a `ServerClient` both referencing each other. Standard way to register such components in `DependencyContainer` will lead to such code: +*/ + +protocol Server: class { + weak var client: ServerClient? {get set} +} + +protocol ServerClient: class { + var server: Server? {get} +} + +class ServerImp: Server { + weak var client: ServerClient? +} + +class ServerClientImp: ServerClient { + var server: Server? + + init(server: Server) { + self.server = server + } +} + +container.register(.ObjectGraph) { + ServerClientImp(server: try! container.resolve()) as ServerClient +} + +container.register(.ObjectGraph) { ServerImp() as Server } + .resolveDependencies { container, server in + server.client = try! container.resolve() as ServerClient +} + +let client = try! container.resolve() as ServerClient +client.server + +/*: +With auto-injection you will have the following code: +*/ + +class InjectedServerImp: Server { + private var injectedClient = InjectedWeak() + var client: ServerClient? { get { return injectedClient.value } set { injectedClient.value = newValue }} +} + +class InjectedClientImp: ServerClient { + private var injectedServer = Injected() + var server: Server? { get { return injectedServer.value} } +} + +container.register(.ObjectGraph) { InjectedServerImp() as Server } +container.register(.ObjectGraph) { InjectedClientImp() as ServerClient } + +let injectedClient = try! container.resolve() as ServerClient +injectedClient.server +injectedClient.server?.client === injectedClient //circular dependencies were resolved correctly + +/*: +You can see that component registration looks much simpler now. But on the otherside it requires some boilerplate code in implementations, also tightly coupling them with Dip. + +There is one more use case when auto-injection can be very helpfull - when you don't create instances by yourself but system creates them for you. It can be view controllers created by Storyboards. Let's say each view controller in your application requires logger, tracker, data layer service, router, etc. You can end up with code like this: +*/ +container.register() { RouterImp() as Router } +container.register() { DataProviderImp() as DataProvider } + +class ViewController: UIViewController { + var logger: Logger? + var tracker: Tracker? + var dataProvider: DataProvider? + var router: Router? + + //it's better not to access container directly in implementation but that's ok for illustration + func injectDependencies(container: DependencyContainer) { + logger = try! container.resolve() as Logger + tracker = try! container.resolve() as Tracker + dataProvider = try! container.resolve() as DataProvider + router = try! container.resolve() as Router + } +} + +let viewController = ViewController() +viewController.injectDependencies(container) +viewController.router + +/*: +With auto-injection you can replace that with something like this: +*/ + +class AutoInjectedViewController: UIViewController { + + var logger = Injected() + var tracker = Injected() + var dataProvider = Injected() + var router = Injected() + + func injectDependencies(container: DependencyContainer) { + container.resolveDependencies(self) + } +} + +let autoViewController = AutoInjectedViewController() +autoViewController.injectDependencies(container) +autoViewController.router.value + +/*: +In such scenario you will need to use property injection anyway, so the overhead of adding additional properties for auto-injection is smaller. Also all the boilerplate code of unwrapping injected properties (if you need that) can be moved to extension, cleaning implementation a bit. + +So as you can see there are certain advantages and disadvatages of using auto-injection. It makes your definitions simpler, especially if there are circular dependencies involved, and lets you get rid of giant constructors overloaded with arguments. But it requires additional properties and some boilerplate code in your implementations, makes your implementatios tightly coupled with Dip. It has also some limitations like that it requires factories for auto-injected types that accept no runtime arguments and have no associated tags to be registered in a container. + +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) diff --git a/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/timeline.xctimeline b/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/timeline.xctimeline new file mode 100644 index 0000000..bf468af --- /dev/null +++ b/DipPlayground.playground/Pages/Auto-injection.xcplaygroundpage/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/DipPlayground.playground/Pages/Shared Instances.xcplaygroundpage/Contents.swift b/DipPlayground.playground/Pages/Shared Instances.xcplaygroundpage/Contents.swift index c17d7d5..d6cc211 100644 --- a/DipPlayground.playground/Pages/Shared Instances.xcplaygroundpage/Contents.swift +++ b/DipPlayground.playground/Pages/Shared Instances.xcplaygroundpage/Contents.swift @@ -152,10 +152,10 @@ dipController = DipViewController(apiClientProvider: container) /*: This way you also does not depend directly on Dip. Instead you provide a boundary between Dip — that you don't have control of — and your source code. So when something chagnes in Dip, you update only the boundary code. -Dependency injection is a pattern as well as singleton. And any pattern can be abused. DI can be use in a [wrong way]((http://www.loosecouplings.com/2011/01/dependency-injection-using-di-container.html)), container can easily become a [service locator](http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/). That's why if you adopt DI in one part of your system it does not mean that you should inject everything and everywhere. The same with using protocols instead of concrete implementations. For every tool there is a right time and the same way as singleton can harm you the same way DI and protocols abuse can make your code unnececerry complex. +Dependency injection is a pattern as well as singleton. And any pattern can be abused. DI can be used in a [wrong way]((http://www.loosecouplings.com/2011/01/dependency-injection-using-di-container.html)), container can easily become a [service locator](http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/). That's why if you adopt DI in one part of your system it does not mean that you should inject everything and everywhere. The same with using protocols instead of concrete implementations. For every tool there is a right time and the same way as singleton can harm you the same way DI and protocols abuse can make your code unnececerry complex. */ -//: [Next: Testing](@next) +//: [Next: Auto-Injection](@next) diff --git a/DipPlayground.playground/Sources/Models.swift b/DipPlayground.playground/Sources/Models.swift index 6182743..fbc7e18 100644 --- a/DipPlayground.playground/Sources/Models.swift +++ b/DipPlayground.playground/Sources/Models.swift @@ -53,3 +53,26 @@ public class ClientServiceImp: Service { public init() {} } +public protocol Logger {} +public protocol Tracker {} +public protocol DataProvider {} +public protocol Router {} + +public class LoggerImp: Logger { + public init() {} +} + +public class TrackerImp: Tracker { + public init() {} +} + +public class RouterImp: Router { + public init() {} +} + +public class DataProviderImp: DataProvider { + public init() {} +} + + + diff --git a/DipPlayground.playground/contents.xcplayground b/DipPlayground.playground/contents.xcplayground index ca67a5f..948f5fe 100644 --- a/DipPlayground.playground/contents.xcplayground +++ b/DipPlayground.playground/contents.xcplayground @@ -1,5 +1,5 @@ - + @@ -9,6 +9,7 @@ + \ No newline at end of file diff --git a/README.md b/README.md index 93fe255..5568bc9 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,44 @@ container.register(.ObjectGraph) { ServerImp() as Server } ``` More infromation about circular dependencies you can find in a playground. +### Auto-Injections + +Auto-injection lets your resolve all the dependencies of the instance (created manually or resolved by container) with just one call to `resolve`, also allowing a simpler sintax to register circular dependencies. + +```swift +protocol Server { + weak var client: Client? { get } +} + +protocol Client: class { + var server: Server? { get } +} + +class ServerImp: Server { + private var injectedClient = InjectedWeak() + var client: Client? { return injectedClient.value } +} + +class ClientImp: Client { + private var injectedServer = Injected() + var server: Server? { get { return injectedServer.value} } +} + +container.register(.ObjectGraph) { ServerImp() as Server } +container.register(.ObjectGraph) { ClientImp() as Client } + +let client = try! container.resolve() as Client + +``` +You can find more use cases for auto-injection in a Playground. + ### Thread safety _Dip_ does not provide thread safety, so you need to make sure you always call `resolve` method of `DependencyContainer` from the single thread. Otherwise if two threads try to resolve the same type they can get different instances where the same instance is expected. ### Errors + The resolve operation is potentially dangerous because you can use the wrong type, factory or a wrong tag. For that reason Dip throws an error `DefinitionNotFond(DefinitionKey)` if it failed to resolve type. When calling `resolve` you need to use a `try` operator. There are rare use cases where your application can recover from this kind of errors (for example you can register new types