Pull to refresh

Comments 16

PinnedPinned comments

Обсудили немного в личке, dishka в норме предлагает делать интеграции с фреймворкам, чтобы просто маркировать зависимости через FromDishka, а библиотека сама оборачивала в контекст и прокидывала конетйнер без использования глобальных переменных. Это можно попробовать реализовать и для django, но надо вникать в корнер кейсы, особенно в сочетании с DRF. Путь звучит понятно, но его должен кто-то пройти, встроенной интеграции мы пока не подготовили.

При этом dishka не запрещает использовать его в стиле как было сделано с punq. Из-за явного контроля scope это увеличивает количество строк с 1 до 2

class BuyProductView(APIView):
    permission_classes = (CustomerRequired,)

    def post(self, request: Request) -> Response:
        serializer = BuyProductSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        with container() as c:
             service = c.get(BuyProductService)
             result = service(product=serializer.validated_data, customer=request.user.customer)

        return Response(
            data=result.asdict(),
            status=status.HTTP_200_OK,
        )

В качестве бонуса такого решения - мы получаем возможность использовать финализацию ресурсов, которые использовались внутри service (в том числе косвенно) или делать несколько раз get, переиспользуя созданные объекты.

Так же коллеги предлагали положить контейнер в request объект, тогда можно избавить от глобальной переменной. А вход в скоуп монжо сделать в middleware, но это обсуждаемо.

Статья неплохая, есть ряд вопросов:

1. container - это глобальный объект?
2. Можем ли иметь несколько контейнеров?
3. Насколько эффективно ресолвятся зависимости?
4. Не совсем понял пример с тем, что меняется входная схема. Да, мы не привязываемся к неймингу, отдаем сырые данные , там под капотом происходит магия. Но в таком случае получается, что мы ничего как клиент не знаем о контракте, если контракт поменяется мы бы в любом случае получили проблему совместимости, но когда явно указываешь схему передачи, ide подсветит тебе, что у тебя синус с косинусом не сходятся или ту же проблему получишь при запуске линтов. Как бы никто не защищен, если от того, что у тебя и исходную схему модернизируют, так что она перестанет работать у клиентов сервиса. Но тут мы это прячем на слой поглубже.

DI в целом очень хорош, нужен ли он в django проекте?

  1. Да

  2. Технически — да, можно создавать контейнеры на уровне app-ки в Django. Вопрос насколько это будет удобно и не понадобится ли какой-то сервис из одной аппки в сервисе другой аппки. Можно легко запутаться если контейнеров много.

  3. Я не проводил бенчмарк-тесты, но это одна из самых легких DI-либ + 100% покрытие тестами. Минимальный шанс, что где-то выстрелит.

4)

Да, мы не привязываемся к неймингу, отдаем сырые данные , там под капотом происходит магия. Но в таком случае получается, что мы ничего как клиент не знаем о контракте, если контракт поменяется мы бы в любом случае получили проблему совместимости, но когда явно указываешь схему передачи, ide подсветит тебе, что у тебя синус с косинусом не сходятся или ту же проблему получишь при запуске линтов

В моем примере я использовал dict потому что это было удобно в случае с сериализатором. Можно написать более явный контракт и передавать именнованные аргументы (а в фабрике соответственно их принимать и маппить в DTO):

service = container.resolve("SomeService", id=1, price=2, amount=3)

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

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

2) Думаю, речь не про уровень аппки, а про то чтобы контейнер создавался при старте и передавался во вьюхи. Чтобы его можно было, в частности, в тестах подменить

4) все ещё не понимаю зачем вы передаете данные при резолвинге сервиса. Ну передайте вы при вызове его методов, предвариательно заложив в интерфейс. Если же это конфигурационные параметры - передавайте при настройке контейнера, а не во вьюхе опять же

4) Думаю здесь вы правы и стоило прокинуть Product в метод call а не конструктор, а зависимости оставить на уровне конструктора, было бы более констистентно, спасибо.

