Dependency injection is one of those techniques that carry around a bad reputation for being overly complicated and hard to understand. In reality, DI(you guessed it, dependency injection) doesn't need any complex framework or fancy library to be useful, you can start using it very easily.
Let's start with an example class Robot, that uses an antenna object for sending information to a remote location.
// In Robot.java
public class Robot {
private String model;
private String serial;
private Antenna antenna;
public Robot(String model, String serial, double antennaLenghtInMeters){
this.model = model;
this.serial = serial;
antenna = new Antenna(antennaLenghtInMeters);
}
public void sendRobotInformation(){
String robotInfo = getRobotInformation();
antenna.sendInformation(robotInfo);
}
private String getRobotInformation(){
String robotInfo = String.format("Model: %s\nSerial: %s", model, serial);
// Any extra steps for gathering/formatting info
return robotInfo;
}
// Other methods
}
// In Antenna.java
public class Antenna {
private int lengthInMeters;
public Antenna(double lengthInMeters){
lengthInMeters = lengthInMeters;
}
public void sendInformation(String information){
// Code to send information using the antenna
System.out.println(information);
}
// Other methods and important things
}
This class has a big issue: it knows way too much about Antenna, creating a dependency towards the class. Any change in any of the following characteristics of Antenna will force Robot to change in response:
- The name of the class
- The arguments for the constructor
- The name of the method for sending information
- The arguments sendInformation requires
Robot class shouldn't know all these things, all it cares about is that Antenna responds to the sendInformation message. So, how can we solve this?
DI to the rescue
We've already seen that Robot depends on Antenna, what if instead of instantiating it on the Robot's constructor we inject it?
// In Robot.Java
public class Robot {
private String model;
private String serial;
private Antenna antenna;
public Robot(String model, String serial, Antenna antenna){
this.model = model;
this.serial = serial;
this.antenna = antenna;
}
// The other methods remain unchanged
// Other methods
}
//Antenna remains unchanged
//In Main
Antenna antenna = new Antenna(7);
Robot robbie = new Robot("awesomebot", "awb-46465", antenna);
robbie.sendRobotInformation();
This code is slightly better, as the responsibility of creating a new Antenna method doesn't belong to Robot, instead, it's injected by an outside source as a dependency.
We can still do better. If you pay attention closely, there is a more subtle problem with the code: implicitly it tells you that Robot is only willing to work with an object of the Antenna class, where in reality, all it needs is an object that responds to the sendInformation message. This limits the flexibility robot objects have for sending information by tying that behavior to a concrete class.
Instead, we can create an abstraction for the sendInformation behavior and let other classes implement it.
// MessageSender.java will contain the interface that represents our abstraction for message sending
public interface MessageSender {
public void sendInformation(String information);
}
// Robot now references the MessageSender type, instead of Antenna
public class Robot {
private String model;
private String serial;
private MessageSender messageSender;
public Robot(String model, String serial, MessageSender messageSender){
this.model = model;
this.serial = serial;
this.messageSender = messageSender;
}
public void sendRobotInformation(){
String robotInfo = getRobotInformation();
messageSender.sendInformation(robotInfo);
}
// Other methods
}
//Antenna now implements MessageSender
public class Antenna implements MessageSender {...}
The advantage is that instead of depending on a concrete class (a volatile entity that is more likely to change), Robot will depend on an abstraction (a much more robust entity with less likelihood of change). Robot no longer cares about the implementation details of Antenna, like the fact it needed a lengthInMeters.
Sending robot data in new ways
We can now send robot information using other options. We can, for example, send messages using a wire that inverts the message. From the point of view of the Robot class, all it cares is that the new class implements the MessageSender interface.
// Inverter.java
public class InverterWire implements MessageSender{
public InverterWire(){
}
public void sendInformation(String information){
// Code for sending information using the antenna
System.out.println(new StringBuilder(information).reverse().toString());
}
// Other methods and important things
}
// In Main
InverterWire wire= new InverterWire();
Robot robbie = new Robot("awesomebot", "awb-46465", wire);
robbie.sendRobotInformation();
This approach gives us much more flexibility, Robot's options for sending messages are now easy to extend.
Oh, get it. Is that the only way?
We just saw the dependency being injected when building a new Robot object, but it's not the only way to achieve DI. We can instead inject the dependency using a mutator method.
public Robot(String model, String serial){
this.model = model;
this.serial = serial;
}
public void setMessageSender(MessageSender messageSender){
this.messageSender = messageSender;
}
See how simple DI is? Now you can go and use it on your own projects, it will help decouple many of those tight dependencies.
DIP: The dependency inversion principle
Dependency Injection is closely related to an important software design principle: the Dependency Inversion Principle (The I of the SOLID principles). The principle tells you that * high-level modules should not depend on low-level modules, both should depend on abstractions*.
Abstractions are much more stable entities than concrete classes, by depending on them, we are honoring the timeless wisdom of depend on things that change less often than you do. Now, we don't need to worry about the volatile nature of a concrete class, we just care about the interface.
Another important advantage is that by depending on abstractions, we stop caring about which class an object belongs to and instead look at what messages(methods) the object can respond to. In the code example above, the Robot class doesn't care about the Antenna anymore, all it cares about is to be given an object that responds to the sendInformation message. This increases the flexibility of the code by letting us use the original class in other contexts.
Realistically, you can't do this all the time, you need to depend on concrete things like the built-in String and Array classes. What happens is that things like String are stable classes you can depend on; you don't expect wild changes on the behavior of such class. The DIP is a way of protecting yourself against unstable classes through development.
What about dynamically typed languages?
Representing abstractions in dynamically typed languages like Ruby or Python works a bit different. You don't have access to an Interface construct like in Java or C#. Instead, you implement that public interface into classes by defining common methods and implementing them. We can rewrite the example above in ruby:
# In robot.rb
class Robot
attr_accessor :model, :serial, :message_sender
def initialize(model, serial, message_sender)
@model = model
@serial = serial
@message_sender = message_sender
end
def send_robot_information
robot_info = get_robot_information();
message_sender.send_information(robot_info)
end
# Other important methods
private
def get_robot_information
"Model: #{model} \nSerial: #{serial}"
end
end
# In antenna.rb
class Antenna
attr_accessor :length_in_meters
def initialize(length_in_meters)
@length_in_meters = length_in_meters
end
def send_information(information)
# code for sending information using the antenna
puts information
end
# Other methods and important things
end
# In main.rb
require_relative 'robot'
require_relative 'antenna'
antenna = Antenna.new(7)
robbie = Robot.new("awesomebot", "46465", antenna)
robbie.send_robot_information();
Robot just needs an object that responds to send_information. As you may notice, the dynamically typed approach can't trust a compiler to ensure the types are correct, that's why you need to document your interfaces with tests and traditional documentation.
What about DI frameworks?
There are different frameworks out there for managing dependency injection. What most of them do is using a markup language (XML for example) where you specify the structure of your application. When you run your application, the framework will read the markup and instantiate+inject all the necessary objects.
Part of the disdain some sectors of the industry feel against DI comes from bad experiences with these seemingly complicated frameworks. Whereas I believe DI can be used on most projects without the need of such frameworks, big projects can benefit a lot from putting the structure in markup and quickly composing different versions of the application. DI frameworks are a powerful tool, but they are not the only way of using DI, you just learned how easy it is to implement, so go and make good use of it.
What to do next:
- Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
- You can find the code samples for this article here
- Read Martin Fowler's awesome article on DI and inversion of control:.
- You can find more information about this topic on chapter 11 of Clean Architecture. This and other very helpful books are in the recommended reading list.
- Send me an email with questions, comments or suggestions (it's in the About Me page)