Clean code 1: Dependency Injection explained simply

clean code août 31, 2020

It is not enough for code to work. (Robert C Martin).

Intro

Faire

This part 1 is the first of a series centered upon a real world application: Faire.(Which is French for “To do”). A GTD-based set of tools for self-organization we'll build with CQRS and Event Sourcing

The goal is to guide you through the entire process of writing code is the right way.

Method

Learning software architecture is no easy task. Not only there is a lot of concepts to grasp. My struggle entering this world, was that the way it is often explained is overwhelming.

The way I like things explained to me, is by introducing one concept at a time, thoroughly.

And only use as a basis to build upon, concepts with a why and a how made totally clear.

That's what this series aims for.

Prerequisites

You need to know Python, and have it installed. That's it. We'll use Python3.8

Dependency Injection, what, why, how ?

The first time I heard the term Dependency Injection I thought to myself:
 Just based on how the name sounds, that looks like a super advanced concept

It's really not.

Elevators

elevator-1207812_1280

To show you how simple and intuitive DI really is, I'll start by an analogy. Though I don't know you I'll make an assumption: You know how to use an elevator (good for you :), congrats !).

When you enter an elevator aiming for the third floor: you press the button with 3 on it (well, if you actually didn't know how to use an elevator, that's how. You're welcome).

Do you know if the elevator works with cables, rails, magnetic suspension ?
Do you know if magnetic suspension elevators are even a thing at all ? (I have no clue, I just made that up).

You don't know, and most importantly, You don't care !

What you know is that pressing a button will bring you to the corresponding floor, regardless of the underlying mechanism.

Interfaces

The very first time I was introduced Interfaces I wondered how on earth a weird "empty class that can't contain code" could ever help me in any way.

And, yeah, that's more or less what it looks like at first glance.

What interfaces realy are: contracts. When a class implements an interface, what it does is saying to its consumers "I'll expose those methods".

Back to our elevator example:
When you use one, you're presented —give or take— this interface:

interface ElevatorInterface{
    button_0()
    button_1()
    button_2()
    button_3()
    button_4()
}

The way it works is Implementation detail.

As a consumer you want to use any elevator the exact same way.
Elevator companies are in charge of making elevators work properly.

In code that's called Separation of concerns (It's the S in the acronym SOLID)

Classes should be like elevators. The concerns we'll want to separate are:
Business logic and Technical logic.

Real world situation

You've just been hired as a coder. Your boss has heard about NoSQL and thinks that's super cool, for no particular technical reason. So you're asked to refacto the code for it to work with xxxNoSQL instead of the current xxxSQL.

Exploring the code, you stumble upon that beauty:

class UserManager:
    def signin(self, user:User):
        if db.exec_query(f"SELECT count(*) FROM user WHERE id='{user.email}'")>0:
            raise UnnecessarilyTypedException(f"A user with email address {user.email} already exists")
        else:
            db.exec_query(f"INSERT INTO user(id, email, password) VALUES(NULL, {user.email}, {user.password})")
    

The first reaction any decent dev should have here is to puke a little.

The second reaction should be to be to remove that ugly strong coupling.

See, that's a toy example. But should it occur in real life, you could expect all the code to be build that way.

What's the problem here ?

Well, aside probable SQL injections, what should really shock you here is
strong coupling. Business logic is written aside communication
with the DB layer. To change the DB while keeping the spirit of this code
you'd have to go through every handwritten queries.
You'd have bugs. Nasty and time consuming bugs.

To be extra clear

Two users can't have the same email address

Is Business logic (kinda, that's arguable. But that's a toy example so just go with it)

Asking the DB all users with a given email address

Is technical logic.

Whatever the project, whatever the technical stack, those two should never be side by side in a block of code.

What to do about it ?

Keep coupling as loose as possible. Don't ever mix business logic and low
level DB stuff (or any low level stuff for that matter)

How to achieve that ?

That's where Interfaces come to scene.

Since interfaces are not a thing in Python, in a first time we'll pretend that given this class:

class FooInterface:
    def bar(self, bla:str) -> int: pass

This one, when declared, will raise an exception because it has no
method bar

class FooImplementation(FooInterface):
    pass

And this one will throw an exception because eventhough it has declared a bar method, the types of args and return don't match those of the interface.

class FooImplementation(FooInterface):
    def bar(self, bla:int) -> list: 
        return []

We'll later see how to make that happen.

Anyway, back to our code.

Our goal is to separate the business and technical logic.

First we'll create an interface:

class UserRepositoryInterface:
    def find_by_email(self, email:str) -> List[User]: pass
    def create(self, user:User) -> None: pass
    def commit(self) -> None: pass

Then we'll rewrite the code of UserManager (assuming you want to keep a class named so vaguely, which you shouldn't)

class UserManager:
    def __init__(self, repository:UserRepositoryInterface):
        self.repository = repository
        
    def signin(self, user:User):
        if self.repository.find_by_email(user.email):
            raise UnnecessarilyTypedException(f"A user with email address {user.email} already exists")
        
        self.repository.create(user)

What just happened ?

When you use an elevator, you're implicitly asking present me buttons with numbers, that's all I want to know.

We've done just that, UserManager is asking: When you instanciate me, give me an object with those methods

What's next ?

Since this is refacto and your first step is to separate concerns, you should in a first place have a code that behaves in the exact same way.

class UserRepositoryXXXSQL(UserRepositoryInterface):
    def find_by_email(self, email:str) -> List[User]:
        return [User(**row) 
                for row in db.exec_query(f"SELECT * FROM user WHERE email='{email}'")
    def create(self, user:User) -> None:
        ...
    def commit(self) -> None: 
        ...

(We've kept the same ugly query. That's not what this article is about, but please don't use user input in a f-string to build SQL queries. Just don't)

Then, at runtime, when building UserManager, we'll give it that repository

user_manager = UserManager(UserRepositoryXXXSQL())

UserManager depends on an implementation of UserRepositoryInterface.
When initialized, we inject an instance of an object that implements said interface.

That's Dependency Injection

Mots clés

Super ! Vous vous êtes inscrit avec succès.
Super ! Effectuez le paiement pour obtenir l'accès complet.
Bon retour parmi nous ! Vous vous êtes connecté avec succès.
Parfait ! Votre compte est entièrement activé, vous avez désormais accès à tout le contenu.