Dependency Injection¶
Jivago provides a powerful dependency injection engine as a means of implementing inversion of control.
Basic Usage¶
Classes annotated with @Component
or @Resource
are automatically registered in the built-in service locator. Dependencies are constructor-injected, and require proper typing hints to be used.
from jivago.inject.annotation import Component
from jivago.lang.annotations import Inject
from jivago.wsgi.annotations import Resource
@Component
class CalculatorClass(object):
def do_calculation(self) -> int:
return 4
@Resource("/calculation")
class CalculatedResource(object):
@Inject
def __init__(self, calculator: CalculatorClass):
self.calculator = calculator
Always make sure that the type hint corresponds exactly to the requested object. (i.e. The type annotation could be used directly as a constructor.) An identically-named, but otherwise different class will not work.
Collections¶
Using a collection type hint, all children of a class can be requested. Take a look at the following example :
import random
from typing import List
from jivago.inject.annotation import Component
from jivago.lang.annotations import Override, Inject
class Calculator(object):
def do_calculation(self, input: int) -> int:
raise NotImplementedError
@Component
class ConstantCalculator(Calculator):
@Override
def do_calculation(self, input: int) -> int:
return 5
class RandomCalculator(Calculator):
@Override
def do_calculation(self, input: int) -> int:
return random.randint(0, 100)
@Component
class CalculationService(object):
@Inject
def __init__(self, calculators: List[Calculator]):
self.calculators = calculators
def calculate(self, input: int) -> List[int]:
return [calculator.do_calculation(input) for calculator in self.calculators]
The CalculationService class is injected with a list of all components which implement the Calculator interface.
Scopes¶
By default, all components are re-instantiated when a request is received. However, a @Singleton
annotation is provided for when unicity is important. (e.g. when making a simple persistence mechanism held in memory.)
from typing import List
from jivago.inject.annotation import Component, Singleton
@Component
@Singleton
class InMemoryMessageRepository(object):
def __init__(self):
self.content = []
def save(self, message: str):
self.content.append(message)
def get_messages(self) -> List[str]:
return self.content
A singleton component will be instantiated when it is first requested, and reused for subsequent calls.
Factory Functions¶
When complex scoping is required for a given component, for example when handling a database connection, factory functions can be used to instantiate and cache components using the @Provider
annotation. In this case, the return type hint defines the class to which the function is registered.
from jivago.inject.annotation import Provider, Singleton
class DatabaseConnection(object):
def __init__(self):
# open a connection, etc.
pass
def query_database(self) -> int:
# use the opened connection, etc.
return 5
connection = None
@Provider
def get_database_connection() -> DatabaseConnection:
global connection
if connection is None:
connection = DatabaseConnection()
return connection
@Provider
@Singleton
def get_singleton_bean(my_dependency: Dependency) -> MySingletonBean:
# Will only be called once
return Dependency(...)
The provider function can take any registered component as arguments. By adding @Singleton
to the provider function, it will be lazily instantiated only once, thereby exhibiting the same behaviour as components.
Manual Component Registration¶
When fine-tuned control is necessary, the service locator should be manually configured by extending the Context object. In order to do so, first override either ProductionJivagoContext
or DebugJivagoContext
. This will be your new application context, which should be passed to the JivagoApplication object. The configure_service_locator
is where component registration is done. Use the self.serviceLocator.bind
method to manually register components. Note that Jivago decorators will not be taken into consideration when using manual component registration.
from jivago.config.production_jivago_context import ProductionJivagoContext
from jivago.lang.annotations import Override
class MyApplicationContext(ProductionJivagoContext):
@Override
def configure_service_locator(self):
super().configure_service_locator()
self.serviceLocator.bind(MessageRepository, InMemoryMessageRepository)
The bind(interface, implementation)
methods registers an implementation to its interface. The service locator acts as a dictionary, where the interface is the key, and the implementation is the value. The interface should always be a class.
- The implementation can be any of the following :
- A class
- An instance of a class
- A function which, when called, returns an instance of a class
When a class is given, the default behaviour is applied : a new instance is created whenever the interface is requested. Registering an instance of the class causes it to act as a singleton. Finally, a registered function will be invoked whenever the interface class is requested.
Service Locator Object¶
Similarily, components can be manually requested by directly invoking the ServiceLocator object. A reference to the ServiceLocator object can be obtained either through dependency injection, or statically.
from jivago.config.abstract_context import AbstractContext
from jivago.inject.annotation import Component
from jivago.inject.service_locator import ServiceLocator
from jivago.lang.annotations import Inject
@Component
class Calculator(object):
def do_calculation(self) -> int:
return 5
# ServiceLocator injection
@Component
class CalculationService(object):
@Inject
def __init__(self, service_locator: ServiceLocator):
self.service_locator = service_locator
self.calculator = self.service_locator.get(Calculator)
# Static access to the ServiceLocator object from anywhere
def calculate() -> int:
service_locator = AbstractContext.INSTANCE.service_locator()
calculator = service_locator.get(Calculator)
return calculator.do_calculation()
The service locator has get
and get_all
methods for requesting components.