Skip to content

Commit

Permalink
Merge branch 'feature/handle-deps-injection'
Browse files Browse the repository at this point in the history
  • Loading branch information
twittemb committed Aug 10, 2020
2 parents cc64239 + fb4f42f commit 8bbe898
Show file tree
Hide file tree
Showing 11 changed files with 860 additions and 33 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
**version 0.19.0**:

* add helper constructors for Feedback to be able to pass dependencies

**version 0.18.0**:

* breaking: Executers are no longer associated to a Reducer but to the whole Spin instead (and can still be overriden in each Feedback)
Expand Down
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- <a href="#the-multiple-ways-to-create-a-feedback">The multiple ways to create a Feedback</a>
- <a href="#feedback-lifecycle">Feedback lifecycle</a>
- <a href="#feedbacks-and-scheduling">Feedbacks and scheduling</a>
- <a href="#what-about-dependencies-in-feedbacks">What about dependencies in Feedbacks</a>
- <a href="#using-spin-in-a-uikit-or-appkit-based-app">Using Spin in a UIKit or AppKit based app</a>
- <a href="#using-spin-in-a-swiftUI-based-app">Using Spin in a SwiftUI based app</a>
- <a href="#using-spin-with-multiple-reactive-frameworks">Using Spin with multiple Reactive Frameworks</a>
Expand Down Expand Up @@ -276,6 +277,90 @@ Spin(initialState: Levels(left: 10, right: 20), executeOn: MainScheduler.instanc

Of course, it remains possible to handle the Schedulers by yourself inside the feedback functions.

# What about dependencies in Feedbacks

As we saw, a Feedback is a wrapper around a side effect. Side effects, by definition, will need some dependencies to perform their work. Things like: a network service, some persistence tools, a cryptographic
utility and so on.

However, side effects signature don't allow to pass dependencies, only a state. How can we take those deps into account ?

These are three possible technics:

### 1: Use a container type

```swift
class MyUseCase {
private let networkService: NetworkService
private let cryptographicTool: CryptographicTool

init(networkService: NetworkService, cryptographicTool: CryptographicTool) {
self.networkService = networkService
self.cryptographicTool = cryptographicTool
}

func load(state: MyState) -> AnyPublisher<MyEvent, Never> {
guard state == .loading else return { Empty().eraseToAnyPublisher() }

// use the deps here
self.networkService
.fetch()
.map { [cryptographicTool] in cryptographicTool.decrypt($0) }
...
}
}

// then we can build a Feedback with this UseCase
let myUseCase = MyUseCase(networkService: MyNetworkService(), cryptographicTool: MyCryptographicTool())
let feedback = Feedback(effect: myUseCase.load)
```

This technic has the benefit to be very familiar in terms of conception and could be compatible with existing patterns in your application.

It has the downside of forcing us to be careful while capturing dependencies in the side effect.

### 2: Use a feedback factory function

In the previous technic, we use the MyUseCase only as a container of dependencies. It has no other usage than that. We can get rid of it by using a function (global or static) that will receive our dependencies and help capture them in the side effect:

```swift
typealias LoadEffect: (MyState) -> AnyPublisher<MyEvent, Never>
func makeLoadEffect(networkService: NetworkService, cryptographicTool: CryptographicTool) -> LoadEffect {
return { state in
guard state == .loading else return { Empty().eraseToAnyPublisher() }

networkService
.fetch()
.map { cryptographicTool.decrypt($0) }
...
}
}

// then we can build a Feedback using this factory function
let effect = makeLoadEffect(networkService: MyNetworkService(), cryptographicTool: MyCryptographicTool())
let feedback = Feedback(effect: effect)
```

### 3: Use the built-in Feedback initializer

Spin comes with some Feedback initializers that ease the injection of dependencies. Under the hood, it uses a generic technic derived from the above one.

```swift
func loadEffect(networkService: NetworkService,
cryptographicTool: CryptographicTool,
state: MyState) -> AnyPublisher<MyEvent, Never> {
guard state == .loading else return { Empty().eraseToAnyPublisher() }

networkService
.fetch()
.map
}

// then we can build a Feedback directly using the appropriate initializer
let feedback = Feedback(effect: effect, dep1: MyNetworkService(), dep2: MyCryptographicTool())
```

Among those 3 technics it is the less verbose one. It feels a little bit like magic but simply uses partialization under the hood.

# Using Spin in a UIKit or AppKit based app

Although a feedback loop can exist by itself without any visualization, it makes more sense in our developer world to use it as a way to produce a State that we be rendered on screen and to handle events emitted by the users.
Expand Down
38 changes: 38 additions & 0 deletions Sources/Common/FeedbackDefinition+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,42 @@ public extension FeedbackDefinition {

self.init(effect: effect, on: executer, applying: strategy)
}

init<Dep1>(effect: @escaping (Dep1, StateStream.Value) -> EventStream,
on executer: Executer? = nil,
applying strategy: ExecutionStrategy = Self.defaultExecutionStrategy,
dep1: Dep1) {
let partializedEffect = partial(effect, arg1: dep1, arg2: .undefined)
self.init(effect: partializedEffect, on: executer, applying: strategy)
}

init<Dep1, Dep2>(effect: @escaping (Dep1, Dep2, StateStream.Value) -> EventStream,
on executer: Executer? = nil,
applying strategy: ExecutionStrategy = Self.defaultExecutionStrategy,
dep1: Dep1,
dep2: Dep2) {
let partializedEffect = partial(effect, arg1: dep1, arg2: dep2, arg3: .undefined)
self.init(effect: partializedEffect, on: executer, applying: strategy)
}

init<Dep1, Dep2, Dep3>(effect: @escaping (Dep1, Dep2, Dep3, StateStream.Value) -> EventStream,
on executer: Executer? = nil,
applying strategy: ExecutionStrategy = Self.defaultExecutionStrategy,
dep1: Dep1,
dep2: Dep2,
dep3: Dep3) {
let partializedEffect = partial(effect, arg1: dep1, arg2: dep2, arg3: dep3, arg4: .undefined)
self.init(effect: partializedEffect, on: executer, applying: strategy)
}

init<Dep1, Dep2, Dep3, Dep4>(effect: @escaping (Dep1, Dep2, Dep3, Dep4, StateStream.Value) -> EventStream,
on executer: Executer? = nil,
applying strategy: ExecutionStrategy = Self.defaultExecutionStrategy,
dep1: Dep1,
dep2: Dep2,
dep3: Dep3,
dep4: Dep4) {
let partializedEffect = partial(effect, arg1: dep1, arg2: dep2, arg3: dep3, arg4: dep4, arg5: .undefined)
self.init(effect: partializedEffect, on: executer, applying: strategy)
}
}
155 changes: 155 additions & 0 deletions Sources/Common/Partial.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//
// Partial.swift
//
//
// Created by Thibault Wittemberg on 2020-08-10.
//

