Skip to content

Commit

Permalink
Merge pull request #27 from TinkoffCreditSystems/MIC-976
Browse files Browse the repository at this point in the history
add weak singleton scope
  • Loading branch information
alexwillrock authored Nov 4, 2019
2 parents 6e72f92 + e955cd8 commit 714b440
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 148 deletions.
6 changes: 6 additions & 0 deletions EasyDi.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
5C88753F236FD22500019260 /* Test_CrossAssemblyInjections_WeakSingletonCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C88753E236FD22500019260 /* Test_CrossAssemblyInjections_WeakSingletonCycle.swift */; };
5C887540236FD22500019260 /* Test_CrossAssemblyInjections_WeakSingletonCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C88753E236FD22500019260 /* Test_CrossAssemblyInjections_WeakSingletonCycle.swift */; };
8BEE13521F9A27C800A02331 /* EasyDi.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BEE13501F9A27C800A02331 /* EasyDi.h */; settings = {ATTRIBUTES = (Public, ); }; };
8BEE13561F9A27EA00A02331 /* EasyDi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEE13551F9A27EA00A02331 /* EasyDi.swift */; };
8BEE13571F9A27F200A02331 /* EasyDi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEE13551F9A27EA00A02331 /* EasyDi.swift */; };
Expand Down Expand Up @@ -52,6 +54,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
5C88753E236FD22500019260 /* Test_CrossAssemblyInjections_WeakSingletonCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test_CrossAssemblyInjections_WeakSingletonCycle.swift; sourceTree = "<group>"; };
8BEE13501F9A27C800A02331 /* EasyDi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EasyDi.h; sourceTree = "<group>"; };
8BEE13511F9A27C800A02331 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
8BEE13551F9A27EA00A02331 /* EasyDi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EasyDi.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -133,6 +136,7 @@
C3614B451F1C8B5F00B1F4A1 /* Test_Context.swift */,
C3614B461F1C8B5F00B1F4A1 /* Test_CrossAssemblyInjections.swift */,
E6DCF5C41F2F62A000D9F8BC /* Test_CrossAssemblyInjections_SingletonCycle.swift */,
5C88753E236FD22500019260 /* Test_CrossAssemblyInjections_WeakSingletonCycle.swift */,
C3614B471F1C8B5F00B1F4A1 /* Test_Injections.swift */,
C3614B481F1C8B5F00B1F4A1 /* Test_Patches.swift */,
C3614B491F1C8B5F00B1F4A1 /* Test_ProtocolBasedInjection.swift */,
Expand Down Expand Up @@ -373,6 +377,7 @@
A5ABB84321A5522400C96320 /* Test_Threadsafety.swift in Sources */,
E6DCF5C61F2F62A600D9F8BC /* Test_CrossAssemblyInjections_SingletonCycle.swift in Sources */,
C3614B581F1C8B6800B1F4A1 /* Test_ProtocolBasedInjection.swift in Sources */,
5C88753F236FD22500019260 /* Test_CrossAssemblyInjections_WeakSingletonCycle.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -403,6 +408,7 @@
C37DEA321F1E9B2100279AD3 /* Test_Injections.swift in Sources */,
C37DEA301F1E9B2100279AD3 /* Test_Context.swift in Sources */,
C37DEA341F1E9B2100279AD3 /* Test_ProtocolBasedInjection.swift in Sources */,
5C887540236FD22500019260 /* Test_CrossAssemblyInjections_WeakSingletonCycle.swift in Sources */,
E6DCF5C71F2F62A700D9F8BC /* Test_CrossAssemblyInjections_SingletonCycle.swift in Sources */,
C37DEA351F1E9B2100279AD3 /* Test_Scope.swift in Sources */,
);
Expand Down
94 changes: 57 additions & 37 deletions Sources/EasyDi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public struct DIContextEmptyLocker {
extension NSRecursiveLock: DIContextLocker {
}

struct WeakSingletonWrapper {
weak var instance: AnyObject?
}

/// This class is used to join assembly instances into separated shared group.
///
/// All assemblies with one context shares object graph stack.
Expand All @@ -36,10 +40,10 @@ extension NSRecursiveLock: DIContextLocker {
/// ```
///
public final class DIContext {

public static var defaultInstance = DIContext()
fileprivate var assemblies: [String: Assembly] = [:]

var objectGraphStorage: [String: InjectableObject] = [:]
var objectGraphStackDepth: Int = 0
let locker: DIContextLocker
Expand All @@ -48,16 +52,17 @@ public final class DIContext {
///
/// Dictionary key is **key** parameter from **define** method
var singletons: [String: InjectableObject] = [:]
var weakSingletons: [String: WeakSingletonWrapper] = [:]

/// Array of applyed substitutions
///
/// Dictionary key is **key** parameter from **define** method
var substitutions: [String: UntypedPatchClosure] = [:]

public init(locker: DIContextLocker = NSRecursiveLock()) {
self.locker = locker
}

/// This method creates assembly instance based on it's return type.
///
/// - returns: Assembly instance
Expand All @@ -69,7 +74,7 @@ public final class DIContext {
let instance = self.instance(for: AssemblyType.self)
return castAssemblyInstance(instance, asType: AssemblyType.self)
}

/// This method creates assembly instance by type
///
/// - parameter assemblyType: Class of the assembly
Expand All @@ -81,7 +86,7 @@ public final class DIContext {
/// ```
public func instance(for assemblyType: Assembly.Type) -> Assembly {
locker.lock(); defer { locker.unlock() }

let assemblyClassName = String(reflecting: assemblyType)
if let existingInstance = self.assemblies[assemblyClassName] {
return existingInstance
Expand Down Expand Up @@ -132,6 +137,9 @@ public enum Scope {
///
/// [Singleton] description contains short example with memory graph illustration
case lazySingleton

/// [WeakSingleton] description contains short example with memory graph illustration
case weakSingleton
}


Expand All @@ -154,9 +162,9 @@ public enum Scope {
///
/// ```
open class Assembly: AssemblyInternal {

public internal(set) weak var context: DIContext!

/// This method creates assembly for specified context or default context if no parameters provided
///
/// - parameter context: DIContext object which assembly should belong to
Expand All @@ -167,16 +175,16 @@ open class Assembly: AssemblyInternal {
let instance = context.instance(for: self)
return castAssemblyInstance(instance, asType: self)
}

/// Helper internal method to create assembly
internal static func newInstance() -> Self {

return self.init()
}

/// Initialiser
public required init() {}

/// This method forces assembly to return result of closure instead of the assembly's dependency.
///
/// It's usefull to stub objects and make A / B testing
Expand All @@ -187,21 +195,21 @@ open class Assembly: AssemblyInternal {
public func addSubstitution<ObjectType: InjectableObject>(
for simpleDefinitionKey: String,
with substitutionClosure: @escaping SubstitutionClosure<ObjectType>) {

let definitionKey = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey
context.substitutions[definitionKey] = substitutionClosure
}

/// This method removes substitution from assembly
///
/// - parameter definitionKey: should exactly match method or property name of substituting dependency
///
public func removeSubstitution(for simpleDefinitionKey: String) {

let definitionKey = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey
context.substitutions[definitionKey] = nil
}

/// The method defines return-only placeholder for object.
///
/// Use this method to inject something, created or injected with runtime parameters.
Expand Down Expand Up @@ -319,13 +327,13 @@ open class Assembly: AssemblyInternal {
scope: Scope = .objectGraph,
init initClosure: @autoclosure @escaping () -> ObjectType,
inject injectClosure: ObjectInjectClosure<ObjectType>? = nil ) -> ResultType {

return define(key: key, definitionKey: definitionKey, scope: scope) { (definition:Definition<ObjectType>) in
definition.initClosure = initClosure
definition.injectClosure = injectClosure
}
}

/// Internal method where main injection logic is performed.
///
/// - parameter key: name of the method or property. Default value should be used in most cases
Expand All @@ -345,19 +353,19 @@ open class Assembly: AssemblyInternal {
definitionKey simpleDefinitionKey: String = #function,
scope: Scope = .objectGraph,
definitionClosure: DefinitionClosure<ObjectType>? = nil) -> ResultType {

guard let context = self.context else {
fatalError("Associated context doesn't exists anymore")
}

context.locker.lock(); defer { context.locker.unlock() }

// Objects are stored in context by key made of Assembly class name and name of var or method
let key: String = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleKey
let definitionKey: String = String(reflecting: self).replacingOccurrences(of: ".", with: "") + simpleDefinitionKey

var result: ObjectType

// First of all it checks if there's substitution for this var or method
if let substitutionClosure = context.substitutions[definitionKey] {

Expand All @@ -366,23 +374,26 @@ open class Assembly: AssemblyInternal {
fatalError("Expected type: \(ResultType.self), received: \(type(of: substitutionObject))")
}
return object

// Next check for existing singletons
// Next check for existing singletons
} else if scope == .lazySingleton, let singleton = context.singletons[key] {

result = singleton as! ObjectType

// And trying to return object from graph
// And trying to return object from graph
} else if scope == .weakSingleton, let wrapper = context.weakSingletons[key], let weakSingletion = wrapper.instance {

result = weakSingletion as! ObjectType

} else if let objectFromStack = context.objectGraphStorage[key],
scope != .prototype,
let unwrappedObject = objectFromStack as? ObjectType {
result = unwrappedObject
} else {

// Create Definition object to store injections and dependencies information
let definition = Definition<ObjectType>()
definitionClosure?(definition)

context.objectGraphStackDepth += 1
guard var object = definition.initObject() else {
fatalError("Failed to initialize object")
Expand All @@ -396,20 +407,29 @@ open class Assembly: AssemblyInternal {
context.objectGraphStackDepth += 1
object = definition.injectObject(object: object)
context.objectGraphStackDepth -= 1

result = object as! ObjectType
}

// When recursion is finished, remove all objects from objectGraph
if context.objectGraphStackDepth == 0 {
context.objectGraphStorage.removeAll()
}

// And save singletons
if context.singletons[key] == nil, scope == .lazySingleton {
context.singletons[key] = result
}

if context.weakSingletons[key] == nil, scope == .weakSingleton {
context.weakSingletons[key] = WeakSingletonWrapper(instance: result as AnyObject)
}

if var wrapper = context.weakSingletons[key], scope == .weakSingleton, wrapper.instance == nil {
wrapper.instance = result as AnyObject
context.weakSingletons[key] = wrapper
}

guard let finalResult = result as? ResultType else {
fatalError("Failed to build result object. Expected \(ResultType.self) received: \(result)")
}
Expand All @@ -420,32 +440,32 @@ open class Assembly: AssemblyInternal {

/// This is type-erasing protocol used to store definition generics and access them
internal protocol DefinitionInternal {

func initObject() -> InjectableObject?
func injectObject(object: InjectableObject) -> InjectableObject
}

/// Definition object is used to store initialization and injection closures
public final class Definition<ObjectType: InjectableObject>: DefinitionInternal {

public var initClosure: ObjectInitClosure<ObjectType>?
public var injectClosure: ObjectInjectClosure<ObjectType>?

func initObject() -> InjectableObject? {

return self.initClosure?()
}

func injectObject(object: InjectableObject) -> InjectableObject {

guard let injectableObject = object as? ObjectType else {
fatalError()
}

guard let actualInjectClosure = self.injectClosure else {
return object
}

return actualInjectClosure(injectableObject)
}
}
Expand Down
15 changes: 3 additions & 12 deletions Tests/Test_Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,17 @@ import XCTest
import EasyDi

class Test_Context: XCTestCase {

func testDefaultContext() {

class TestAssembly: Assembly {
}
class TestAssembly: Assembly { }

let assemblyInstance1 = TestAssembly.instance()
let assemblyInstance2 = TestAssembly.instance()

XCTAssertTrue(assemblyInstance1 === assemblyInstance2)
}


func testSameContext() {

class TestAssembly: Assembly {
}
class TestAssembly: Assembly { }

let context: DIContext = DIContext()
let assemblyInstance1 = TestAssembly.instance(from: context)
Expand All @@ -36,9 +30,7 @@ class Test_Context: XCTestCase {
}

func testDifferentContexts() {

class TestAssembly: Assembly {
}
class TestAssembly: Assembly { }

let assemblyInstance1 = TestAssembly.instance()

Expand All @@ -47,5 +39,4 @@ class Test_Context: XCTestCase {

XCTAssertFalse(assemblyInstance1 === assemblyInstance2)
}

}
Loading

0 comments on commit 714b440

Please sign in to comment.