In this series we are looking at the SOLID design principles. In this post, we’ll cover the last principle in the acronym: the Dependency Inversion principle (DIP).
If you missed the previous posts in this sequence, you can find them here:
- SOLID Design Principles Part 1 – Single Responsibility Principle
- SOLID Design Principles Part 2 – Open-Closed Principle
- SOLID Design Principles Part 3 – Liskov Substitution Principle
- SOLID Design Principles Part 4 – Interface Segregation
The Dependency Inversion Principle
The principle is usually stated as follows:
“Details should depend upon abstractions. Abstractions should not depend upon details”.
Robert C. Martin’s definition of the Dependency Inversion principle consists of two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
The idea behind this principle is very simple: high-level modules (which provide complex logic) should be easily reusable without being affected by changes in low-level modules (which provide utility features).
To achieve this, we introduce an additional abstraction (usually a well-defined interface) that decouples the high-level and low-level modules from each other. Both the high-level and the low-level modules depend on this interface. We end up with two dependencies:
- the high-level module depends on the abstraction (interface), and
- the low-level modules depend on the same abstraction (interface).
This interface abstraction removes the dependencies between the higher-level and lower-level software components.
This sounds more complex than it actually is. If we consistently apply both the Liskov Substitution principle and the Open-Closed principle to our code, we will automatically follow the Dependency Inversion principle. The Open-Closed principle says that a software component should be open for extension, but closed for modification. We can easily do this by introducing a high level interface (which is itself closed for modification). We can then create different class implementations of that interface. If our classes follow the Liskov Substitution principle, then we can replace them with other implementations of the same interface without breaking our application.
Example
XStream is a simple, easy to use Java library for serializing Java objects to XML or JSON and vice versa.
XStream uses the Dependency Inversion principle by creating a HierarchicalStreamDriver
interface that provides a high level abstraction to model a stream reader and writer. XStream provides a number of concrete driver classes. Each concrete driver class implements this HierarchicalStreamDriver
interface. The interface decouples the main XStream
class from the actual driver class being used to read and write the stream.
Both the high-level module (the XStream
class) and the low-level modules (the concrete driver classes) depend on the same abstraction (the HierarchicalStreamDriver
interface). They can be changed independently of each other without breaking the application, as long as they conform to the same interface.
Here’s a simple example serializing a Person
class to both XML and JSON. The XStream
class has a toXML()
method that serializes a Java object to XML. Depending on the driver used, the toXML()
method will either serialize to XML or to JSON.
import com.thoughtworks.xstream.XStream; // DomDriver reads from *and* writes to XML import com.thoughtworks.xstream.io.xml.DomDriver; // JettisonMappedXmlDriver.class writes to *and* reads from JSON import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver; // JsonHierarchicalStreamDriver *only* serializes/writes to JSON import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver; import java.io.*; import java.util.*; public class XStreamTest { public static void main (String args[]) throws IOException { // the heart of it... XStream xstream; // create an Address object Address address = new Address("42", "Yellow Brick road", "Emerald City", "Land of Oz"); // create a Person object Person p1 = new Person("Dorothy Gale", 17, Gender.FEMALE, address); // writing to XML xstream = new XStream(new DomDriver()); String xml = xstream.toXML(p1); System.out.println("\nUsing the DomDriver:\n" + xml); // writing to JSON xstream = new XStream(new JettisonMappedXmlDriver()); String json1 = xstream.toXML(p1); System.out.println("\nUsing the JettisonMappedXmlDriver:\n" + json1); // writing to JSON xstream = new XStream(new JsonHierarchicalStreamDriver()); String json2 = xstream.toXML(p1); System.out.println("\nUsing the JsonHierarchicalStreamDriver:\n" + json2); // deserializing from XML; also works from JSON Person p2 = (Person) xstream.fromXML(xml); System.out.println("\ntoString():\n" + p2); } } // end of class
This is a great illustration of both the Dependency Inversion principle and the Liskov Substitution principle at work!
This is the end of the series on the SOLID design principle. Tune in next week, same time, same channel, for more exciting Java tips!
As always, please share your comments and questions. If you missed it, you can find the previous articles in this series on the blog as well.