DEV Community

José Miguel Álvarez Vañó
José Miguel Álvarez Vañó

Posted on • Originally published at jmalvarez.dev

Dependency Inversion Principle in TypeScript

Depend upon abstractions, not concretions.

In our applications we can differentiate between two types of classes:

  • Low level classes which do operations like reading from a database or saving a file.
  • High level classes which implement the business logic and use those low level classes.

What this principle proposes is that high level classes depend on interfaces instead of concrete implementations. This is easier to understand with an example.


In the following bad example we have the OrderService class that saves orders in a database. The OrderService class depends directly on the low level class MySQLDatabase.

If in the future we wanted to change the database that we are using we would have to modify the OrderService class.

class OrderService {
  database: MySQLDatabase;

  // constructor

  save(order: Order): void {
    if (order.id === undefined) {
      this.database.insert(order);
    } else {
      this.database.update(order);
    }
  }
}

class MySQLDatabase {
  insert(order: Order) {
    // insert
  }

  update(order: Order) {
    // update
  }
}
Enter fullscreen mode Exit fullscreen mode

We can improve this by creating an interface and make the OrderService class dependant of it. This way, we are inverting the dependency. Now the high level class depends on an abstraction instead of a low level class.

class OrderService {
  database: Database;

  // constructor

  save(order: Order): void {
    this.database.save(order);
  }
}

interface Database {
  save(order: Order): void;
}

class MySQLDatabase implements Database {
  save(order: Order) {
    if (order.id === undefined) {
      // insert
    } else {
      // update
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we can add new databases without modifying the OrderService class.

Latest comments (2)

Collapse
 
thealbertdev profile image
Albert • Edited

Nice Series! Thanks for sharing 😊
In the case where different databases with different technologies (mySQL, MongoDB, etc.) coexist and multiple implementations of the interface Database are available, which is the proper/cleaner way to choose/indicate which implementation of the interface Database has to be used?
Thanks in advance 😉

Collapse
 
jmalvarez profile image
José Miguel Álvarez Vañó

Thank you for your comment Albert!

Regarding your question, it depends. For example, if you only had one database for development and another one for production, you could use a simple conditional or something similar.

class MySQLDatabase implements Database {
  save(order: Order) {
    // production database
  }
}

class PostgreSQL implements Database {
  save(order: Order) {
    // development database
  }
}

const database =
  process.env.NODE_ENV === "development"
    ? new PostgreSQL()
    : new MySQLDatabase();
Enter fullscreen mode Exit fullscreen mode

In other cases you could manually choose the database that you want to use. If you have an entry point for development in your application you could just do this:

const database = new PostgreSQL()
const orderService = new OrderService(database);
Enter fullscreen mode Exit fullscreen mode

And in production this:

const database = new MySQLDatabase()
const orderService = new OrderService(database);
Enter fullscreen mode Exit fullscreen mode

If your code is more complex you could go with the Factory pattern, you can learn this pattern here:

Please let me know if you have any more questions!