Внедрение зависимостей - это практика передачи всего, что вам нужно, в качестве параметра, вместо того, чтобы позволить подпрограммам обращаться к глобальной переменной. На словах это может показаться немного сложным, но на самом деле это очень простая концепция в коде:

# Not using Dependency Injection
person = Person()
def print_age():
    print(person.age)
print_age()

# Using Dependency Injection
def print_age(p):
    print(p.age)
person = Person()
print_age(person)

Вот и все. Хотя для объявления и передачи параметров вам необходимо дополнительно набрать текст, это чрезвычайно полезно, поскольку может значительно повысить читаемость кода. Следует отметить, что, хотя все примеры написаны на Python, концепция внедрения зависимостей универсальна и может применяться практически ко всем языкам программирования.

Чем плохи глобальные переменные?

class Example():
    def test_1(self):
        self.var1 = 1
        self.some_method()
    def test_2(self):
        var2 = 2
        self.some_method()
    def some_method(self):
        # implementation omitted

Здесь я использую термин глобальная переменная в широком смысле для обозначения любого общего состояния в области видимости, независимо от того, является ли эта область классом, файлом или всей программой. В приведенном выше примере var1 будет глобальной переменной, а var2 - нет.

Проблема с глобальными переменными в том, что к ним можно получить доступ отовсюду. В приведенном выше примере в методе test_1 после вызова some_method нет гарантии, что self.var1 останется равным 1. Но мы может гарантировать, что в test_2 var2 останется 2, потому что var2 не является глобальной переменной.

Это по-прежнему управляемо, когда класс небольшой, но ваша проблема будет линейно масштабироваться с общими строками кода. В конце концов, вы будете постоянно читать код, чтобы узнать, будет ли изменена переменная. Избавившись от глобальных переменных и отдав предпочтение локальным, мы структурируем код таким образом, чтобы было тривиально определить, будет ли переменная изменена и когда.

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

Предположим, вы избавились от всех глобальных переменных и заменили их локальными переменными. Чтобы использовать их, вы должны передать их как параметры. Другими словами, внедрение зависимостей, естественно, будет принудительным. Давайте посмотрим на более сложный пример:

class ListBuilder:
    def A():
        self.list = []
        self.B()
        self.C()
        self.D()
        return self.list
    def B():
        # implementation omitted
    def C():
        # implementation omitted
    def D():
        # implementation omitted

Этот класс ListBuilder предоставляет методы для создания списка. Я пропустил реализацию методов B, C и D, поскольку они не являются необходимыми. Для нашей цели мы можем представить, что каждый из них имеет 2000 строк, и об этом очень сложно рассуждать - код, который в идеале вам не нужно читать.

Теперь ответьте на вопрос: какие функции влияют на построение списка? Поскольку self.list является общим изменяемым состоянием, методы B, C и D с одинаковой вероятностью выполнят некоторую операцию по изменению списка. Чтобы ответить на него, вам придется прочитать весь файл.

Представьте, что код написан немного иначе:

class ListBuilder:
    def A():
        my_list = self.B()
        self.C(my_list)
        self.D()
        return my_list
    def B():
        # implementation omitted
    def C(my_list):
        # implementation omitted
    def D():
        # implementation omitted

Теперь ответить на тот же вопрос будет намного проще. Избавившись от глобальных переменных, мы вынуждены писать класс более явным образом.
Просто взглянув на метод A, мы уже многое знаем о том, как строится список: метод B вернет список в качестве отправной точки, и метод C (возможно) изменит список, тогда как метод D вообще не изменит результат. Имея в виду эту высокоуровневую структуру, мы знаем, что можем просто сосредоточиться на методах B и C и игнорировать метод D для нашей цели.

(Примечание: технически это не совсем так. Функция C может тайно установить self.my_list в переданный параметр, а функция D может тайно получить доступ self.my_list, чтобы изменить его. Но это плохая практика, и мы можем предположить, что код не реализован таким образом.)

Резюме

Использование локальных переменных и внедрения зависимостей - простая, но мощная идея, позволяющая сделать ваш код более явным и понятным. Он дает понять, изменяется ли и где переменная, независимо от того, сколько у вас кода.