Dependency Inversion Principle (DIP) from SOLID
What is the Dependency Inversion Principle (DIP)? What is it about? What does it looks like in Ruby?
SOLID is one of the principles I’ve heard many times but haven’t read more about yet. I usually wait until I encounter the theory manifest in code before I read more about a principle. Otherwise, the information I get from reading quickly exits my brain as they have no real examples to relate to. And the opportunity finally is here!
Last week I was writing a temperature converter that outputs specific text based on user input from IO. The code works but then I realised I didn’t know how to test it with rspec, as it involves simulating the behavior of getting user input from the IO stream. And I found a solution from this post about testing Ruby methods that involve puts or gets. It uses dependency injection to separate the usage of the method (getting user input) from the creation of the object (the IO Stream).
Interesting, but wait, I still don’t grasp fully what dependency injection is. Looks like it’s a technique to make a class independent of its dependencies, and it’s a way to help you follow the Dependency Inversion Principle from SOLID.
Ok, so what is the Dependency Inversion Principle?
Depend upon Abstractions. Do not depend upon concretions.
Following DIP, high level objects should not depend on lower level implementation. It is better if your code only depends on something that can respond to a method, instead of mandating it to be dependent on a specific class.
So what does it look in Ruby? The kind of dependency DIP concerns usually happens when a higher level class (abstraction) uses a method from a lower level class (concretion), for example:
class Abstraction
def initialize
...
end
def do_thing
concretion = Concretion.new
concretion.do_somthing
end
end
Here Abstraction#do_thing
depends on the creation of Concretion specifically.
But what is the problem?
Joining the dots from other OOP resources I’ve read, because abstractions by nature are more general and therefore less likely to change than concretion (Sandi Matez POODR).
Instead, if we inject the dependency in the initialisation:
class Abstraction
def initialize(concretion)
...
@concretion = concretion
end
def do_thing
@concretion.do_somthing
end
end
Our Abstraction#do_thing
no longer depends on the Concretion class specifically, and it now only needs something that can do_something
!
Back to the example that leads me to read about DIP in the first place
So instead of relying on actual user input, I can test my method by simply creating an object that also provides a #gets
method to mimic the user input. For example, using a StringIO
instance:
RSpec.describe Converter do
describe "#run_prompts" do
it "output correct prompts base on user input" do
output = run_prompts_with_input(:F)
expect(output).to eq...
end
end
private
def run_prompts_with_input(*user_input)
input = StringIO.new(user_input.join("\n"))
output = StringIO.new
converter = Converter.new(input: input, output: output)
converter.run_prompts
output.string
end
end
A StringIO
instance works perfectly here because it provides both #gets
and #puts
. These allow us to set up the test condition, the user input, and test against the output. This is exactly the same as suggested in testing Ruby methods that involve puts or gets. But now after reading more about the principle, I felt that I’m not just copying the code and have learned something!
To cement the learning by seeing the code: in my temperature converter example instead of
def initialize
...
end
def run_prompts
user_input = gets
puts "Select the temperature unit: " + user_input
end
I now have
def initialize(input: $stdin, output: $stdout)
...
@input = input
@output = output
end
def run_prompts
user_input = @input.gets
@output.puts "Select the temperature unit: " + user_input
end
Learning conclusion
I love concluding my learning by forming questions I can use next time when I write code. For DIP, perhaps next time when I see an abstract class using a concrete class method, I will ask if it is the dependency is in the right direction (“Depend upon Abstractions”), if not, I will question if the dependency is being handled so it’s independent!
Note: (For those who are also wondering about what makes a class an abstraction vs concretion (I did!): whichever class represents a more general feature of a group (or categories) of object is more abstract.)
I also like to conclude my learning with an analogy of something I already knew. So here it is:
Imagine you are building a house, you don’t want to limit yourself to only using steel scaffolds for support, you’re actually looking for whatever can support the building. For example, in Hong Kong, bamboo works too. In this analogy, House is the more abstract class, and the type of scaffold is a concrete class.
So in code instead of
class House
def initialize(scaffold)
...
@scaffold = scaffold
end
def do_thing
scaffold = Scaffold.new
scaffold.support
end
end
it will be better to use
class House
def initialize(scaffold)
...
@scaffold = scaffold
end
def do_thing
@scaffold.support
end
end
That’s my learning write up on the Dependency Inversion Principle (DIP) from SOLID. Hope you find it useful.
See you next time 👋
Julia