Skip to content

Commit

Permalink
circular dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
Ilya Puchka authored and ilyapuchka committed Nov 16, 2015
1 parent b15fc47 commit 77b9a66
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 69 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
[#8](https://github.com/AliSoftware/Dip/pull/8), [@ilyapuchka](https://github.com/ilyapuchka)
* Parameter `tag` is now named in all register/resolve methods
* Playground added to project [#10](https://github.com/AliSoftware/Dip/pull/10), [@ilyapuchka](https://github.com/ilyapuchka)
* Added support for circular dependencies [#11](https://github.com/AliSoftware/Dip/pull/11), [@ilyapuchka](https://github.com/ilyapuchka):
* Added `ObjectGraph` scope to reuse resolved instances
* Removed container thread-safety to enable recursion. Access to container across threads should be handled by clients for now.

## 2.0.0

Expand Down
6 changes: 6 additions & 0 deletions Dip/Dip.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
094526B41BEA51540034E72A /* RuntimeArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094526B31BEA51540034E72A /* RuntimeArguments.swift */; };
094526B61BEA520B0034E72A /* Definition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094526B51BEA520B0034E72A /* Definition.swift */; };
094526B81BEA536A0034E72A /* RuntimeArgumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 094526B71BEA536A0034E72A /* RuntimeArgumentsTests.swift */; };
0989323F1BEBC8CD00ACDA2B /* ComponentScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0989323E1BEBC8CD00ACDA2B /* ComponentScopeTests.swift */; };
09969C551BEB7C0A00F93C70 /* Dip.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 09969C541BEB7C0A00F93C70 /* Dip.podspec */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand All @@ -37,6 +39,8 @@
094526B31BEA51540034E72A /* RuntimeArguments.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = RuntimeArguments.swift; sourceTree = "<group>"; tabWidth = 2; };
094526B51BEA520B0034E72A /* Definition.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = Definition.swift; sourceTree = "<group>"; tabWidth = 2; };
094526B71BEA536A0034E72A /* RuntimeArgumentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuntimeArgumentsTests.swift; sourceTree = "<group>"; };
0989323E1BEBC8CD00ACDA2B /* ComponentScopeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComponentScopeTests.swift; sourceTree = "<group>"; };
09969C541BEB7C0A00F93C70 /* Dip.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Dip.podspec; path = ../Dip.podspec; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -95,6 +99,7 @@
children = (
094526A01BEA1CFF0034E72A /* DipTests.swift */,
094526B71BEA536A0034E72A /* RuntimeArgumentsTests.swift */,
0989323E1BEBC8CD00ACDA2B /* ComponentScopeTests.swift */,
094526A21BEA1CFF0034E72A /* Info.plist */,
);
path = DipTests;
Expand Down Expand Up @@ -219,6 +224,7 @@
buildActionMask = 2147483647;
files = (
094526A11BEA1CFF0034E72A /* DipTests.swift in Sources */,
0989323F1BEBC8CD00ACDA2B /* ComponentScopeTests.swift in Sources */,
094526B81BEA536A0034E72A /* RuntimeArgumentsTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
46 changes: 44 additions & 2 deletions Dip/Dip/Definition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,62 @@ func ==(lhs: DefinitionKey, rhs: DefinitionKey) -> Bool {
public enum ComponentScope {
/// Indicates that a new instance of the component will be created each time it's resolved.
case Prototype
/// Indicates that instances will be reused during resolve but will be discurded when topmost `resolve` method returns.
case ObjectGraph
/// Indicates that resolved component should be retained by container and always reused.
case Singleton
}

///Definition of type T describes how instances of this type should be created when they are resolved by container.
public final class DefinitionOf<T>: Definition {

/**
Changes scope of the component.

- parameter scope: new scope value. New definitions have `Prototype` scope
*/
public func inScope(scope: ComponentScope) -> DefinitionOf<T> {
self.scope = scope
return self
}

/**
Sets the block that will be used to resolve dependencies of the component.
This block will be called before `resolve` returns.

- parameter block: block to use to resolve dependencies

- note:
If you have circular dependencies at least one of them should use this block
to resolve it's dependencies. Otherwise code enter infinite loop.

**Example**

```swift
container.register { [unowned container] ClientImp(service: container.resolve() as Service) as Client }

container.register { ServiceImp() as Service }
.resolveDependencies { container, service in
service.delegate = container.resolve() as Client
}
```

*/
public func resolveDependencies(block: (DependencyContainer, T) -> ()) -> DefinitionOf<T> {
self.resolveDependenciesBlock = block
return self
}

let factory: Any
let scope: ComponentScope
var scope: ComponentScope
var resolveDependenciesBlock: ((DependencyContainer, T) -> ())?

init(factory: Any, scope: ComponentScope = .Prototype) {
init(factory: Any, scope: ComponentScope) {
self.factory = factory
self.scope = scope
}

///Will be stored only if scope is `Singleton`
var resolvedInstance: T? {
get {
guard scope == .Singleton else { return nil }
Expand Down
3 changes: 1 addition & 2 deletions Dip/Dip/Dip.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
// Dip.h
// Dip
//
// Created by Ilya Puchka on 04.11.15.
// Copyright © 2015 AliSoftware. All rights reserved.
// This code is under MIT Licence. See the LICENCE file for more info.
//

#import <Foundation/Foundation.h>
Expand Down
119 changes: 84 additions & 35 deletions Dip/Dip/Dip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import Foundation
// MARK: - DependencyContainer

/**
* _Dip_'s Dependency Containers allow you to do very simple **Dependency Injection**
* by associating `protocols` to concrete implementations
*/
_Dip_'s Dependency Containers allow you to do very simple **Dependency Injection**
by associating `protocols` to concrete implementations
*/
public class DependencyContainer {

/**
Expand All @@ -42,8 +42,7 @@ public class DependencyContainer {
case Int(IntegerLiteralType)
}

private var dependencies = [DefinitionKey : Definition]()
private var lock: OSSpinLock = OS_SPINLOCK_INIT
var definitions = [DefinitionKey : Definition]()

/**
Designated initializer for a DependencyContainer
Expand All @@ -66,9 +65,7 @@ public class DependencyContainer {
Clear all the previously registered dependencies on this container.
*/
public func reset() {
lockAndDo {
dependencies.removeAll()
}
definitions.removeAll()
}

// MARK: Register dependencies
Expand All @@ -79,7 +76,14 @@ public class DependencyContainer {
- parameter tag: The arbitrary tag to associate this factory with when registering with that protocol. Pass `nil` to associate with any tag. Default value is `nil`.
- parameter factory: The factory to register, with return type of protocol you want to register it for

- note: You must cast the factory return type to the protocol you want to register it with (e.g `MyClass() as MyAPI`)
- note: You must cast the factory return type to the protocol you want to register it for.
Inside factory block if you need to reference container use it as `unowned` to avoid retain cycle.

**Example**
```swift
container.register { ServiceImp() as Service }
container.register { [unowned container] ClientImp(service: container.resolve()) as Client }
```
*/
public func register<T>(tag tag: Tag? = nil, factory: ()->T) -> DefinitionOf<T> {
return register(tag: tag, factory: factory, scope: .Prototype) as DefinitionOf<T>
Expand All @@ -88,11 +92,13 @@ public class DependencyContainer {
/**
Register a Singleton instance associated with optional tag.

- parameter tag: The arbitrary tag to associate this instance with when registering with that protocol. `nil` to associate with any tag.
- parameter tag: The arbitrary tag to associate this instance with when registering with that protocol.
Pass `nil` to associate with any tag.
- parameter instance: The instance to register, with return type of protocol you want to register it for

- note: You must cast the instance to the protocol you want to register it with (e.g `MyClass() as MyAPI`)
*/
@available(*, deprecated, message="Use inScope(:) method of DefinitionOf instead to define scope.")
public func register<T>(tag tag: Tag? = nil, @autoclosure(escaping) instance factory: ()->T) -> DefinitionOf<T> {
return register(tag: tag, factory: { factory() }, scope: .Singleton)
}
Expand Down Expand Up @@ -120,9 +126,7 @@ public class DependencyContainer {
public func register<T, F>(tag tag: Tag? = nil, factory: F, scope: ComponentScope) -> DefinitionOf<T> {
let key = DefinitionKey(protocolType: T.self, factoryType: F.self, associatedTag: tag)
let definition = DefinitionOf<T>(factory: factory, scope: scope)
lockAndDo {
dependencies[key] = definition
}
definitions[key] = definition
return definition
}

Expand All @@ -131,9 +135,11 @@ public class DependencyContainer {
/**
Resolve a dependency.

If no instance/factory was registered with this `tag` for this `protocol`, it will try to resolve the instance/factory associated with `nil` (no tag).
If no definition was registered with this `tag` for this `protocol`,
it will try to resolve the definition associated with `nil` (no tag).

- parameter tag: The arbitrary tag to look for when resolving this protocol.

*/
public func resolve<T>(tag tag: Tag? = nil) -> T {
return resolve(tag: tag) { (factory: ()->T) in factory() }
Expand Down Expand Up @@ -161,37 +167,74 @@ public class DependencyContainer {
public func resolve<T, F>(tag tag: Tag? = nil, builder: F->T) -> T {
let key = DefinitionKey(protocolType: T.self, factoryType: F.self, associatedTag: tag)
let nilTagKey = tag.map { _ in DefinitionKey(protocolType: T.self, factoryType: F.self, associatedTag: nil) }
var resolved: T!
lockAndDo { [unowned self] in
resolved = self._resolve(key, nilTagKey: nilTagKey, builder: builder)

guard let definition = (self.definitions[key] ?? self.definitions[nilTagKey]) as? DefinitionOf<T> else {
fatalError("No definition registered with \(key) or \(nilTagKey)."
+ "Check the tag, type you try to resolve, number, order and types of runtime arguments passed to `resolve()`.")
}
return resolved

let usingKey: DefinitionKey? = definition.scope == .ObjectGraph ? key : nil
return _resolve(usingKey, definition: definition, builder: builder)
}

/// Actually resolve dependency
private func _resolve<T, F>(key: DefinitionKey, nilTagKey: DefinitionKey?, builder: F->T) -> T {
guard let definition = (self.dependencies[key] ?? self.dependencies[nilTagKey]) as? DefinitionOf<T> else {
fatalError("No instance factory registered with \(key) or \(nilTagKey)")
}
private func _resolve<T, F>(key: DefinitionKey?, definition: DefinitionOf<T>, builder: F->T) -> T {

if let resolvedInstance = definition.resolvedInstance {
return resolvedInstance
resolvedInstances.incrementDepth()
defer { resolvedInstances.decrementDepth() }

if let previouslyResolved: T = resolvedInstances.previouslyResolved(key, definition: definition) {
return previouslyResolved
}
else {
let resolved = builder(definition.factory as! F)
definition.resolvedInstance = resolved
return resolved
let resolvedInstance = builder(definition.factory as! F)

//when builder calls factory it will in turn resolve sub-dependencies (if there are any)
//when it returns instance that we try to resolve here can be already resolved
//so we return it, throwing away instance created by previous call to builder
if let previouslyResolved: T = resolvedInstances.previouslyResolved(key, definition: definition) {
return previouslyResolved
}

resolvedInstances.storeResolvedInstance(resolvedInstance, forKey: key, definition: definition)
definition.resolveDependenciesBlock?(self, resolvedInstance)

return resolvedInstance
}
}

// MARK: - Private

private func lockAndDo(@noescape block: Void->Void) {
OSSpinLockLock(&lock)
defer { OSSpinLockUnlock(&lock) }
block()
let resolvedInstances = ResolvedInstances()

///Pool to hold instances, created during call to `resolve()`.
///Before `resolve()` returns pool is drained.
class ResolvedInstances {
var resolvedInstances = [DefinitionKey: Any]()

func storeResolvedInstance<T>(instance: T, forKey key: DefinitionKey?, definition: DefinitionOf<T>) {
self.resolvedInstances[key] = instance
definition.resolvedInstance = instance
}

func previouslyResolved<T>(key: DefinitionKey?, definition: DefinitionOf<T>) -> T? {
return (definition.resolvedInstance ?? self.resolvedInstances[key]) as? T
}

var depth: Int = 0

func incrementDepth() {
depth++
}

func decrementDepth() {
guard depth-- > 0 else { fatalError("Depth can not be lower than zero") }
if depth == 0 {
resolvedInstances.removeAll()
}
}
}

}

extension DependencyContainer.Tag: IntegerLiteralConvertible {
Expand Down Expand Up @@ -229,8 +272,14 @@ public func ==(lhs: DependencyContainer.Tag, rhs: DependencyContainer.Tag) -> Bo
}

extension Dictionary {
subscript(key: Key?) -> Value! {
guard let key = key else { return nil }
return self[key]
subscript(key: Key?) -> Value? {
get {
guard let key = key else { return nil }
return self[key]
}
set {
guard let key = key else { return }
self[key] = newValue
}
}
}
Loading

0 comments on commit 77b9a66

Please sign in to comment.