Dependency Injection Explained with Burgers
Part 1: Fried Burgers
Dependency Injection is often touted as one of the pillars of object-orientated design. But what is it? How do we use it? Can it be explained using burger analogies? Let’s find out.
Suppose I have the class below, called BurgerChef, that uses a frying pan for cooking tasty, but greasy burgers. It has one a constructor, which gets a new frying pan, and it has one method, that uses the frying pan to cook a burger.
public class BurgerChef {private FryingPan theFryingPan;public BurgerChef() {this.theFryingPan = new FryingPan(); // call FryingPan's constructor}public CookedBurger cookBurger(RawBurger burgerToCook) {CookedBurger aCookedBurger = theFryingPan.cookBurger(burgerToCook);return aCookedBurger;}}
This is the FryingPan class:
public class FryingPan {public FryingPan (){ //constructor//get a clean frying pan from the cupboard}public CookedBurger cookBurger(RawBurger aRawBurger) {// Put oil in the pan and heat it up// Fry the burger in the greasy frying pan}}
To use the BurgerChef class we would do:
BurgerChef mike = new BurgerChef();RawBurger aRawBurger = new RawBurger();CookedBurger aCookedBurger = mike.cookBurger(aRawBurger);
(For brevity I have omitted the code for the raw and cooked burgers)
Part 2: Chargrilled Burgers
Now suppose our facilities are getting upgraded and we are getting a chargrill for healthier and less greasy burgers. We need to change our class to use a chargrill instead of a frying pan. Not a big deal in this case, as BurgerChef is only a small class.
However, imagine if our class was very large and used the frying pan in lots of different places, that would be a real pain to change. Before our class gets that big it would be a good idea to stop this becoming a problem in the future. The way to achieve this is called Dependency Injection.
If we change our class to use an abstraction instead of a frying pan, say a….cooking tool, then we can change the cooking tool without having to change our class at all next time.
First, we should create our abstraction using an interface (you could also use an abstract class):
public interface cookingTool {public CookedBurger cookBurger(RawBurger theRawBurger);}
Then we should change our frying pan to implement this interface, to show that it can be used to cook burgers, we can also create the new Chargrill class.
public class FryingPan implements CookingTool {public FryingPan (){ //constructor//get a clean frying pan from the cupboard}@Overridepublic CookedBurger cookBurger(RawBurger aRawBurger) {// Put oil in the pan and heat it up// Fry the burger in the greasy frying pan}}public class Chargrill implements CookingTool {public Chargrill (){ //constructor// Turn on the chargrill and let it heat up}@Overridepublic CookedBurger cookBurger(RawBurger aRawBurger) {// Cook the burger on the hot chargrill}}
Now we have to change our BurgerChef class to use a cookingTool instead of a frying pan:
note: We are going to use constructor-based dependency injection in this example, the cooking tool is decided when we create a new BurgerChef object.
public class BurgerChef {private CookingTool theCookingTool; /* can hold any object that implements our CookingTool interface */public BurgerChef(CookingTool toolToUse) { /* here we set the cooking tool to use when we construct our BurgerChef */this.theCookingTool = toolToUse;}public CookedBurger cookBurger(RawBurger burgerToCook) {CookedBurger aCookedBurger = theCookingTool.cook(burgerToCook);returned aCookedBurger;}}
Now we can create a BurgerChef object, and can pass any object into the constructor that implements the CookingTool interface.
Chargrill theChargrill = new Chargrill(); //this could easily be a frying panBurgerChef mike = new BurgerChef(theChargrill);RawBurger aRawBurger = new RawBurger();CookedBurger aCookedBurger = mike.cookBurger(aRawBurger);
We have basically achieved our goal, we no longer have to change our BurgerChef class to change the cooking tool, but we are now choosing the cooking tool in the calling method. This may sometimes be what we want, but another option is to have “injector” classes whose sole responsibility is to select the type of CookingTool. This adheres to SOLID principles (look them up) and is useful if we know our cooking tool is not going to change on the fly.
Our injector class could be:
public class CookingToolInjector() {public CookingTool getCookingTool() {return new Chargrill();}}
and could be used like so:
CookingToolInjector ourInjector = new CookingToolInjector; /* or make it a static method and you don't need this line */BurgerChef mike = new BurgerChef(ourInjector.getCookingTool());RawBurger aRawBurger = new RawBurger();CookedBurger aCookedBurger = mike.cookBurger(aRawBurger);
Now if we ever upgrade our cooking tool again, to say….. a burger-making robot, then we only have to change our CookingToolInjector class and no other part of our code (unless we want to sack our burger chef). However if our chargrill was second-hand and unreliable, we might want a different solution where we could change our cooking tool during program run-time, I’ll let you consider that, there are many different ways, but setter-based injection could come in handy.
Although I haven’t touched on it here, the main benefit of Dependency Injection is for easy unit testing. It makes it very easy to pass dummy objects into your methods at test-time, so you can test them with reliable results (i.e. use a dummy database).
-Mike Gregory