5

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
  • Isn't this typescript? Given : MsSqlConnection
    – Alexander
    Commented May 19, 2021 at 14:37
  • Uhm yeah, totally my fault on this one; tag should be typescript indeed
    – Etfrerrr
    Commented May 19, 2021 at 16:23

3 Answers 3

11

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 using MsSqlConnection to MongoDbConnection 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
  • Wow, that was actually really helpful! The only thing that still concerns me is whether this flexibility of swapping from one cup to another, which you demonstrated above, can also be applied when it is an IoC container or some underlying DI framework that handles the dependency injection instead of an interface. It seems as though using an IoC container to handle the 'passing of the cup type' into the constructor of Good Barista would make us lose the flexibility since we no longer have an interface.
    – Etfrerrr
    Commented May 19, 2021 at 15:37
  • 1
    @Etfrerrr "or some underlying DI framework that handles the dependency injection instead of an interface" It doesn't have to be an interface, but it's also not something that an IOC container can cover by itself. Your class (with the injectable dependencies) must use a base type or interface for its dependency. You can't just randomly inject any arbitrary object without having any type expectation whatsoever, and then expect the class to handle them all gracefully. You need strong typing here so that you can rely on the range of possibilities all having the same interface to work with.
    – Flater
    Commented May 19, 2021 at 17:19
  • 1
    @Etfrerrr: This is a stab in the dark, so do correct me if I'm wrong, but since you've tagged your question as Javascript (even though your code examples are TypeScript). I get the feeling that you're more used to a weakly typed language like JS, since your argument about "letting the IOC container handle it" seems to be based on the notion that you can pass any arbitrary object into a dependency = weak typing. While not impossible in languages like JS, DI is at its most powerful in a strongly typed language where you can concretely define the commonality between interchangeable components.
    – Flater
    Commented May 19, 2021 at 17:32
  • @Etfrerrr: Also, as a bit of an aside, note the difference between DI and IOC. IOC = making sure classes receive their dependencies instead of instantiating their own. DI = an automated container that instantiates a class instance and all of its dependencies (recursively). DI relies on IOC, but IOC can be done without DI. Technically, my answer only focuses on IOC, not DI. It is unspoken as to whether there is an automated DI container being used in my example, or whether you're manually passing the dependencies into the class instance.
    – Flater
    Commented May 19, 2021 at 17:40
  • 1
    "IoC container or some underlying DI framework that handles the dependency injection instead of an interface" - to add to what Flater said, IoC containers and frameworks that handle DI for you, like in, say, Angular, are made to work in conjunction with interfaces (or more generally, base types). But the containers and the frameworks are just tools, and are optional; the flexibility doesn't come from them, it comes from the design technique of DI (polymorphism, constructor injection). It lets you use those objects in different contexts by injecting context-specific dependencies. Commented May 19, 2021 at 22:24
10

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
  • Ohh, I see. Now please correct me if I'm wrong, but from my understanding, there are two ways of handling dependencies under DI: through interfaces and frameworks (this is from softwareengineering.stackexchange.com/questions/371722/…, once again). But if we are not using interfaces and only using frameworks instead, can we still benefit from this 'flexibility' advantage?
    – Etfrerrr
    Commented May 18, 2021 at 20:39
  • 1
    @Etfrerrr, uwhen writing unittests for your User class, do you provide it with an MsSqlConnection instance that connects to a real database, or would you rather provide an instance that fakes the connection and instead takes some cues from the test code? There you have your two implementations of MsSqlConnection: a real one for use in production environments and a fake one for testing other components that depend on a database connection. Commented May 19, 2021 at 7:38
  • 1
    @Etfrerrr: why would we 'run different implementations'? Suppose we have a User class who checks the time to decide whether they should sleep or work. They rely on a Clock dependency to tell the time. Obviously, when our application goes live, a real user will look at a real clock to tell what time it really is. But when we are testing the user to see if they would go to sleep after midnight, you don't want to be using a real clock (because you could only test after midnight), you want to use a fake clock that pretends like it's past midnight. Different clocks = different implementations
    – Flater
    Commented May 19, 2021 at 12:15
  • 1
    @Etfrerrr [..] This is where the "swappability" comes into play. No matter if we use a real clock or a fake clock, the user class works the same way. We are able to swap clocks without having to refactor any code in our User class. The User is able to work with any Clock that you inject into it.
    – Flater
    Commented May 19, 2021 at 12:16
  • 2
    @Etfrerrr: If the dependency is declared inside the User class, then changing the dependency (e.g. to mock or not) requires changing the User class. DI very specifically avoids needing to make changes to classes. It's better not to touch things, as that means that you're significantly less likely to have introduced a bug there. With DI, you can effectively choose your db connection without every having to change the User class again. Even if two years from now you create a FutureDbConnection, as long as it's an IDatabaConnection, User won't need to change to properly handle it.
    – Flater
    Commented May 19, 2021 at 17:37
5

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.