Here comes the SOLID series's final letter: the D for Dependency Inversion Principle, which is personally my favorite after the SRP. Note that it's dependency inversion and not dependency injection. Many people come to the D and think it stands for dependency injection, so I should pass arguments through the constructor, etc... Even though it allows for dependency inversion, the two are not equal. Dependency Inversion != Dependency Injection So what does dependency inversion actually refer to? Well, it states: "High-level module should not depend upon low-level modules; instead they should depend upon abstractions", but also the "low-level modules too should depend upon abstractions". That might be abstract, I know, so let's dissect this definition. Dependency Inversion Principle Definition Simply put, dependency inversion allows us to decouple our code, and really that's what all of this comes down to; it all comes back to how we can design better applications. But what do we mean by "high-level" and "low-level" code? When we say "high-level", it refers to code that isn't concerned with the specific details. However, "low-level" code is the opposite: it's intimate with the specifics in the details. So when we say, "High-level module should not depend upon low-level modules", we can translate this by saying, "A class should never be forced to depend upon a specific implementation; instead, it should depend upon a contract, or an abstraction or interface (protocol)”. Once again, let's illustrate this via an example. Let's suppose we fetch from Github the list of the trending repositories that we save locally to the user device and maybe show them when he is offline. We might do something like this:

struct GithubRepo { }


class CoreDataStack {  
  func loadGithubRepos() -> [GithubRepo] {
    // fetch from core data and return the list of github repos
  }
}

class GithubRepositoryLoader { 
  private var coreDataStack: CoreDataStack
  
  init(coreDataStack: CoreDataStack) {
    self.coreDataStack = coreDataStack
  }
  
  func loadRepo() -> [GithubRepo]  {
    return coreDataStack.loadGithubRepos()
  }
}

The code above is bad in a number of ways. Take a minute, even a second, to think about the downside of this approach. The first question that should pop up is why GithubRepositoryLoader has any interest in what our caching system is? Fine, we're using Core Data, but why should GithubRepositoryLoader care about that? Now, suppose you were to associate dependency injection with dependency inversion. In that case, you might think that is fine because we're injecting the dependency through the constructor. Still, as we said initially, those two are different, and what we do in the code example breaks the Dependency Inversion Principle. Remember, the high-level module, the GithubRepositoryLoader in our example, should not depend upon low-level modules, the CoreDataStack. High-level modules should depend upon abstractions, not concrete implementations. If that sounds confusing, don't forget to bring all of this back to the idea of knowledge: does GithubRepositoryLoader needs to know how you retrieve the Github repos: no, it just needs to know that there are some data available to load, how to load them should not be its concern. In this case, let's refactor the code and use a protocol as our abstraction:

struct GithubRepo { }

protocol GithubRepoLoader {
  func loadRepos() -> [GithubRepo]
}

class GithubRepositoryLoader {
  private var loader: GithubRepoLoader
  
  init(loader: GithubRepoLoader) {
    self.loader = loader
  }
  
  func loadRepo() -> [GithubRepo]  {
    return loader.loadRepos()
  }
}

We code to an interface here and inject it to the GithubRepositoryLoader class (dependency inversion 🤝 dependency injection). But hold on, we're not done yet. The principle states that low-level modules should depend upon abstractions as well. So we'll have the low-level module CoreDataStack that should conform to the GithubRepoLoader protocol:

class CoreDataStack: GithubRepoLoader {
  func loadRepos() -> [GithubRepo] {
    // fetch from core data and return the list of github repos
  }
}

Now we have the high-level and low-level modules depending on the abstraction:

struct GithubRepo { }

protocol GithubRepoLoader {
  func loadRepos() -> [GithubRepo]
}

class CoreDataStack: GithubRepoLoader {
  func loadRepos() -> [GithubRepo] {
    // fetch from core data and return the list of github repos
  }
}


class GithubRepositoryLoader {
  private var loader: GithubRepoLoader
  
  init(loader: GithubRepoLoader) {
    self.loader = loader
  }
  
  func loadRepo() -> [GithubRepo]  {
    return loader.loadRepos()
  }
}

let loader: GithubRepoLoader = CoreDataStack()
let repos = loader.loadRepos()

And as you see, dependency injection is not equal to dependency inversion. However, dependency injection gives us a methodology to conform to that principle. You might be wondering what the benefits of doing this, of using this principle are? The first advantage, obviously, as we said above, is to decouple our code; the better our code is decoupled, the better. The second reason is testability. By doing so, we can test our system more easily. Since the loadRepos function is causing a side-effect, we can directly mock it in our tests and give it the behavior we want thus testing it in isolation. ✌️ The third benefit is flexibility. If one day we decide not to use Core Data anymore and want a simple JSON file stored in our document directory, we could simply create a new class conforming to the GithubRepoLoader like so:

struct GithubRepo { }

protocol GithubRepoLoader {
  func loadRepos() -> [GithubRepo]
}

class JSONLoader: GithubRepoLoader {
  func loadRepos() -> [GithubRepo] {
    // fetch from core data and return the list of github repos
  }
}

class GithubRepositoryLoader {
  private var loader: GithubRepoLoader
  
  init(loader: GithubRepoLoader) {
    self.loader = loader
  }
  
  func loadRepo() -> [GithubRepo]  {
    return loader.loadRepos()
  }
}

let loader: GithubRepoLoader = JSONLoader() 
let repos = loader.loadRepos()

And because JSONLoader conforms to the GithubRepoLoader protocol, it definitely meets its requirement then when injecting it to the GithubRepositoryLoader, our program will work as before.

Final Thoughts

We saw that the core idea behind the DIP is high-level code should never ever depend on low-level code, it should rely upon abstraction, and the low-level code depends on that same abstraction. This allows for the complete decoupling of our code while still providing some ways for the two modules to an interface. Here comes the end of our SOLID series, do not hesitate to revise the other principles. I hope you found them helpful. 🤗