Dependency Injection

In Python

(using Aglyph framework)

Praveen Shirali

Test Architect, RiptideIO

BangPypers MeetUp - December 17th 2016, Bangalore, India.

The OOP way: Part 1

  • A class forms the basic building block
  • Each class has a single responsibility
  • The class exposes an interface

The OOP way: Part 2

  • Then, there's a class with some higher-level behaviour
  • This class depends on instances of other lower-level classes
  • It encapsulates the behavior of lower-level classes.
  • It has its own responsibility, and its own interface.
  • Repeat --> At the highest level, you have your application.

A tree of dependencies -- the real world


Car
 |
 |----\ Exterior
 |       |-- Body
 |       |-- Wheels
 |       |...
 |
 |----\ Interior
 |       |-- Dashboard
 |           |-- Steering Assembly
 |       |...
 |
 |----\ EngineBay
 |       |-- Engine
 |       |...
 |
 |----\ Some other components...
 ...					

A tree of dependencies -- software


SomeOOPApplication
       |
       |----\ Frontend
       |       |-- Frontend Dependency 1
       |       |-- Frontend Dependency 2
       |       |...
       |
       |----\ Server
       |       |-- Server Dependency 1
       |           |-- It's dependency ...
       |       |...
       |
       |----\ Database
       |       |-- DB Dependency 1
       |       |...
       |
       |----\ Some other components...
       ...					

So, lets build an app...


from app.frontend import Frontend
from app.server import Server
from app.database import Database

class RockSolidApplication(object):

	def __init__(self):
		self.frontend = FrontEnd()
		self.server = Server()
		self.database = Database()

	def run(self):
	    ...
    ...

if __name__ == "__main__":
	app = RockSolidApplication()
	app.run()
                    

But, what's wrong with it?


from app.frontend import Frontend
from app.server import Server
from app.database import Database

class RockSolidApplication(object):   <-- Can't reuse application for something else

	def __init__(self):
		self.frontend = FrontEnd()    <-- Dependencies are built from within
		self.server = Server()        <-- Can't introduce other/better deps
		self.database = Database()    <-- Bring your own deps. Hard to share.

	def run(self):
	    ...
    ...

if __name__ == "__main__":
	app = RockSolidApplication()      <-- Rock solid. Will this float on water?
	app.run()
                    

Dependency Injection

The process of building dependencies separately and
injecting/assembling them into a higher-level product

Think -- assembly line (automobiles)

A (slightly) better approach


from app.frontend import Frontend
from app.server import Server
from app.database import Database

class RockSolidApplication(object):

	def __init__(self, frontend, server, database):
		self.frontend = frontend
		self.server = server
		self.backend = database

	def run(self):
	    ...
    ...

if __name__ == "__main__":
	app = RockSolidApplication(
		frontend=FrontEnd(),
		server=Server(),
		database=Database()
	)
	app.run()
                    

A (slightly) better approach -- How?


from app.frontend import PrettierFrontend
from app.server import BetterServer
from app.database import FasterDatabase

class RockSolidApplication(object):

	def __init__(self, frontend, server, database):
		self.frontend = frontend
		self.server = server
		self.backend = database

	def run(self):
	    ...
    ...

if __name__ == "__main__":
	app = RockSolidApplication(
		frontend=PrettierFrontEnd(),    <-- replace with better components
		server=BetterServer(),          <-- or replace them with test doubles
		database=FasterDatabase()       <-- mocks/stubs/etc for testing
	)
	app.run()
                    

A (slightly) better approach -- How?


from app.frontend import PrettierFrontend
from app.server import BetterServer
from app.database import FasterDatabase
from app.base import BaseApplication

class AirborneApplication(BaseApplication):    <-- this can fly!
    ...

class BuoyantApplication(BaseApplication):     <-- this floats on water
    ...

class RockSolidApplication(BaseApplication):   <-- this doesnt
    ...

if __name__ == "__main__":
	app = BuoyantApplication(
		frontend=PrettierFrontEnd(mermaids=True),
		server=BetterServer(food="unlimited", drinks="bottomless"),
		database=FasterDatabase(i_haz_nos=True)
	)
	app.run()
                    

BUT

There is always a but ...

I still have to:

  • Know dependencies before hand and instantiate them.
  • Maintain order. Least dependent go first, and the rest follow.
  • Multiple places to update for one change. Hard to track.
  • Handle, track, log errors. Much pain.

