Making an IOC container in Python

Part 3 The Service Container

Service Container

A service container may be used to store all the services the program needs and provide a way to register each service and how to create them. This article shows a simplest service container and a simplest injector look like.

A simplest service container may look like the following:

class ServiceContainer(IServiceContainer):
    def __init__(self):
        self._type_container = []

    def register_service(self, service: Type):
       self._type_container.append(service)

    def create_service(self, t: Type):
        Service = next(ServiceType for ServiceType in self._type_container if ServiceType == t)
        return Service()

The above code uses a list to store all the services, and when the create_service method gets called, it takes the service from the list and creates the service.

Injector

As we mentioned before, an injector may be used to resolve the dependency a class needs and create an instance of the class.

Making an IOC container in Python

Part 4 Service Providers

With the simplest service container we created in the previous article, now we can look at making the service container provide a way to customise service creation. To achieve this, service providers may be used.

Imagine how a MySqlDBConnection service would look like:

class MySqlConnection:
    def __init__(self, host, port, database):
        ...

We do not want the MySqlConnection service to read the configuration by itself, as this part of code is not relating to what this service should provide.

Typically, we will need to read the host address, the port number, and the database from a configration file to instantiate the above service. We may introduce a service provider class to achieve this.

Service Provider

A provider class for the MySqlDBConnection service should be:

class MySqlConnectionProvider(IServiceProvider):
    def __init__(self, configuration: IConfiguration):
        self._configuration = configuration

    def provide(self):
        return MySqlConnection(
            self._configuration["db_host"], 
            self._configuration["db_port"], 

Type Hinting in Python

In Python, the types of variables may change during the execution of the program. It makes the code more flexable but it also confuses developers and the IDE sometimes, as both of them may need to analyse the code to find out the types of variables.

To solve this issue, Python introduced the Type Hinting feature. It allows developers declare the types of variables like what developers do in static-typed languages. For example:

def add(a: int, b: int):
    return a + b

When calling the above function with some other types of parameters, IDE will be able to tell the developers it is not what the function expects. For example:

sum('a', 'bcd')  # Developers will be warned

It also helps IDEs to provide suggestions. For example:


l = ['a', 'b', 'c', 'd']


def foo(arr):
    for i in arr:
        i.  # type of i is unknown, the IDE is not able to provide any suggestions

# VS

Making an IOC container in Python

Part 2 Resolving Dependency

The most essential part for resolving the dependency is retrieving the dependency declared by the __init__. To achieve this, we may use the inspect module. This article will share how I managed to do this with this module in my past projects.

The inspection module's documentation can be found on this page.

Retrieving the types of parameters

We may use the signature function to retrieve parameters and their types.

def test(a: int): 
    ...

inspect.signature(test) # <Signature (a: int)>

parameters = signature.parameters  # mappingproxy(OrderedDict([('a', <Parameter "a: int">)]))

The signature function returns a Signature object, from which we can access the parameter declaraction including the type hinting of each parameter by accessing it's parameters property.

How we may do it looks like the following

class Test:
    def __init__(self, a: IConfiguration):

Making an IOC container in Python

Part 1 Dependency Injection

Class decoupling

Imagine there is a class UserRepository which reads configuration using ConfigurationManager, processes user-related actions, and stores user data in the database using MySqlConnection.

class UserRepository:
    sqlConnection: MySqlConnection
    configurationManager: ConfigurationManager 
    
    def __init__(self):
        self.sqlConnection = MySqlConnection("localhost", 3306, "username", "password", "utf8")
        self.configurationManager = ConfigurationManager("/app.xml")
    # ...

This class depends on ConfigurationManager and MySqlConnection, which implement services instead of interfaces. Therefore, if we were to switch the database from Mysql to SqlServer, it would cost developers a lot of effort to change the dependency. Also, a class should only care about how its implementation instead of the implementation of other services.

To decouple the above classes, we should make an ISqlConnection interface for MySqlConnection and an IConfigurationManager interface for ConfigurationManager and let both types inherit from the interfaces. Then, we can refactor UserRepository to the following:

class UserRepository:
    sqlConnection: ISqlConnection
    configurationManager: IConfigurationManager 
    
    def __init__(self):
        self.sqlConnection = ServiceFactory.createService(ISqlConnection)
        self.configurationManager = ServiceFactory.createService(IConfigurationManager)