Внедрение зависимостей the Python way

от автора

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

Python-inject — это небольшая библиотека для внедрения зависимостей в Питоне. Третья версия написана в unix-стиле, т.е. она прекрасно выполняет только одну фукнцию и не пытается быть всем. В отличие от уже упомянутых Спринга и Джуса Инжект не ворует конструкторы классов у разработчиков, не навязывает разработчикам необходимость писать приложение в каком-то определенном стиле и не пытается управлять всем графом объектов приложения.

Инжект практически не требует конфигурации (об этом подробнее подкатом) и очень прост в использовании.

Например в тестах

# Возможные зависимости class Db(object): pass class Mailer(object): pass  # Внедряем зависимости в класс пользователя class User(object):     db = inject.attr(Db)     mailer = inject.attr(Mailer)          def __init__(self, name):         self.name = name          def register(self):         self.db.save(self)         self.mailer.send_welcome_email(self.name)    # Используем в тестах inmemory базу данных и моки. class TestUser(unittest.TestCase):     def setUp(self):         inject.clear_and_configure(lambda binder: binder \             .bind(Db, InMemoryDb()) \             .bind(Mailer, Mock()))                  self.mailer = inject.instance(Mailer)          def test_register__should_send_welcome_email(self):         # Пример теста.         user = User('John Doe')                  # Регистрируем нового пользователя.         user.register()                  # Должно отправиться письмо с приветствием.         self.mailer.send_welcome_email..assert_called_with('John Doe') 

Использование

Лучше всего поставить с PyPI, хотя можно и скачать архив с сайта проекта:

[sudo] pip install inject 

В приложении:

# Импортируем единственный модуль. import inject  # Описываем опциональную конфигурацию def my_config(binder):     binder.install(my_config2)  # Импортируем другую конфигурацию     binder.bind(Db, RedisDb('localhost:1234'))     binder.bind_to_provider(CurrentUser, get_current_user)   # Создаем инжектор. inject.configure(my_config)  # Внедряем зависимости с помощью inject.instance и inject.attr class User(object):     db = inject.attr(Db)      @classmethod     def load(cls, id):         return cls.db.load('user', id)      def __init__(self, id):         self.id = id      def save(self):         self.db.save('user', self)  def foo(bar):     cache = inject.instance(Cache)     cache.save('bar', bar)  # Создаем нового пользователя и сохраняем  # во внедренную базу данных. user = User(10) user.save() 

Типы байндингов

Конфигурация инжектора описывается с помощью байндингов. Байндинги отвечают за инициализацию
зависимостей. Существует четыре типа:

  1. Instance bindings, которые всегда возвращают один и тот же объект:
    redis = RedisCache(address='localhost:1234') def config(binder):     binder.bind(Cache, redis) 

  2. Constructor bindings, которые создают синглтон при первом обращении:
    def config(binder):     # Creates a redis cache singleton on first injection.     binder.bind_to_constructor(Cache, lambda: RedisCache(address='localhost:1234')) 

  3. Provider bindings, которые вызываются при каждом внедрении зависимостей:
    def get_my_thread_local_cache():     # Custom code here     pass  def config(binder):     # Executes the provider on each injection.     binder.bind_to_provider(Cache, get_my_thread_local_cache) 

  4. Runtime bindings, которые автоматически создают синглтоны классов, если в конфигурации для этих классов нет явных байндингов. Runtime bindings сильно сокращают размер конфигурации. Например, в коде ниже только класс Config имеет явную конфигурацию:
    class Config(object): pass  class Cache(object):     config = inject.attr(Config)  class Db(object):     config = inject.attr(Config)  class User(object):     cache = inject.attr(Cache)     db = inject.attr(Db)      @classmethod     def load(cls, user_id):         return cls.cache.load('users', user_id) or cls.db.load('users', user_id)  inject.configure(lambda binder: binder.bind(Config, load_config_file())) user = User.load(10) 

Почему нет областей видимости (scopes?)

За много лет использования Спринга и Джуса в Джава-приложениях я так и не полюбил их области видимости (саму концепцию). Инжект по умолчанию создает все объекты как синглтоны. Ему не требуется prototype scope/NO_SCOPE, потому что он позволяет использовать нормальные конструкторы классов, в отличие от Спринга и Джуса, в которых все объекты должны быть инициализированны в рамках контекста/инжектора.

Другие области видимости, например, request scope или session scope, по-моему, хрупкие, увеличиваю связанность компонентов в приложении и сложны в тестировании. В инжекте, если нужно ограничить область видимости объекта, всегда можно написать собственный провайдер.

Заключение

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

ссылка на оригинал статьи http://habrahabr.ru/post/212217/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *