Davide De Rosa

A true case for Dependency Injection

When studying the foundations of a programming language, for example, during a career in Computer Science, very rarely are design patterns and the concept of software architecture presented in a way that can be transferred to real-world scenarios. Add to that, you may never use most of the well-known patterns, and it’s okay because you don’t want to be a hammer looking for a nail. You shouldn’t force a design pattern for the sake of using it.

Below, I’ll show you how a popular design pattern helped me rework Passepartout for Mac to distribute it outside of the App Store, without messing too much with the existing and stable codebase.

Experience produces patterns

Patterns are simply an outcome of your programming experience, in that you recognize a common problem for which a common solution also exists. Yet, the amount of buzzwords in the online communities around what some erroneously call “architectures” is ridiculous –the MVVM acronym being the one I stand the least–, to the point that the poor newcomers may feel intimidated. The risk of being ostracized for not using “the right name for the thing” is real, even if the same thing could be named differently the moment you visit a different community.

But hey, listen, the reality is brighter: compilers don’t give a damn about the name you give to your grand architectures. The only names that matter, at the end of the day, are those coming from the syntax of the programming language you use.

Nevertheless, some design patterns are so popular that their names are de facto universal:

You’re bound to cross them at least once in your lifetime. If you like practice more than theory, you definitely used any of the above without the need to give them a name. These patterns are undoubtly useful, they resolve basic architectural problems and, contrary to some beliefs, by no means are they tied to OOP.

Then there is Dependency Injection (DI). I hate giving this one a name, but there it is. More than a design pattern, DI is an approach to making software. I call it that way to avoid the terrible mistake of thinking that there is only one way to do it. I mean, if you’re a Java programmer, you don’t need the Spring Framework to do DI.

Here’s how I like to phrase it: DI is the act of decoupling your software from the underlying implementations of its dependencies.

How DI helps me scale Passepartout

Weeks ago, I decided to make Passepartout for Mac available outside of the App Store. Making such a VPN app standalone is no trivial task because the way it operates is radically different: the UI is still a frontend to the VPN backend, but the app and the VPN processes execute as different users, and they do not easily communicate. Specifically, the VPN is deployed as System Extension, and therefore runs as root.

By leaving the App Store, I was about to lose a big chunk of features:

  • App Groups: required for app/VPN IPC (inter-process communication), data sharing, and logs
  • Shared keychain: used to persist the VPN profiles
  • In-app purchases
  • iCloud

In-app purchases and iCloud could be postponed by stripping paid features, but the core functionality of Passepartout relied on both App Groups and the shared keychain. I hit a tough blocker.

Here’s where DI comes to the rescue: you don’t change the business logic of your software, you rather change how that logic is implemented deep down in the leaves of the dependency tree. I still wanted to “share data between the app and the VPN process” (business logic), but I needed to change how that data was shared (dependency). Remember, this isolation is only possible if the two concerns are sharply separated, but this is the core principle behind DI.

The core library of Passepartout, Partout, is 100% pure Swift for this reason: the logic of the library is always the same, with the “low-level” implementations abstracted as protocols, and for which the app is free to provide (inject) truly different implementations. In my case, it turned out that I could just craft new implementations for the standalone Mac app, whereas anything else would stay untouched.

Swap out the broken pieces

The process is fairly simple:

  • For each dependency, identify the “broken” implementation of a protocol
  • Write a new implementation, compatible with the target
  • Unit test behavior as per the protocol pre/post conditions
  • Swap the unsupported implementation with the new one

After these steps, passing the tests would imply that the app will work like before, but regardless of being downloaded from the App Store or not. I can’t tell how thrilling it is when you see the magic of programming in action. This is a real-world situation where good practices and constant refactoring pay off.

The new Mac app eventually works around the missing functionalities by dynamically replacing (supportsAppGroups is false in this case):

  • UserDefaultsEnvironment with NETunnelEnvironment, which does IPC by sending messages to the System Extension, rather than sharing data through the App Group UserDefaults. They both implement the TunnelEnvironmentReader protocol, meant for reading VPN data from the app.
public protocol TunnelEnvironmentReader: Sendable {
    func environmentValue<T>(forKey key: TunnelEnvironmentKey<T>) -> T? where T: Decodable
}
  • KeychainNEProtocolCoder with ProviderNEProtocolCoder, which installs the VPN profiles without the keychain, using the Network Extension map in providerConfiguration. They both implement the NEProtocolCoder protocol, meant for VPN profiles serialization.
public protocol NEProtocolCoder: Sendable {
    func protocolConfiguration(from profile: Profile, title: (Profile) -> String) throws -> NETunnelProviderProtocol

    func profile(from protocolConfiguration: NETunnelProviderProtocol) throws -> Profile
}

Do you really need DI?

You’d better follow the DI approach early in your software development, but not immediately, because you may never need to convert a code module into a pluggable dependency. If you foresee that your software will scale enough to justify such an abstraction, then start pulling out third parties from your concrete classes.

By the way, in many corporate projects people use DI because “everybody does”, rather than understanding the real benefits and tradeoffs. The overhead of maintaining abstract layers, or even third parties, for entities that will always have one implementation is sadly laughable. Please, do not avoid concrete classes just because you read that on some book. As I said in the beginning, you shouldn’t use a solution for a problem you don’t have, and most projects don’t need bulky external third parties to deal with their trivial dependencies. If you wasted hours to track down the order and lifecycle of the objects that an app creates on launch, you know what I’m talking about.

Sometimes, it really seems that some modern programmers don’t know how to initialize objects in the first place. In that case, rather than a dependency injection engine –whatever that means–, why not go back to the basics?