Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
So we finally got the app building and all tests passing…
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.
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?
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:
- 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.
- 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.
- 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.
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.
And to prove this, let’s go through each problem and demonstrate exactly how it was solved.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
It is very common for APIs that work with
Equatable
types to be defined alongside similar APIs that work with any type given a predicate function(A, A) -> Bool
. For instance, inCombine
there is aremoveDuplicates()
method on publishers of equatable values, while there is a similar,removeDuplicates(by:)
method on publishers of any value.In our architecture, the
assert
helper is currently constrained over equatable values and actions:public 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 )
Using
removeDuplicates(by:)
as a template, define a version ofassert
that works on non-equatable values and actions:Solution
You can update the function signature to take
valueIsEqual
andactionIsEqual
functions:public func assert<Value, Action, Environment>( valueIsEqual: (Value, Value) -> Bool, actionIsEqual: (Action, Action) -> Bool, initialValue: Value, reducer: Reducer<Value, Action, Environment>, environment: Environment, steps: Step<Value, Action>..., file: StaticString = #file, line: UInt = #line )
And then, rather than call to
XCTAssertEqual
, we can callXCTAssert
with the result ofactionIsEqual
andvalueIsEqual
:XCTAssert(actionIsEqual(action, step.action), file: step.file, line: step.line) … XCTAssert(valueIsEqual(state, expected), file: step.file, line: step.line)
In this episode we see a problem with the
offlineNthPrime
dependency where inefficient computations entirely hang the interface. Update this dependency to run on a non-UI thread to fix this bug.Solution
By using
subscribe(on:)
andreceive(on:)
, you can ensure that the work is done on a global background queue and received on the main UI queue:public func offlineNthPrime(_ n: Int) -> Effect<Int?> { Deferred { Future { callback in … callback(.success(nthPrime)) } } .subscribe(on: DispatchQueue.global()) .receive(on: DispatchQueue.main) .eraseToEffect() }
Update the higher-order
activityFeed
reducer to provide the current date from the reducer’s environment.Solution
You can describe the dependency of plucking a date out of the reducer’s environment like so:
func activityFeed( _ reducer: @escaping Reducer<AppState, AppAction, AppEnvironment>, date: @escaping (AppEnvironment) -> Date ) -> Reducer<AppState, AppAction, AppEnvironment>
Which gets the date controlled and even lets you simply swap out
Date()
fordate(environment)
.state.activityFeed.append(.init(timestamp: date(environment), type: .removedFavoritePrime(state.count))) … state.activityFeed.append(.init(timestamp: date(environment), type: .addedFavoritePrime(state.count))) … state.activityFeed.append(.init(timestamp: date(environment), type: .removedFavoritePrime(state.favoritePrimes[index])))
While it may seem scary to invoke this side effect without the cycle of running an
Effect
and feeding an action back into the system, it is perfectly safe and reasonable to do!Explore the alternative: what it would it look like to introduce the effect–action cycle in this higher-order reducer? What are the trade-offs of both approaches?
References
Dependency Injection Made Easy
Brandon Williams & Stephen Celis • Monday May 21, 2018This is the episode that first introduced our Current
environment approach to dependency injection.
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.
Dependency Injection Made Comfortable
Brandon Williams & Stephen Celis • Monday Jun 4, 2018Let’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.
How to Control the World
Stephen Celis • Monday Sep 24, 2018Stephen 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.
Effectful State Management: Synchronous Effects
Brandon Williams & Stephen Celis • Monday Oct 14, 2019This 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.
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.
Testable State Management: Reducers
Brandon Williams & Stephen Celis • Monday Nov 25, 2019This 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.