Inverting Dependencies

04 Nov 2014

The “D” in the SOLID principles stands for Dependency Inversion (not Injection). I recently stumbled on such a good example of this principle being applied that I had to blog about it.

The dependency inversion principle

The principle states the following (quote from Wikipedia):

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.

B. Abstractions should not depend on details. Details should depend on abstractions.

The example in this article is about item B. Let’s jump straight into the example and everything will be clearer.

The direct dependency

This example came up when I was giving a shot at a open issue in the awesome Ruby Object Mapper project.

ROM has the advantage of supporting several different databases and it does so through adapters. If, for instance, my application needs to store data in MongoDB, I’d use ROM with the Mongo adapter.

ROM also has an Adapter superclass. This class defines the standard interface for adapters and it’s responsible for initialising the database-specific implementations. It does so through the setup method. Here’s what this method used to look like:

class Adapter
  ...
  def self.setup(uri_string)
    uri = Addressable::URI.parse(uri_string)
    adapter =
        case uri.scheme
        when 'sqlite', 'jdbc' then Adapter::Sequel
        when 'memory' then Adapter::Memory
        when 'mongo' then Adapter::Mongo
        else
          raise ArgumentError, "#{uri_string.inspect} uri is not supported"
        end

    adapter.new(uri)
  end
  ...
end

The interesting bit for the case in point is the case statement. From a dependency inversion perspective, there are two issues with this code:

Piotr Solnica, ROM’s core developer, cleverly suggested inverting the dependency, and so I did.

The inverted dependency

Piotr’s idea was to remove the case statement and add a register method to the Adapter class. Each implementation would call Adapter.register(self) when loaded. The adapter classes would also have a schemes method that returns a list of schemes supported by the adapter.

With this concept implemented, Adapter could look up and initialise the correct implementation based on the scheme. Here’s what the relevant part of the code looks like:

# adapter.rb

class Adapter
  @adapters = []

  def self.setup(uri_string)
    uri = Addressable::URI.parse(uri_string)

    unless adapter = self[uri.scheme]
      raise ArgumentError, "#{uri_string.inspect} uri is not supported"
    end

    adapter.new(uri)
  end

  def self.register(adapter)
    @adapters << adapter
  end

  def self.[](scheme)
    @adapters.detect { |adapter| adapter.schemes.include?(scheme.to_sym) }
  end
end

# adapter/memory.rb
class Adapter
  class Memory
    def schemes
      [:memory]
    end

    # Methods to communicate with DB omitted.

    Adapter.register(self)
  end
end

Adapter.setup("memory://test").class
# => Adapter::Memory

Things to note in this example:

Conclusion

In this example, I had a direct dependency from an high level Adapter class with low-level, specific implementations.

Then I turned the dependency around: made the low level implementations depend on the high level superclass.

The inverted dependency reduced code coupling, leading to more flexible, testable and maintainable code. And who doesn’t love that?

In case you’re interested, here’s the full diff.

Questions? Thoughts? Comment on this Reddit thread or hit me up on twitter.