Репутація українського ІТ. Пройти опитування Асоціації IT Ukraine
×Закрыть

Dependency Injector — dependency injection фреймворк для Python

Привет,

Я создатель Dependency Injector. Это dependency injection фреймворк для Python.

В этом посте хочу рассказать про свой фреймворк.

Что такое dependency injection?

Принцип dependency injection позволяет снизить связывание (coupling) и увеличить сцепление (cohesion). Твой код становится более гибким, понятным и лучше поддается тестированию.

Как реализовать dependency injection?

Объекты больше не создают друг друга. Вместо этого они предоставляют способ передать им нужную зависимость.

Было:

class ApiClient:

    def __init__(self):
        self.api_key = os.getenv('API_KEY')
        self.timeout = os.getenv('TIMEOUT')


class Service:

    def __init__(self):
        self.api_client = ApiClient()

Стало:

class ApiClient:

    def __init__(self, api_key: str, timeout: int):
        self.api_key = api_key
        self.timeout = timeout


class Service:

    def __init__(self, api_client: ApiClient):
        self.api_client = api_client
Кто теперь создает объекты? Смотри следующий пример.

Зачем нужен Dependency Injector?

Dependency Injector предоставляет тебе контейнер и набор провайдеров, которые помогают собирать объекты твоего приложении, когда ты применяешь принцип dependency injection:

from dependency_injector import containers, providers


class ApiClient:

    def __init__(self, api_key: str, timeout: int):
        self.api_key = api_key
        self.timeout = timeout


class Service:

    def __init__(self, api_client: ApiClient):
        self.api_client = api_client


class Container(containers.DeclarativeContainer):

    config = providers.Configuration()

    api_client = providers.Singleton(
        ApiClient,
        api_key=config.api_key,
        timeout=config.timeout.as_int(),
    )

    service = providers.Factory(
        Service,
        api_client=api_client,
    )


if __name__ == '__main__':
    container = Container()
    container.config.from_yaml('config.yml')

    service = container.service()

    assert isinstance(service.api_client, ApiClient)

Тут можно посмотреть больше примеров.

Фреймворк простой. Реализован на Cython. Построен по принципу «явное лучше неявного (PEP20)». Не делает магических вещей с твоим кодом и не загрязняет его @inject декораторами, аннотациями, не очевидными правилами именования и типизации.

Что дальше?

Если заинтересовался, но сомневаешься, просто попробуй применить подход пару раз. Он неинтуитивный. Нужно время чтобы привыкнуть и прочувствовать. Польза стает ощутимой, когда проект вырастает до 30+ компонентов в контейнере.


Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Твой код становится более гибким, понятным и лучше поддается тестированию.

Вот как раз гибкость и понятность кода обычно противоположны. И любые абстракции очень тяжело разруливать. Сравните у себя количество и простоту кода в «было» и «стало». Тут уже и контрол-клик в ИДЕ не поможет, когда в проекте начались фабрики.

Переходы по Ctrl + клик работают во всех примерах в этом посте.

Это достигается за счет 2-х вещей:

  • Использование типизации в коде. В метода Service.__init__(self, api_client: ApiClient) можно нажать на ApiClient и перейти к его определению.
  • Использование связи по ссылке в контейнере. В Dependency Injector не используются строковые идентификаторы. Дизайн построен на ссылочной связи. Когда указываешь зависимость используешь объект другого провайдера, а не строку-идентификатор. Это сделано специально чтобы Ctrl + клик работал и можно было перейти к зависимости.
Вот как раз гибкость и понятность кода обычно противоположны. И любые абстракции очень тяжело разруливать.

В PEP20 есть такой принцип «Complex is better than complicated». Вот пример иллюстрации: pbs.twimg.com/...​media/Bky1fNJCQAAztUg.png

Бывают простые (1), сложные (2) и сложные и запутанные проекты (3). Принцип dependency injection помогает перейти от сложного и запутанного проекта (3) к просто сложному (2).

Когда у вас будет контейнер со всеми компонентами и их зависимости по проекту нужно будет гораздо реже передвигаться поиском.

