EP 93 · Modular Dependency Injection · Mar 2, 2020 ·Members

Video #93: Modular Dependency Injection: The Point

smart_display

Loading stream…

Video #93: Modular Dependency Injection: The Point

Episode: Video #93 Date: Mar 2, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep93-modular-dependency-injection-the-point

Episode thumbnail

Description

It’s time to prove that baking an “environment” of dependencies directly into the Composable Architecture solves three crucial problems that the global environment pattern could not.

Video

Cloudflare Stream video ID: b5b04a2ab9658641f90b0ac1fa8f4b33 Local file: video_93_modular-dependency-injection-the-point.mp4 *(download with --video 93)*

References

Transcript

0:05

So we finally got the app building and all tests passing…

0:18

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.

0:34

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?

0:45

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:

1:17

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.

1:31

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.

1:54

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.

2:09

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.

2:23

And to prove this, let’s go through each problem and demonstrate exactly how it was solved. Multiple environments

2:32

The first problem we mentioned that the Current environment technique has is that things become a little unwieldy when each feature module has its own environment. This meant that every time we want to write a test that involves some logic from two features, we must remember to mock out each of their environments. func testIntegration() { Counter.Current = .mock FavoritePrimes.Current = .mock }

3:07

The new environment technique completely solves this problem. To demonstrate this we’re going to write an integration test that tests the effects in two different features. In order to do this we want to use the assert helper that has been so helpful in the past. We first defined it in the CounterTests modules, but in between episodes we extracted it to a dedicated ComposableArchitectureTestSupport module so that any module can use it.

4:04

We can import this new test support module and we’ll immediately have access to the assert helper: import ComposableArchitectureTestSupport

4:20

We want to write a test on the full app state, app actions, app environment and app reducer. So let’s start by getting a stub of an assert in place: func testIntegration() { assert( initialValue: AppState(), reducer: appReducer, environment: ( fileClient: .mock, nthPrime: { _ in .sync { 17 } } ) ) }

5:18

Right now this doesn’t compile because AppState and AppEvent need to be equatable, so let’s do that real quick: struct AppState: Equatable { … struct Activity: Equatable { … enum ActivityType: Equatable { … } } struct User: Equatable { … } } enum AppAction: Equatable { … }

5:40

And now the test target is building, and technically the test will pass because we aren’t actually testing any steps. Let’s test what happens when we ask for the nth prime in the counter view, and then we load our list of favorite primes from disk. In one single test we will be executing the side effects from two different feature modules.

6:14

The test is straightforward to write: func testIntegration() { var fileClient = FileClient.mock fileClient.load = { _ in return Effect<Data?>.sync { try! JSONEncoder().encode([2, 31, 7]) } } assert( initialValue: AppState(count: 4), reducer: appReducer, environment: ( fileClient: fileClient, nthPrime: { _ in .sync { 17 } } ), steps: Step(.send, .counterView(.counter(.nthPrimeButtonTapped))) { $0.isNthPrimeButtonDisabled = true }, Step(.receive, .counterView(.counter(.nthPrimeResponse(17)))) { $0.isNthPrimeButtonDisabled = false $0.alertNthPrime = PrimeAlert(prime: 17) }, Step(.send, .counterView(.counter(.alertDismissButtonTapped))) { $0.alertNthPrime = nil }, Step(.send, .favoritePrimes(.loadButtonTapped)), Step( .receive, .favoritePrimes(.loadedFavoritePrimes([2, 31, 7])) ) { $0.favoritePrimes = [2, 31, 7] } ) } We forgot to test a step in the video, where we simulate the alert dismiss button being tapped. This test is simulating the user script of:

6:14

User taps the “what is the nth prime” button

6:38

The “nth prime” button is disabled

6:48

A side effect feeds an action into the system, which is a response with the number 17. This value comes directly from the dependency we have in our environment

8:10

The “nth prime” button is re-enabled and an alert is shown The user taps the dismiss button on the alert and the alert is closed The alert is dismissed

9:10

Then the user taps the “load” button in the favorite primes screen

9:49

State doesn’t change

10:53

And finally an action is received from an effect that loads some primes into the state. Again this value comes directly from the dependency we have in our environment.

