I’m currently studying dependency injection and I’m having some issues with the so called ‘flexibility’ advantage of using dependency injection.
The flexibility advantage is mentioned in post Criticism and disadvantages of dependency injection, where it is mentioned that dependency injection enables
“switching implementations quickly (DbLogger instead of ConsoleLogger for example)”
I’m really having qualms about how this ‘switching’ can occur, and this is probably due to my limited experience. To elaborate on my confusion, I will mainly use constructor injection, which is the one I wish to understand and am working with.
Suppose I have a class User
that depends on a class MsSqlConnection
. According to the above post (and many other articles), it seems as though User
could switch from using MsSqlConnection
to MongoDbConnection
with much greater ease when using dependency injection. But I fail to see that.
Let’s consider the following example where constructor injection is used.
class User {
constructor(connection: MsSqlConnection) {}
}
class MsSqlConnection {
constructor() {}
}
const connection_instance = new MsSqlConnection()
const user = new User(connection_instance );
Here, I’m passing connection_instance
as the argument of the constructor of User
. But if I wanted a different kind of connection, say MongoDbConnection
, I will have to declare connection_instance
to be a MongoDbConnection
. It should look like:
const connection_instance = new MongoDbConnection() // changed to MongoDbConnection; note MongoDbConnection class was not defined
const user = new User(connection_instance);
However, in addition to creating a connection_instance
of type MongoDbConnection
, the above code would simply not work. This is because our User
class looks like
class User {
constructor(connection: MsSqlConnection) {}
}
and the constructor of User
only takes an argument of type MsSqlConnection
, not of type MongoDbConnection
. So where is the flexibility? I would have to adapt my constructor and change the code inside of User
to adapt to MongoDbConnection
.
I feel like I might be missing something rudimentary here… Any answers are very appreciated, and it’d be even greater if they are catered to some beginner (like me).
2
The principle
I’m really having qualms about how this ‘switching’ can occur, and this is probably due to my limited experience. To elaborate on my confusion, I will mainly use constructor injection, which is the one I wish to understand and am working with.
Have you ever walked into a coffee shop, and seen a customer hand the barista their personal coffee cup, so they don’t have to drink from a cardboard cup? In programming terms, the coffee cup is an injected dependency in the coffee making process.
Without dependency injection, that barista would be incapable of making a coffee in anything other than a cardboard cup, or only the types of cup that the barista was explicitly trained in. Just imagine the effort wasted in having to retrain your staff whenever a cup they haven’t seen before is handed to them.
While from a coffee making perspective, there’s nothing inherently wrong with only knowing how to make coffee in a cardboard cup, it’s a pointless hurdle from a customer-oriented perspective.
This also gets at the core benefit of dependency injection. The main purpose is not to the benefit of the class you inject the dependencies in (the barista), the benefit is gained on the higher level, organisationally speaking (the coffee shop and their customers).
The practice
Suppose I have a class User that depends on a class
MsSqlConnection
. According to the above post (and many other articles), it seems as though User could switch from usingMsSqlConnection
toMongoDbConnection
with much greater ease when using dependency injection. But I fail to see that.
What you’ve missed here is that the swappability of two concrete classes only works when these classes have a shared ancestry (interface or base class), and the injected dependency is of that shared type.
So instead of:
class User {
constructor(connection: MsSqlConnection) {}
}
You should be doing something along the lines of:
class User {
constructor(connection: IDatabaseConnection) {}
}
interface IDatabaseConnection {
// ...
}
class MsSqlConnection implements IDatabaseConnection {
// ...
}
class MongoDbConnection implements IDatabaseConnection {
// ...
}
If a concrete example helps better, let’s look back at our barista. The bad barista doesn’t inject their dependency at all. He just uses a cardboard cup, and he is incapable of making coffee in anything else.
class BadBarista {
makeEspresso() {
let cup = new CardboardCup();
let coffee = new Coffee();
cup.Add(coffee);
return cup;
}
}
Note: I’m using method injection instead of constructor injection, but functionally speaking this is the same principle of DI at play.
Your code example, when applied to our barista, would be something like this:
class YourBarista {
makeEspresso(cup: CardboardCup) {
let coffee = new Coffee();
cup.Add(coffee);
return cup;
}
}
Technically, the cup is being injected. However, it’s stil forced to be a CardboardCup
. That doesn’t really yield a benefit. The goal was to make it so that any cup could be passed.
So a better implementation would be:
class GoodBarista {
makeEspresso(cup : ICup) {
let coffee = new Coffee();
cup.Add(coffee);
return cup;
}
}
Now, any class that implements the ICup
interface can be passed into this method. You are no longer forced to use cardboard cup, therefore making it easy to swap which cup you use without needing to change/update your barista.
The documentation
In case this is new to you, look up polymorphism (and interfaces) instead of dependency injection. Dependency injection, or at least the swappability of components that you can leverage using dependency injection, relies on the concepts of polymorphism, i.e. the ability to substitute one thing for another, provided that they are defined as being (partially) the same thing.
5
It only lets you switch between different implementations of the same interface. This wouldn’t typically be used for something like switching between Mongo and MsSql, unless you had a common interface between them, like if different customers used different databases. Typically, it’s used to run different implementations in test and production, so you don’t have to spin up an entire database just to run unit tests.
7
This is where you’re doing it wrong:
constructor(connection: MsSqlConnection)
Rather than taking an object of type MsSqlConnection
, you should define an interface which includes the functionality you need from any database connection object, make your connection objects implement that interface, and your constructor take an instance of the interface, not of the specific types.
0