Modular Dependency Injection: The Point

Episode #93 • Mar 2, 2020 • Subscriber-Only

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.

Modular Dependency Injection: The Point
Introduction
00:05
Multiple environments
02:32
Local dependencies
13:52
Sharing dependencies
23:55
Conclusion
41:54

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.

This episode is for subscribers only.

Subscribe to Point-Free

Access this episode, plus all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Exercises

  1. 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, in Combine there is a removeDuplicates() 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 of assert that works on non-equatable values and actions:

    Solution

    You can update the function signature to take valueIsEqual and actionIsEqual 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 call XCTAssert with the result of actionIsEqual and valueIsEqual:

    XCTAssert(actionIsEqual(action, step.action), file: step.file, line: step.line)
    …
    XCTAssert(valueIsEqual(state, expected), file: step.file, line: step.line)
    
  2. 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:) and receive(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()
    }
    
  3. 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() for date(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, 2018

This 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, 2018

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.

How to Control the World

Stephen Celis • Monday 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.

Effectful State Management: Synchronous Effects

Brandon Williams & Stephen Celis • Monday 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.

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, 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.