Skip to content

Commit

Permalink
Optimize creation of factory cache id
Browse files Browse the repository at this point in the history
  • Loading branch information
hmlongco committed Jun 10, 2022
1 parent 82625e1 commit 6e85deb
Show file tree
Hide file tree
Showing 3 changed files with 22 additions and 11 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Factory Changelog

### 1.0.4

* Optimize creation of factory cache id

### 1.0.3

* Streamline handling of optionals in registered
Expand Down
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class ContentViewModel: ObservableObject {
...
}
```
You can use the container directly or the property wrapper if you prefer, but either way for clarity I'd suggest grouping all of a given object's dependencies in a single place and marking them as private.
You can call the factory or the property wrapper if you prefer, but either way for clarity I'd suggest grouping all of a given object's dependencies in a single place near the top of the class and marking them as private.

## Mocking and Testing

Expand Down Expand Up @@ -116,7 +116,7 @@ struct ContentView_Previews: PreviewProvider {

Note the line in our preview code where we’re gone back to our container and registered a new closure on our factory. This function overrides the default factory closure.

Now when our preview is displayed `ContentView` creates a `ContentViewModel` which in turn depends on `myService` using the Injected property wrapper. But when the factory is asked for an instance of `MyServiceType` it now returns a `MockService2` instance instead of the `MyService` instance originally defined.
Now when our preview is displayed `ContentView` creates a `ContentViewModel` which in turn depends on `myService` using the Injected property wrapper. But when the factory is asked for an instance of `MyServiceType` it now returns a `MockService2` instead of the `MyService` type originally defined.

We can do this because we originally cast the result of the myService factory to be the protocol `MyServiceType`. And since `MockService2` conforms to the `MyServiceType` protocol, we’re good and we can replace one with the other.

Expand All @@ -125,14 +125,16 @@ extension Container {
static let myService = Factory<MyServiceType> { MyService() }
}
```
If not specialized, the type of the factory is inferred to be the type returned by the factory closure. You could also get the same result from a cast. Both are equivalent.
If not specialized, the type of the factory is inferred to be the type returned by the factory closure. You could also get the same result from explicitly specializing the generic Factory as shown below. Both are equivalent.

```swift
extension Container {
static let myService = Factory { MyService() as MyServiceType }
static let myService = Factory<MyServiceType> { MyService() }
}
```

One additional thing to notice is that the result of our registration block must also conform to the type of the original factory. If it’s not and if you try to return something else Swift will complain about it and give you an error. In short, registrations are also compile-time safe.

If we have several mocks that we use all of the time, we can also add a setup function to the container to make this easier.

```swift
Expand Down Expand Up @@ -176,18 +178,19 @@ Unless altered, the default scope is `unique`; every time the factory is asked f

Other common scopes are `cached` and `shared`. Cached items are saved until the cache is reset, while shared items persist just as long as someone holds a strong reference to them. When the last reference goes away, the weakly held shared reference also goes away.

You can also add your own special purpose scopes to the mix.
You can also add your own special purpose caches to the mix.

```swift
extension Container.Scope {
static var session = Cached()
}

extension Container {
static let authentication = Factory(scope: .session) { Authentication() }
static let authenticatedUser = Factory(scope: .session) { AuthenticatedUser() }
static let profileImageCache = Factory(scope: .session) { ProfileImageCache() }
}
```
Once created, a single instance of `Authentication` will be provided to anyone that needs one... up until the point where the session scope is reset, perhaps by a user logging out.
Once created, a single instance of `AuthenticatedUser` and `ProfileImageCache` will be provided to anyone that needs one... up until the point where the session scope is reset, perhaps by a user logging out.

```swift
func logout() {
Expand Down Expand Up @@ -286,6 +289,12 @@ Container.Scope.reset() // all scopes except singletons
Container.Scope.reset(includingSingletons: true) // all including singletons
```

## Resolver

Factory will probably mark the end of Resolver. I learned a lot from that project, and it even won me an [Open Source Peer Bonus from Google](https://opensource.googleblog.com/2021/09/announcing-latest-open-source-peer-bonus-winners.html). (I always thought it a bit strange for an iOS developer to get an award from Google, but there you have it.)

But Factory is smaller, faster, cleaner and all in all a much better solution than Resolver could ever be.

## Installation

Factory is available as a Swift Package. Just add it to your projects.
Expand Down
6 changes: 2 additions & 4 deletions Sources/Factory/Factory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ public struct Factory<T> {
/// Resolves and returns an instance of the desired object type. This may be a new instance or one that was created previously and then cached,
/// depending on whether or not a scope was specified when the factory was created.
public func callAsFunction() -> T {
let id = Int(bitPattern: ObjectIdentifier(T.self))
if let instance: T = scope?.cached(id) {
SharedContainer.Decorator.cached?(instance)
return instance
} else {
let instance = SharedContainer.Registrations.registered(id) ?? factory()
let instance: T = SharedContainer.Registrations.registered(id) ?? factory()
scope?.cache(id: id, instance: instance)
SharedContainer.Decorator.created?(instance)
return instance
Expand All @@ -62,19 +61,18 @@ public struct Factory<T> {
///
/// All registrations are stored in SharedContainer.Registrations.
public func register(factory: @escaping () -> T) {
let id = Int(bitPattern: ObjectIdentifier(T.self))
SharedContainer.Registrations.register(id: id, factory: factory)
scope?.reset(id)
}

/// Deletes any registered factory override and resets this Factory to use the factory closure specified during initialization. Also
/// resets the scope so that a new instance of the original type will be returned on the next resolution.
public func reset() {
let id = Int(bitPattern: ObjectIdentifier(T.self))
SharedContainer.Registrations.reset(id)
scope?.reset(id)
}

private let id: Int = Int(bitPattern: ObjectIdentifier(T.self))
private var factory: () -> T
private var scope: SharedContainer.Scope?
}
Expand Down

0 comments on commit 6e85deb

Please sign in to comment.