From 0c367fe739fad523e682d3edce08a54dabffa9b3 Mon Sep 17 00:00:00 2001 From: ryu1sazae Date: Sat, 4 Feb 2023 23:40:57 +0900 Subject: [PATCH 1/2] Add a support of a weak instance with the identity --- Sources/NeedleFoundation/Component.swift | 98 +++++++++++++++++++ .../ComponentTests.swift | 26 +++++ 2 files changed, 124 insertions(+) diff --git a/Sources/NeedleFoundation/Component.swift b/Sources/NeedleFoundation/Component.swift index af05bc1f..0d4e46e4 100644 --- a/Sources/NeedleFoundation/Component.swift +++ b/Sources/NeedleFoundation/Component.swift @@ -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 { + 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() @@ -153,6 +165,45 @@ open class Component: 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(__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(property: String, skipThisLevel: Bool) -> T { guard let itemCloure = localTable[property] else { return parent.find(property: property, skipThisLevel: false) @@ -174,6 +225,10 @@ open class Component: Scope { private let sharedInstanceLock = NSRecursiveLock() private var sharedInstances = [String: 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: ".") @@ -262,6 +317,45 @@ open class Component: 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(__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(dynamicMember keyPath: KeyPath) -> T { return dependency[keyPath: keyPath] } @@ -270,6 +364,10 @@ open class Component: Scope { private let sharedInstanceLock = NSRecursiveLock() private var sharedInstances = [String: 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: ".") diff --git a/Tests/NeedleFoundationTests/ComponentTests.swift b/Tests/NeedleFoundationTests/ComponentTests.swift index a80cdb29..500d3611 100644 --- a/Tests/NeedleFoundationTests/ComponentTests.swift +++ b/Tests/NeedleFoundationTests/ComponentTests.swift @@ -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 { @@ -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 { @@ -70,3 +92,7 @@ private protocol ClassProtocol: AnyObject { private class ClassProtocolImpl: ClassProtocol { } + +private class TestClass { + var number: Int? = nil +} From 837542821e000977736eaec691da2ce5ff68e212 Mon Sep 17 00:00:00 2001 From: ryu1sazae Date: Sat, 4 Feb 2023 23:41:25 +0900 Subject: [PATCH 2/2] Add the documentation for a weak constructor --- API.md | 6 +++++- Documents/ko_KR/API.md | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/API.md b/API.md index 069d3e3a..0292be79 100644 --- a/API.md +++ b/API.md @@ -59,6 +59,10 @@ class LoggedInComponent: Component { return shared { ScoreStreamImpl() } } + func mutableScoreStream(id: String) -> MutableScoreStream { + return weak(hash: id) { ScoreStreamImpl() } + } + var loggedInViewController: UIViewController { return LoggedInViewController(gameBuilder: gameComponent, scoreStream: scoreStream, scoreSheetBuilder: scoreSheetComponent) } @@ -66,7 +70,7 @@ class LoggedInComponent: Component { ``` **Note:** It's up to you to decide what items make sense on the *DI Graph* and which items can be just local properties in your `ViewController` subclass. Anything that you'd like to mock during a test needs to be passed in (as a protocol) as Swift lacks an `OCMock` like tool. -The `shared` construct in the example is a utility function we provide (in the `Component` base class) that simply returns the same instance every time this `var` is accessed (as opposed to the one below it, which returns a new instance each time). This ties the lifecycle of this property to the lifecycle of the Component. +The `shared` construct in the example is a utility function we provide (in the `Component` base class) that simply returns the same instance every time this `var` is accessed (as opposed to the one below it, which returns a new instance each time). This ties the lifecycle of this property to the lifecycle of the Component. And you can use the `weak` construct to use a same instance as long as a specified `hash` argument is same and the instance has been referenced somewhere. You could also use the component to construct the `ViewController` that is paired with this component. As you can see in the example above, this allows us to pass in all the dependencies that the `ViewController` needs without the `ViewController` even being aware that you're using a DI system in your project. As noted in the "Benefits of DI" document, it's best to pass in protocols instead of concrete classes or structs. diff --git a/Documents/ko_KR/API.md b/Documents/ko_KR/API.md index 6595c62a..db4fc6ef 100644 --- a/Documents/ko_KR/API.md +++ b/Documents/ko_KR/API.md @@ -61,6 +61,10 @@ class LoggedInComponent: Component { return shared { ScoreStreamImpl() } } + func mutableScoreStream(id: String) -> MutableScoreStream { + return weak(hash: id) { ScoreStreamImpl() } + } + var loggedInViewController: UIViewController { return LoggedInViewController(gameBuilder: gameComponent, scoreStream: scoreStream, scoreSheetBuilder: scoreSheetComponent) } @@ -68,7 +72,7 @@ class LoggedInComponent: Component { ``` **참고:** *DI 그래프*에서 의미가 있는 항목과 `ViewController` 하위 클래스의 지역 변수일 수 있는 항목을 결정하는 것은 사용자의 몫입니다. Swift에는 'OCMock'과 같은 도구가 없기 때문에 테스트 중에 mocking하고 싶은 것은 무엇이든 프로토콜로 전달해야 합니다. -예제의 `shared` 구문은 저희가 (`Component` 기본 클래스 내부에서) 제공하는 유틸리티 함수로 이 `var`에 액세스할 때마다 단순하게 동일한 인스턴스를 반환합니다. (아래에 선언된 프로퍼티는 대조적으로 새로운 매번 인스턴스를 반환합니다). 이렇게 하면 이 프로퍼티의 라이프사이클이 Component의 라이프사이클에 연결됩니다. +예제의 `shared` 구문은 저희가 (`Component` 기본 클래스 내부에서) 제공하는 유틸리티 함수로 이 `var`에 액세스할 때마다 단순하게 동일한 인스턴스를 반환합니다. (아래에 선언된 프로퍼티는 대조적으로 새로운 매번 인스턴스를 반환합니다). 이렇게 하면 이 프로퍼티의 라이프사이클이 Component의 라이프사이클에 연결됩니다. 그리고 지정된 `hash` 인수가 동일하고 인스턴스가 어딘가에서 참조되는 한 `weak` 구성을 사용하여 동일한 인스턴스를 사용할 수 있습니다. Component를 사용하여 이 component와 쌍을 이루는 `ViewController`를 구성할 수도 있습니다. 위의 예제에서 볼 수 있듯이, 이것은 `ViewController`가 프로젝트에서 DI 시스템을 사용하고 있다는 사실을 모르고도 `ViewController`가 필요로 하는 모든 의존성을 전달할 수 있도록 합니다. **"DI의 이점"** 문서에서 언급했듯이 구체적인 클래스나 구조체 대신 프로토콜을 전달하는 것이 가장 좋습니다.