Библиотека представляет реализацию IoC контейнера.
Оглавление:
При написании приложение со слабосвязанной архитектурой, приятным, но необязательным элементом является DI контейнер
Данная библиотека реализует контейнер для библиотек семейства пакетов classic. Она призвана ускорить работу над приложением упрощая запуск приложения и разрешение зависимостей.
Для понимания зачем это нужен DI и в каких случаях он применим, рекомендуется к прочтению книга:
"Внедрение зависимостей на платформе .NET" Авторы: Вильчинский Н., Симан Марк
Пример простого приложения:
from abc import ABC, abstractmethod
class InterfaceRepo(ABC):
@abstractmethod
def get_by_id(self): ...
class Accounts(InterfaceRepo):
def get_by_id(self):
print('Обращение к хранилищу данных')
class Account:
# Dependency Injection
def __init__(self, repo: InterfaceRepo):
self.repo = repo
# Ручное разрешение зависимостей
impl = Accounts()
service = Account(repo=impl)
# Разрешение зависимостей через контейнер
from classic.container import container
container.register(Accounts, Account)
container.build(Account)
При малых объемах кода ручное внедрение зависимостей выглядит достаточно лаконично. Сборка же через контейнер на оборот, выглядит громоздко. При увеличении объемов кода, разница будет в пользу контейнера.
При возникновении ошибки бывает сложно понять, что в контейнере пошло не так.
Во время разработки рекомендуется устанавливать пакет в debug
варианте:
pip install classic-container[debug]
Это подтянет пакет traceback_with_variables
:
https://pypi.org/project/traceback-with-variables/
Для использования вовремя отладки нужно импортировать в композите с ошибкой.
Пример:
from traceback_with_variables import activate_by_import
Пример трейса:
File "home/classic/container/builder.py", line 105, in build
instance = factory(**factory_kwargs)
self = <classic.container.builder.Builder object at 0x7fb37c9a68f0>
target = <class '__main__.Interface'>
cached = None
target_settings = <container.Settings(scope=SINGLETON)>
target_settings_layer = <classic.container.builder.Builder object at 0x7fb37c9a68f0>
factory = <class '__main__.ErrorImplementation'>
factory_settings = <container.Settings(scope=SINGLETON, init={'some_str': [1, 2, 3]})>
factory_kwargs = {'some_str': [1, 2, 3]}
signature = <Signature (some_str: str)>
parameter = <Parameter "some_str: str">
В трейслоге вас интересует:
-
target
запрошенный класс -
target_settings
настройки запрошенного класса -
factory
фабрика построения класса -
factory_settings
настройки фабрики -
factory_kwargs
подаваемые параметры фабрики -
signature
сигнатура фабрики https://docs.python.org/3/library/inspect.html#introspecting-callables-with-the-signature-object -
parameter
последний параметр опрошенный в цикле, для которого был вызванbuild
Предоставляет четыре метода - register
, add_settings
, reset
и resolve
.
-
register
нужен для регистрации классов, интерфейсов, функций и даже модулей. -
add_settings
добавляет или обновляет настройки контейнера. Ключом является класс, значение - настройки. -
reset
удаляет добавленные настройки контейнера и ссылки на инстансы уже созданных классов -
resolve
принимает какой-либо класс (интерфейс), и возвращает инстанс указанного интерфейс с разрешенными зависимостями.
Ради удобства был сделан дефолтный контейнер, чтобы не инстанцировать каждый раз в ручную. Конечно, вы можете создавать свои инстансы контейнера, когда вам нужно.
# Импортирование дефолтного контейнера
from classic.container import container
# Самостоятельное создание контейнера
from classic.container import Container
manual_container = Container()
В примере выше модуль container
предоставляет возможность использовать
методы класса Container
.
Дальше по каждому методу подробней:
При обращении принимает список компонентов. Определяет тип, заносит в реестр, каждому типу сопоставляет список фабрик, способных построить указанный тип.
Существует только один способ зарегистрировать компоненты:
import os
from classic.container import container
def some_factory() -> SomeClass:
pass
container.register(os, some_factory, Composition)
Элементами списка могут быть: абстрактные классы, классы, фабрики (функции, возвращающие один инстанс любого класса) и модули.
- Абстрактные классы регистрируется в реестре только как ключи, без фабрик.
- Нормальные классы регистрируются как ключ и соответсвующая ему фабрика - конструктор самого класса.
- Фабрики регистрируются сложнее, ключом будет являться результат из аннотации функции, а значением сама фабрика. Пример:
def some_factory() -> SomeClass:
# будет зарегистрировано как SomeClass: [some_factory]
pass
- Модули не регистрируются напрямую. Регистратор рекурсивно обходит указанный модуль и все его дочерние модули, и регистрирует в реестре все классы и фабрики из каждого модуля.
import os
# будут зарегистрированы os и os.path но не sys
container.register(os)
Метод register
у контейнера является классом Registrator
, реализация
контейнера не подразумевает прямого оперирования объектом из вне.
Добавляет или обновляет настройки контейнера. Ключом является класс,
значение - настройки.
В роле настроек выступает объект класса Settings
:
-
init
используется для передачи простых объектов (чисел, строк). -
factory
описывает способ создания объекта (фабрика, класс, абстрактный класс). -
scope
регулирует жизненны цикл объекта, возможные значения указанны в константах текущего пакета: SINGLETON, TRANSIENT -
instance
создания настроек с готовым объектом при разрешении зависимостей
from abc import ABC, abstractmethod
from classic.container import container
class Interface(ABC):
@abstractmethod
def method(self): ...
class Implementation(Interface):
def method(self):
return 1
class Composition:
def __init__(self, impl: Interface):
self.impl = impl
class NextLevelComposition:
def __init__(self, obj: Composition):
self.obj = obj
def composition_factory(obj: Interface) -> Composition:
return Composition(obj)
container.register(
composition_factory, Implementation, Composition, NextLevelComposition
)
container.add_settings({
Interface: container.factory(Implementation),
Composition: container.factory(composition_factory)
})
resolved = container.build(NextLevelComposition)
Подробное описание в классе Settings
Удаляет добавленные настройки контейнера и ссылки на инстансы уже созданных классов. Подразумевается использование в тестировании.
from dataclasses import dataclass
from classic.container import container
class SomeCls:
pass
@dataclass
class AnotherCls:
some: SomeCls
result_1 = container.build(AnotherCls)
container.reset()
result_2 = container.build(AnotherCls)
result_1 is not result_2
Разрешает зависимости для указанной реализации, создает и возвращает инстанс класса.
Рекурсивно обходит дерево зависимостей, начиная с указанного класса. На каждый шаг рекурсии для указанного класса ищется фабрика в реестре. Далее для найденной фабрики собираются аргументы, чтобы вызвать фабрику и построить объект. При этом:
- пропускаются аргументы простых типов, аргументы без аннотаций и функции;
- подставляются значения из init для аргументов, указанных в этом же init;
- для аргументов, проаннотированных классами, повторяется рекурсия.
В процессе разрешения могут возникать ситуации, когда:
- для интерфейса (абстрактного класса) не нашлось реализации;
- для класса нашлось больше 1 фабрики и ни одна не указана в настройках для этого класса;
- фабрика для аргумента вернула None и для аргумента не указан значение по умолчанию;
- при вызове фабрики не был указан обязательный аргумент. Во всех этих случаях контейнер выкидывает ResolutionError.
Все ошибки состоят из двух частей. Первая часть уникальна для ошибки и объясняет причину, во второй части описано, что и в каком порядке пытался построить контейнер. Она состоит из строк, по три элемента в каждой:
- Target: полное имя класса (some.module.SomeClass);
- Factory: полное имя фабрики (another.module.SomeFactory);
- Arg: имя аргумента фабрики.
Пример:
from abc import ABC, abstractmethod
from classic.container import container
class Interface(ABC):
@abstractmethod
def method(self): ...
class Implementation(Interface):
def __init__(self):
raise NotImplemented
def method(self):
return 1
class Composition:
def __init__(self, impl: Interface):
self.impl = impl
class SomeClass:
def __init__(self, obj: Composition):
self.obj = obj
container.register(Interface, Implementation, SomeClass, Composition)
container.build(SomeClass)
classic.container.exceptions.ResolutionError: Class \
<class 'example.Interface'> do not have registered implementations.
Resolve chain:
Target: app.SomeClass, Factory: app.SomeClass, Arg: obj
Target: app.Composition, Factory: app.Composition, Arg: impl
Target: app.Interface, Factory: app.Implementation, Arg: -
Метод resolve
у контейнера является классом Resolver
, реализация
контейнера не подразумевает прямого оперирования объектом из вне.
Класс хранит настройки resolv-а контейнера.
Используется для хранения способа создания объекта или самого объекта при разрешении зависимостей.
Позволяет установить значения аргументов для фабрики при построении объекта. Самое частое использование - передача простых объектов (чисел, строк).
from classic.container import Container, Settings, init
class SomeClass:
def __init__(self, some_value: int):
# Для int будет неудобно указывать фабрику,
# так как много у каких классов может быть параметр типа int
# (справедливо для любого простого типа), поэтому
# библиотека оставляет возможность
# указать параметр через init
self.some_value = some_value
container = Container()
container.register(SomeClass)
# Длинный способ через конструктор
container.add_settings({
SomeClass: Settings(init=dict(some_value=2))
})
# Вызов "цепочкой"
container.add_settings({
SomeClass: Settings().init(some_value=2)
})
# А можно через алиас
container.add_settings({SomeClass: init(some_value=2)})
Позволяет явно передать способ создания компонента системы. Значением может являться любой вызываемый объект возвращающий инстанс любого объекта, и имеющий аннотацию типов.
from abc import ABC, abstractmethod
from classic.container import Container, Settings, factory
class Interface(ABC):
@abstractmethod
def method(self): ...
class Implementation(Interface):
def method(self):
return 1
class SomeClass:
def __init__(self, impl: Interface):
self.impl = impl
def composition_factory(obj: Interface) -> SomeClass:
return SomeClass(obj)
container = Container()
container.register(Implementation, SomeClass, composition_factory)
# Длинный способ через конструктор
container.add_settings({
SomeClass: Settings(factory=composition_factory)
})
# Вызов "цепочкой"
container.add_settings({
SomeClass: Settings().factory(factory=composition_factory)
})
# А можно через алиас
container.add_settings({SomeClass: factory(composition_factory)})
Данная настройка регулирует жизненны цикл объекта, который может быть SINGLETON и TRANSIENT.
При значении SINGLETON контейнер создаст объект только один раз, все последующие запросы будут использовать тот же самый объект. Является значением по умолчанию.
При TRANSIENT контейнер будет создавать новый объект при каждом resolve.
Для каждого класса настройка scope добавляется отдельно!
from abc import ABC, abstractmethod
from classic.container import Container, Settings, TRANSIENT, scope
class Interface(ABC):
@abstractmethod
def method(self): ...
class Implementation(Interface):
def method(self):
return 1
container = Container()
container.register(Interface, Implementation)
# Длинный способ через конструктор
container.add_settings({Implementation: Settings(scope=TRANSIENT)})
# Вызов "цепочкой"
container.add_settings({
Implementation: Settings().scope(name=TRANSIENT)
})
# А можно через алиас
container.add_settings({Implementation: scope(TRANSIENT)})
Настройка позволяет подать готовый инстанс класса.
Подразумевается основное использование при потребности подачи в разные классы готовых объектов, но настроенных по-разному.
Класс сделан для удобства, тоже самое можно сделать через фабрики.
from abc import ABC, abstractmethod
from classic.container import Container, Settings, instance
class Interface(ABC):
some_value: int
@abstractmethod
def method(self): ...
class Implementation(Interface):
def __init__(self, some_value):
self.some_value = some_value
def method(self):
return 1
class SomeClass:
def __init__(self, impl: Interface):
self.impl = impl
container = Container()
container.register(
Interface, Implementation, SomeClass,
)
impl = Implementation(1)
# Длинный способ через конструктор
container.add_settings({
SomeClass: Settings(instance=impl)
})
# Вызов "цепочкой"
container.add_settings({
SomeClass: Settings().instance(instance=impl)
})
# А можно через алиас
container.add_settings({SomeClass: instance(impl)})