Skip to content

Commit

Permalink
Add a support of a weak instance with the identity
Browse files Browse the repository at this point in the history
  • Loading branch information
ryu1sazae committed Feb 4, 2023
1 parent e2ad104 commit b4b6976
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 1 deletion.
100 changes: 99 additions & 1 deletion Sources/NeedleFoundation/Component.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ import Foundation
/// The base protocol of a dependency, enabling Needle's parsing process.
public protocol Dependency: AnyObject {}

private final class Weak<T> {
private weak var _value: AnyObject?

fileprivate init(_ value: AnyObject? = nil) {
self._value = value
}

fileprivate var value: T? {
return _value as? T
}
}

#if NEEDLE_DYNAMIC
public protocol Registration {
func registerItems()
Expand Down Expand Up @@ -153,6 +165,45 @@ open class Component<DependencyType>: Scope {
return instance
}

/// Share the enclosed object as long as this instance has been referenced somewhere.
/// This allows this scope as well as all child scopes to share a same instance of the object.
///
/// - note: Shared dependency's constructor should avoid switching threads
/// as it may cause a deadlock.
///
/// - parameter hash: The key to distinguish among same #function key.
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func weak<T, Hash: Hashable>(__function: String = #function, hash: Hash = "\(#function)", _ factory: () -> T) -> T {
// Use function name as the key, since this is unique per component
// class. And also, use a value confirming to Hashable as the key as
// well so that you're able to get an instance whose hash value is same.
// At the same time, this is also 150 times faster than
// interpolating the type to convert to string, `"\(T.self)"`.
weakInstancelock.lock()
defer {
weakInstancelock.unlock()
}

var hasher = Hasher()
hasher.combine(__function)
hasher.combine(hash)
let key = hasher.finalize()

// Additional nil coalescing is needed to mitigate a Swift bug appearing
// in Xcode 10. see https://bugs.swift.org/browse/SR-8704. Without this
// measure, calling `shared` from a function that returns an optional type
// will always pass the check below and return nil if the instance is not
// initialized.
guard let instance = weakInstances[key]?.value as? T else {
let instance = factory()
weakInstances[key] = Weak(instance as AnyObject)
return instance
}

return instance
}

public func find<T>(property: String, skipThisLevel: Bool) -> T {
guard let itemCloure = localTable[property] else {
return parent.find(property: property, skipThisLevel: false)
Expand All @@ -173,7 +224,11 @@ open class Component<DependencyType>: Scope {
// MARK: - Private

private let sharedInstanceLock = NSRecursiveLock()
private var sharedInstances = [String: Any]()
private var sharedInstances = [Int: Any]()

private let weakInstancelock = NSRecursiveLock()
private var weakInstances: [Int: Weak] = [:]

private lazy var name: String = {
let fullyQualifiedSelfName = String(describing: self)
let parts = fullyQualifiedSelfName.components(separatedBy: ".")
Expand Down Expand Up @@ -262,6 +317,45 @@ open class Component<DependencyType>: Scope {
return instance
}

/// Share the enclosed object as long as this instance has been referenced somewhere.
/// This allows this scope as well as all child scopes to share a same instance of the object.
///
/// - note: Shared dependency's constructor should avoid switching threads
/// as it may cause a deadlock.
///
/// - parameter hash: The key to distinguish among same #function key.
/// - parameter factory: The closure to construct the dependency object.
/// - returns: The dependency object instance.
public final func weak<T, Hash: Hashable>(__function: String = #function, hash: Hash = "\(#function)", _ factory: () -> T) -> T {
// Use function name as the key, since this is unique per component
// class. And also, use a value confirming to Hashable as the key as
// well so that you're able to get an instance whose hash value is same.
// At the same time, this is also 150 times faster than
// interpolating the type to convert to string, `"\(T.self)"`.
weakInstancelock.lock()
defer {
weakInstancelock.unlock()
}

var hasher = Hasher()
hasher.combine(__function)
hasher.combine(hash)
let key = hasher.finalize()

// Additional nil coalescing is needed to mitigate a Swift bug appearing
// in Xcode 10. see https://bugs.swift.org/browse/SR-8704. Without this
// measure, calling `shared` from a function that returns an optional type
// will always pass the check below and return nil if the instance is not
// initialized.
guard let instance = weakInstances[key]?.value as? T else {
let instance = factory()
weakInstances[key] = Weak(instance as AnyObject)
return instance
}

return instance
}

public subscript<T>(dynamicMember keyPath: KeyPath<DependencyType, T>) -> T {
return dependency[keyPath: keyPath]
}
Expand All @@ -270,6 +364,10 @@ open class Component<DependencyType>: Scope {

private let sharedInstanceLock = NSRecursiveLock()
private var sharedInstances = [String: Any]()

private let weakInstancelock = NSRecursiveLock()
private var weakInstances: [Int: Weak<AnyObject>] = [:]

private lazy var name: String = {
let fullyQualifiedSelfName = String(describing: self)
let parts = fullyQualifiedSelfName.components(separatedBy: ".")
Expand Down
26 changes: 26 additions & 0 deletions Tests/NeedleFoundationTests/ComponentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ class ComponentTests: XCTestCase {
let component = TestComponent()
XCTAssert(component.optionalShare === component.expectedOptionalShare)
}

func test_weak_veirfySingleInstance() {
let component = TestComponent()

let id1 = "id1"
let id2 = "id2"

var weak: TestClass? = component.weak(hash: id1)
weak?.number = 0
XCTAssert(weak === component.weak(hash: id1), "Should have returned same shared object")
XCTAssert(component.weak(hash: id1).number == 0)
XCTAssertFalse(component.weak(hash: id1) === component.weak(hash: id2))

weak = nil
weak = component.weak(hash: id1)
XCTAssertNotNil(weak)
XCTAssertNil(component.weak(hash: id1).number)
}
}

class TestComponent: BootstrapComponent {
Expand All @@ -61,6 +79,10 @@ class TestComponent: BootstrapComponent {
fileprivate var optionalShare: ClassProtocol? {
return shared { self.expectedOptionalShare }
}

fileprivate func weak(hash: String) -> TestClass {
return weak(hash: hash) { TestClass() }
}
}

private protocol ClassProtocol: AnyObject {
Expand All @@ -70,3 +92,7 @@ private protocol ClassProtocol: AnyObject {
private class ClassProtocolImpl: ClassProtocol {

}

private class TestClass {
var number: Int? = nil
}

0 comments on commit b4b6976

Please sign in to comment.