Skip to content

Commit

Permalink
Refactored scope caching implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
hmlongco committed Jun 9, 2022
1 parent ce6c558 commit b64e23d
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 76 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Factory Changelog

### 1.0.2

* Refactored common scope cache mechanism
* Allow global scope cache resets

### 1.0.1

* Improves performance on happy path factory resolution code
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Failure to find a matching type can lead to an application crash if we attempt t
* **Safe:** Factory is compile-time safe; a factory for a given type *must* exist or the code simply will not compile.
* **Flexible:** It's easy to override dependencies at runtime and for use in SwiftUI Previews.
* **Powerful:** Like Resolver, Factory supports application, cached, shared, and custom scopes, customer containers, arguments, decorators, and more.
* **Lightweight:** With all of that Factory is slim and trim, coming in at about 200 lines of code.
* **Lightweight:** With all of that Factory is slim and trim, coming in under 300 lines of code.
* **Performant:** Little to no setup time is needed for the vast majority of your services, resolutions are extremely fast, and no compile-time scripts or build phases are needed.
* **Concise:** Defining a registration usually takes just a single line of code.
* **Tested:** Unit tests ensure correct operation of registrations, resolutions, and scopes.
Expand Down Expand Up @@ -268,6 +268,24 @@ final class FactoryCoreTests: XCTestCase {
}
}
```

## Reset

You can also reset a registration to bring back the original factory closures. Or, if desired, you can reset everything back to square one with a single command.

```Swift
Container.myService.reset() // single
Container.Registrations.reset() // all
```

The same applies to scope management. You can reset a single cache, or all of them if desired.

```Swift
Container.Scope.cached.reset() // single
Container.Scope.reset() // all scopes except singletons
Container.Scope.reset(includingSingletons: true) // all including singletons
```

## Installation

Factory is available as a Swift Package. Just add it to your projects.
Expand Down
146 changes: 72 additions & 74 deletions Sources/Factory/Factory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,51 @@ open class SharedContainer {
private static var registrations: [Int:() -> Any] = [:]
private static var stack: [[Int:() -> Any]] = []
private static var lock = NSRecursiveLock()

}

/// Defines the base implementation of a scope.
/// Defines an abstract base implementation of a scope cache.
public class Scope {
private init() {}

fileprivate init(box: @escaping (_ instance: Any) -> AnyBox) {
self.box = box
Self.scopes.append(self)
}

/// Resets the cache. Any factory using this cache will return a new instance after the cache is reset.
public func reset() {
defer { lock.unlock() }
lock.lock()
cache = [:]
}

/// Internal cache resolution function used by Factory
fileprivate func cached<T>(_ id: Int) -> T? {
fatalError()
defer { lock.unlock() }
lock.lock()
return cache[id]?.instance as? T
}

/// Internal cache function used by Factory
fileprivate func cache(id: Int, instance: Any) {
fatalError()
defer { lock.unlock() }
lock.lock()
cache[id] = box(instance)
}
fileprivate func reset(_ id: Int) {}
fileprivate func reset() {}

/// Internal reset function used by Factory
fileprivate func reset(_ id: Int) {
defer { lock.unlock() }
lock.lock()
cache.removeValue(forKey: id)
}

public var isEmpty: Bool { cache.isEmpty }

private var box: (_ instance: Any) -> AnyBox
private var cache = [Int:AnyBox](minimumCapacity: 32)
private var lock = NSRecursiveLock()

}

/// Defines decorator functions that will be called when a factory is resolved.
Expand All @@ -173,89 +205,55 @@ open class SharedContainer {

extension SharedContainer.Scope {

/// Instance of the cached scope. The same instance will be returned by the factory until the cache is reset.
/// Defines a cached scope. The same instance will be returned by the factory until the cache is reset.
public static let cached = Cached()

/// Instance of the shared (weak) scope. The same instance will be returned by the factory as long as someone maintains a strong reference.
public static let shared = Shared()

/// Instance of the singleton scope. Once created, one and only once instance of the object will be created and returned by the factory.
public static let singleton = Cached()

/// Resets all scope caches.
public static func reset() {
Self.scopes.forEach { $0.reset() }
}

/// Defines the cached scope. The same instance will be returned by the factory until the cache is reset.
public final class Cached: SharedContainer.Scope {
public override init() {
super.init()
Self.scopes.append(self)
}
/// Resets the cache. Anything using this cache will return a new instance after the cache is reset.
public override func reset() {
defer { lock.unlock() }
lock.lock()
cache = [:]
}
fileprivate override func cached<T>(_ id: Int) -> T? {
defer { lock.unlock() }
lock.lock()
return cache[id] as? T
}
fileprivate override func cache(id: Int, instance: Any) {
defer { lock.unlock() }
lock.lock()
cache[id] = instance
public init() {
super.init { StrongBox(instance: $0 as AnyObject) }
}
fileprivate override func reset(_ id: Int) {
defer { lock.unlock() }
lock.lock()
cache.removeValue(forKey: id)
}
private var cache = [Int:Any](minimumCapacity: 32)
private var lock = NSRecursiveLock()
}

/// Defines the shared (weak) scope. The same instance will be returned by the factory as long as someone maintains a strong reference.
/// Defines a shared (weak) scope. The same instance will be returned by the factory as long as someone maintains a strong reference.
public static let shared = Shared()
public final class Shared: SharedContainer.Scope {
public override init() {
super.init()
Self.scopes.append(self)
}
/// Resets the cache. Anything using this cache will return a new instance after the cache is reset.
public override func reset() {
defer { lock.unlock() }
lock.lock()
cache = [:]
}
fileprivate override func cached<T>(_ id: Int) -> T? {
defer { lock.unlock() }
lock.lock()
return cache[id]?.instance as? T
public init() {
super.init { WeakBox(instance: $0 as AnyObject) }
}
fileprivate override func cache(id: Int, instance: Any) {
defer { lock.unlock() }
lock.lock()
cache[id] = WeakBox(instance: instance as AnyObject)
}
fileprivate override func reset(_ id: Int) {
defer { lock.unlock() }
lock.lock()
cache.removeValue(forKey: id)
}

/// Defines a singleton scope. The same instance will always be returned by the factory.
public static let singleton = Singleton()
public final class Singleton: SharedContainer.Scope {
public init() {
super.init { StrongBox(instance: $0 as AnyObject) }
}
private struct WeakBox {
weak var instance: AnyObject?
}

/// Resets all scope caches.
public static func reset(includingSingletons: Bool = false) {
Self.scopes.forEach {
if !($0 is Singleton) || includingSingletons {
$0.reset()
}
}
private var cache = [Int:WeakBox](minimumCapacity: 32)
private var lock = NSRecursiveLock()
}

private static var scopes: [SharedContainer.Scope] = []

}

private protocol AnyBox {
var instance: AnyObject? { get }
}

private struct StrongBox: AnyBox {
let instance: AnyObject?
}

private struct WeakBox: AnyBox {
weak var instance: AnyObject?
}

/// Convenience property wrappeer takes a factory and creates an instance of the desired type.
@propertyWrapper public struct Injected<T> {
private var factory: Factory<T>
Expand Down
1 change: 1 addition & 0 deletions Tests/FactoryTests/FactoryCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ final class FactoryCoreTests: XCTestCase {
override func setUp() {
super.setUp()
Container.Registrations.reset()
Container.Scope.reset()
}

func testBasicResolution() throws {
Expand Down
1 change: 1 addition & 0 deletions Tests/FactoryTests/FactoryInjectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class FactoryInjectionTests: XCTestCase {
override func setUp() {
super.setUp()
Container.Registrations.reset()
Container.Scope.reset()
}

func testBasicInjection() throws {
Expand Down
1 change: 1 addition & 0 deletions Tests/FactoryTests/FactoryRegistrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ final class FactoryRegistrationTests: XCTestCase {
override func setUp() {
super.setUp()
Container.Registrations.reset()
Container.Scope.reset()
}

func testPushPop() throws {
Expand Down
52 changes: 51 additions & 1 deletion Tests/FactoryTests/FactoryScopeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ final class FactoryScopeTests: XCTestCase {
override func setUp() {
super.setUp()
Container.Registrations.reset()
Container.Scope.cached.reset()
Container.Scope.reset()
}

func testUniqueScope() throws {
Expand Down Expand Up @@ -95,4 +95,54 @@ final class FactoryScopeTests: XCTestCase {
XCTAssertTrue(service2.id != service3.id)
}

func testValueCachedScope() throws {
let service1 = Container.valueService()
let service2 = Container.valueService()
XCTAssertTrue(service1.id == service2.id)
Container.Scope.cached.reset()
let service3 = Container.valueService()
XCTAssertTrue(service2.id != service3.id)
}

func testValueSharedScope() throws {
let service1 = Container.sharedValueService()
let service2 = Container.sharedValueService()
XCTAssertTrue(service1.id != service2.id) // value types can't be shared
}

func testGlobalScopeReset() throws {
let _ = Container.cachedService()
let _ = Container.singletonService()
let _ = Container.sharedService()
let _ = Container.sessionService()
XCTAssertFalse(Container.Scope.cached.isEmpty)
XCTAssertFalse(Container.Scope.session.isEmpty)
XCTAssertFalse(Container.Scope.shared.isEmpty)
XCTAssertFalse(Container.Scope.singleton.isEmpty)
Container.Scope.reset()
XCTAssertTrue(Container.Scope.cached.isEmpty)
XCTAssertTrue(Container.Scope.session.isEmpty)
XCTAssertTrue(Container.Scope.shared.isEmpty)
// following should not reset
XCTAssertFalse(Container.Scope.singleton.isEmpty)
}

func testGlobalScopeResetIncludingSingletons() throws {
let _ = Container.cachedService()
let _ = Container.singletonService()
let _ = Container.sharedService()
let _ = Container.sessionService()
XCTAssertFalse(Container.Scope.cached.isEmpty)
XCTAssertFalse(Container.Scope.session.isEmpty)
XCTAssertFalse(Container.Scope.shared.isEmpty)
XCTAssertFalse(Container.Scope.singleton.isEmpty)
Container.Scope.reset(includingSingletons: true)
XCTAssertTrue(Container.Scope.cached.isEmpty)
XCTAssertTrue(Container.Scope.session.isEmpty)
XCTAssertTrue(Container.Scope.shared.isEmpty)
// following should reset
XCTAssertTrue(Container.Scope.singleton.isEmpty)
}


}
9 changes: 9 additions & 0 deletions Tests/FactoryTests/MockServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ class MockService: MyServiceType {
}
}

struct ValueService: MyServiceType {
let id = UUID()
func text() -> String {
"ValueService"
}
}

extension Container {
static let myServiceType = Factory<MyServiceType> { MyService() }
static let mockService = Factory { MockService() }
Expand All @@ -35,6 +42,8 @@ extension Container {
static let singletonService = Factory(scope: .singleton) { MyService() }
static let optionalService = Factory<MyServiceType?> { MyService() }
static let sessionService = Factory(scope: .session) { MyService() }
static let valueService = Factory(scope: .cached) { ValueService() }
static let sharedValueService = Factory(scope: .shared) { ValueService() }
}

extension Container.Scope {
Expand Down

0 comments on commit b64e23d

Please sign in to comment.