I’m not sure why I’m so fascinated by dependency injection, but I am! I read a couple of articles about it earlier, and thought it would be interesting to list out the different types in Swift.
1. A central Dependency Manager
This was my go-to approach until recently.
Pros
- Everything is checked at compile time.
- Good for switching dependencies across the whole app, e.g., to switch out an API layer or an auth implementation.
Cons
- Can be a bit heavyweight in the code.
- The dependency file gets really long and unwieldy
2. Constructor injection
This is now my preferred method.
Pros
- Everything is checked at compile time.
- Very lightweight.
- Communicates intent well.
- Good for when you only need injection for unit testing
Cons
- Can only switch out a single dependency.
- If you use anything other than the constructor default, the caller is responsible for setting the dependency. This is a big red flag for production because of the coupling it introduces (but fine for unit testing).
3. Variable injection
I hate this, and see it as a necessary evil.
Pros
- Everything is checked at compile time.
- Useful for view controllers, or other places where you can’t (easily) inject in the constructor.
Cons
- Variables have to be
public
orinternal
, which makes the API untidy. - It’s not obvious that the variables are being used for dependency injection.
4. Parameter injection
Pretty much the same as constructor injection, but rather than injecting via the constructor and caching the dependency on the type, you pass it into a function as a parameter.
Pros / Cons
- As for constructor injection.
5. Dependency structs
This is basically the same as constructor or variable injection, but with the dependencies grouped in a struct. I haven’t used this, but read about it in Dave Poirier’s article, DI using struct Dependencies.
Pros
- All the pros of constructor / variable injection.
- Much better at communicating intent in variable injection (i.e., it’s clear that the variable is being used for dependency injection)
Cons
- It adds a bit more boilerplate code.
6. Function injection
This approach works in the same way as constructor and variable injection, but instead of injecting a type that knows how to perform a function, you inject the function itself. This John Sundell tip illustrates it well: Dependency injection using functions
Pros
- Everything is checked at compile time.
- It’s lighter weight than creating a protocol and implementation.
Cons
- It can be harder to understand if you’re not an advanced Swift user.
7. Dependency caching
This is where you cache references to a dependency by a string or other type. Dependencies are registered at runtime, then resolved from the cache. It’s stores dependencies centrally, similar to central dependency manager approach.
Pros
- It’s lightweight.
Cons
- The compiler won’t tell you if dependencies aren’t set. You could easily get a crash when running a not-very-well-used part of the app.
- Looks automagic to people unfamiliar with the project.
A note on 3rd party libraries
It seems to me like 3rd party dependency management libraries aren’t worth the effort. They add complexity but don’t add anything over and above what you can create yourself.
Some popular frameworks that exist:
- Swinject. This one looks like it uses dependency caching, so I’d avoid it.
- Needle. I’m not sure how this one works, but it claims to be compile-time safe.
- Weaver. This lets you add dependencies with annotations. It uses code generation with SourceKitten to generate compiler-safe dependencies. This one sounds interesting.
NB I haven’t used any of these, so my assessment it based only on their landing page on GitHub.