Documentation
Warning
Current implementation is under development, consider to avoid using it in production until 1.0.0
Overview
md.di component provides dependency injection container designed for automatically class instantiating and centralization the way objects are constructed.
Implementation provides constructor, property, setter types of dependency injection, service factory, container configuration dynamic processing, persistence and etc.
Architecture overview
Terminology:
- service — any class instance initialized by service container
- service definition — instruction how to initialize service (class instance); contains constructor arguments, list of methods calls, etc. part of container configuration
- service definition reference — reference to existing definition, used to define dependency for concrete service definition; represents basic object with identifier attribute.
- service container — dependency injection container implementation instance, contains service instantiating logic
- container configuration — contains set of definitions, definition aliases, and parameters
Note
Due to following single responsibility principle
md.di component does not provides container configuration persistence,
processing, etc out of box. Container (md.di.Container
) assumes works with completed
container configuration.
All related features are decoupled into independent components:
- md.di.configuration
- md.di.autowire
- md.di.persistence
Installation
Usage example
For example, typical code without dependency injection container would look like:
The simplest case of dependency injection container usage is contain of configuration is a service without parameters at all, it looks like:
-
Majority number of services does not require to be retrieved within related
container.get
method call, otherwise service definition visibility option should be passed explicitly -
When code entity loaded, it could be passed as a service identifier as is:
container.get(id_=Greeter)
; otherwise string value could be passed:container.get(id_='Greeter')
Warning
In example above there is no configuration validation, so invalid setup (eg. service circular reference) may cause error at runtime. To enable configuration validation, consider to use to md.di.configuration component.
Service definition
Constructor configuration
To configure service constructor parameter arguments
argument value should be provided:
Constructor injection
#!/usr/bin/env python3
class Greeter:
def __init__(self, name: str = 'Greeter') -> None:
self._name = name
def greet(self, who: str) -> str:
return f'{self._name} greets {who}'
if __name__ == '__main__':
import md.di
# arrange
container_configuration = md.di.Configuration(
definition_map={
'Greeter': md.di.Definition(
class_=Greeter,
public=True, # (1)
),
},
)
container = md.di.Container(configuration=container_configuration)
# act
greeter = container.get(id_='Greeter') # (2)
assert isinstance(greeter, Greeter)
print(greeter.greet('demo example'))
If service requires to be build depending on runtime take a look for md.di.configuration / definition argument processing
Method call
Service may require a call to it, to be finally initialized, for example:
- dependency injection case
- to solve circular reference issue
- to keep constructor signature backward compatibility when new dependency or option added in version (that not requires major version increment)
- service initialization
import md.di
class Greeter:
def __init__(self, name: str = 'Greeter') -> None:
self._name = name
self._greeting = 'Hello'
def set_greeting(self, greeting: str) -> None:
self._greeting = greeting
def greet(self, who: str) -> str:
return f'{self._name} greets {who}: {self._greeting}'
container_configuration = md.di.Configuration(
definition_map={
'Greeter': md.di.Definition(
class_=Greeter,
calls=[
(
'set_greeting', # method name
[], # *args
{'greeting': 'hi'} # **kwargs
),
# or:
# ('set_greeting', ['hi'], {})
])
},
)
Setter injection
Property injection
Implementation of property injection is special kind of setter injection,
realized with magic __setattr__
method call.
import md.di
container_configuration = md.di.Configuration(
definition_map={
'Greeter': md.di.Definition(
class_=Greeter,
calls=[
(
'__setattr__', # method name
[], # *args
{'name': 'property_name', 'value': 'property_value'} # **kwargs
),
# or:
# ('__setattr__', ['property_name', 'property_value'], {})
])
},
)
Service visibility
Every service defined is private by default.
When a service is private, it cannot be accessed directly from
the container using container.get()
.
As a best practice, it should only be created private
services and fetch services using dependency
injection instead of using container.get()
.
Service sharing
Dependency injection container assumes to reuse service instances since it created, e.g. when few services depends on other, since it instance instantiated it will be injected into first services.
This practice requires to design services clean and stateless, and also thread-safe for multithreading applications.
TODO EXAMPLE
By default, each service is shared.
When service is required to be instantiated each time on demand,
then related service definition shared
option should be set to True
, e.g.
TODO USE-CASES
Service factory
- function
- service method invocation
Synthetic service
Synthetic service is a service added in runtime after container instance initialized, such services may have no definition.
Note
Contract psr.container.ContainerInterface
has no set
method,
but only md.di.Container
; in some cases related instance check
may be required, eg.
Argument default value
Definition defines ARGUMENT_DEFAULT_VALUE
value as a sentinel,
and used to explicitly set definition argument value
to default value in related target method signature, for example:
class Greeter:
def __init__(self, name: str = 'Greeter') -> None:
self._name = name
self._greeting = 'Hello'
def set_greeting(self, greeting: str = 'Hey') -> None:
self._greeting = greeting
import md.di
md.di.Configuration(
definition_map={
'Greeter': md.di.Definition(
class_=Greeter,
arguments={
'name': md.di.Definition.ARGUMENT_DEFAULT_VALUE,
},
calls=[
('set_greeting', (), {'greeting', md.di.Definition.ARGUMENT_DEFAULT_VALUE})
]
),
}
)
Explicitly set this value to definition argument value is optional, and mostly used for dynamic processing purposes.
Container omits this parameter on service factory or service call method invocation.
Container configuration
Container configuration represents simple data structure containing maps of parameters, service definition and service definition alias.
Note
Container configuration designed to be passed into container before container could be used, so at that moment configuration is expected to be processed and valid.
Warning
md.di component has no configuration validation out from box, so invalid setup (eg. service circular reference) may cause error at runtime. To enable configuration validation, consider to use to md.di.configuration component.
Container parameters
Service may require to be configured with parameter, which may depends, for example on application environment.
or external resource:
import os
import md.di
import sqlalchemy
container_configuration = md.di.Configuration(
definition_map={
'sqlalchemy.Engine:default': md.di.Definition(
factory=sqlalchemy.create_engine,
arguments={
'url': '{connection.default.dsn}'
}
)
},
parameter_map={
'connection.default.dsn': os.environ.get('CONNECTION_DEFAULT_DSN')
}
)
Service alias
import md.di
container_configuration = md.di.Configuration(
definition_map={
'Greeter': md.di.Definition(class_=Greeter)
},
definition_alias_map={
'Greeter_alias': 'Greeter'
},
)
Default contract implementation
For components following Dependency Inversion Principle, dependency references to an abstraction, not concrete implementation. For such design configuration, alias feature is the solution for such design.
Typical example is usage logger contract, for example:
#!/usr/bin/env python3
import psr.log
import md.log
import md.di
class Example:
def __init__(self, logger: psr.log.LoggerInterface) -> None:
self._logger = logger
def act(self) -> None:
self._logger.debug('...')
if __name__ == '__main__':
# arrange
container_configuration = md.di.Configuration(
definition_map={
'Example': md.di.Definition(class_=Example, public=True),
'md.log.Logger': md.di.Definition(class_=md.log.Logger, arguments=...),
},
definition_alias_map={
'psr.log.LoggerInterface': 'md.log.Logger'
},
)
container = md.di.Container(configuration=container_configuration)
# act
example_service = container.get(id_=Example)
assert isinstance(example_service, Example)
example_service.act()