Pull to refresh
7
Данил Федоров@youngWishes

Python-разработчик

5
Subscribers
Send message

Если непонятно с первого раза, спроси второй, третий, четвёртый.

Было дело, помню, без зуба остался.

А статья огонь, спасибо!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Приветствую

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

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

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

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

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

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

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

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

  1. Да

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

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

4)

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

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

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

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

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

DoSomeService не лучше. С учетом что сервисы вообще бывают доменные, прикладные и инфрастурктурные - пусть лучше будет процессор. А лучше просто DoSome. Ещё было слово Interactor

Мы пришли к умозаключению (и вы вправе с ним не соглашаться), что если мы говорим о сервисном слое, давайте так же назовем и классы. Некий UL в рамках подхода и кода. Просьбу «поищи такой то код в таком сервисе» легче понять. Ищешь классы которые заканчиваются именно на «…Service», а не на что-то другое. Эту разницу особо остро чувствуют разработчики, которые только знакомятся с проектом.

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

Я не совсем про то, что происходит в комментариях на Хабре. Кто-то спорит, что Земля плоская, и это их право. Я скорее про то, что происходит в рамках команды разработчиков, которые принимают те или иные решения и проводят друг другу ревью.

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

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

Не поймите меня неправильно - я не противник DDD. Проблема не в подходе, а в реалиях с которыми приходится сталкиваться когда заходишь на проект. И DDD почти нигде нельзя нормально применить, потому что это будет очень дорого и бизнес к такому как правило не готов.

В моем случае, мы имеем более простую реализацию, более простую идею и как следствие практически применимую функциональность которая может упростить и структурировать код даже на запущенном проекте.

В теории - да, а на практике в живом проекте, где уже все перемешано в кучу - к сожалению нет.

Главная проблема DDD - его тяжело встретить в реальном мире. Ну, может только я такой невезучий и все проекты уже давно перешли на него :=)

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

Представьте если в методе book() нужно будет использование другой доменной модели, которая никак не связана с моделью Appartment. Например, первоначальный % по депозиту хранится в каких-то настройках:

class SiteDefaults(models.Model):
  deposit = FloatField("первоначальный взнос для бронированяи на сайте")

Теперь уже кажется, что как-будто размещая эту логику в методе book - мы выйдем за пределы контекста? Или все еще нет..? А если вдруг бронирование предполагает взаимодействие с каким-то внешним сервисом, то тут остается два пути:

  1. "Пихать" работу с внешним сервисом внутри метода book

  2. Вызов метода book больше не означает что квартира забронирована должным образом. Мы делаем некий класс который будет работать поверх метода book и делать доп. запрос во внешний сервис (тот самый сервисный слой)

В первую очередь потому, что в таком случае мы рискуем очень сильно раздуть доменную модель. По нашему опыту в компании - так и случилось и мы имеем класс почти на 5 тысяч строк. Не буду лукавить и говорить что все, кто притрагивался к этому классу, правильно размещали методы и декомпозировали код должным образом, но даже при правильном подходе там было бы точно больше тысячи строк. А это уже многовато. И по итогу все равно привело бы к раздуванию. Код становится банально сложно читать из-за того что там полотно в одном классе.

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

Доменная логика это и есть бизнес логики, разве что есть какая то другая интерпретация (как это часто бывает в IT) о которой я не слышал :)

Приведу пример чтобы было понятно, что я имею в виду:

Допустим у нас есть доменная модель квартиры, которую пользователь может купить в ипотеку, и эта модель имеет свою доменную логику - она может подсчитать, например, итоговую стоимость в зависимости от способа оплаты - ипотека/рассрочка/кеш. Предположим что каждый способ оплаты имеет свою наценку. И в качестве упрощения я просто сделаю поле "markup".

class Appartment(models.Model):
  area = FloatField("полезная площадь")
  price = FloatField("цена за квадратный метр")
  markup = FloatField("наценка на area * price")

  def get_total_price(self) -> float:
    return (area * price) * (markup + 1)

Также представим что квартира может быть в разных статусах:

class Appartment(models.Model):
  FREE, BOOKED, SOLD = range(1, 4)
  
  status = SmallIntegerField("статус (FREE/BOOKED/SOLD)")
  area = FloatField("полезная площадь")
  price = FloatField("цена за квадратный метр")
  markup = FloatField("наценка на area * price")

  def get_total_price(self) -> float:
    return (area * price) * (markup + 1)

  def is_available(self) -> bool:
    return self.status == FREE

И наша задача написать сервис который будет бронировать квартиру и вносить первоначальный взнос - это уже бизнес-логика, конкретный бизнес-процесс. Здесь мы можем напридумывать сколько угодно бизнес правил, например, что квартиру нельзя забронировать если первоначальный взнос меньше 10% от ее стоимости.

@dataclass(slots=True, kw_only=True, frozen=True)
class BookAppartmentService:
  deposit: int

  @log_service_error
  def __call__(self, *, appartment: Appartment) -> None:
    deposit_in_percent = self._get_deposit_in_percent(appartment=appartment)
    if deposit_in_percent < 0.1:
      raise DepositNotEnoughError(
        deposit=deposit,
        appartment_price=appartment.get_total_price(),
      )
  
    if appartment.is_available:
      appartment.status = BOOK
      appartment.save(update_fields=["status"])
    
  def _get_deposit_in_percent(self, *, appartment: Appartment) -> float:
    return self.deposit / appartment.get_total_price()
    

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

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

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

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

Вы удобным себе образом процитировали только часть моего комментария. Не вижу смысла дальше заниматься буквоедством.

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

дай Бог здоровьечка

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

SOLD = 0
FREE = 1
BOOK = 2

или же

class ProductStatus(enum.IntEnum):
  SOLD = 0
  FREE = 1
  BOOK = 2

во втором случае все три переменные объединены общим контекстом и в этом сила использования класса над никак не связанными фрагментами кода (функциями или чем бы то ни было)

кстати, эту же идею Вы можете обнаружить в книге "Чистый Код" от Роберта Мартина

А в чем проблема ранов и прочих хэндлов?

  1. Когда называют метод process - назовут класс DoSomeProcessor. Когда называют метод execute - назовут класс DoSomeExecutor. И все в таком духе, по итогу получается «50 оттенков сервисов», в которых сложнее разобраться, особенно при онбординге новым разрабом

  2. Небольшим, но плюсом является, что мы не сталкиваем лбами ценителей одного нейминга с ценителями другого, мы его просто убираем, потенциально экономя человеко-часы.

  3. Делаем код немного короче и лаконичнее, т.к. больше не нужно прописывать название конкретного метода при вызове сервиса.

Единая точка входа ≠ один метод на весь класс.

Будем стараться, спасибо :)

1

Information

Rating
Does not participate
Location
Нижний Новгород, Нижегородская обл., Россия
Date of birth
Registered
Activity

Specialization

Backend Developer
Python
Git
Docker
Django
Fastapi
Apache Kafka
Kubernetes
CI/CD
PostgreSQL
English