The final SOLID principle is known as the Dependency Inversion principle. Arguably the most important of the five principles, the Dependency Inversion principle can be thought of as a culmination of the principles preceding it. Systems that abide by the other SOLID principles tend to follow the Dependency Inversion principle as a result. The principle states:
"High-level modules should not depend on low-level modules."
A better way to think about it is:
"Abstractions should not depend upon details. Details should depend upon abstractions."
In a static-typed language like Java, "abstractions" can be implemented and enforced explicitly via interfaces. However, in a dynamic language like Ruby, we depend on duck-typing to describe an object's interface. Even without explicit interfaces in Ruby, the Dependency Inversion principle still holds value! We should still aim to depend on abstractions rather than details.
Let's look at an example! We'll revisit a simple example from my blog post on the Open/Closed Principle.
A Simple String Transformer
Suppose we have a class called Transformer that takes a string and transforms it into a some other object or value. For starters, we'll have it transform JSON strings into Ruby hashes:require 'json' class Transformer def initialize(string) @string = string end def transformed_string JSON.parse(@string) end end Transformer.new('{"foo": "bar"}').transformed_string # { "foo" => "bar" }Now, we'll extend the functionality of our Transformer by allowing it to transform strings into binary:
require 'json' class Transformer def initialize(string) @string = string end def transformed_string(type) if type == :json JSON.parse(@string) elsif type == :binary @string.unpack('B*').first end end end Transformer.new('Hello').transformed_string(:binary) # "0100100001100101011011000110110001101111"Now, our Transformer takes strings and transforms them into one of two different types: a Ruby hash or its binary representation. At this point, we should notice some code-smell! The transformed_string method is very dependent on JSON.parse and String.unpack. These are implementation details that our Transformer shouldn't care about.
Let's apply the Dependency Inversion principle by making Transformer depend on an abstraction rather than coupling to concrete details!
The Transformation Abstraction
The basic functionality of our Transformer class is to transform strings into several different types of objects or values. It does this by utilizing different transformations. This seems like an abstraction we can extract and encapsulate! We'll make Transformer depend on a new abstraction called Transformation:class Transformer def initialize(string) @string = string end def transformed_string(transformation) transformation.transform(string) end end class BinaryTransformation def self.transform(string) string.unpack('B*').first end end Transformer.new('Hello').transformed_string(BinaryTransformation) # "0100100001100101011011000110110001101111" require 'json' class JSONTransformation def self.transform(string) JSON.parse(string) end end Transformer.new('{"foo": "bar"}').transformed_string(JSONTransformation) # { "foo" => "bar" }Rather than having Transformer depend on low-level implementation details (JSON.parse and String.unpack), it now depends on a single method: transform. This single method is what makes up the interface of our Transformation abstraction! Now, we can create as many Transformations as we want without modifying Transformer:
require 'digest' class MD5Transformation def self.transform(string) Digest::MD5.hexdigest string end end Transformer.new('Hello').transformed_string(MD5Transformation) # "8b1a9953c4611296a827abf8c47804d7"
Conclusion
As you can see, the Open/Closed principle is highly correlated with the Dependency Inversion principle! We actually end up following the Open/Closed principle by abiding by the Dependency Inversion principle. In fact, some form of dependency abstraction is often required to abide by all the other SOLID principles. If there's one principle to remember out of all the SOLID principles, it's the Dependency Inversion principle: depend on abstractions, not low-level details!Happy coding!