github.com/google/pinject — как с аннотациями, так и без

Да, есть такой. Переходите на Dependency Injector, он лучше чем pinject.

Dependency Injector отличается от pinject такими вещами:

      У pinject нет понятия контейнера. Это минус, так как без контейнера нет возможности посмотреть на приложение целиком и понять где что используется. У Dependency Injector эта возможность есть.
      У pinject есть авторесолвинг зависимостей по именам. Это плохой механизм. Он заставляет писать код по правилам именования pinject, а не по правилам здравого смысла. Кроме того что это неудобно, в определенный момент это перестает работать. Тогда приходится добавлять исключения в виде binding specs. Код превращается в кашу так как уже не понятно где используется авторесолвинг, а где исключение в binding specs. Dependency injector построен на принципе «явное лучше неявного (PEP20)» и такими минусами не обладает. Авторесолвинга по именам или типам в Dependency Injector нет специально.
      У pinject есть аннотации (хотя в начале документации они пишут что нет). Аннотации плохой механизм. Наличие аннотаций сборки в коде косвенно противоречит принципу dependency injection. Мы применяем dependency injection чтобы убрать из кода информацию о сборке, а аннотации ее косвенно приносят обратно. Кроме того аннотации привязывают ваш код к фреймворку pinject. В Dependency Injector аннотаций нет специально. Декоратор @inject был в первой версии — убрал его во второй версии with opened eyes. Люди начинают им пользоваться, а когда проект вырастает — страдают и ругаются.
      Dependency Injector быстрее pinject. Dependency Injector написан на Cython и не приводит к деградации производительности. Pinject это чистый Python. До того как я переписал Dependency Injector на Cython он был в 50 раз медленнее.
      Dependency Injector популярнее, чем pinject. Dependency Injector скачивают с PyPI 200 тыс. раз в месяц, а pinject — 19 тыс.. Звездочек на гитхабе у pinject больше, но динамика прироста лучше у Dependency Injector.

    На рахунок швидкості, як правило основний час від’їдає низькорівневий код, і як раз його є сенс оптимізовувати, переписувати частинами на C/C++, в реальному важкому сервісі різниці між фасадом-одиночкою чим по суті і є контейнер написаним на Python і на Cython майже не буде.

    Хоча для чогось малого це явно перевага.

    Я переписывал Dependency Injector на Cython по двум причинам:

    • бросал тогда курить
    • CV-driven development

    Как бонус удалось получить неплохой прирост производительности.

    Проблем при переписывании было немало. После тоже. Второй бы раз хорошо подумал над этим шагом.

    Знання Cython корисне, якщо ще знати С/С++ можна дуже добре оптимізовувати вузькі ділянки коду, в Python з циклами все геть погано. Так що не дарма переписали, розібралися.

    Є думка що якщо треба DI то треба злізати з пітону на серйознішу мову

    Естественно можно переписать все на Расте ибо более серьезных альтернатив нет, но зачем?

    на серйознішу мову

    Python — несерьезный язык?

    без тайпінгів важко коллаборацію робити

    Тот неловкий момент, когда тайпхинты в питоне с 2015 года...

    тот неловкий момент, когда их никто не юзает и они не енфорсятся и тайпчекинг опциональный

    никто не юзает и они не енфорсятся и тайпчекинг опциональный

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

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

    В Python есть typing. Он используется в примерах в посте.

    Количество даунлоадов ни о чем. Я обычно смотрю на статистику контрибьюторов

    github.com/...​nject/graphs/contributors

    По pinject хорошо видно — было автору в 14 году интересно, был рост. На текущий момент опять основной контрибьютор — есть движение ±

    github.com/...​ector/graphs/contributors
    один автор, ближайший крупный контрибьютор — два коммита.

    Если автор полностью работает на твое приложение, не вопрос — он с детищем разберется. Можно использовать.

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

    Нпр по анзиблу
    github.com/...​sible/graphs/contributors

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

    Приятно, когда твой фреймворк сравнивают с Ansible. Это первый раз, спасибо :)

    Количество даунлоадов ни о чем

    «ни о чем» — неконструктивный аргумент. Dependency Injector скачивают в 10 раз больше, чем pinject.

    Я просто привел пример «активной коммюнити» из пайтона. Вот еще пример — в 2015-2016 был короткий всплеск популярности github.com/...​t/lux/graphs/contributors , отличные идеи, неопределенное число команд стало его использовать , но как только автор потерял интерес, не подхватили. Поэтому количество даунлоадов ни о чем, если основной разработчик теряет интерес, а ядра разработки нет. У того же ansible, ansible-container даже с тремя core разработчиками — был тихо похоронен и deprecated.

    На счет контрибьюторов.

    Создатель pinject не занимается своим фреймворком с 14 года. Я занимаюсь Dependency Injector постоянно. Не всегда много контрибьюторов == хорошая поддержка. Наличие лидера в опенсорс проекте — решающий фактор.

    Я поддерживаю Dependency Injector самостоятельно. Непофикшенных багов в нем нет. Простые баги фикшу за часы. Фичи занимают дни или недели. Спорные фичи могут занять несколько месяцев так как функционал добавляю аккуратно. Если сомневаюсь в необходимости или потенциальной опасности фичи, даю автору временное решение и оставляю ее «на подумать» перед добавлением в фреймворк.

    Если хотите присоединиться, пишите в личку. Помощи буду рад.

    1. Все зливати в один конфіг не завжди зручно.
    2. Навіщо цілий фреймворк для такої речі. Архітектура підбирається під конкретний проект індивідуально і має, як правило, свої особливості.

    Классные вопросы, спасибо. Отвечаю:

    1. Все зливати в один конфіг не завжди зручно.

    Да, правда. Dependency Injector поддерживает работу с несколькими контейнерами. Есть 2 варианта:

    1. Контейнер для каждого слоя: Core, Infrastructure, Gateways, Services, WebApi (Пример)
    2. Контейнер для пакета (контекста): Users, Photos, Billing, Payment (Пример)
    Навіщо цілий фреймворк для такої речі.

    Когда применяешь dependency injection код сборки объектов становится вот таким:

    service = Service(ApiClient(config['api_key'], config['timeout']))
    

    Такой код со временем дублируется и менять структуру становится тяжелее. Dependency Injector помогает описать сборку в одном месте и убрать дублирование:

    service = container.service()
    

    Еще Dependency Injector дает бонус в виде переопределения любого провайдера с помощью метода .override():

    from unittest import mock
    
    
    with container.api_client.override(mock.Mock()):
        service = container.service()
        assert isinstance(service.api_client, mock.Mock)
    

    Это помогает при тестировании. Можно применять для настройки проекта для работы в разных окружениях: подставить стабы для API на дев и стейдж.

    Архітектура підбирається під конкретний проект індивідуально і має, як правило, свої особливості.

    Dependency Injector не привязывает к определенной архитектуре. Он помогает собирать код, который написан по принципу dependency injection. Его можно применять для построения проекта с нуля и для части проекта. Такое обычно происходит при пошаговом рефакторинге.

    Мабуть вартувало відразу привести другий приклад, де видно явну перевагу в спрощенні життя програмісту. Бо мені спочатку здалось, що основну його фічу можна за пів часа реалізувати, що явно простіше ніж розбиратися в чужому коді і додавати залежність, що потім грозить натягуванням сови на глобус.

    Да, нужно было это описать. Я уже пошел и поменял README. Пост трогать уже не буду для исторической сохранности :) Еще раз спасибо за вопросы

    Я не робив такий експеримент, але можете розглянути такий цікавий варіант щоб натягнути ваші секвіси з фасадом-одиночкою на патерн композит. Може вийти цікавий результат у вигдяді більш зручного збирання скелетону майбутнього аплікейшена. Але це тільки ідея, я б спочатку б трішки зменшив розмазаність логіки і сильніше б відокремив абстрактні класи. Хоч і вже виглядає непогано) Але це виключно моя думка.

    Работаю над обновлением доки сейчас. Примерами тоже займусь. Про композит не понял. Если что пишите в личку, соберём что-то вместе.

    Подписаться на комментарии