11:51

The favorite primes array is set in state

12:50

Now what’s cool about this test is that we are forced by the compiler to provide a complete environment to even run this code. The assert helper requires a single environment right here: environment: ( fileClient: fileClient, nthPrime: { _ in .sync { 17 } } ),

12:58

And so there is no possibility to forget to provide a particular part of the environment, and if new environment fields are added we will be forced, by the compiler, to provide those new dependencies. To contrast this with the way we were previously handling this, our test code would have run even if we didn’t mock out a single dependency.

13:19

So our new adaptation of the environment technique has definitely solved the messiness of having multiple environments to worry about. Local dependencies

13:52

The next problem of the global environment we described is that we don’t get the opportunity to customize the type of effects that are used for each feature. For example, when running the app in production, the Counter feature uses the Wolfram Alpha API.

14:08

It is not possible to use the CounterView in two different places in the application where one instance uses the Wolfram Alpha API, and the other instance uses a completely different API.

14:19

Perhaps there’s a new computing API that we want to try out, but only for select users. It would be very difficult to allow some instances of the CounterView to use one effect and others use another effect.

14:34

However, with our new style of environment this is very easy. To show this, we are going to use a version of the nthPrime function that instead of calling out to an external API it’s going to simply do the computation locally. It does its work in a very naive way, but it gets the job done: public func offlineNthPrime(_ n: Int) -> Effect<Int?> { Future { callback in var nthPrime = 1 var count = 0 while count <= n { nthPrime += 1 if isPrime(nthPrime) { count += 1 } } callback(.success(nthPrime)) } .eraseToEffect() }

15:00

This models our effect by returning a Future , which allows us to perform some work that may take a long time, and once we finish we can emit the value from the publisher by invoking the callback. The work on the inside loops through every number greater than 1, and increments a count each time it encounters a prime. Once the prime count hits the number we are looking for, we invoke the future’s callback, thus completing the publisher.

15:26

Let’s now use this new offline dependency to demonstrate that we can use the CounterView many times in our application, each with different dependencies. Let’s create a whole new top level navigation link that allows us to drill-down into a version of the CounterView that runs offline.

15:40

We can start by getting a stub of a navigation link in place: NavigationLink( "Offline counter demo", destination: CounterView( store: self.store.view( value: { $0.counterView }, action: { .counterView($0) } ) ) )

15:56

This is currently navigating to the same counter view as the other navigation link. If instead we want it to navigate to a version of the CounterView that runs with its own effects, we need to model that in the domain of the feature. In particular, we need to introduce a new set of actions that are specific to the offline counter.

16:12

This means we can change the AppAction to be: enum AppAction: Equatable { case counterView(CounterViewAction) case offlineCounterView(CounterViewAction) case favoritePrimes(FavoritePrimesAction) }

16:26

And then the navigation link becomes: NavigationLink( "Offline counter demo", destination: CounterView( store: self.store.view( value: { $0.counterView }, action: { .offlineCounterView($0) } ) ) )

16:42

This allows the new screen to have its own set of actions that are different from the “online” counter view.

16:48

We’ve got one compiler error, and that is in our activity feed higher-order reducer. This is actually a great compiler error to have, because we do in fact want to tap into these new actions and append new activity feed items when favorite primes are added and removed from the offline counter: case .counterView(.counter), .offlineCounterView(.counter), .favoritePrimes(.loadedFavoritePrimes), .favoritePrimes(.loadButtonTapped), .favoritePrimes(.saveButtonTapped): break case .counterView(.primeModal(.removeFavoritePrimeTapped)), .offlineCounterView(.primeModal(.removeFavoritePrimeTapped)): state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(state.count) ) ) case .counterView(.primeModal(.saveFavoritePrimeTapped)), .offlineCounterView(.primeModal(.saveFavoritePrimeTapped)): state.activityFeed.append( .init( timestamp: Date(), type: .addedFavoritePrime(state.count) ) )

17:32

If we were to run the app right now we would find that the offline counter demo is not functional at all. Tapping any of the buttons doesn’t do anything, and that’s because we don’t have a reducer to handle its functionality.

17:48

