Modular Systems Wear Their Dependencies On The Outside

Just a quick brain dump about dependency inversion, dependency injection and the “Russian dolls” style of object composition that a lot of developers ask me about.
Consider this piece of legacy code for a video rental application:


import requests
from video import Video
class Pricer(object):
def price(self, imdbID):
response = requests.get("http://www.omdbapi.com/?i=" + imdbID + "&apikey=6487ec62")
json = response.json()
title = json["Title"]
rating = float(json["imdbRating"])
price = 3.95
if rating > 7:
price += 1.0
if rating < 4:
price -= 1.0
return Video(imdbID, title, rating, price)

view raw

Pricer.py

hosted with ❤ by GitHub

The pricing logic of our app uses IMDB ratings that it fetches from a web API. If we wanted to unit test this logic, or use a different source of video ratings, we’re stuck because the code to fetch the rating is embedded with the pricing logic.

We can extract the fetching code into its own method in its own class and inject an instance of it in the constructor of Pricer.


from video import Video
class Pricer(object):
def __init__(self, imdb_api):
self.api = imdb_api
def price(self, imdbID):
rating, title = self.api.fetch(imdbID)
price = 3.95
if rating > 7:
price += 1.0
if rating < 4:
price -= 1.0
return Video(imdbID, title, rating, price)

view raw

Pricer.py

hosted with ❤ by GitHub

It’s now possible to substitute a different implementation of ImdbAPI (e.g., a stub, for unit testing) from outside Pricer.
But if we look one level up in our call stack, we see we still have a problem.


from pricer import Pricer
from imdb_api import ImdbAPI
class Rental(object):
def __init__(self, customer, imdbID):
self.customer = customer
self.video = Pricer(ImdbAPI()).price(imdbID)
def __str__(self):
return "Video Rental – customer: " + self.customer \
+ ". Video => title: " + self.video.title \
+ ", price: £" + str(self.video.price)

view raw

Rental.py

hosted with ❤ by GitHub

Our Rental class knows about Pricer and knows about ImdbAPI, so Rental is not unit-testable. We can fix this by injecting Pricer into Rental.


class Rental(object):
def __init__(self, customer, imdbID, pricer):
self.pricer = pricer
self.customer = customer
self.video = self.pricer.price(imdbID)
def __str__(self):
return "Video Rental – customer: " + self.customer \
+ ". Video => title: " + self.video.title \
+ ", price: £" + str(self.video.price)

view raw

Rental.py

hosted with ❤ by GitHub

This is the “Russian dolls” style of composition I mentioned at the beginning. ImdbAPI is injected into Pricer, and Pricer is injected into Rental. So we swap the pricing logic without changing Rental, and we can swap the ratings source without changing Pricer, and every object in the chain only knows about the next object in the chain – and only its interface (method signatures). Notice how the object at the bottom of the call stack is created first.


from imdb_api import ImdbAPI
from pricer import Pricer
from rental import Rental
def main():
customer = sys.argv[1]
imdbID = sys.argv[2]
rental = Rental(customer, imdbID, Pricer(ImdbAPI()))
print(rental)
if __name__ == "__main__": main()

view raw

Program.py

hosted with ❤ by GitHub

Notice now that our highest-level module, Program.py, is “wearing” those dependencies. Program knows which implementations for pricing and fetching movie ratings are being used. Our core internal logic knows none of these details.
This is a natural consequence of dependency inversion and the “Russian dolls” style of composition – effective modular systems wear their dependencies on the outside.
The eagle-eyed among you will have noticed that Pricer has one concrete dependency, on Video.


from video import Video
class Pricer(object):
def __init__(self, imdb_api):
self.api = imdb_api
def price(self, imdbID):
rating, title = self.api.fetch(imdbID)
price = 3.95
if rating > 7:
price += 1.0
if rating < 4:
price -= 1.0
return Video(imdbID, title, rating, price)

view raw

Pricer.py

hosted with ❤ by GitHub

But you should also notice that, while Pricer creates a Video, it doesn’t use the Video. This is very important: objects that reference other objects’s implementations shouldn’t call their methods, and objects that call other objects’ methods shouldn’t reference their implementations.

That’s a more essential form of dependency inversion. Any class that uses a Video‘s methods shouldn’t bind directly to an implementation. So we could stub Pricer to return any object that looks like a Video from the outside. This is only possible if Pricer is swappable.

Author: codemanship

Founder of Codemanship Ltd and code craft coach and trainer

Leave a comment