Injecting Dependencies with a simple Annotation
• 4 minute read
Sometimes I would have liked to have dependencies injected in an easier way which reduces boilerplate code. After fiddling around with generics I finally found a satisfying pattern.
When I switched from web technologies to the native technology stack in the Apple platforms some patterns appeared odd at first. In example that a lot of things which I previously knew as “services” were called “managers”. Or how dependencies between such objects are managed. Things work a bit different compared to in example PHP applications and their dynamic run time conditions. In the projects I have worked on this far the concrete implementations of dependencies are always known during development already. No dynamically resolved payment services based on runtime information, for example. Hence this simple and rock solid pattern can be applied.
lazy var credentialsManager: CredentialsManaging = CredentialsManager.shared
It is a bit verbose and depending on the purpose of the type it can be repetitive over the codebase having a lot of references to this shared global object. It does the job, though. I have not encountered a practical problem with this approach yet.
The lazy
keyword takes care that the dependency is instantiated not before it is actually used.
This avoids a potentially costly build up of a dependency tree during app launch.
The variable (var
) and existential type CredentialsManaging
(to be any CredentialsManaging
with Swift 6) enables injection of arbitrary concrete types.
Be it the default and only implementation, some variation or a mock object you wrote by hand or generated with tools like SwiftyMocky.
The default value and non-optional nature of the property also ensures you can always rely on an object being available.
It can be quite repetitive, though. And it requires more boilerplate code to write in unit tests, too, to inject mock objects.
The Goal
I wanted to reduce this further. I was thinking about the dependency injection solutions I encountered in other languages. I wanted something really compact and generic. Something like this:
@Injected var accountManager: AccountManaging
The @Injected
property wrapper is the key.
It abstracts the access to a central facility which manages the references to and initialization of such global objects.
The inversion of control simplifies the object initialization in tests. You only need to set up and configure a bunch of mock objects once and can reuse them in every test class without reconfiguring the subject under test. Ok, some inversion of control could also be achieved by using explicit dependency injection through intializers but that appears to move boilerplate code from A to B only.
The Solution
// Register a service before continueing with the actual business logic of your app.
ServiceContainer.shared.register(AccountManager.self, for: AccountManaging.self)
// Fetch the registered object implicitly and lazily through the `@Injected` property wrapper.
class App {
@Injected var accountManager: AccountManaging?
func run() {
accountManager?.signIn()
}
}
I figured out a working solution. It is not much code but still was difficult to find out for me. Options, property wrappers, generics, existential types and cryptic compiler errors are a confusing mix.
My solution is implemented as a Swift package and available on GitHub. It contains an executable module and target which demonstrates the use. It is not 100% of what I thought of initially due to the optional type of the injected dependency but that is an acceptable compromise.
I work with generics only rarely due the lack of need.
Finding a way to my goal was a good exercise which improved my understanding of their concept and how it can be put to use.
Especially after understanding the any
keyword, what existential types are and their impact on performance this is a valuable learning.
I am not sure I used terminology correctly and mixed things up.
In example: my service container is not really a container.
I would prefer to call it provider because you can still work without or around it.
Also the dependency injection and inversion of control actually is not fully applied because the consuming object retrieves its dependency through the property wrapper from the ServiceContainer
.