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