Не раскрыта тема внедрения зависимостей. Ваш сервис по факту ни от чего не зависит - ни от адаптера для доступа к БД, ни от вспомогательных объектов, реализующих БЛ. Вместо этого почему-то он принимает продукт в init, а не как данные. Это немного странно

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

Суммарно, это больше выглядит как паттерн ServiceLocator, а не Dependency Injection.

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

Приветствую

Не раскрыта тема внедрения зависимостей. Ваш сервис по факту ни от чего не зависит - ни от адаптера для доступа к БД, ни от вспомогательных объектов, реализующих БЛ.

Может я не совсем понял о чем речь — есть пример, где для работы сервиса нужно передать другой сервис который работает с CRM и инжектится внутри фабрики.

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

Суммарно, это больше выглядит как паттерн ServiceLocator, а не Dependency Injection.

Я понимаю что вы имеете в виду — скорее всего использование контейнера внутри вьюхи. Да, эта часть выглядит как ServiceLocator и это единственное место где сервис инжектится подобным образом, но! По моему скромному мнению для джанги и его вьюшек - это допустимое поведение. Использование всяких декораторов над методом или переопределение конструктора вьюхи — совсем печально выглядит. Но и это относится только к вьюхам — остальные зависимости попадают через фабрику.

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

Что есть — то есть. Насколько знаю в punq-е можно определять два поведения глобально для каждого объекта - singletone и создавать новый объект. Но при использовании это поведение определить на ходу нельзя. Однако для нас это не играет большой роли.

Спасибо, что прокомментировал, для меня это действительно важно.

На самом деле, хотелось бы услышать побольше про выбор контейнеров, с чем сравнивали, почему не подошли? Я соглашусь, что большинство их в питоне очень низкого качества, но у вас наверно своё мнение есть. В частности, у меня есть свой проект - dishka, который решает, например, задачу контроля скоупов. Рассматривали ли вы его?

Перед тем как начать писать статью, я смотрел во все стороны, что мог. С вашего позволения я просто процитирую свое сообщение от 6 января из своего телеграмм-канала:

Друзья, всем привет!

Я начал работу над второй частью. А именно – посидел и посмотрел, какие реализации DI (Dependency Injection) предлагает опенсорс в контексте Django. Вот что мы имеем на данный момент👇

1. Dishka – отличный фреймворк, но вообще никак не подходит для доброго-доброго Django. Об этом, кстати, говорит сам разработчик.

2. python-dependency-injector – уже ближе. Есть даже страница в документации про то, как применять в Джанге. Но тоже мимо. Меня не устраивает, что он «инжектит» именно экземпляры классов. Это не ложится в нашу логику с датаклассами. Да и выглядит ес честно не очень красиво. Типичная болячка фреймворка который «может во все». Выглядит как интеграция ради интеграции.

3. Обертка над Injector — какое-то сомнительное решение переопределять конструктор у вьюх. Тоже забраковал. + он вроде не поддерживается.

4. Смотрел еще всякие библиотеки, все выглядит как код ради кода, а не ради того, чтоб жить проще стало.

Короче. Django – это не про DI. Нет нормального способа заинжектить какой-то сервис во вьюху. Фреймворк имеет свое видение, свои шаблоны и паттерны. Не нужно пытаться идти против него – будешь постоянно натыкаться на препятствия.

Но, в общем-то, это не проблема. DI в рамках Джанги ≠ DI внутри сервисов. Поэтому, будем смотреть дальше, но уже немного в другую сторону.

Ну что ж. Я автор. Я говорил обычно что-то другое. Скорее всего "зачем вам DI, если у вас джанга". Так что я тут скорее заинтересован в вашем мнении, почему же не подходит?

Обсудили немного в личке, dishka в норме предлагает делать интеграции с фреймворкам, чтобы просто маркировать зависимости через FromDishka, а библиотека сама оборачивала в контекст и прокидывала конетйнер без использования глобальных переменных. Это можно попробовать реализовать и для django, но надо вникать в корнер кейсы, особенно в сочетании с DRF. Путь звучит понятно, но его должен кто-то пройти, встроенной интеграции мы пока не подготовили.

