Video #92: Dependency Injection Made Modular
Episode: Video #92 Date: Feb 24, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep92-dependency-injection-made-modular

Description
Now that we’ve baked the “environment” of dependencies directly into the Composable Architecture, we’re ready to refactor our app’s frameworks and tests to work with them in a modular and more lightweight way.
Video
Cloudflare Stream video ID: ef4e54e2df7c9d9bc3e37b1fbe54e8f3 Local file: video_92_dependency-injection-made-modular.mp4 *(download with --video 92)*
References
- Discussions
- How to Control the World
- 0092-modular-dependency-injection-pt2
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
The ComposableArchitecture is now building again, but we have greatly simplified the API of the Store type.
— 0:30
Now let’s get the rest of the app building.
— 0:35
Luckily the app is fully modularized, and so we can start with our simplest modules that have the fewest dependencies, and work our way back to the main app target. Also lucky for us is that we are already using the environment technique in our code, and that will make converting to this new style very easy. Using the architecture’s environment
— 0:54
Let’s start with the PrimeModal module. It doesn’t actually have any effects, and therefore doesn’t use an environment, but we still have to update its reducer to use the new signature. So what type of environment should it have? public func primeModalReducer( state: inout PrimeModalState, action: PrimeModalAction, environment: <#???#> ) {
— 1:15
Since this feature doesn’t need an environment we can just use Void . This signifies an environment that doesn’t hold anything meaningful, and therefore doesn’t need any dependencies to do its job: public func primeModalReducer( state: inout PrimeModalState, action: PrimeModalAction, environment: Void ) {
— 1:25
And now the PrimeModal module is building.
— 1:28
Let’s jump to the next complicated module, the FavoritePrimes module. This does have an effect, but overall this feature is quite simple. We first need to fix the reducer: public func favoritePrimesReducer( state: inout [Int], action: FavoritePrimesAction ) {
— 1:33
This time we do need an environment, and in fact we have already created an environment that holds all the dependencies necessary. It’s called FavoritePrimesEnvironment , and right now it just holds a FileClient , but in the future it could hold a lot more. So let’s use that as our reducer’s environment: public func favoritePrimesReducer( state: inout [Int], action: FavoritePrimesAction, environment: FavoritePrimesEnvironment ) {
— 1:50
We have a small error right now: Constant cannot be declared public because its type uses an internal type And this is because FavoritePrimesEnvironment is internal, so we need to make it public: public struct FavoritePrimesEnvironment { var fileClient: FileClient }
— 2:08
And technically this module is now building, but we aren’t actually making use of the environment that we are given in the reducer. We are still calling out to the global Current . So let’s update the reducer: case .saveButtonTapped: return [ environment.fileClient.save( "favorite-primes.json", try! JSONEncoder().encode(state) ) .fireAndForget() ] case .loadButtonTapped: return [ environment.fileClient.load("favorite-primes.json") .compactMap { $0 } .decode(type: [Int].self, decoder: JSONDecoder()) .catch { error in Empty(completeImmediately: true) } .map(FavoritePrimesAction.loadedFavoritePrimes) .eraseToEffect() ]
— 2:33
And now amazingly we can delete our Current value since we are fully depending on the environment that is passed into our reducer: // var Current = FavoritePrimesEnvironment.live
— 2:40
Now this module is building, but before moving on to the next module let’s take advantage of the fact that our app is modularized. We can test this view directly in a playground right now to make sure everything is working correctly.
— 2:50
This is only possible because of the work we’ve done to modularize the app. We don’t need to get the entire app building in order to test out this change. We just needed to get this module building. This can be really powerful in a large project. It gives you a ton of flexibility to play with potential refactors and changes to your APIs because you can test it out on just a small part of your app without converting the whole thing.
— 3:09
We already have a playground for this module, but it isn’t going to compile because it’s using the old Current environment technique. Instead of mutating the global Current value, let’s just create a little local environment for us to play with: var environment = FavoritePrimesEnvironment.mock environment.fileClient.load = { _ in Effect.sync { try! JSONEncoder().encode(Array(1...10)) } }
— 3:34
And then when constructing the store for the view to run in this playground we can just pass along this environment: PlaygroundPage.current.liveView = UIHostingController( rootView: NavigationView { FavoritePrimesView( store: Store<[Int], FavoritePrimesAction>( initialValue: [2, 3, 5, 7, 11], reducer: favoritePrimesReducer, environment: environment ) ) } )
— 3:41
And just like that our playground is building, running and it behaves exactly as it did before.
— 3:54
Again, we just want to reiterate how powerful it is to be able to make changes like this so quickly and to iterate on these ideas in isolation. If we had to update the entire application just to test out this change we may be discouraged from even trying. After all, maybe we find out this change isn’t very good, and we end up reverting it all. It would be a bummer to have gotten the entire application building just to throw away all of that work.
— 4:18
Let’s now move onto the Counter module. Not only does this module have some side effects, but it also has something new that we didn’t see in the previous modules. Let’s start with the counterReducer . We know we need to introduce an environment, and we’ve already got one ready, the CounterEnvironment , which holds the nthPrime effect for computing the nth prime. public func counterReducer( state: inout CounterState, action: CounterAction, environment: CounterEnvironment ) -> [Effect<CounterAction>] {
— 4:43
And just like before we need to make the environment public, and we can get rid of the global Current variable: public struct CounterEnvironment { var nthPrime: (Int) -> Effect<Int?> } // var Current = CounterEnvironment.live
— 4:57
And finally, we need to use the environment we pass into the reducer instead of Current . case .nthPrimeButtonTapped: state.isNthPrimeButtonDisabled = true return [ environment.nthPrime(state.count) .map(CounterAction.nthPrimeResponse) .receive(on: DispatchQueue.main) .eraseToEffect() ]
— 5:06
Next we have to fix the pullback. Right now we are only pulling back along the state and action of our reducers, but we need to also incorporate their environments so that we can describe how the local counter and prime modal reducers should be embedded into the greater reducer that encompasses both of their functionalities.
— 5:27
To do this we have to describe how to transform the parent’s environment into each of the counter and prime modal environments. Let’s start by giving a type to the counterViewReducer so that we know what exactly we are transforming: public let counterViewReducer: Reducer< CounterViewState, CounterViewAction, <#???#> > = combine( pullback( counterReducer, value: \CounterViewState.counter, action: /CounterViewAction.counter, environment: { <#???#> } ), pullback( primeModalReducer, value: \.primeModal, action: /CounterViewAction.primeModal, environment: { <#???#> } ) )
— 5:53
What should we use for the environment? It needs to have all of the dependencies for both the counter reducer and the prime modal reducer. But, the prime modal reducer has a Void environment, so really we can just use the CounterEnvironment for this combined reducer: public let counterViewReducer: Reducer< CounterViewState, CounterViewAction, CounterEnvironment >
— 6:12
Then, for each pullback we need to describe how to transform this CounterEnvironment into the respective environment of the reducer we are pulling back. For the counter reducer we can just take the whole environment and for the prime modal reducer we don’t want to take anything, so we can ignore the counter environment and just return void: public let counterViewReducer = combine( pullback( counterReducer, value: \CounterViewState.counter, action: /CounterViewAction.counter, environment: { $0 } ), pullback( primeModalReducer, value: \.primeModal, action: /CounterViewAction.primeModal, environment: { _ in () } ) )
— 6:22
And just like that the Counter module is building.
— 6:27
We only have the app target left to fix, but before doing that, let’s make sure everything is good in our playground. To get the playground compiling we just need to stop using the Current variable, and pass along an environment to the store: var environment = CounterEnvironment.mock environment.nthPrime = { _ in .sync { 7236893748932 }} PlaygroundPage.current.liveView = UIHostingController( rootView: CounterView( store: Store<CounterViewState, CounterViewAction>( initialValue: CounterViewState( alertNthPrime: nil, count: 0, favoritePrimes: [], isNthPrimeButtonDisabled: false ), reducer: logging(counterViewReducer), environment: environment ) ) )
— 6:56
Everything seems to work just like it did before.
— 7:31
We’re at our final step of the refactor: the app target. This target is responsible for taking all of the reducers and views defined in all of our feature modules, and composing them together to form the full application. There are only a few compiler errors, the first of which is with the pullback, which is understandable since we need to describe how to transform the environments. Let’s start with the pullback. Right now it looks like this: let appReducer = combine( pullback( counterViewReducer, value: \AppState.counterView, action: /AppAction.counterView ), pullback( favoritePrimesReducer, value: \.favoritePrimes, action: /AppAction.favoritePrimes ) )
— 7:53
To fix this, let’s figure out what type of environment we expect to have at the app level. It needs to contain all of the dependencies for both the counterViewReducer and the favoritePrimesReducer . We could create a struct that holds each of those environments: struct AppEnvironment { var counter: CounterEnvironment var favoritePrimes: FavoritePrimesEnvironment }
— 8:40
And then when we pull back each of our feature reducers we just need to describe how to transform the app environment into the respective feature’s environment. This is as easy as just plucking out the struct field that we need: let appReducer: Reducer< AppState, AppAction, AppEnvironment > = combine( pullback( counterViewReducer, value: \AppState.counterView, action: /AppAction.counterView, environment: { $0.counter } ), pullback( favoritePrimesReducer, value: \.favoritePrimes, action: /AppAction.favoritePrimes, environment: { $0.favoritePrimes } ) )
— 8:54
And now the pullback is compiling. It’s cool to see this transformation in practice. We are allowing parent features to contain all of the dependencies of their children, and then when we combine a bunch of child features together we just have to slice off the piece of the environment it cares about. And most importantly, all 3 of these transformations are statically checked and so we can be somewhat confident that our features are plugging in together correctly if it compiles.
— 9:17
The next compiler error is in our activity feed higher-order reducer. To get it compiling again we only need to introduce the environment generic and reducer argument and pass it along to the reducer we are transforming: func activityFeed( _ reducer: @escaping Reducer<AppState, AppAction, AppEnvironment> ) -> Reducer<AppState, AppAction, AppEnvironment> { return { state, action, environment in … return reducer(&state, action, environment) } }
— 9:41
And our final compiler error is when creating the main content view, which requires creating a store, which now requires providing an environment. We can do so by providing a live version of all of our dependencies: window.rootViewController = UIHostingController( rootView: ContentView( store: Store( initialValue: AppState(), reducer: with( appReducer, compose( logging, activityFeed ) ), environment: AppEnvironment( counter: .live, favoritePrimes: .live ) ) ) )
— 10:24
And now for the first time in awhile, we have a fully building app, and if we run it everything should continue working exactly as it did before. It’s really cool to see how modularity helped us in this set of refactors of our architecture! We made some pretty sweeping changes and then were able to update and test each module at a time without having to refactor everything at once. Tuplizing the environment
— 10:46
Before moving on I think it’s worth asking if the environments structs are pulling their weight. Having environment structs for each feature module is unnecessarily siloing dependencies from each other. Each struct holds onto its own version of the dependency, and it would even be possible for two different features to want access to the same dependency, and we’d be forced to have it duplicated in the parent environment, like if the FavoritePrimes module also needed the Wolfram Alpha “nth prime” endpoint.
— 11:36
We don’t actually need any features of a struct for the environment. We don’t need to add methods to it, or mutate it, or conform it to protocols. All that is really needed is for us to be able to pass around multiple dependencies at once, and so type aliases and tuples may be simpler tools to help with this, and it may help us flatten the nested dependency problem we are seeing.
— 12:05
In the prime modal module, we’re using Void for the environment, which is actually a type alias for the empty tuple. public typealias Void = () So we appear to already be tuple-ized here.
— 12:25
In the FavoritePrimes module, instead of wrapping the file client in a struct, let’s just type alias the FavoritePrimesEnvironment to be the FileClient : // public struct FavoritePrimesEnvironment { // var fileClient: FileClient // } public typealias FavoritePrimesEnvironment = FileClient
— 12:45
If in the future this module needs more dependencies we would upgrade this typealias to a tuple with named arguments. We can also get rid of all of this code: // extension FavoritePrimesEnvironment { // public static let live = FavoritePrimesEnvironment( // fileClient: .live // ) // }
— 13:27
To make use of this new environment definition we just need to update our reducer to use the environment directly instead of accessing the fileClient field: case .saveButtonTapped: return [ environment.save( "favorite-primes.json", try! JSONEncoder().encode(state) ) … ] case .loadButtonTapped: return [ environment.load("favorite-primes.json") … ]
— 13:26
And instead of creating a mock for the entire FavoritePrimesEnvironment let’s just create a mock for the FileClient , which can be used in tests to create a FavoritePrimesEnvironment : #if DEBUG extension FileClient { static let mock = FileClient( load: { _ in Effect<Data?>.sync { try! JSONEncoder().encode([2, 31]) } }, save: { _, _ in .fireAndForget {} } ) } #endif
— 14:13
We can do the same for the CounterEnvironment . It has a single nthPrime dependency, which we’ll assign directly in the type alias. // public struct CounterEnvironment { // var nthPrime: (Int) -> Effect<Int> // } public typealias CounterEnvironment = (Int) -> Effect<Int?>
— 14:37
And in the future, if this environment grows, we will convert it to a tuple with fields for each dependency.
— 14:52
We can comment out the “live” environment. // extension CounterEnvironment { // public static let live = CounterEnvironment( // nthPrime: Counter.nthPrime // ) // }
— 15:05
And then in the counterReducer we will call out to the environment directly: case .nthPrimeButtonTapped: state.isNthPrimeButtonDisabled = true return [ environment(state.count) … ]
— 15:21
And finally, in the ContentView , instead of using a nested struct for the app environment, which makes it difficult to share dependencies between multiple downstream features, we will make a flat tuple that holds all of the dependencies that each feature needs: typealias AppEnvironment = ( fileClient: FileClient, nthPrime: (Int) -> Effect<Int?> )
— 16:25
Then, when pulling back we just need need to pluck out whatever dependencies we care about from the app environment and pass them along to the respective feature: let appReducer: Reducer< AppState, AppAction, AppEnvironment > = combine( pullback( counterViewReducer, value: \AppState.counterView, action: /AppAction.counterView, environment: { $0.nthPrime } ), pullback( favoritePrimesReducer, value: \.favoritePrimes, action: /.AppActionfavoritePrimes, environment: { $0.fileClient } ) )
— 17:12
And finally, in the SceneDelegate , when constructing the store we can create a nice flat environment tuple to pass along: window.rootViewController = UIHostingController( rootView: ContentView( store: Store( initialValue: AppState(), reducer: with( appReducer, compose( logging, activityFeed ) ), environment: AppEnvironment( fileClient: .live, nthPrime: Counter.nthPrime ) ) ) )
— 18:31
It’s much nicer to have a flat list of dependencies at the root here, and then whenever we pull back reducers we can decide which dependencies we want to hand down.
— 18:54
There is one small thing that we lose in moving to tuples, and that’s tooling around autocomplete. Struct initializers come up on autocomplete, but tuple type aliases do not. Hopefully this will change someday and be supported. Testing with the environment
— 19:50
And now our entire app is finally building, and it works exactly as it did before, but we have removed our reliance on the global environment from our features and instead are explicitly passing environments around. Most importantly, the passing of environments is baked directly into the definition of the architecture, and so it wasn’t difficult to make use of. Just when we perform pullbacks we have to describe how to transform the parent environment into the child environment.
— 20:22
Although the app is building, its tests are not. So let’s fix those real quick so that we can get a fully working project, and understand what it is we have accomplished.
— 20:48
Prime modal
— 20:48
If we start with the PrimeModal module we will see that there isn’t much to do because this feature didn’t even need an environment, we just used Void . So, to get this test compiling we need to pass along a void value to our reducers: class PrimeModalTests: XCTestCase { func testSaveFavoritesPrimesTapped() { var state = (count: 2, favoritePrimes: [3, 5]) let effects = primeModalReducer( state: &state, action: .saveFavoritePrimeTapped, environment: () ) let (count, favoritePrimes) = state XCTAssertEqual(count, 2) XCTAssertEqual(favoritePrimes, [3, 5, 2]) XCTAssert(effects.isEmpty) } func testRemoveFavoritesPrimesTapped() { var state = (count: 3, favoritePrimes: [3, 5]) let effects = primeModalReducer( state: &state, action: .removeFavoritePrimeTapped, environment: () ) let (count, favoritePrimes) = state XCTAssertEqual(count, 3) XCTAssertEqual(favoritePrimes, [5]) XCTAssert(effects.isEmpty) } }
— 21:23
And these tests now build and pass.
— 21:32
Next we will hop over to the FavoritePrimesTests module, which tests a feature that does need an environment. It uses a FileClient dependency in order to save and load data from disk. We can comment out our test setup, since we no longer have a Current environment to mock out. class FavoritePrimesTests: XCTestCase { // override func setUp() { // super.setUp() // Current = .mock // }
— 21:56
The first test can be fixed by passing a mock file client directly to the reducer: func testDeleteFavoritePrimes() { var state = [2, 3, 5, 7] let effects = favoritePrimesReducer( &state, .deleteFavoritePrimes([2]), .mock ) XCTAssertEqual(state, [2, 3, 7]) XCTAssert(effects.isEmpty) }
— 22:20
The next test works with a specially crafted environment. It wants to tap into the save endpoint of the file client so that we can make sure it was actually called from the reducer. Instead of mutating the Current value, we can construct a new file client that does exactly what we want: func testSaveButtonTapped() { var didSave = false var environment = FileClient.mock environment.save = { _, data in .fireAndForget { didSave = true } }
— 23:16
And then to use this environment we pass it along to the reducer: var state = [2, 3, 5, 7] let effects = favoritePrimesReducer( &state, .saveButtonTapped, environment ) XCTAssertEqual(state, [2, 3, 5, 7]) XCTAssertEqual(effects.count, 1) effects[0].sink { _ in XCTFail() } XCTAssert(didSave) }
— 23:21
The final test in this file also wants a specially crafted environment, but this time it wants to simulate the situation where the load endpoint in the FileClient loads some specific data. To do this we will construct a file client that does just that: func testLoadFavoritePrimesFlow() { var environment = FileClient.mock environment.load = { _ in .sync { try! JSONEncoder().encode([2, 31]) } }
— 23:44
And then we pass that environment along to the reducer in our tests: var state = [2, 3, 5, 7] var effects = favoritePrimesReducer( &state, .loadButtonTapped, environment ) XCTAssertEqual(state, [2, 3, 5, 7]) XCTAssertEqual(effects.count, 1) var nextAction: FavoritePrimesAction! let receivedCompletion = self.expectation( description: "receivedCompletion" ) effects[0].sink( receiveCompletion: { _ in receivedCompletion.fulfill() }, receiveValue: { action in XCTAssertEqual(action, .loadedFavoritePrimes([2, 31])) nextAction = action } ) self.wait(for: [receivedCompletion], timeout: 0) effects = favoritePrimesReducer(&state, nextAction, environment) XCTAssertEqual(state, [2, 31]) XCTAssert(effects.isEmpty) }
— 23:58
And now this test target is building and all the tests are passing.
— 25:11
Next up we have the Counter module, which has an environment that holds the endpoint we hit to load the nth prime from a side effect.
— 25:29
This isn’t compiling right now because the assert helper needs to be fixed. Right now it isn’t aware of the environment. To fix this we need to introduce another generic for the environment so that we can use it for the reducer: func assert<Value: Equatable, Action: Equatable, Environment>( initialValue: Value, reducer: Reducer<Value, Action, Environment>, environment: Environment,
— 26:24
And then inside the body of the helper we invoke this reducer, which means it needs an environment to work with. To fix this we can pass in an environment directly into the assert helper: func assert<Value: Equatable, Action: Equatable, Environment>( initialValue: Value, reducer: Reducer<Value, Action, Environment>, environment: Environment, steps: Step<Value, Action>..., file: StaticString = #file, line: UInt = #line ) { … effects.append(contentsOf: reducer(&state, action, environment))
— 26:35
And now we have some errors in our counter tests. We can start by commenting out the setup we were previously doing to mock out the global environment. class CounterTests: XCTestCase { // override func setUp() { // super.setUp() // Current = .mock // }
— 26:46
Next we have a snapshot test, which played through a whole user script and took snapshots along the way to make sure the UI changed the way we expected. We need to fix this by providing an environment when constructing the store: let store = Store( initialValue: CounterViewState(), reducer: counterViewReducer, environment: { _ in .sync { 17 } } )
— 27:38
The other tests in the file are a little different than the others we have seen so far because this is the one we created our assert helper that makes it super easy to test large reducers.
— 27:41
For example, to test the incrementing and decrementing logic we could simply do: func testIncrDecrButtonTapped() { assert( initialValue: CounterViewState(count: 2), reducer: counterViewReducer, steps: Step(.send, .counter(.incrTapped)) { $0.count = 3 }, Step(.send, .counter(.incrTapped)) { $0.count = 4 }, Step(.send, .counter(.decrTapped)) { $0.count = 3 } ) }
— 27:57
Now that the assert helper is environment-aware, we can fix the counter tests. For the first one we can pass along an nthPrime effect that simply returns 17 immediately: func testIncrDecrButtonTapped() { assert( initialValue: CounterViewState(count: 2), reducer: counterViewReducer, environment: { _ in .sync { 17 } }, steps: Step(.send, .counter(.incrTapped)) { $0.count = 3 }, Step(.send, .counter(.incrTapped)) { $0.count = 4 }, Step(.send, .counter(.decrTapped)) { $0.count = 3 } ) }
— 28:20
We can even do the same for the next one: func testNthPrimeButtonHappyFlow() { assert( … environment: { _ in .sync { 17 } }, … ) }
— 29:06
The next test is similar to the previous, except its environment needs an nthPrime endpoint that fails: func testNthPrimeButtonUnhappyFlow() { assert( … environment: { _ in .sync { nil } }, … ) }
— 29:31
One last test in this file, and it’s purpose was to demonstrate how one would write a kind of “integration test” for reducers, where we are simultaneously exercising many parts of the composed reducer in a single test. To get this compiling we need to hand it a mock environment: func testPrimeModal() { assert( … environment: { _ in .sync { 17 } }, … ) }
— 30:28
And now this target is compiling, and let’s run tests! failed - Assertion failed to handle 1 pending effect(s)
— 30:37
Oops, that wasn’t expected! This is only happening because a little while ago we added an effect to our counter reducer to show how easy it is to add complex effects. Let’s go remove that effect: case .decrTapped: state.count -= 1 let count = state.count return [ // .fireAndForget { // print(count) // }, // // Just(.incrTapped) // .delay(for: 1, scheduler: DispatchQueue.main) // .eraseToEffect() ] It’s pretty amazing that the Composable Architecture could automatically catch this regression for us!
— 31:51
And now tests are passing! Next time: what’s the point?
— 31:55
So we finally got the app building and all tests passing…
— 32:08
But now that we’ve done yet another big refactor of the Composable Architecture, maybe we should slow down and ask “what’s the point?”. Because although we have weened ourselves off of the global environment, it doesn’t seem like it has materially changed our application much. We are still mostly constructing effects in the same way and writing tests in the same way.
— 32:24
So have we actually solved the problems that we said the environment technique has, and will this really help out our applications at the end of the day?
— 32:35
And the answer is definitely yes! This adaption of the environment technique has solved all of the problems we described at the beginning of this series of episodes:
— 33:07
We had the problem of multiple environments: if each of your feature modules has its own environment, it can be difficult to know how to control all of them simultaneously, and you lose some static guarantees around that.
— 33:21
There’s also the idea of local dependencies. With each module having only one environment, it’s impossible to reuse a screen with different environments.
— 33:44
And then there’s the problem of sharing dependencies. Each of our features may have environments with common dependencies, and the “global” environment of a module made it difficult to share these common dependencies.
— 33:59
And it’s important to note that our tweak of the environment technique was only possible due to us adopting the Composable Architecture, which gave us a single, consistent way of building our features and solving these problems.
— 34:13
And to prove this, let’s go through each problem and demonstrate exactly how it was solved…next time! References Dependency Injection Made Easy Brandon Williams & Stephen Celis • May 21, 2018 This is the episode that first introduced our Current environment approach to dependency injection. Note Today we’re going to control the world! Well, dependencies to the outside world, at least. We’ll define the “dependency injection” problem and show a lightweight solution that can be implemented in your code base with little work and no third party library. https://www.pointfree.co/episodes/ep16-dependency-injection-made-easy Dependency Injection Made Comfortable Brandon Williams & Stephen Celis • Jun 4, 2018 Note Let’s have some fun with the “environment” form of dependency injection we previously explored. We’re going to extract out a few more dependencies, strengthen our mocks, and use our Overture library to make manipulating the environment friendlier. https://www.pointfree.co/episodes/ep18-dependency-injection-made-comfortable How to Control the World Stephen Celis • Sep 24, 2018 Stephen gave a talk on our Environment -based approach to dependency injection at NSSpain 2018. He starts with the basics and slowly builds up to controlling more and more complex dependencies. https://vimeo.com/291588126 Effectful State Management: Synchronous Effects Brandon Williams & Stephen Celis • Oct 14, 2019 This is the start of our series of episodes on “effectful” state management, in which we explore how to capture the idea of side effects directly in our composable architecture. Note Side effects are one of the biggest sources of complexity in any application. It’s time to figure out how to model effects in our architecture. We begin by adding a few new side effects, and then showing how synchronous effects can be handled by altering the signature of our reducers. https://www.pointfree.co/episodes/ep76-effectful-state-management-synchronous-effects Testable State Management: Reducers Brandon Williams & Stephen Celis • Nov 25, 2019 This is the start of our series of episodes on “testable” state management, in which we explore just how testable the Composable Architecture is, effects and all! It’s time to see how our architecture handles the fifth and final problem we identified as being important to solve when building a moderately complex application testing! Let’s get our feet wet and write some tests for all of the reducers powering our application. https://www.pointfree.co/episodes/ep82-testable-state-management-reducers Downloads Sample code 0092-modular-dependency-injection-pt2 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .