SoundCloud’s iOS codebase faced a radical change a couple of years ago. The team had decided to modularize the codebase into frameworks, mainly to speed up development and give it a shape where it scales as new contributors join. The reasoning and roadmap of this transformation are explained in our Leveraging Frameworks to Speed Up Our Development on iOS — Part 1 blog post.
In this move toward modularized architecture, the team has benefited directly from faster development every day. However, when making architectural decisions on such a large scale, tradeoffs are almost always inevitable. Ours was dependency management. Without supervision of dependencies, dependency graphs end up being convoluted. The famous Spaghetti Code analogy stands out as one of the outcomes of these kinds of graphs.
Restricting dependencies is an excellent start toward loosely coupled frameworks. Modules — frameworks in our case — need other modules for interoperability. This relationship can be cleaned up by providing an API of a module as needed instead of exposing the entire module. This technique is called dependency inversion.
The dependency inversion principle (DIP) is the D in the widely regarded SOLID principles. We’ll begin with a quick definition of the concept. A Swift example will follow to show how the concept works in real-life examples. At the end, we’ll explain how we’re using DIP among frameworks in our modularized architecture.
The dependency inversion principle suggests that a module shouldn’t directly depend on another module. Direct dependency leads to a tight coupling between high-level (consumer) and low-level (consumed) modules. With tightly coupled modules, chances are that a high-level module needs to reflect every alteration in the low-level module. Autonomous modules are better for maintainability, as we already know from microservice architectures. High- to low-level dependency looks like this:
class ModuleA { //High Level
let foo: ModuleB //Low Level
}
Loosening the connection is key, but modules are meant to work together, so we still need a connection. The principle solves the problem with a supervisor contract that has the rules of the connection. It says modules should depend on the contract instead of each other. This contract determines what goes out of the higher module and what goes into the lower module, and it guarantees both modules that the communication will be done according to its terms. So, the initial dependency is broken into two and reverted to the contract. This is what we call dependency inversion.
The best way to understand is by applying the principle to a real-life example. Imagine we have a module called ProfilePresenter
. This module is responsible for logical decisions about what to show on our screen, and it’s the standard architectural component in most modern mobile development.
ProfilePresenter
is our high-level module, or the consumer module. It needs a view or views to present what it needs to present. ProfileViewController
(the low-level module) is our view layer for visualizing profiles under the service of the ProfilePresenter
. Let’s see how this looks without DIP:
class ProfileViewController: UIViewController {
func showFullName(_ fullName: String) { }
}
class ProfilePresenter {
private weak var profileView: ProfileViewController?
init(profileView: ProfileViewController) {
self.profileView = profileView
}
func presentProfile(_ profile: Profile) {
let fullName = profile.name + " " + profile.surname
profileView?.showFullName(fullName)
}
}
struct Profile {
let name: String
let surname: String
let artworkUrl: String
}
The ProfilePresenter
receives the full profile entity, extracts the full name, and commands the view to show it. It’s the responsibility of the ProfileViewController
to show the full name. In other words, a unidirectional flow.
What’s the problem here? Using concrete types seems straightforward. The ProfilePresenter
knows which component it’s talking to, so there are no surprises. But this is what we call an overly attached relationship or a tightly coupled dependency: We can’t easily separate the low-level module from the high-level module right now, in spite of this example being basic.
If we need an entirely different profile layout for our premium users (which we’d accomplish by injecting different views for different subscription models), and if we need to create a new view for it, we cannot easily replace the current view with the new premium user view.
Currently, our dependency relationship looks like this:
So the dependency is from ProfilePresenter
to ProfileViewController
. DIP says that this dependency needs to be broken and inverted — not all the way back to ProfilePresenter
, but as a new contract between these two, i.e. an abstraction. So we break the current dependency into two weaker dependencies and point them to the new abstraction:
ProfileViewing
is the contract that enforces the relationship rules of both parties. What ProfileViewing
does not guarantee is the concrete types of both modules. This is intended by the principle, in order to have flexibility:
protocol ProfileViewing: class {
func showFullName(_ fullName: String)
}
class ProfileViewController: UIViewController, ProfileViewing {
func showFullName(_ fullName: String) { }
}
class ProfilePresenter {
private weak var profileView: ProfileViewing?
init(profileView: ProfileViewing) {
self.profileView = profileView
}
func presentProfile(_ profile: Profile) {
let fullName = profile.name + " " + profile.surname
profileView?.showFullName(fullName)
}
}
Not much has changed, actually. There’s now a contract visible to both modules to break the unhealthy relationship. If we have a new profile view — something like PremiumProfileViewController
— we need to conform to ProfileViewing
. We can then hook it up to ProfilePresenter
with no additional work, and it would work just like the original profile view.
The above example is a standard use of DIP for Swift. The initial behavior of creating relationships between modules should follow this approach. There may be exceptions, but it’s good to start with the DIP approach and tighten the relationship if needed. Direct dependency is tempting because it’s straightforward, whereas we create a lot of boilerplate code to have DIP. And it might seem like it requires a lot of work to take the DIP route, but we’re actually saving some refactoring time in the long term.
Now that we understand the mechanics of DIP, let’s see how we can use it on the level of frameworks.
If you’ve been modularizing your project into frameworks, you probably had issues with the dependencies among them. When the number of frameworks in a project increases, their dependency on each other becomes complicated, and it introduces problems ranging from broken syntax highlighting to longer compile times. By inverting some dependencies, we can save ourselves from this hectic dependency graph.
The problem is similar to the previous example. Framework-A needs a piece of Framework-B, but Framework-A doesn’t want to depend on the entirety of Framework-B. Who can blame Framework-A, right? Instead, Framework-A can be given the exact piece it needs from Framework-B: an injection per se.
Again, the two modules don’t like their direct dependency, so they need a lightweight, more manageable contract. Since we’re working on the framework level, the contract should be placed in its own framework. This new framework holds the contract and a minimum number of pieces that the contract needs. It’s important to keep this framework thin because other frameworks will depend on it. Since certain pieces need to be injected into the framework, and since the main app target is the one injecting, the main app also needs to depend on the new contract framework:
To visualize this, let’s say we have two frameworks named Authentication
and Payment
, each of which contains code associated with its name. Payment
needs an authenticator when the user wants to make the payment. We have a class named Authenticator
, located in the Authentication
framework, exactly for this purpose:
public class Authenticator {
public init() { }
public func authenticate() { }
}
The Payment
framework needs this piece in its PaymentManager
class:
public class PaymentManager {
private let authenticator: Authenticator
public init(authenticator: Authenticator) {
self.authenticator = authenticator
}
func doFoo() {
authenticator.authenticate()
}
}
So far, it looks almost exactly like the previous example. We learned from the previous example that we shouldn’t use the concrete Authenticator
type; we need to invert this dependency to a contract. So we need a new framework called AuthenticationContract
to put the protocol in. The AuthenticationContract
framework needs to be imported to Payments
, Authentication
, and the Main App
:
public protocol Authenticating {
func authenticate()
}
import AuthenticationContract
public class PaymentManager {
private let authenticator: Authenticating
public init(authenticator: Authenticating) {
self.authenticator = authenticator
}
func doFoo() {
authenticator.authenticate()
}
}
import AuthenticationContract
public class Authenticator: Authenticating {
public init() { }
public func authenticate() { }
}
So dependencies are inverted to a new protocol called Authenticating
, and it’s safe to import AuthenticationContract
to both frameworks. One last thing to do here is to give Authenticator
to PaymentManager
, which will be done in the Main App
:
import AuthenticationContract
import Payment
import Authentication
let authenticator = Authenticator()
let paymentManager = PaymentManager(authenticator: authenticator)
Briefly, PaymentManager
needed a piece of the Authentication
framework, the Authenticator
. Instead of creating a dependency from the Payment
framework to the Authentication
framework, we inverted this dependency to the new AuthenticationContract
framework, which is way more lightweight than Authentication
. We saved ourselves from the tightly coupled relationship between two rather heavy frameworks.
This process can be replicated for every framework that needs to serve others. For example, a framework named Subscriptions
needs some parts from Payment
, and then a contract framework named PaymentContract
can be created for Payment
. Ultimately, frameworks come with their own companion contract frameworks.
When building software on any scale, individual modules need to work in harmony; hence we need dependencies. The dependency inversion principle doesn’t remove the dependency need for modules; they still need to work together. Rather, it sets up contracts that regulate the traffic between modules while assuring them that their internal changes won’t affect the other modules.
Inverted dependencies increase the chance that changes won’t be propagated into our software. Even if we want to replace a module whose dependency is inverted entirely, a new module assembly and integration will be way easier than a tightly coupled version. This is a good practice to lighten the burden of dependencies, but of course it’s not the only one!