При этом dishka не запрещает использовать его в стиле как было сделано с punq. Из-за явного контроля scope это увеличивает количество строк с 1 до 2

class BuyProductView(APIView):
    permission_classes = (CustomerRequired,)

    def post(self, request: Request) -> Response:
        serializer = BuyProductSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        with container() as c:
             service = c.get(BuyProductService)
             result = service(product=serializer.validated_data, customer=request.user.customer)

        return Response(
            data=result.asdict(),
            status=status.HTTP_200_OK,
        )

В качестве бонуса такого решения - мы получаем возможность использовать финализацию ресурсов, которые использовались внутри service (в том числе косвенно) или делать несколько раз get, переиспользуя созданные объекты.

Так же коллеги предлагали положить контейнер в request объект, тогда можно избавить от глобальной переменной. А вход в скоуп монжо сделать в middleware, но это обсуждаемо.

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

Dishka-Django хакатону быть?

Лучшее решение станет примером в доке Дишки для будущих поколений бедолаг джангистов.

выглядит как ServiceLocator

В Dimension-DI можно получать зависимости через аннотации (DI-way) и напрямую из контейнера (Service Locator - way). DI предпочтителен, Service Locator - использую только в редких случаях (пример использования SL в Dimension-UI).

Использование всяких декораторов над методом или переопределение конструктора вьюхи — совсем печально выглядит.

Это же стандарт в DI - внедрять зависимости через конструктор. Через поля и методы не рекомендуется делать. Или это про другое?

p.s. платформа Java

Внутри джанговских вьюх (особенно class-based, как у нас на проекте) использование DI через конструктор/аннотации — смэрть.

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

А вот внутри сервисов так и происходит — прокидываю через конструктор.

Короче говоря, внутри вьюх используем Service Locator, а зависимости сервиса — DI-way.

Спасибо за комментарий!

Внутри джанговских вьюх (особенно class-based, как у нас на проекте) использование DI через конструктор/аннотации — смэрть.

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

Можно поподробнее (не хочу ИИ беспокоить по этому пустяковому вопросу). Мне непонятно, почему так в Django?

Вот в мире Enterprise разработки на Java не стоит вопрос как внедрять зависимости - основная рекомендация - только через конструктор - объяснение есть в вопросах и ответах по собеседованию на позицию Junior.. Это рекомендуют сами разработчики Spring IoC кстати - да и везде такая практика - остальные способы как исключение из правил. Очень интересно узнать почему здесь так. Очень странно, как по мне.

Не забываем что в джаве нет функций

Можно поподробнее (не хочу ИИ беспокоить по этому пустяковому вопросу). Мне непонятно, почему так в Django?

Вот в мире Enterprise разработки на Java не стоит вопрос как внедрять зависимости - основная рекомендация - только через конструктор - объяснение есть в вопросах и ответах по собеседованию на позицию Junior.. Это рекомендуют сами разработчики Spring IoC кстати - да и везде такая практика - остальные способы как исключение из правил. Очень интересно узнать почему здесь так. Очень странно, как по мне.

Думаю, что при проектировании фреймворка никто не предполагал, что в конструктор view будет хоть что-то передаваться. Он используется для внутренней DRF-ной логики и при обычном написании кода вам вообще не нужно и не следует касаться этого компонента.

Мы пишем код так, будто конструктора и нет вообще, и это выглядит замечательно. Прокинуть туда DI = попытаться заставить делать компонент то, для чего он не предназначался изначально. Можно, а нужно ли? Чувство, что больше потерял, чем приобрел.

Ну и еще одна особенность — много различных вариаций того, что в Django считается вьюхой: обычная функция, реализация через View, реализация через ViewSet, реализация через декоратор @action внутри ViewSet-ов.

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

Sign up to leave a comment.

Articles