Dependency Injection



Принцип внедрения зависимостей, будучи достаточно простым, концептуально оказывается часто неочевидным.



Суть его в том, что когда у нас одному из объектов требуется другой, то он не создает или ищет его сам, а принимает извне. Например, если вашей функции нужно соединение с БД, то она не должна ни импортировать его, ни брать из глобальной переменной, ни создавать сама. Ей это соединение должны передать.



Само собой, какой-то код будет создавать эти зависимости, и тут мы стараемся отделять его от кода, использующего их. Благодаря этому:

во-первых, делаем этим зависимости более явными;

во-вторых, можем управлять тем, будет ли использован один экземпляр зависимости или разные;

в-третьих, можем использовать один и тот же код с разными реализациями зависимостей.



Представьте, что вашему классу нужны некоторые параметры конфигурации, которые влияют на его поведение, и вы хотите протестировать разные варианты. Если бы класс сам грузил настройки, то вам пришлось бы в тестах учитывать, как именно он это делает, и возможно манипулировать теми объектами, которые обычно не меняются в процессе работы программы. Если же код класса получает эти настройки извне, то вы просто сделаете несколько вызовов с разными настройками. И даже если код класса изменится, тесты останутся корректными.



Можно выделить три способа внедрения зависимостей:

1. Внедрение через параметры функции/метода. Просто передаем зависимость как ещё один параметр:



 def clear_users(cursor):

cursor.execute("TRUNCATE users;")



cursor = connection.cursor()

clear_users(cursor)

clear_users(cursor)



2. Внедрение через параметры конструктора:



 class UsersDAO:

def __init__(self, cursor):

self.cursor = cursor

def clear_users(self):

self.cursor.execute("TRUNCATE users;")



dao = UsersDAO(connection.cursor())

dao.clear_users()

dao.clear_users()



3. Внедрение через атрибуты экземпляра (см. так же двухфазная инициализация):



 class UsersDAO:

def clear_users(self):

self.cursor.execute("TRUNCATE users;")



dao = UsersDAO()

dao.cursor = connection.cursor()

dao.clear_users()

dao.clear_users()



Популярные заблуждения:

Это что-то из мира java, в моем языке его нет. Неверно. Для того чтобы в языке можно было реализовать внедрение зависимостей, необходимо лишь иметь возможность передать ссылку на функцию или на объект. Это применимо как к Javascript и Python, так и к C и Golang.

Он нужен только большим enterprise приложениям. Скорее, он критичен для больших приложений. Небольшие приложения без автоматических тестов могут действительно существовать без внедрения зависимостей, но по мере усложнения необходимость в нем будет все более и более заметна.

Для него нужен специальный фреймворк/IoC-контейнер. Как показано выше, для DI не нужен никакой фреймворк, это возможность вашего языка. Фреймворки лишь помогут упростить построение графа зависимостей (когда у вас достаточно много разных связанных объектов) или решить какие-то прикладные задачи связанные с этим. Хуже, что неосторожное использование таких фреймворков может наоборот лишить вас DI, хотя вы будете думать что это не так.



Дополнительные материалы:

https://martinfowler.com/articles/dipInTheWild.html

https://www.jamesshore.com/v2/blog/2006/dependency-injection-demystified