Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a support of a weak instance with the identity. #452

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,18 @@ class LoggedInComponent: Component<LoggedInDependency> {
return shared { ScoreStreamImpl() }
}

func mutableScoreStream(id: String) -> MutableScoreStream {
return weak(hash: id) { ScoreStreamImpl() }
}

var loggedInViewController: UIViewController {
return LoggedInViewController(gameBuilder: gameComponent, scoreStream: scoreStream, scoreSheetBuilder: scoreSheetComponent)
}
}
```
**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.

Expand Down
6 changes: 5 additions & 1 deletion Documents/ko_KR/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,18 @@ class LoggedInComponent: Component<LoggedInDependency> {
return shared { ScoreStreamImpl() }
}

func mutableScoreStream(id: String) -> MutableScoreStream {
return weak(hash: id) { ScoreStreamImpl() }
}

var loggedInViewController: UIViewController {
return LoggedInViewController(gameBuilder: gameComponent, scoreStream: scoreStream, scoreSheetBuilder: scoreSheetComponent)
}
}
```
**참고:** *DI 그래프*에서 의미가 있는 항목과 `ViewController` 하위 클래스의 지역 변수일 수 있는 항목을 결정하는 것은 사용자의 몫입니다. Swift에는 'OCMock'과 같은 도구가 없기 때문에 테스트 중에 mocking하고 싶은 것은 무엇이든 프로토콜로 전달해야 합니다.

예제의 `shared` 구문은 저희가 (`Component` 기본 클래스 내부에서) 제공하는 유틸리티 함수로 이 `var`에 액세스할 때마다 단순하게 동일한 인스턴스를 반환합니다. (아래에 선언된 프로퍼티는 대조적으로 새로운 매번 인스턴스를 반환합니다). 이렇게 하면 이 프로퍼티의 라이프사이클이 Component의 라이프사이클에 연결됩니다.
예제의 `shared` 구문은 저희가 (`Component` 기본 클래스 내부에서) 제공하는 유틸리티 함수로 이 `var`에 액세스할 때마다 단순하게 동일한 인스턴스를 반환합니다. (아래에 선언된 프로퍼티는 대조적으로 새로운 매번 인스턴스를 반환합니다). 이렇게 하면 이 프로퍼티의 라이프사이클이 Component의 라이프사이클에 연결됩니다. 그리고 지정된 `hash` 인수가 동일하고 인스턴스가 어딘가에서 참조되는 한 `weak` 구성을 사용하여 동일한 인스턴스를 사용할 수 있습니다.

Component를 사용하여 이 component와 쌍을 이루는 `ViewController`를 구성할 수도 있습니다. 위의 예제에서 볼 수 있듯이, 이것은 `ViewController`가 프로젝트에서 DI 시스템을 사용하고 있다는 사실을 모르고도 `ViewController`가 필요로 하는 모든 의존성을 전달할 수 있도록 합니다. **"DI의 이점"** 문서에서 언급했듯이 구체적인 클래스나 구조체 대신 프로토콜을 전달하는 것이 가장 좋습니다.

Expand Down
98 changes: 98 additions & 0 deletions 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 @@ -174,6 +225,10 @@ open class Component<DependencyType>: 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: ".")
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
}