Пользователь
0,0
рейтинг
11 февраля 2014 в 12:53

Разработка → Внедрение зависимостей 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, по-моему, хрупкие, увеличиваю связанность компонентов в приложении и сложны в тестировании. В инжекте, если нужно ограничить область видимости объекта, всегда можно написать собственный провайдер.

Заключение


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

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (38)

  • +1
    А пробовали zope.interface?
    • 0
      Это скорее про моделирование интерфейсов. А вообще zope — это попытка сделать энтерпрайз в питоне. Во всяком случае так было раньше.
      • +1
        Там как раз про DI (читать про adapters). Там нет ничего про энтерпрайз, особенно сейчас и особенно в zope.interface.
  • +5
    Расскажите пожалуйста, для чего собственно эти инжекты нужны в питоне? В статье как-то не увидел практических применений
    • 0
      Скорее, зачем вообще нужно внедрение зависимостей, потому что сама концепция вне языка.

      Смотрите, у вас в приложении много взаимодействующих компонентов. Например, как в статье, у вас есть Mailer, который отвечает за отправку электронной почты. Это может быть какая-то очередь, а может SMTP-клиент. Этот Mailer используется во многих местах: при регистрации пользователя, подтверждении почты, восстановлении пароля, в рекламных рассылках. Т.е. этот компонент используют различные классы.

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

      Самой концепции много лет. Подробнее можно почитать на википедии, хотя информация там довольно запутанная. Нужно ли вообще внедрение зависимостей? Да, это не только мое мнение. Посмотрите на количество фреймворков, в т.ч. и от Гугла: Google Guice, Spring, Google Pinject, Objective-C Typhoon и многие другие.
      • +6
        Вот меня интересовало в рамках Python, т.к. тот же mock делается легко и просто и, как по мне, сильно нагляднее, чем это сделано здесь.
        • –3
          А как объекты узнают про ваш мок? Напишите, пожалуйста, как бы вы переписали код с тестом из самого первого примера с User + Mailer.
          • +3
            # conf.py
            if sys.environ.get('ENV_TYPE') == 'PROD':
                db = ...
                mailer = ...
            else: 
                db = ...
                mailer = ...
            

            # main.py
            import conf
            
            foo(conf.db)
            bar(conf.mailer)
            

            По сути у вас делается то же самое, только запутанней.
            • 0
              Скорее тоже самое, только без двуликого conf.py, который одновременно изящное решение в маленьком проекте и костылик в большом, потому как инициализируется неочевидным способом.
              Идея инжектов (блин! как похож он на zope.interface с его концепцией адаптеров), чтобы сделать код менее связным, не надо заранее за пользователя класса User решать в каком контексте он будет запускать свой код и прописывать ветки conf.py
              • +4
                Конфиг или не конфиг — не важно. Важно, что есть уровень абстракции, единая сущность, на которую ссылаются все заинтересованные объекты. При необходимости поменять настройки, достаточно заменить реализацию, лежащую за уровнем абстракции, и код поменяется везде. Это старая и довольно простая идея, и для неё не нужно придумывать новые имена, а уж тем более изобретать библиотеки.

                В Java костыль в виде DI нужен, чтобы побороть (якобы) статическую типизацию и считывать настройки из конфиг файлов (Spring IoC вообще в каком-нибудь другом контексте используется?). Код на Python не нужно компилировать, поэтому конфиги можно писать прямо в .py файлах, утиная типизация избавляет от необходимости пораждать тонны интерфейсов, ну а динамическая диспетчеризация по хеш-таблицам позволяет в любой момент менять любые сущности вплоть до модулей. Ну и для чего тогда нужна отдельная запутанная процедура инъекции зависимостей?
          • +3
            При конфигурировании можно использовать разные конфиг файлы, как минимум.
            А при моках, эм, стандартно библиотекой mock.
            Не совсем понял, зачем объектам знать, что они замочены?
            • –2
              Чтобы обращаться к моку, а не к обычному классу.
              • +4
                вы меня прям в тупик загнали, зачем вам обращаться к моку и почему ваш код должен знать, мок это или нет?
                Вы всего лишь мокаете, для примера, метод send() и потом проверяете, что его вызывали с теми параметрами, что надо.
                • –2
                  Обращаться к моку нужно чтобы подменить вызов реального объекта (в примере базы или мэйлера) вызовом мока. Чтобы подменить вызов нужен способ подменить вызываемую сущность. Самый простой способ это делать — внедрять эту зависимость извне объекта.
                  • +5
                    Самый простой способ подменить вызов это, извините, подменить вызов:

                    foo.send = mock_send
                    

                    А все эти огороды с Dependency Injection нужны там, где гибкость языка не позволяет простые вещи делать простыми. Извините, если кого обидел.
        • 0
          А еще проще DI реализуется с помощью банальных импортов :)
  • +4
    Почему это внедрение зависимости? Объект сам запрашивает зависимость из контейнера.
    Это не DI.
    • НЛО прилетело и опубликовало эту надпись здесь
  • +3
    Нафига козе боян?

    А если серьезно — в питоне это не нужно, потому как ту отсутствует строгая типизация, и бороться с ней с помощью инъекций бессмысленно. Это только порождает дополнительную сложность, а это не python way, на мой взгляд.

    Как уже указали выше — простые конфигурационные файлы вполне достаточны. По примеру джанги, где в сетингах указываешь различные классы и никаких зависимостей.

    Кроме этого можно пачить классы в рантайме, по примеру гевентов, без проблем!
    • НЛО прилетело и опубликовало эту надпись здесь
      • +3
        Потому что все DI библиотеки пытаются решить проблему инъекции внутри класса. У питона такой проблемы не существует.

        class User(object):
            db = Fake() # db connection
        
            def do_some():
                do.save(True)
        
        .....
        
        User.db = RedisDb('localhost:1234') # explicit injection or 'setattr' can be used for text based config files
        user = User()
        user.do_some() # the redis db is used
        
        


        Поэтому используя парадигмы других языков вы пытаетесь решить несуществующую проблему в питоне. Нравиться DI — используй ее на здоровье.

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

        А вообще хороший ответ по теме на стековерфлове: Why is IoC / DI not common in Python
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Я к тому, что на питоне эта задача достаточна тривиальна (если она действительно нужна) и решается одним методом в 5 строк:

            class Container:
                def __init__(self, system_data):
                    for component_name, component_class, component_args in system_data:
                        if type(component_class) == types.ClassType:
                            args = [self.__dict__[arg] for arg in component_args]
                            self.__dict__[component_name] = component_class(*args)
                        else:
                            self.__dict__[component_name] = component_class
            


            Оригинал: Inversion of Control

            В статье не хватает примеров и не показана использование инъекции внутренних объектов в локальной области (иначе задача вообще тривиальная, и сводиться к использованию глобального конфига) из за этого и возникло непонимание. Т.е. если нужен глобальный инжекшн то конфигов вполне достаточно.
            Так же не показан в общем метод реализации автора, в чем его смысл.
            • НЛО прилетело и опубликовало эту надпись здесь
              • 0
                IoC — принцип, DI — реализация, что не так?
                В примере с контейнером как раз показана DI
                • НЛО прилетело и опубликовало эту надпись здесь
                  • +2
                    Извините, а вы на пайтоне пишите?
                    • НЛО прилетело и опубликовало эту надпись здесь
    • +2
      потому как ту отсутствует строгая типизация

      Это в Питоне-то типизация не строгая?
      бороться с ней с помощью инъекций бессмысленно

      DI это не борьба со строгой типизацией, это, как правило, борьба с высокой связанностью.

      А ваш пример без DI должен выглядеть примерно так:
      class User(object):
          def db_some():
              RedisDb('localhost:1234').save(True)
      

      Патчите
      • +1
        Я не против DI. Я к тому что для этого особых фреймворков в питоне не нужно и все реализуется достаточно тривиально. По поводу глобальной инъекции уже указали на конфиги как самое простое и очевидное решение.

        Типизация как раз и приводит к высокой связанности. На пайтоне можно писать вообще без типов и внешних связей и делать инекцию атрибутов по мере выполнение(пример выше с юзером) — это уже само по себе DI.

        По моему тут просто нужно показать простые и тривиальные методы реализации DI и решения. Тогда было бы более понятно что и для чего.
        • +1
          Типизация со связанностью никак не связана. Связанность показывает число внешних сущностей в широком смысле слова, плюс степень их конкретизации — в узком, от которых зависит рассматриваемая сущность. Ваш User, например, связан с классом Fake, пускай и не жестко, мой с классом RedisDb и жестко. Вот если бы свой класс сделали без инициализации свойства db, то его связанность резко бы упала, всё что требовалось бы — инициализация извне этого свойства, объектом реализующим метод save() (по сути реализующий интерфейс сохранения, в статически типизированных языках это мог быть какой нибудь экземпляр наследника класса AbstractStorage или объект, реализующий интерфейс Persistentable).

          Ну а так, да, согласен, что без тривиальных методов с указанием на их недостатки пост выглядит не совсем понятным.
          • 0
            Fake — класс-фальшивка, так обычно пишут в примерах. Никак не связан.
            Короче в пайтоне проблем с инъекцией нет — вам нужно, делайте хоть к классу, хоть к инстансу. И многие так делают, когда это надо — это python way и в этом нет никаких проблем или сложностей.
            И да, было бы наверно более интересно обсудить эти методы и рецепты.
            • 0
              Ну сначала вы утверждали, что инъекции вообще не нужны в связи с нестрогой типизацией :) Я же пытался донести, что а) нужны независимо от типизации, б) типизация и связанность ортогональны и в) в Питоне типизация строгая (TypeError: unsupported operand type(s) for +: 'int' and 'str')
              • 0
                Не инъекции — а библиотеки и «костыли» которые надо устанавливать и поддерживать. Лучше просто примеры. Строгая динамическая, моя ошибка. Имел ввиду статическая.
  • +4
    За целый день так не не отписался кто-то понимающий что такое DI и почему он в питоне тоже нужен? Эх :(
    • 0
      Ага, не удалась моя попытка для людей что-то новое и хорошее сделать. Мне уже в комментах предлагают даже модули патчить в рантайме и методы классов подменять для тестов. Брр.
      • +7
        Так а чем оно хорошее то? Есть стандартные рабочие решения, опробованные на больших долгоживущих проектах, а вы приходите и говорите, что всё прежнее — фуфло, и на самом деле надо вот так. При этом продвигаете, во-первых, стороннюю библиотеку (которая уже дополнительная зависимость), а во-вторых, нарушает сразу 2 пункта из Python Zen — «явное лучше неявного» и «простое лучше сложного».

        Просто вы зачем-то пытаетесь перенести некоторые концепции из Java в Python. При этом игнорируете тот факт, что эти концепции были разработаны для обхода ограничений конкретного языка, и в другом языке могут просто не иметь смысла (так же, как, например, паттерн Стратегия не имеет в Питоне смысла просто потому что функции и так являются объектами первого рода).
      • +4
        Если что, «чем оно хорошее» — это не риторический вопрос (как и в другом моём комментарии выше). Если вы действительно уверены, что так лучше, то расскажите почему, поделитесь опытом. Никто же не против нового и хорошего. Но ведь надо ещё и знать, зачем оно.

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