Why Dependency Injection?¶
Take a look at the following code.
import os
class ApiClient:
def __init__(self):
self.api_url = os.getenv("API_URL") # a dependency
self.api_key = os.getenv("API_KEY") # a dependency
def get(self, path: str, *, token: str):
return { url: f'{self.api_url}/{path}', status: 'ok', data: [] }
class Service:
def __init__(self):
self._api_client = ApiClient() # a dependency
# do some init
def do_something(self):
res = self._api_client.get('abc')
print("Service doing something")
def some_func(*args, **kwargs) -> None:
service = Service() # a dependency
service.do_something()
print("serivce has done something")
if __name__ == "__main__":
some_func()
This code will run as expected. However:-
-
Testing it will be difficult
For example, to test
Servicewe need a fakeApiClientas we don't what to make real api calls or might not have the credentials to do so. SinceServicecreates it's ownApiClientinstance, it is impossible to safely mock theApiClientfor tests. -
Lacks flexibility and extensibility
It's imppossible to create an additional
ApiClientinstance that uses a differentAPI_URLand/orAPI_KEY.
So what do we do? We should decouple our objects from their dependencies.
That is, objects should not create each other anymore. They should provide a way to inject the dependencies instead.
Here's how.
class ApiClient:
def __init__(self, api_url: str, api_key: str): # we let the caller provide the dependencies
self.api_url, self.api_key = api_url, api_key
class Service:
def __init__(self, api_client: ApiClient): # we let the caller provide the dependency
self._api_client = api_client
def some_func(*args, service: Service, **params): # we let the caller provide the dependency
...
But remember, with freedom comes more responsibility.
The responsibility is left to the "caller" who has to know, assemble and provide the dependencies.
some_func(
*args,
service=Service(
api_client=ApiClient(
api_key=os.getenv("API_KEY"),
api_url=os.getenv("TIMEOUT"),
),
),
**kwargs,
)
some_func() from multiple places.
Duplicating the assembly code with make it harder to change in the future.
With Uzi.¶
Simple DI using uzi's low level API.
from uzi import Injector, Scope, Container
container = Container()
# register the ApiClient
container.singleton(
ApiClient,
api_url=os.getenv("API_URL"), # <-- provide value from env
api_key=os.getenv('API_KEY') # <-- provide value from env
) # <-- make it provide only one instance.
# Register the Service.
container.factory(Service)
# Since we did not specify a value for api_client. `ApiClient` will get injected
# as it matches the type annotation `api_client: ApiClient`
# Create the scope
scope = Scope(container)
# Create an injector
injector = Injector(scope)
# Use the injector to run `some_func`
result = injector.make(some_func, 'xyz', 23, **params) # <-- dependency `Service` is injected automatically