Можно поподробнее (не хочу ИИ беспокоить по этому пустяковому вопросу). Мне непонятно, почему так в Django?
Вот в мире Enterprise разработки на Java не стоит вопрос как внедрять зависимости - основная рекомендация - только через конструктор - объяснение есть в вопросах и ответах по собеседованию на позицию Junior.. Это рекомендуют сами разработчики Spring IoC кстати - да и везде такая практика - остальные способы как исключение из правил. Очень интересно узнать почему здесь так. Очень странно, как по мне.
Думаю, что при проектировании фреймворка никто не предполагал, что в конструктор view будет хоть что-то передаваться. Он используется для внутренней DRF-ной логики и при обычном написании кода вам вообще не нужно и не следует касаться этого компонента.
Мы пишем код так, будто конструктора и нет вообще, и это выглядит замечательно. Прокинуть туда DI = попытаться заставить делать компонент то, для чего он не предназначался изначально. Можно, а нужно ли? Чувство, что больше потерял, чем приобрел.
Ну и еще одна особенность — много различных вариаций того, что в Django считается вьюхой: обычная функция, реализация через View, реализация через ViewSet, реализация через декоратор @action внутри ViewSet-ов.
В долгоживущем старом проекте как правило все намешано. И тут стоит задача — угодить всем реализациям, при этом не в ущерб каждой из них, что крайне проблематично.
Перед тем как начать писать статью, я смотрел во все стороны, что мог. С вашего позволения я просто процитирую свое сообщение от 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 и создавать новый объект. Но при использовании это поведение определить на ходу нельзя. Однако для нас это не играет большой роли.
Спасибо, что прокомментировал, для меня это действительно важно.
Технически — да, можно создавать контейнеры на уровне app-ки в Django. Вопрос насколько это будет удобно и не понадобится ли какой-то сервис из одной аппки в сервисе другой аппки. Можно легко запутаться если контейнеров много.
Я не проводил бенчмарк-тесты, но это одна из самых легких 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 - мы выйдем за пределы контекста? Или все еще нет..? А если вдруг бронирование предполагает взаимодействие с каким-то внешним сервисом, то тут остается два пути:
"Пихать" работу с внешним сервисом внутри метода book
Вызов метода 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
во втором случае все три переменные объединены общим контекстом и в этом сила использования класса над никак не связанными фрагментами кода (функциями или чем бы то ни было)
кстати, эту же идею Вы можете обнаружить в книге "Чистый Код" от Роберта Мартина
Когда называют метод process - назовут класс DoSomeProcessor. Когда называют метод execute - назовут класс DoSomeExecutor. И все в таком духе, по итогу получается «50 оттенков сервисов», в которых сложнее разобраться, особенно при онбординге новым разрабом
Небольшим, но плюсом является, что мы не сталкиваем лбами ценителей одного нейминга с ценителями другого, мы его просто убираем, потенциально экономя человеко-часы.
Делаем код немного короче и лаконичнее, т.к. больше не нужно прописывать название конкретного метода при вызове сервиса.
Было дело, помню, без зуба остался.
А статья огонь, спасибо!
Dishka-Django хакатону быть?
Лучшее решение станет примером в доке Дишки для будущих поколений
бедолагджангистов.Думаю, что при проектировании фреймворка никто не предполагал, что в конструктор view будет хоть что-то передаваться. Он используется для внутренней DRF-ной логики и при обычном написании кода вам вообще не нужно и не следует касаться этого компонента.
Мы пишем код так, будто конструктора и нет вообще, и это выглядит замечательно. Прокинуть туда DI = попытаться заставить делать компонент то, для чего он не предназначался изначально. Можно, а нужно ли? Чувство, что больше потерял, чем приобрел.
Ну и еще одна особенность — много различных вариаций того, что в Django считается вьюхой: обычная функция, реализация через View, реализация через ViewSet, реализация через декоратор @action внутри ViewSet-ов.
В долгоживущем старом проекте как правило все намешано. И тут стоит задача — угодить всем реализациям, при этом не в ущерб каждой из них, что крайне проблематично.
Внутри джанговских вьюх (особенно class-based, как у нас на проекте) использование DI через конструктор/аннотации — смэрть.
Они под это не приспособлены. Выглядит как очень небрежная пристройка, с которой еще нужно повозиться, чтобы оно работало корректно.
А вот внутри сервисов так и происходит — прокидываю через конструктор.
Короче говоря, внутри вьюх используем Service Locator, а зависимости сервиса — DI-way.
Спасибо за комментарий!
Перед тем как начать писать статью, я смотрел во все стороны, что мог. С вашего позволения я просто процитирую свое сообщение от 6 января из своего телеграмм-канала:
4) Думаю здесь вы правы и стоило прокинуть Product в метод call а не конструктор, а зависимости оставить на уровне конструктора, было бы более констистентно, спасибо.
Приветствую
Может я не совсем понял о чем речь — есть пример, где для работы сервиса нужно передать другой сервис который работает с CRM и инжектится внутри фабрики.
Я понимаю что вы имеете в виду — скорее всего использование контейнера внутри вьюхи. Да, эта часть выглядит как ServiceLocator и это единственное место где сервис инжектится подобным образом, но! По моему скромному мнению для джанги и его вьюшек - это допустимое поведение. Использование всяких декораторов над методом или переопределение конструктора вьюхи — совсем печально выглядит. Но и это относится только к вьюхам — остальные зависимости попадают через фабрику.
Что есть — то есть. Насколько знаю в punq-е можно определять два поведения глобально для каждого объекта - singletone и создавать новый объект. Но при использовании это поведение определить на ходу нельзя. Однако для нас это не играет большой роли.
Спасибо, что прокомментировал, для меня это действительно важно.
Да
Технически — да, можно создавать контейнеры на уровне app-ки в Django. Вопрос насколько это будет удобно и не понадобится ли какой-то сервис из одной аппки в сервисе другой аппки. Можно легко запутаться если контейнеров много.
Я не проводил бенчмарк-тесты, но это одна из самых легких DI-либ + 100% покрытие тестами. Минимальный шанс, что где-то выстрелит.
4)
В моем примере я использовал
dictпотому что это было удобно в случае с сериализатором. Можно написать более явный контракт и передавать именнованные аргументы (а в фабрике соответственно их принимать и маппить в DTO):Это правда, что никто не защищен. Однако в данном случае при изменении исходной схемы мы не зависим от внутренней реализации сервиса. Что-то менять нужно и там, и там, однако, при использовании фабрики — многие вещи становятся гораздо удобнее. Взять то же версионирование сервисов — мы можем реализовать его в фабрике, и не выносить этот if-код наружу. Опять же - скрываем детали реализации.
Мы пришли к умозаключению (и вы вправе с ним не соглашаться), что если мы говорим о сервисном слое, давайте так же назовем и классы. Некий UL в рамках подхода и кода. Просьбу «поищи такой то код в таком сервисе» легче понять. Ищешь классы которые заканчиваются именно на «…Service», а не на что-то другое. Эту разницу особо остро чувствуют разработчики, которые только знакомятся с проектом.
Я не совсем про то, что происходит в комментариях на Хабре. Кто-то спорит, что Земля плоская, и это их право. Я скорее про то, что происходит в рамках команды разработчиков, которые принимают те или иные решения и проводят друг другу ревью.
В целом я не буду спорить, что большой разницы нет. Вы правы, что главное - единообразие и вы спокойно можете использовать другой нейминг если в рамках вашей команды он вас больше устраивает.
Спасибо, что поделились своим опытом. Возможно, по итогу, и мы придем к такому же стайлгайду, только с примерами на питоне :=), ну а пока будем решать проблемы по мере их поступления.
Не поймите меня неправильно - я не противник DDD. Проблема не в подходе, а в реалиях с которыми приходится сталкиваться когда заходишь на проект. И DDD почти нигде нельзя нормально применить, потому что это будет очень дорого и бизнес к такому как правило не готов.
В моем случае, мы имеем более простую реализацию, более простую идею и как следствие практически применимую функциональность которая может упростить и структурировать код даже на запущенном проекте.
В теории - да, а на практике в живом проекте, где уже все перемешано в кучу - к сожалению нет.
Главная проблема DDD - его тяжело встретить в реальном мире. Ну, может только я такой невезучий и все проекты уже давно перешли на него :=)
Ну и все таки если вы говорите про ограниченные контексты, значит имеете в виду, что логика доменной модели не должна выходить за какой-то ее доменный контекст (который, к слову, определяет сам разработчик).
Представьте если в методе book() нужно будет использование другой доменной модели, которая никак не связана с моделью Appartment. Например, первоначальный % по депозиту хранится в каких-то настройках:
Теперь уже кажется, что как-будто размещая эту логику в методе book - мы выйдем за пределы контекста? Или все еще нет..? А если вдруг бронирование предполагает взаимодействие с каким-то внешним сервисом, то тут остается два пути:
"Пихать" работу с внешним сервисом внутри метода book
Вызов метода book больше не означает что квартира забронирована должным образом. Мы делаем некий класс который будет работать поверх метода book и делать доп. запрос во внешний сервис (тот самый сервисный слой)
В первую очередь потому, что в таком случае мы рискуем очень сильно раздуть доменную модель. По нашему опыту в компании - так и случилось и мы имеем класс почти на 5 тысяч строк. Не буду лукавить и говорить что все, кто притрагивался к этому классу, правильно размещали методы и декомпозировали код должным образом, но даже при правильном подходе там было бы точно больше тысячи строк. А это уже многовато. И по итогу все равно привело бы к раздуванию. Код становится банально сложно читать из-за того что там полотно в одном классе.
Во-вторых, как я уже сказал, сервисный слой может взаимодействовать с несколькими доменными областями, он как бы объединяет несколько "бизнес-поинтов" в один бизнес процесс. Наверно, можно сказать что это некоего рода юз-кейс, но я бы не хотел так его называть.
Приведу пример чтобы было понятно, что я имею в виду:
Допустим у нас есть доменная модель квартиры, которую пользователь может купить в ипотеку, и эта модель имеет свою доменную логику - она может подсчитать, например, итоговую стоимость в зависимости от способа оплаты - ипотека/рассрочка/кеш. Предположим что каждый способ оплаты имеет свою наценку. И в качестве упрощения я просто сделаю поле "markup".
Также представим что квартира может быть в разных статусах:
И наша задача написать сервис который будет бронировать квартиру и вносить первоначальный взнос - это уже бизнес-логика, конкретный бизнес-процесс. Здесь мы можем напридумывать сколько угодно бизнес правил, например, что квартиру нельзя забронировать если первоначальный взнос меньше 10% от ее стоимости.
Таким образом получается что сервис по сути один за другим проверяет бизнес-правила. В моем понимании это и есть бизнес-логика. Примеры доменной модели и сервиса дико упрощены, сервис может работать с несколькими доменными моделями при необходимости - в зависимости от бизнес требований. А доменная модель иметь более сложную логику чем то что я написал, но сути дела от этого не изменится.
Спасибо за комментарий, прочитал статью. Кажется, что в ней автор говорит о доменной логике, а не бизнес-логике, с чем я полностью согласен, что она должна находиться в доменных моделях.
Мое упущение что я забыл включить это в статью, постараюсь дополнить в следующей.
Вы удобным себе образом процитировали только часть моего комментария. Не вижу смысла дальше заниматься буквоедством.
дай Бог здоровьечка
Имхо, гораздо легче воспринимать и читать код, когда разработчик позаботился о том, чтобы завернуть его в класс. Наличие класса дает всем методам единый контекст их использования. Кстати именно такую идею продвигают, например, те же самые Enum-ы:
или же
во втором случае все три переменные объединены общим контекстом и в этом сила использования класса над никак не связанными фрагментами кода (функциями или чем бы то ни было)
кстати, эту же идею Вы можете обнаружить в книге "Чистый Код" от Роберта Мартина
Когда называют метод process - назовут класс DoSomeProcessor. Когда называют метод execute - назовут класс DoSomeExecutor. И все в таком духе, по итогу получается «50 оттенков сервисов», в которых сложнее разобраться, особенно при онбординге новым разрабом
Небольшим, но плюсом является, что мы не сталкиваем лбами ценителей одного нейминга с ценителями другого, мы его просто убираем, потенциально экономя человеко-часы.
Делаем код немного короче и лаконичнее, т.к. больше не нужно прописывать название конкретного метода при вызове сервиса.
Единая точка входа ≠ один метод на весь класс.
Будем стараться, спасибо :)