Pull to refresh

Understanding Dependency Inversion Principle (DIP)

At some early point of my software developer career I learned about SOLID principles. At first glance S, O, L, I seemed obvious, easy to understand and follow (only at first glance) but D (Dependency Inversion Principle) wasn't as obvious to me. It took more years that I'm proud to admit to understand what D really means (at least I think now that I understand). So I want to share my understanding of DIP principle.

Path to DIP understanding

I decided to share my path to DIP understanding as I feel it may reveal some useful details. This part can be safely skipped.

Being Java (Spring) developer at that time and working at typical body-shop company, easiest explanation I could get from my colleges and fast googling was that DIP is some form of DI (Dependency Injection). My understanding about Dependency Injection was strongly affected by Spring at that time (you put magic annotations at some places and then dependencies automagically appear where you want them).

After some time I changed project and got to work with completely different stack - MEAN (MongoDB, Express, Angular, NodeJS) with Typescript. It forced me to rethink a lot of concepts in software development as there weren't magical annotations which hide a lot of details. So here came my first step/question to actually understanding DIP:
"If I don't have framework which magical annotations (xml config files, generated code from diagrams - whatever) can I still do Dependency Injection?"
Trying to answer this question led me to understanding that DI is essentially very simple practice - instead of instantiating your dependencies inside component you instantiate them somewhere else and pass them to component.

// Not DI
class Service {
  private final Dependency dep;
  
  Service() {
    this.dep = new Dependency(...);
  }
}


// DI
class Service {
  private final Dependency dep;
  
  Service(Dependency dependency) {
    this.dep = dependency;
  }
}

class Application {
  public static void main() {
    var dep = new Dependency(...);
    var service = new Service(dep);
    ...
  }
}

OK, that's great, but what does DIP (especially Inversion) has to do with it? More googling and discussions and here comes next answer: "DIP is DI but instead of injecting implementations, component accepts interfaces in constructor". Reading books like "Clean Code" I felt that I have figured it out but I still wasn't able to articulate how exactly Inversion relates to this.

Also, there was another important question on the table

"Why should I do DI and DIP?".

I figured very practical answer to this question -

"If you instantiate dependencies inside component - you can't reuse them. If you depend on implementation - you won't be able to provide another implementations".

Tho this is correct, now I understand that idea behind DIP is deeper. And this answer still doesn't explain Inversion part.

This lacking of deeper DIP understanding led me to create code structure like this (roughly):

my_app
├── service_package
│   ├── Service.java
├── repository_package
│   ├── DBRepositoryImpl.java
│   ├── InMemoryRepositoryImpl.java
│   ├── RepositoryInterface.java
├── Application.java

Service depends on RepositoryInterface and Application provides implementation depending on some conditions. At this level of understanding magic (together with fear and appeal) of frameworks like Spring started to disappear (I'm skipping DI containers, proxies and other capabilities of such frameworks although they also helped with overall understanding).

Then I started learning and using Go (mostly for pet projects). Learning Go made me thinking about many things I thought I figured out previously (like OOP) - one of these things was DIP. What made me think about DIP again was implicit interfaces. I have seen implicit interfaces previously (typescript have them in some way), but in Go they were very obvious. Here is quick definition for implicit interfaces:

A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword.

So what's the big deal about implicit interfaces? Migrating my_app application from Java to Go one-to-one will result in something like this:

my_app
├── service_package
│   ├── service.go
├── repository_package
│   ├── dbRepositoryImpl.go
│   ├── inMemoryRepositoryImpl.go
│   ├── repositoryInterface.go
├── application.go

Seems like nothing changed, but one important thing has changed. RepositoryInterface is defined in repository_package but searching for interface usages will reveal that it's used only in service_package. This is because you don't need to declare that implementations implement some interface (this happens implicitly). So logical move would be moving interface to service_package.

my_app
├── service_package
│   ├── service.go
│   ├── repositoryInterface.go
├── repository_package
│   ├── dbRepositoryImpl.go
│   ├── inMemoryRepositoryImpl.go
├── application.go

When I first did this, it felt little bit strange - interface is declared on consumer level... and this was my last (at least for now) step in understanding DIP.

OK, so what's DIP

Lets imagine simple case when there's Service component which calls some functions from Repository component.

Service depends on repository
Service depends on repository

Service code usually contains higher level details than Repository code and Repository is just one of the lower level details for Service. In this case Service is both calling and depending on Repository. Changes in Repository will likely result in changes in Service. This is problematic as Service may contain multiple dependencies and we want to protect our service from changes in them. Ultimately we don't want our higher-level code to depend on lower-level details.

So one solution which I usually see and did myself for a long time is creating and interface between them

Both Service and Repository depend on interface
Both Service and Repository depend on interface

Apart from allowing us to provide different implementations and specifying contract between Service and Repository, this should help us to solve problem of depending on lower-level details. But just introducing interface is not enough. Dependency from Service towards Repository isn't gone, it's just hidden with interface.

Repository depends on Service
Repository depends on Service

Now Repository Interface is part of Service. And dependency is now... Inverted (it opposes flow of control). Lower-level Repository now depends on higher level Service. Of-course Repository Interface shouldn't be declared in the same file as Service, but Service should be declaring and controlling this interface.

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.