Dependency Injection in Python

Object oriented design principals are not language specific. While there is variation from language to language on details of implementations, and some techniques are not appropariate to all languages, for the most part, good design is good design.

One principal worth emphasizing in any Object Oriented system is the Single Responsibility Principal. An object should do one thing, and do it well. However, that object will often need another object to complete a portion of its work. In simple cases, one object can control the lifespan of the dependency. As systems get more complex, these relationships grow more complex. In order to make objects reusable throughout a system and possibly even in other systems, the developer refactors the code, not chaning the behavior, but only the structure. One step is to Introduce a parameter to the constructor, in order to extract the construction code from the dependant object. What code now links up this dependency?

One reason that the Singleton Design Pattern is commonly used is that it provides an unmistakble instance of the object to use. Ideally, a Dependency Injection framework would provide the same degree of confidence, but with a more flexible means of providing mechanism for specifying how to create the dependent object. Using a singleton to get an object usually looks like this:

myobject=packagename.Instance

Which, of course, assumes that the instace has already been created. If you can’t be assured of that fact, the code is more likely to look like:

myobject=packagename.Instance()

Where instance is logic along the lines of:

instance=None
def Instance():
     if not instance:
         instance = MyLocalInstanceCreator()
     return instance

As you see here, I’ve split up the construction of the function from the caching of the resultant variable. Remember this technique…

In the Keystone code, we have a business object that encpsulates the Identity operations of the system: managing users, tenants, and roles. There are several implementations of this business object, depending on where the data is stores. We have one instace for SQL, and another for LDAP, as well as several others. To indicate that a given installation uses the SQL back end, the administrator should add the following line into the configuration file:

[identity]
driver = keystone.identity.backends.sql.Identity

The configuration for this Driver is then pulled from the appropriate section. For SQL, the block would look something like this:

[sql]
connection = sqlite:///bla.db
idle_timeout = 200
min_pool_size = 5
max_pool_size = 10
pool_timeout = 200

Whereas, if the Driver is set to the LDAP driver you would have text like this:

[ldap]
url = ldap://localhost
user = dc=Manager,dc=example,dc=com
password = freeipa4all
suffix = cn=example,cn=com

[identity]
driver = keystone.identity.backends.ldap.Identity

The class that implements the identity is now accessble from the configuration object via the api:

identity = CONF.identity.driver

And the SQL identity Driver loads itself. However, if random code in the application wants to access the identity driver, it now has to go looking for the singleton instance that has already constructed the identity object and called on that. SQL dervices from code in Common, as does the LDAP code, but the construction mechanisms are very different.

We can take these two branches and unify them into a single approach with the example above.

For SQL, we first define a function that creates the SQL Identity provider. To keep things simple for now, we will continue to use the constructor. We do the same for LDAP. To determine which to call looks like this:

identity=None
def Identity(CONF):
     if not identity:
         identity = get_class(CONF.idenity.driver)()
     return identity

I’m assuming get_class is something along the lines described here.

We are going to be seeing this pattern over and over again. For example, the identity driver needs the SQL database connection, but other business objects will also need the SQL database connection. The same goes for every external resource in the application. With a minor extension to our code, we can create a pattern of object access that is consistant across all global objects.

When we start up the application, we register the factory functions that create the instances based on the unique key used to look them up. In this case, we will use the same key were using before: the python class name.

def create_identity_driver():
     return get_class(CONF.identity.driver)()

def factories = {keystone.identity.Driver: create_identity_driver}

instances = dict()

def get_instance(classname):
    try:
        instance = instances(classname)
    except:
        instance = factories[classname]()
        instances[classname] = instance
    return instance

This is the simplest case of inversion of control. Instead of creating objects directly, you register their factories, and let the application create them for you on demand. I’ve taken this a little further.

Please check out my full code, to include test cases, on github. This project will continue to evolve.

2 thoughts on “Dependency Injection in Python

  1. Hi

    I’m trying to come up with a keystone extension that provides dependency injections as described in the keystone docs (and other extensions) but when i try to access the dependency’s functions in my controller, i get an “AttributeError: ‘MyExtControllerV3’ object has no attribute ‘myext_api'”

    In my core.py file, i have :-
    @dependency.provider(‘myext_api’)
    class Manager(manager.Manager):

    in controllers.py:-
    @dependency.requires(‘myext_api’)
    class MyExtControllerV3(controller.V3Controller):

    Any pointers?

  2. The object tagged with @dependency.provider(‘myext_api’) needs to be created before it is used/called on. It is a pretty severe shortcoming with out approach. Make sure you component is a named component in the pipe line: it should be trigged by the code in keystone-paste.ini at app start up time. Ping me on IRC if you need more help.

Leave a Reply

Your email address will not be published. Required fields are marked *