15.8 · Closure-based clients, dependency lifetimes, generic repositories and building the graph lazily
The first pass said to inject through initialisers. The questions that follow in a real codebase are about the shape of what you inject, how long it lives, how generic it is and when it gets built.
Mental model. Every dependency has a shape (protocol, struct of closures, generic), a lifetime (app, session, screen) and a moment of creation. Choose each on purpose.
Shape of a dependency
Protocol or struct of closures, and protocols with associated types.
- When is a struct of closures a better dependency than a protocol, and what do you give up?
A protocol with five methods needs a new conforming type for every test double, and each double must implement all five, including the four the test never… - How do you inject a protocol that has an associated type: primary associated types, any versus some, and generic view models?
A Repository with associatedtype Model cannot be used as a plain type: let repo: Repository is not allowed, because the compiler does not know what all()…
Lifetime and creation
Scopes that die at logout, and a graph that is not built at launch.
- How do you scope dependencies by lifetime (app, session, screen), and what leaks when a longer-lived object captures a shorter-lived one?
Not every dependency lives as long as the app. App scope: logging, analytics, the HTTP client, feature flags. Session scope: everything tied to a signed-in… - Why should the object graph not be built eagerly at launch, and how do lazy factories keep the composition root cheap?
A composition root that constructs every feature’s dependencies in its initialiser runs all that work before the first frame: databases opened, SDKs…
More in Architecture & API design
- 15.1 · MVC, MVVM, MVP, VIPER, RIBs, TCA and Clean, and unidirectional data flow
- 15.2 · Coordinators, routers and dependency injection
- 15.3 · Feature modules and boundaries, and strangling a legacy codebase
- 15.4 · throws, Result, typed throws and mapping errors at boundaries
- 15.5 · Designing a Swift API: naming, ownership, documentation and evolution
- 15.6 · SOLID without dogma, and building a platform for many apps
- 15.7 · Effects and cancellation, modelling state, composing reducers
- 15.9 · Binary breaks from innocent changes, and what an import exposes
- 15.10 · Types that prevent misuse, error types for libraries, one-shot values and when to trap