To add this functionality we need to combine another instance of the pulled back counterViewReducer into the appReducer , but with its action pulled back along the offlineCounterView case: pullback( counterViewReducer, value: \AppState.counterView, action: /AppAction.offlineCounterView, environment: { $0.nthPrime } ),

18:35

But this isn’t correct yet. With this as-is, the offline counter demo will behave exactly as the online one, and that’s because we are handing it an environment where its nthPrime effect uses the live Wolfram API.

18:48

Instead, we want to pass in the offline effect. pullback( counterViewReducer, value: \AppState.counterView, action: /AppAction.offlineCounterView, environment: { $0.offlineNthPrime } ),

18:54

In order to do that we need to represent it in our app environment: public typealias AppEnvironment = ( fileClient: FileClient, nthPrime: (Int) -> Effect<Int?>, offlineNthPrime: (Int) -> Effect<Int?> )

19:09

And now the only compiler error we have is that we need to specify an offlineNthPrime effect in the environment when we create the store for our entire application: environment: AppEnvironment( fileClient: .live, nthPrime: Counter.nthPrime, offlineNthPrime: Counter.offlineNthPrime )

19:25

And now our entire application builds, and the new offline counter view feature works as expected. When we ask if for the “nth prime” of a particular count, we almost immediately get the result back because we are doing the computation locally.

19:55

However, before you ask why not only use the offlineNthPrime effect instead of the Wolfram API, let’s see what happens when we try to ask for the “nth prime” for really, really large number: initialValue: AppState(count: 20_000),

20:12

Now when we tap the “what is the nth prime” button we see that the interface hangs for awhile before showing the alert. And if we changed the count to something just a bit higher, like say 40,000: initialValue: AppState(count: 40_000),

21:05

We’ll see that the computation takes much longer to finish. You’ll also notice that the entire UI freezes while the computation is happening, and we have some exercises for the viewer to fix that problem.

21:46

So this demonstrates that we truly have unlocked a new capability from our CounterView . We have given it the freedom to work with many different types of effects, not just the Wolfram Alpha API. When combining its reducer into the application’s reducer we get to choose which effects it will execute when certain actions happen in its domain.

22:05

This may seem like a silly example, but there are definitely real world uses. We may want to perform an A/B experiment on our application where half our users get the counter view with the Wolfram API, and the other half get the offline “nth prime” computation. In that case we would definitely need the capability of telling the counter view how it should execute its side effects.

22:25

