В качестве иллюстрации для этой статьи рассмотрим проект-пример. Предположим, вы пишете код приложения-чатбота. Вы хотите, чтобы некоторые классы можно было переиспользовать от бота к боту, чтобы не переделывать всякий раз всю работу заново. Для начала пишете главные классы: Простое приложение для чата Класс Chat вполне понятен. В нем требуется источник, из которого предоставляются пользовательские сообщения, затем дается генератор ответов, чтобы получать соответствующие ответы, а затем записыватель вывода, отвечающий за обработку ответа. Как вариант, можно добавить инструмент логирования, который будет записывать беседу в файл. UserMessageSource и OutputMessageWriter – это абстрактные классы, которые можно реализовать для кастомизации поведения чатбота. В реалистичном сценарии мы могли бы создать две разные реализации. Версия для работы через интерфейс командной строки (CLI) считывает ввод из консоли и записывает в нее ответы, такой режим полезен для отладки в локальной среде. Простой чат для интерфейса командной строки     Эта простая версия позволяет считывать ввод из CLI и выводить ответы, а также логировать беседу в текстовом файле. Версия для промышленного использования могла бы подключаться к какому-нибудь решению с очередями сообщений, например, к RabbitMQ, чтобы соотносить поток пользовательского ввода с соответствующими ответами бота. Поскольку в рамках этой статьи детали реализации не слишком важны, рассмотрим простую вымышленную версию: Сымитированный чат, подключенный через очередь сообщений В этой версии нам понадобится экземпляр MqConfig, который бы совместно использовался между MqUserMessageSource и MqOutputWriter. ConversationLogger здесь не используется, его можно было бы добавить как независимый потребитель очереди сообщений. Код работает, но в нем остались некоторые болевые точки: Создание новой конфигурации: подразумевается, что при этом будет создаваться новый экземпляр Chat и все его зависимости. Если это решение не кажется вам работоспособным – помните, это всего лишь пример, вряд ли вы стали бы его придерживаться, если бы вам потребовалось работать более чем со 100 классами зависимостей. Добавление/удаление/замена возможности: изменение группы логически связанных зависимостей может быть изнурительным (вам придется удалить 2 класса и добавить еще 3, чтобы у вас был чатбот, подключенный к очереди сообщений). беседы требует соблюдать формат лога, то вам пришлось бы проверить все конфигурации, в которых он используется, и отредактировать их вручную. Здесь слишком много шаблонного кода, в нем легко допустить ошибку и ценность его невелика. Одним из решений, которое позволит это сгладить, является внедрение зависимости, автоматически связывающей каждый класс с его зависимостями.  Вот несколько примеров, демонстрирующих, как при помощи библиотеки opyoid упростить ваше приложение. Версия для интерфейса командной строки, использующая библиотеку opyoid Как видите, тут создаются связки, конфигурирующие, экземпляры каких классов следует создавать – а все остальное делает opyoid. Здесь для Chat требуется экземпляр UserMessageSource, а CliUserMessageSource связан с этим типом, поэтому его экземпляр создается, когда это нужно. То же касается OutputWriter, который связан с версией CLI. Когда вы хотите привязать класс к нему же самому, например, Chat или AnswerGenerator, то вам не приходится объявлять их дважды. ConversationLogger связан с экземпляром самого себя, он будет использоваться напрямую, когда это станет необходимо. Все связки даются Injector, который затем может использовать их для создания экземпляров новых объектов. Обратите внимание, что единственное требование для привязки класса заключается в том, чтобы конструктор использовал подсказки типов. В нашем примере мы никоим образом не меняли классы. Кроме того, порядок привязок не имеет значения. Класс можно привязать до его зависимостей или после, об этом даже не стоит задумываться. Как же теперь выглядит версия с очередью сообщений?  Версия с очередью сообщений, использующая opyoid Обратите внимание, как мы воспользовались InstanceBinding для MqConfig. Это полезно, когда требуется внедрить конфигурационные классы данных, содержащие типы-примитивы – например, строки, целые числа или булевы значения. Этот MqConfig автоматически связан со всеми подключениями очереди сообщений, поэтому при добавлении нового не потребовалось бы никакой дополнительной конфигурации кроме самого класса. Также здесь видно, что при удалении ConversationLogger привязка не представляет проблем, поскольку имеет значение по умолчанию в конструкторе Chat. Нужны только параметры без значений по умолчанию. А что, если в моем коде отсутствуют подсказки типов? Значит ли это, что мне придется переписать весь код, чтобы воспользоваться возможностями, описанными выше? А что, если я завишу от внешней библиотеки, которую не контролирую? Можете быть уверены, вы сможете написать собственный провайдер, в который сможете обернуть свои классы, чтобы и их можно было внедрять. Провайдер для класса Chat  Здесь ChatProvider будет использоваться для создания каждого необходимого экземпляра Chat, даже если в его конструкторе не будет подсказок типов. Обратите внимание на ProviderBinding в инициализации Injector. Бывает, что собственный провайдер пишут и для того, чтобы добавить в нем логику, описывающую, как создаются экземпляры некоторых классов.  Значит ли это, что я должен создать единственный файл со всеми моими связками? Я все равно должен продублировать много связок между моими вариантами конфигурации. Вот почему можно пользоваться модулями для группирования связок – так их можно будет многократно использовать в разных конфигурациях и просто выбирать те, что вам нужны. Модуль обычно используется, чтобы сгруппировать все классы, относящиеся к той или иной возможности. Добавляя или удаляя модуль, можно с легкостью включать или выключать данную возможность. Конфигурация значительно упростилась В модуле вы можете объявить столько связок, сколько хотите, а также установить другие модули по мере необходимости. Так, здесь мы установили ChatModule в CliChatModule. Обратите внимание: при этом вы по-прежнему можете использовать связки поверх модуля в вашем инъекторе. Версия очереди сообщений с модулями  А вы могли бы сделать лучше? Мы уже говорили о сокращении шаблонного кода, но мне все равно придется писать все эти связки?  Довольно слов: При использовании опции auto_bindings классы автоматически привязываются сами к себе, поэтому вам всего лишь потребуется написать другие связки. Конечно же, вы можете и дальше пользоваться модулями и связками, а такие автосвязки будут создаваться только в качестве последнего варианта. В данном примере мы удалили ChatModule, поскольку он содержал только ClassBindings. В сравнении с кодом, который был у нас в самом начале, теперь имеем:  Автоматическое связывание между классами и их зависимостями, гораздо меньше серьезных переделок, когда меняются требования, автоматическое обнаружение недостающих зависимостей   Никакого дублирования между конфигурациями Количество шаблонного кода сведено до абсолютного минимума Мы обнаружили, что в экосистеме Python отсутствуют серьезные кандидаты для этого. Лучшей из библиотек, которую мы протестировали, была pinject, но мы хотели воспользоваться преимуществами типизации при внедрении наших классов. Другая альтернатива - python-dependency-injector, но для ее начальной настройки требуется достаточно много кода.  Поэтому мы решили написать новую библиотеку для внедрения зависимостей в  Python, opyoid, которая помогала бы нам разрешать зависимости между классами и помогала при начальной настройке больших приложений. Вот основные цели, которые ставились при создании этой библиотеки: Автоматически предоставлять зависимости для каждого класса  Использовать типизацию для их разрешения, поскольку они становятся нормой в Python  Иметь возможность внедрять сторонние классы Иметь возможность работать без обязательных декораторов во всех классах Мы также добавили некоторые другие продвинутые возможности, которые нужны в данном случае: Область видимости каждой связки, чтобы иметь возможность совместно использовать некоторые экземпляры во всем коде, в то же время создавая множество экземпляров многих других классов  Провайдеры для кастомизации того, как создаются классы, либо как откладывается создание экземпляра  Аннотации, чтобы можно было иметь множество связок для одного и того же типа  Многое другое… Вот и все, у нас наконец-то получился чистый удобочитаемый код, разделенный на простые классы, без возни с написанием шаблонного кода. Можно с легкостью привязывать новые классы, нужные для каждой возможности, создавать модули, чтобы их можно было группировать и легко активировать. Проект полностью выложен на Github вместе с дополнительными примерами и документацией.