Comments 45
А чем это принципиально отличается от фикстур в pytest?
Честно, я их не использовал. Но тут выходит далеко за рамки тестирование.
Ничем, они (фикстуры) своего рода и есть di.
А мне все-таки больше нравится вариант с иньекцией зависимостей через аргументы. Потому, что хочется глядя на сигнатуру функции видеть все ее зависимости. А так это не так уж силно отличается от глобальных переменных.
Да, я с Вами согласен, есть сходство с глобальными переменными. Но как по-моему, такой подход удобнее, так как нет зависимости от неймспейса. + как упоминал, будет динамический инжектор.
А никого первый пример не смутил?
А что не так? Как я говорил притянуто за уши, это псевдокод.
Вот это ваш текст сразу после примера:
"Ну и что? - спросите вы - нормальный код".
Он не нормальный, потому-что просто нерабочий. Смотрим на методы класса. Они не staticmethod и не classmethod, Итого первый аргумент должен быть self. Ну, как назовете, неважно - но это тип класса. Никак не int. И что тогда везде означает a+b? Ну, попробуйте, запустите.
Раз уж зашла речь о контейнерахв питоне, я обязан высказаться.
Dependency injection это полезный паттерн, который говорит что мы не должны сами брать зависимости ни из глобальной переменной, ни создавать по месту. Если мы просто делаем глобальный сторадж, который предоставляет доступ к куче разных объектов - это паттерн SericeLocator, к DI он не имеет отношения. DI-фреймворк (IoC-контейнер) может быть похож на него если юзать через глобал, но это не обязательно делать так.
Что же делает Ioc-container? Во-первых, умеет создавать сложную иерархию объектов, передавая одни в другие - собственно реализуя DI. Во-вторых, понимает где надо создать новый объект, а где переиспользовать старый, то есть имеет некоторые скоупы. Хороший контейнер умеет чуть больше: требует минимального вмешательства в сами объекты, умеет в финализацию, изолированные куски графа зависимостей, позднее связывание зависимостей. Я для себя формулировал такой список требований: https://dishka.readthedocs.io/en/stable/requirements/technical.html
Как обращаться к контейнеру? Тут могут быть варианты. В общем случае, он не должен быть фиксированно в одном экземпляре. У нас так же должна быть возможность подменить граф зависимостей для тестов. Я считаю самым удачным решением - использовать возможности фреймворков для передачи инстанса контейнера в конченые обработчики, но это не всегда возможно.
Какие могут быть подводные камни при разработке? Во-первых, сделать удобное api. Для обозначения разных объектов достаточно нативно используют типы. Использовать названия для всех сущностей достаточно сложно, так как среди них могут быть одноимённые и вообще следить за уникальностью имен по большомц проекту нереально. Во-вторых, скоупы можно реализовать по разному, но в любом случае нужен их контроль и иерархия. Я считаю что число скоупов должно быть произвольным, но некоторые разработчики начинают с классических 3, упуская, что уже в классических же либах их стало больше. В-третьих, мне кажется важным чтобы граф зависимостей можно было отлаживать. То есть чтобы ошибки были понятные, чтобы был полный контроль за ним.
Могу предложить посмотреть свой проект dishka, про него уже было несколько статей на Хабре. Там мы постарались сделать очень простое API, реаилзововав всё выше названное и даже больше.
Очень было интересно прочитать Ваш комментарий. Я понимаю, что мое решение не совсем соответствует стандартному определению. Я пытался сделать что-то новое и легкое в использовании. Не знаю получилось-ли это. В любом случае я буду допиливать мое решение. По началу просто не хотел его делать слишком переусложненным.
Я не знаю стандартного определения, это моё описание того что подразумевают под такими инструментами и мой опыт. Когда я пилил dishka я опирался на свой опыт использования "плохих ioc-контейнеров" и у меня вышло то что мне нравится. Если не углубляться в детали, то для использования надо всего 3-4 вызова функций, просто не таких как у вас и тоже не таких как у аналогов.
Уже после релиза dishka мне дали почитать книгу "Внедрение зависимостей в .Net" Марка Симана. Могу порекомендовать для ознакомления, она во многом передает мои представления, хотя по сравнению с ней, я несколько переосмыслил, например, работу со скоупами.
Одно радует, что в голосовании большинство проголосовало за не использует. Почему нельзя заинъектить зависимости руками не понятно
Моя позиция: можно и нужно. А вот когда зависимостей станет много и следить за "скоупами" станет лень, можно брать контейнер. Такой, который по минимуму влезает в существующий код
Когда зависимость стало много особенно не стоит полагаться на магию. Чего-нибудь отвалится, задублируешь или неправильно напишешь ключи и замучаешься разбираться. Нетипизированные dict[str, Any], глобальные переменные по капотом и инициализация размазанная по всему коду очень «помогают» пониманию кода. Явное лучше не явного - лучше в одном месте явно передать зависимости, с нормальными типами, чтобы редактор или mypy тебя лишней раз проверил
Ну вот поэтому у меня в dishka всё полагается на типы и есть достаточно строгая валидация графа
Это конечно лучше, но все равно не понятно, какую проблему это решает. Стало кода меньше - нет. Стал он понятней - нет. Тогда зачем это нужно?
Гарантируете вы, что при вызове container.get(SomeDeps), что чего-то вернется - думаю нет, привет эксепшен в рантайме
Вы в любом месте приложения можете дернуть container.get получить любую зависимость в результате у вас нет четких границ между уровнями абстракции в коде и все постепенно превращается в big ball of mud
Стало меньше кода?
Да, стало. Теперь чтобы создать 10 объектов и прокинуть куда надо какие надо с переисползованием мне нужно писать 20+ строк фабрик и потом ещё неизвестно сколько чтобы в тестах переопределять. Я просто регистрирую классы, в простом случае по одной строке на класс. Если где-то появляется контекстный менеджер, мне снова не надо тащить его по всей иерархии. Меняется только 3 строки, а его использующие классы больше не должны волноваться о пробросе exit.
Вы в любом месте приложения можете дернуть container.get
Такая проблема действительно есть. Частично она решается тем, что контейнер не используется напрямую, а везде используетсч inject
и конкретные типы завимостей. Это работает только на обработчиках фреймворков, так что риск что "любая часть кода" полезет куда не надо - сильно меньше. Мы хотим тут тоже проверять доступность, но пока нет действительно хороших идей
В норме вся механика DI используется за счёт constructor injection, а контейнер - лишь способ получить временный объект в обработчике, на границе его жизни.
Да, стало. Теперь чтобы создать 10 объектов и прокинуть куда надо какие надо с переисползованием мне нужно писать 20+ строк фабрик и потом ещё неизвестно сколько чтобы в тестах переопределять
Вот это не понял. Если скоуп все приложение, то создаете один экземпляр в сomposition root приложения и передаете его и проблем нет. Если скокуп более более узкий, то пишите одну универсальную типизированную обертку для создания фабричной функции и передаете фабричную функцию
class Factory[D, **P]:
def __int__(self, dep_type: type[D], *args: P.args, **kwargs: P.kwargs) -> None:
self._dep_type = dep_type
self._args = args
self._kwargs = kwargs
def __call__(self) -> D:
return self._dep_type(*self._args, **self._kwargs)
Если у вас контекстный менеджер, то вызываете его перед предачей и передаете ровно так же, как у вас вызывается контекстный метод для создания скоупа
Я вам более того скажу я если честно не видел приложений, где зависимостей много и при этом можно обойтись механикой скоупов. Реальных в приложениях, где зависимостей много возникает и сложная логика создания зависимостей. Вот примерчик. Есть платежный движок с многими десятками интеграций с банками. В рамках скоупа платежа создается соответствующий адаптер. Казалось бы лафа для вашего DI, но оказывается что еще есть несколько десятков типов платежей и в результате каждый из адаптеров нужно параметраризировать под каждый вид платежа при создании по разному. И вы все равно пишете развернутую логику создания зависимостей
Уточню, что я не считаю IoC-контейнер must-have инструментом, без которого приложение жить не может. Это вспомогательная штука для создания вот таких фабрик. Его прелесть раскрывается когда фабрик становится много и среди них начинают встречаться контекстные менеджеры или какой-нибудь объект меняет скоуп. Унифицированный подход упрощает работу с этим. Но опять же, можно сделать вручную. Я много лет так и жил и до сих пор некоторые проекты так вполне комфортно работают.
Конечно же полностью отказаться от ручного написания фабрик нельзя. Идея в том, чтобы не писать их там, где не требуется. Если мы можем связать большую часть приложения на основе типов и явных деклараций "протокола-используемая реализация" (а ещё до использования проверить что нет противоречий в графе) - это уже неплохо. Если надо написать несколько фабрик вручную - напишем только их, это не противоречит концепции.
Более того, моя позиция в том, что почти всё приложение не должно знать о существовании контейнера. Да и контейнер у меня сделан максимально тупо, чтобы с минимальными изменениями можно было переехать на ручные фабрики и обратно.
Уточню, что я не считаю IoC-контейнер must-have инструментом, без которого приложение жить не может
А я считаю, что он не нужен, так как не решает никаких проблем
Это вспомогательная штука для создания вот таких фабрик. Его прелесть раскрывается когда фабрик становится много и среди них начинают встречаться контекстные менеджеры или какой-нибудь объект меняет скоуп
Нет проблемы многих фабрик - универсальная фабричная функция занимает несколько строк. Нет проблем проблемы контекстных менеджеров - вызов
dep = Constructor()
заменяется на
with Constructor() as dep
Количество строк не меняется, вы явно видите, где у вас контекстный менеджер
В результате не решается никаких реальных проблем и предлагается затащить здоровую зависимость, которая там якобы под капотом разруливает все edge кейсы асинхронности и многопоточки большая часть из которых в конкретном проекте скорее всего не нужна - спасибо, но нет
Кстати про тесты - где там экономия?
С контейнером
Создается контейнер
Создается мок зависимости
Регистрируется мок зависимости
Внедряется контейнер в тестируемый объект
Без
Создается мок зависимости
Внедряется мок в тестируемый объект
Нет проблем проблемы контекстных менеджеров
Есть проблема когда объекту А нужен объект Б, которому нужен объект С, который контекстный менеджер, а вам нужен именно А. Приходится через всю ирерахию протаскивать __enter__
/__exit__
Я не предлагаю втаскивать контейнер туда где не надо. Я предлагаю посмотреть, на проект где есть хотя бы десятка три фабрик и сравнить с тем что получится с контейнером. Если помогает - берем, если не помогает, значит не надо. В любом случае хороший контейнер существует виден только на границе скоупа, там где мы и так что-то дергаем.
Контейнер имеет смысл только в интеграционных тестах (то есть там где уже всё сложно), в обычных тестах я пишу так как вы сказали - просто передаю мок/фейк.
Есть проблема когда объекту А нужен объект Б, которому нужен объект С, который контекстный менеджер, а вам нужен именно А. Приходится через всю ирерахию протаскивать
enter
/__exit__
Я чего-то не понимаю пример. Если у зависимость А стала контекстным менеджером, то ее создание я заменил на with и передал в конечное место и больше ничего не нужно менять. Если объект C стал контекстным менеджером, я заменил его создание на with передал дальше в конструктор Б. Если у меня нормальный код, абстракции уровня С не текут вниз и мне больше ничего не надо менять. Или я не понял пример?
Контейнер имеет смысл только в интеграционных тестах (то есть там где уже всё сложно), в обычных тестах я пишу так как вы сказали - просто передаю мок/фейк
В интеграционном тесте вы создаете почти всю иерархию объектов, подменив на тестовые только некоторые инфраструктурные зависимости, и не меняете зависимости слоя бизнес-логика. Я не понимаю в чем тут экономия. Вы будете вынуждены написать новую функцию, которая собирает composition root и зарегистрировать все зависимости инфрового уровня некоторые, как некоторые как обычно. Я сделаю просто создам их
Я чего-то не понимаю пример. Если у зависимость А стала контекстным менеджером, то ее создание я заменил на with и передал в конечное место и больше ничего не нужно менять.
От того что C стала контекстным менеджером, A и B им не станет сам и фабрика А тоже. Это надо написать. В подходе с dishka у вас топ левел всегда контекст, а внутри он уже разрулит где юзать конеткст, а где просто создавать. Небольшая унификация, можно сделать самому.
Вы будете вынуждены написать новую функцию, которая собирает composition root и зарегистрировать все зависимости инфрового уровня некоторые, как некоторые как обычно
Нет, в случае с контейнером вы напишете другую фабрику для конкретной зависимости и всё остальное оставите как было. А контейнер сам соберет ваш composition root из того что в данной конфигурации сделано.
Да, здесь дикт самая большая проблема. Вскоре исправлю. Может через Аннотейтед будет. Посмотрим
И если нам ее нужно вызывать во многих местах, значит мы много раз создаем зависимости и не избавились от проблемы многократного создания
Не понял зачем создавать инстанс класса для каждого вызова функции так же не кто не делает, его один раз создали например в файле dependencies и оттуда импортируют или вообще можно модуль делать чем это хуже?
И использовать dict имхот пахнет костылем, это же просто ДИКТ Карл, зачем использовать вашу библиотеку, если я и сам его могу создать и пихать в словарь "зависимости"
Тему DI в данной статье считаю не раскрытой
Делают. По классике есть зависимости типа "singletone", а есть"scoped". Если у вашего объекта есть Стейт, который не должен шариться между обработчиками запроса (банально, Коннект к бд) - это scoped зависимость и её надо бы пересоздавать (оптимизации приемлемы, но концептуально, один инстанс не должен одновременно использоваться)
Если есть scope, то в python обычно используют contextvars
Я скорей про то что в пайтон есть более популярный и поддерживаемых инструмент для di
Context vars - не лучшее, что можно придумать. Во-первых, они неявно копируются, во-вторых, скоуп не всегда совпадает с асинк таской. Я предпочитаю явное использование контекстного менеджера.
Что значит scope не совпадает? Это где вы такое видели?
Возьмём вкбсокет. У вас есть цикл обработки сообщений в нем. Есть объект который должен быть доступен все время доступности вкбсокет (не знаю, адаптер для отправки в него структурированных данных), а вот на каждое сообщение мы хотим временный скоуп иметь, даже если они в одной таске последовательно обрабатываются.
Я правильно понимаю, что deps["something"] возвращает просто некий объект и ни смотрящий код, ни среда разработки не знают, какого типа этот объект?
К сожалению, Вы правы. Пока что это наибольшая проблема моего решения. В теории, можно типизировать переменную, например:
something: Something = deps["something"]
Но это не даст полного контроля. В будущем исправлю. Если у Вас есть предложение что можно сделать, готов выслушать.
Это невозможно в принципе, если не передавать нужный тип/интерфейс в качестве параметра, если в Питоне есть дженерики на методы, или не использовать тип переменной слева от "равно". Редактор кода знает типы только на основе информации времени компиляции, а тут весь функционал во времени выполнения, потому что коллекция гетерогенная. Но ведь и нужен объект очень конкретного, заранее известного класса, поэтому минимально достаточным будет приведение типа, как у вас, и использование (редактором кода, в том числе) уже типизированной переменной.
Что если я не хочу постоянно иметь в памяти созданный экземпляр класса? Представим, что внутри функции создается не абстрактный Calculator, а HTTPClient. Так вот зачем мне иметь постоянно созданный HTTPClient, который, может быть вообще ни разу не использован. Или использован единожды при старте программы?
Помимо всего этого, это не потокобезопасно. Если объект из контейнера будут менять из разных потоков, то будут конфликты.
Как я говорил, будет динамической инжектор который когда нужно будет создавать экземпляры. А про потоки, кто использует в качестве постоянной зависимости списки? Можете навести пример?
А уничтожать не будет ведь? И следовательно висеть все время в памяти будет.
А при чем тут списки? Проблема одновременного доступа к объекту из разных потоков существует для всех объектов, а не только для списков.
Раз я тут в этом треде уже несколько раз говорил про скоупы, приложу своё видео где я про это немного говорил http://www.youtube.com/watch?v=gWOBaZ3I4gc
И создавать, и уничтожать. А за списки... Я имел ввиду что зависимость, почти всегда, статическая.
каждый раз, когда открываю подобные статьи, ожидаю увидеть сначала обзор существующих решений, их преимущества и недостатки, на фоне которых описываемый продукт выделяется, а так ... ну велосипед очередной
DI в Python, Easy-DI: спаситель в сложном мире зависимостей