enum Partial {
case undefined
}

func partial<Arg1, Result>(_ block: @escaping (Arg1) -> Result,
arg1: Arg1) -> () -> Result {
return {
block(arg1)
}
}

func partial<Arg1, Arg2, Result>(_ block: @escaping (Arg1, Arg2) -> Result,
arg1: Arg1,
arg2: Partial) -> (Arg2) -> Result {
return { futureArg2 in
block(arg1, futureArg2)
}
}

func partial<Arg1, Arg2, Result>(_ block: @escaping (Arg1, Arg2) -> Result,
arg1: Partial,
arg2: Arg2) -> (Arg1) -> Result {
return { futureArg1 in
block(futureArg1, arg2)
}
}

func partial<Arg1, Arg2, Arg3, Result>(_ block: @escaping (Arg1, Arg2, Arg3) -> Result,
arg1: Arg1,
arg2: Arg2,
arg3: Partial) -> (Arg3) -> Result {
return { futureArg3 in
block(arg1, arg2, futureArg3)
}
}

func partial<Arg1, Arg2, Arg3, Result>(_ block: @escaping (Arg1, Arg2, Arg3) -> Result,
arg1: Arg1,
arg2: Partial,
arg3: Arg3) -> (Arg2) -> Result {
return { futureArg2 in
block(arg1, futureArg2, arg3)
}
}

func partial<Arg1, Arg2, Arg3, Result>(_ block: @escaping (Arg1, Arg2, Arg3) -> Result,
arg1: Partial,
arg2: Arg2,
arg3: Arg3) -> (Arg1) -> Result {
return { futureArg1 in
block(futureArg1, arg2, arg3)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4) -> Result,
arg1: Arg1,
arg2: Arg2,
arg3: Arg3,
arg4: Partial) -> (Arg4) -> Result {
return { futureArg4 in
block(arg1, arg2, arg3, futureArg4)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4) -> Result,
arg1: Arg1,
arg2: Arg2,
arg3: Partial,
arg4: Arg4) -> (Arg3) -> Result {
return { futureArg3 in
block(arg1, arg2, futureArg3, arg4)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4) -> Result,
arg1: Arg1,
arg2: Partial,
arg3: Arg3,
arg4: Arg4) -> (Arg2) -> Result {
return { futureArg2 in
block(arg1, futureArg2, arg3, arg4)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4) -> Result,
arg1: Partial,
arg2: Arg2,
arg3: Arg3,
arg4: Arg4) -> (Arg1) -> Result {
return { futureArg1 in
block(futureArg1, arg2, arg3, arg4)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Arg5, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Result,
arg1: Arg1,
arg2: Arg2,
arg3: Arg3,
arg4: Arg4,
arg5: Partial) -> (Arg5) -> Result {
return { futureArg5 in
block(arg1, arg2, arg3, arg4, futureArg5)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Arg5, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Result,
arg1: Arg1,
arg2: Arg2,
arg3: Arg3,
arg4: Partial,
arg5: Arg5) -> (Arg4) -> Result {
return { futureArg4 in
block(arg1, arg2, arg3, futureArg4, arg5)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Arg5, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Result,
arg1: Arg1,
arg2: Arg2,
arg3: Partial,
arg4: Arg4,
arg5: Arg5) -> (Arg3) -> Result {
return { futureArg3 in
block(arg1, arg2, futureArg3, arg4, arg5)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Arg5, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Result,
arg1: Arg1,
arg2: Partial,
arg3: Arg3,
arg4: Arg4,
arg5: Arg5) -> (Arg2) -> Result {
return { futureArg2 in
block(arg1, futureArg2, arg3, arg4, arg5)
}
}

func partial<Arg1, Arg2, Arg3, Arg4, Arg5, Result>(_ block: @escaping (Arg1, Arg2, Arg3, Arg4, Arg5) -> Result,
arg1: Partial,
arg2: Arg2,
arg3: Arg3,
arg4: Arg4,
arg5: Arg5) -> (Arg1) -> Result {
return { futureArg1 in
block(futureArg1, arg2, arg3, arg4, arg5)
}
}
Loading

0 comments on commit 8bbe898

Please sign in to comment.