We can demonstrate this by computing a random boolean value, and then using that to determine which navigation link we show: let isInExperiment = Bool.random() struct ContentView: View { @ObservedObject var store: Store<AppState, AppAction> var body: some View { NavigationView { List { if !isInExperiment { NavigationLink( "Counter demo", destination: CounterView( store: self.store.view( value: { $0.counterView }, action: { .counterView($0) } ) ) ) } else { NavigationLink( "Offline counter demo", destination: CounterView( store: self.store.view( value: { $0.counterView }, action: { .offlineCounterView($0) } ) ) ) }

23:01

This of course isn’t production-worthy. Your A/B test will need to be a lot more nuanced, but it at least shows that it’s possible. Sharing dependencies

23:55

The third, and final, problem that we described for the global environment technique is that it is difficult to share dependencies between features. If two different modules need access to the same dependency, we either have to create a new instance of that dependency for each module, or we need to higher level coordination to make sure that each module is using the same dependency.

24:28

To show that we have solved this we are going to add yet another feature to our humble little counting app. When on the favorite primes screen, we are going to allow the user to tap any row in order to ask what the “nth prime” is for that number. Yes, we will be using a prime number to ask what the “nth prime” is. Very meta.

25:10

This will show how to use a single dependency, in this case the Wolfram Alpha API endpoint, in multiple modules.

25:27

To begin, let’s turn each row of the favorite primes list view into a button: public var body: some View { List { ForEach(self.store.value.favoritePrimes, id: \.self) { prime in Button("\(prime)") { } }

25:36

Inside the action closure of this button we want to send an action to the store. We can pass along the prime we tapped. public var body: some View { List { ForEach(self.store.value.favoritePrimes, id: \.self) { prime in Button("\(prime)") { self.store.send(.primeButtonTapped(prime)) } }

25:49

Then we need to represent this action in our action enum: public enum FavoritePrimesAction: Equatable { … case primeButtonTapped(Int) }

26:01

And handle the new action in our reducer: case let .primeButtonTapped(prime): return [ ]

26:12

The work we want to do in here is to fire off a side effect that can compute the “nth prime”, we don’t care whether it does this with the Wolfram Alpha API or some other means. This means we need to update our environment to have access to that effect. To do this we will upgrade the simple type alias to a tuple: public typealias FavoritePrimesEnvironment = ( fileClient: FileClient, nthPrime: (Int) -> Effect<Int?> )

26:50

This means we need to update the way we access the fileClient in the reducer: environment.fileClient.save( "favorite-primes.json", try! JSONEncoder().encode(state.favoritePrimes) ) … environment.fileClient.load("favorite-primes.json")

27:09

And now we can access the nthPrime dependency when tapping on the prime. When we fire off this effect it will eventually give us back an optional integer. We want to wrap that data in a new action so that it can be fed back into the store. But, we need to keep track of which number we tapped on also because we display that information in the alert, “The 5th prime is 11”.

27:59

So, let’s add a new action for accepting those two pieces of information: case nthPrimeResponse(n: Int, prime: Int?)

29:34

And this is the action our effect needs to emit when you tap a prime: case let .primeButtonTapped(prime): return [ environment.nthPrime(prime) .map { FavoritePrimesAction.nthPrimeResponse(n: prime, prime: $0) } .receive(on: DispatchQueue.main) .eraseToEffect() ]

29:21

The way we handled the nthPrimeResponse action over in the counterReducer was to have an optional PrimeAlert value so that when it is non- nil the alert is shown, and when it is nil the alert is dismissed. We want to do something similar here, which means we need to represent this in our state: public typealias FavoritePrimesState = ( alertNthPrime: PrimeAlert?, favoritePrimes: [Int] )

30:11

However, we don’t have access to the PrimeAlert type in this module, it currently lives in the Counter module. There really is no reason for it to live in the Counter module though. There could be many features that want to display a prime alert. So, let’s extract it to its own module so that we can simply import that module to get access to the type: import PrimeAlert

30:59

And now when we can upgrade our module to work with FavoritePrimesState . public func favoritePrimesReducer( state: inout FavoritePrimesState, action: FavoritePrimesAction, environment: FavoritePrimesEnvironment ) -> [Effect<FavoritePrimesAction>] { …

31:36

And then the new action in the reducer we can create the PrimeAlert value: case let .nthPrimeResponse(n, prime): state.alertNthPrime = prime.map { PrimeAlert(n: n, prime: $0) } return []

32:09

With this new state in our feature we can implement the SwiftUI view portion. We will update the FavoritePrimesView to use a store for the full favorites state: public struct FavoritePrimesView: View { @ObservedObject var store: Store<FavoritePrimesState, FavoritePrimesAction> public init( store: Store<FavoritePrimesState, FavoritePrimesAction> ) { self.store = store }

32:33

And with that state available to us in the body of the view, we can add an .alert modifier to our view hierarchy: .alert( item: .constant(self.store.value.alertNthPrime) ) { primeAlert in Alert( title: <#???#>, dismissButton: .default(Text("OK")) { <#???#> } ) }

33:26

We want to use the same title here that we did for the counter view, so maybe we should extract that lil bit of logic to the PrimeAlert type: extension PrimeAlert { public var title: String { return "The \(ordinal(self.n)) prime is \(self.prime)" } } public func ordinal(_ n: Int) -> String { let formatter = NumberFormatter() formatter.numberStyle = .ordinal return formatter.string(for: n) ?? "" }

34:30

And now we use the title: Alert( title: Text(primeAlert.title), dismissButton: .default(Text("OK")) { <#???#> } )

35:25

And we want to send a new action to the store when the “OK” button is tapped: dismissButton: .default(Text("OK")) { self.store.send(.alertDismissButtonTapped) }

35:39

And of course we have to add that to our action enum: public enum FavoritePrimesAction: Equatable { … case alertDismissButtonTapped }

35:45

And handle it in our reducer: case .alertDismissButtonTapped: state.alertNthPrime = nil return []

36:02

And now the FavoritePrimes module is building. Before trying to fix the rest of the application, let’s try running it in our dedicated playground. We just need to specify the new state and environment of our feature: var environment: FavoritePrimesEnvironment = ( fileClient: .mock, nthPrime: { _ in .sync { 17 } } ) environment.fileClient.load = { _ in Effect.sync { try! JSONEncoder().encode(Array(1...10)) } } PlaygroundPage.current.liveView = UIHostingController( rootView: NavigationView { FavoritePrimesView( store: Store<FavoritePrimesState, FavoritePrimesAction>( initialValue: ( alertNthPrime: nil, favoritePrimes: [2, 3, 5, 7, 11] ), reducer: favoritePrimesReducer, environment: environment ) ) } )

37:27

When we tap a row we see that it causes an alert to show.

38:04

To get the whole application building we only need to do a few small things. First we need to fix the pullback: pullback( favoritePrimesReducer, value: \.favoritePrimes, action: /AppAction.favoritePrimes, environment: { $0.fileClient } )

38:17

First it’s no longer correct to pullback state along just the favoritePrimes field of the AppState , we want both the favoritePrimes and the alertNthPrime field.

38:31

To do that we just need a custom computed property to get the key path: extension AppState { var favoritePrimesState: FavoritePrimesState { get { (self.alertNthPrime, self.favoritePrimes) } set { (self.alertNthPrime, self.favoritePrimes) = newValue } } }

39:27

Then we can use that new key path in the pullback: pullback( favoritePrimesReducer, value: \.favoritePrimesState, action: /AppAction.favoritePrimes, environment: { $0.fileClient } )

39:34

It’s also no longer valid to pullback along the environment by only passing along the fileClient , it also needs the nthPrime effect. We can pluck out both dependencies the favorite primes feature needs in the pullback: pullback( favoritePrimesReducer, value: \.favoritePrimesState, action: /AppAction.favoritePrimes, environment: { ($0.fileClient, $0.nthPrime) } )

40:17

We just have a few more things to fix to get things building. We need to import the prime alert module, send the new favorite primes state along to the favorite primes view, and update the activity feed to ignore these new actions. import PrimeAlert … case .favoritePrimes(.primeButtonTapped), .favoritePrimes(.nthPrimeResponse), .favoritePrimes(.alertDismissButtonTapped): break … NavigationLink( "Favorite primes", destination: FavoritePrimesView( store: self.store.view( value: { $0.favoritePrimesState } action: { .favoritePrimes($0) } ) ) )

41:13

And just like that the app is building again, which may seem surprising, but remember, we are reusing an existing dependency, so the app environment needed no changes. Conclusion

41:54

Phew, ok! That was quite a bit of work, but we were able to demonstrate that this new style of environment dependencies solves 3 crucial problems that the global environment has.

42:08

We can succinctly unite the environments from many different feature modules at once.

42:14

We can allow features to be instantiated multiple times with different dependencies.

42:23

And we can share dependencies between feature modules.

42:30

It took us a lot of work to get here, both to refactor the Composable Architecture in this new style and to refactor our application to take advantage of it, but we are now in a much stronger position to write expressive, testable applications.

42:45

Now that we have a single top level environment that is sliced up and handed to each individual feature, we get a global view of not only has state is changed in our application but also how dependencies and effects are used in our application. This is providing clarity and understanding to a potentially complex situation. If anything goes wrong with a dependency, we have a single consistent way to trace its origins all the way back to the root.

43:16

And some of the refactors we did may seem like a slog, but you also have to remember that we do that work so that we have a strong, static bond between our features. When the pullbacks compile we are getting static acknowledgment that our features are plugging in together correctly. And not only that, but having this static glue between features means that when we write an integration test for our application we are testing every aspect of the architecture, from top to bottom.

44:02

And I think the most amazing thing about this refactor is that we were able to get some many benefits out of it without breaking composability or testability. We are still allowed to build our features in isolation, and those little features are still open to be plugged into the greater application. That is really powerful.

44:23

Well, that’s it for this episode, until 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 0093-modular-dependency-injection-pt3 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 .