Inversion of Control (IoC)

Empower a 3rd party to do something for you.
Request that 3rd party to assemble what you want.

Think -- Restaurant (with service) = DI using IoC

Example: The south-indian self-service fast-food restaurant situation..


from darshini.kitchen.steamcooker import Idly
from darshini.kitchen.wetgrinder import Chutney
from darshini.kitchen.largebucket import Sambar

from darshini.self_service.counter import Plate, Spoon, Fork, Bowl

from bangalore.citizen import Me
from datetime import datetime

if __name__ == "__main__":

    one_plate_idly = Plate()
    one_plate_idly.add(Spoon(), Fork())
    one_plate_idly.add(Idly(), Idly())
    one_plate_idly.add(Bowl(Sambar()))
    one_plate_idly.add(Bowl(Chutney()))

    me = Me() # singleton
    me.eat(one_plate_idly, at=datetime.now())
                    

So, I walk into the service wing, and..


from darshini.service_wing import get_waiter
from darshini.service_wing import Menu

from bangalore.citizen import Me
from datetime import datetime

if __name__ == "__main__":

    me = Me()               # singleton
    waiter = get_waiter()   # returns a waiter who is available
    menu = Menu()

    me.see(menu.page1)
    idly_plate = me.say(menu.page1.idly_plate, to=waiter)  # blocking call
    me.eat(idly_plate)
    assert me.status == "Happy!"
                    

Deepdive -- Actors

  • Client That's me -- the customer
  • Assembler That's the waiter, chef, etc
  • Context Menu+Recipie book
    • Component Menu<->Recipie mapping
    • Class/Class.factory Recipie
  • Service Name of the item on the menu - Example:`Idly - Plate`

Workflow - Restaurant:

  • I look at the menu. Ask waiter to get me an item on the menu.
  • Waiter goes to kitchen. Chef prepares it. Waiter brings it. I eat it.
  • I didn't have to tell the waiter: 1 plate idly = 2 Idly() + 1 Sambar() + 1 Chutney()
  • All changes make it to the Menu+Recipie book. All customers and staff are on the same page w.r.t changes.

Workflow - Application:

  • A context-file with component-ids mapping to classes is loaded and assembled into a context.
  • Client requests a `service` from the Assembler/Context
  • Assembler/Context/Dispatcher goes through the context, resolves dependencies, inits everything in the correct order.
  • Client doesn't need care about dependencies. Client gets the requested service and uses it.

Python DI+IoC Frameworks

  • Aglyph http://aglyph.readthedocs.io/en/latest/
  • SpringPython http://springpython.webfactional.com/
  • .. probably many more ..

Aglyph Example: XML Context





  
  
  

  
    
      
      
      
    
  


                    

Aglyph Example: Code


from aglyph.context import XMLContext
from aglyph.asssembler import Assembler

if __name__ == "__main__":
	context = XMLContext("app-context.xml")
	assembler = Assembler(context)

	# this will build app.core.Application and inject all 3 dependencies
	app = assembler.assemble("my-app")

                    

Aglyph Support

  • Supports Python 2 and Python 3. Well tested. Excellent Documentation
  • Supports both XML config based contexts as well as python API
  • Supports Prototype (default), Singleton, Borg, Weakref for all classes.
  • Can configure references as well as python native datatypes as args, kwargs.
  • ... and lots of other features.

Advantages

  • A single place from where you can wire your app and its components
  • No need to worry about dependency order during configuration. The framework takes care of it during runtime.
  • Lazy initialization. Dependencies are assembled as required, only when required.
  • Checks against cycling dependencies.
  • Singleton, Borg etc needs not be implemented by a class. Any class can be made to behave like a Singleton etc.
  • ... and lots of other features.

Demo

https://github.com/pshirali/stubby

Some essential reading

  • Dependency Injection by Martin Fowler 2004 http://martinfowler.com/articles/injection.html
  • Python Dependency Injection by Alex Martelli Google, 2008 http://www.aleax.it/yt_pydi.pdf

We are hiring !

Interns and experienced python developers
http://riptideio.com/careers/

jobs@riptideio.com

Q&A, feedback...


Praveen Shirali
praveengshirali@gmail.com

https://pshirali.github.io/dependency_injection/