Dependency Injection

surprisingly similar to teenage sex ...

·

3 min read

Dependency Injection and Inversion of Control is a bit like teenage sex - everyone is talking about it, everyone wants to try it, and yet nobody seems to understand it. A quick google search will find you overwhelmed by meaningless statements like

Dependency injection is a programming technique ... [that] decoupl[es] the usage of an object from its creation. source

Or that

Inversion of Control is a principle in software engineering which transfers the control of objects or portions of a program to a container or framework source

Or that the

loosely coupled structure of code using dependency injection makes it easier to reuse business logic implementations in different locations around your codebase. source

My biggest issue with Dependency Injection and Inversion of Control is that no one seems able to explain it simply which inevitably means no one truly understands it.

In this blog, I will try to lay the framework for what is now called Dependency Injection.

A simple example

Like all things in software development it begins with simple code, so let's imagine a simple situation - storing text in a database. If we have a database table configured we can just do:

if __name__ == "__main__": 
    message = "Hello World"

    database_connection = connect(
                            host="127.0.0.1", 
                            user="user", 
                            password="password", 
                            db_name="database", 
                            port=3006
    ) 

    database_connection.execute(
        "INSERT INTO messages (message) VALUES %s", (message)
    )

Now if we wanted to get a little fancier, we could put everything inside a function like:

def saveMessage(): 
    message = "Hello World"

    database_connection = connect(
                            host="127.0.0.1", 
                            user="user", 
                            password="password", 
                            db_name="database", 
                            port=3006
    ) 

    database_connection.execute(
        "INSERT INTO messages (message) VALUES %s", (message)
    )

if __name__ == "__main__": 
    saveMessage()

Perhaps we want a more dynamic message, so we could make the message a parameter of the function like:

def saveMessage(message: str): 

    database_connection = connect(
                            host="127.0.0.1", 
                            user="user", 
                            password="password", 
                            db_name="database", 
                            port=3006
    ) 

    database_connection.execute(
        "INSERT INTO messages (message) VALUES %s", (message)
    )

if __name__ == "__main__": 
    saveMessage("Hello World")

What if we want an even more dynamic message perhaps generated by a function? We could move the generation of the message to its own function:

def generateMessage(): 
    return "Hello World" + str(datetime.now())

def saveMessage(message: str): 
    database_connection = connect(
                            host="127.0.0.1", 
                            user="user", 
                            password="password", 
                            db_name="databse", 
                            port=3006
    ) 

    database_connection.execute(
        "INSERT INTO messages (message) VALUES %s", (message)
    )

if __name__ == "__main__": 
    message = generateMessage()
    saveMessage(message)

Now, rather than generating my message, saving it as a variable, and passing it through as a parameter, what if we just passed the generateMessage() function as a parameter to saveMessage()?

def generateMessage(): 
    return "Hello World" + str(datetime.now())

def saveMessage(functionToGenerateMessage): 
    database_connection = connect(
                            host="127.0.0.1", 
                            user="user", 
                            password="password", 
                            db_name="databse", 
                            port=3006
    ) 

    message = functionToGenerateMessage()

    database_connection.execute(
        "INSERT INTO messages (message) VALUES %s", (message)
    )

if __name__ == "__main__": 
    saveMessage(generateMessage())

And then we could do the same thing with the database connection:

def generateMessage(): 
    return "Hello World" + str(datetime.now())

def databaseConnection(): 
    database_connection = connect(
                            host="127.0.0.1", 
                            user="user", 
                            password="password", 
                            db_name="databse", 
                            port=3006
    ) 
    return database_connection


def saveMessage(functionToGenerateMessage, functionToGetDB): 
    message = functionToGenerateMessage()
    database_connection = functionToGetDB()

    database_connection().execute(
        "INSERT INTO messages (message) VALUES %s", (message)
    )

if __name__ == "__main__": 
    saveMessage(generateMessage(), databaseConnection())

Now look what we've done. Through a series of incremental changes we have taken the hardcoded message and database connection and moved them to their own functions. Our original saveMessage function which controlled and was responsible for everything now just inserts the message - nothing more.

This is Inversion of Control and Dependency Injection! We have taken the responsibility out of saveMessage and pushed those responsibilities upstream. By passing in functions to saveMessage we are injecting it with the necessary functions.

The reason Dependency Injection can be preferable is that we can now reuse our databaseConnection in another function.

At its core, Dependency Injection is dead simple - just passing in functions to another function. In that sense, dependency injection is a lot like sex to teenagers - it's dead simple, yet there is so much hype, misunderstanding, and fumbling.