We can place an application’s modules along a spectrum of higher-level to lower-level behaviours—the higher-level ones are the business models of the program that contain important policy decisions, while the more detailed, lower-level ones contain implementation details. It’s a difference between knowing what should be done versus how it should be done.

It may seem obvious that high-level modules should not be dependent upon low-level ones—we don’t want the implementation details influencing important business decisions—however, it is a common result of traditional procedural design. The Dependency Inversion Principle (DIP) encourages us to invert the direction of these dependencies by having high-level modules depend on abstractions instead of low-level modules.

It is not uncommon for a class to violate multiple SOLID principles at once, and classes that violate the Open-Closed Principle (OCP) are frequently offenders of DIP violations, too. In fact, we’re going to borrow the code example from the prior post on the OCP to also demonstrate a violation of the DIP.

As a refresher, this code is from my Ruby implementation of Battleship. I expect both human and computer players to place a move on a board during their respective turns; however, I want the human players to supply the location of their next move via the user interface, and I want the computer players to randomly generate a location. Here’s an implementation this feature that violates the DIP.

class Player
  attr_reader :type

  def initialize(type:)
    @type = type
  end

  def take_turn(board:)
    location = case type
    when :human
      get_human_move
    when :computer
      get_computer_move
    end
    board.get_tile(location).destroy
  end

  def get_human_move
    # ask for a move via the user interface
  end

  def get_computer_move
    # randomly generate a move
  end
end

We’ve already discussed how this code violates the OCP, but it also violates the rules of the DIP. In the context of the Battleship game application, Player is a fairly high-level class; yet here it is clearly dependent upon the implementation of getting a move for a human or a computer player. We can limit this dependency by creating an abstraction in the same way that we did when we addressed the OCP violation.

class Player
  attr_reader :move_getter

  def initialize(move_getter:)
    @move_getter = move_getter
  end

  def take_turn(board:)
    location = move_getter.get_move
    board.get_tile(location).destroy
  end
end

class MoveGetter
  def get_move
    # ask for a move via the user interface
  end
end

class RandomMoveGetter
  def get_move
    # randomly generate a move
  end
end

Now that the high-level Player depends on the abstract get_move duck type, it no longer knows anything about how to get a move for a player.

The authors of Agile Software Development: Principles, Patterns, and Practices (PPP) warn that despite our best efforts to have our classes depend on abstractions rather than concretions, a program is usually going to violate the DIP at least once. After all, something has to instantiate the concrete classes.

The authors of PPP further explain that there is no real reason for adhering to the DIP for classes that are concrete but nonvolatile. If a concrete class is not going to change and we’re not going to create any derivatives of it, then it does very little harm to depend on it. Depending on these concrete but stable classes will save us from introducing unnecessary complexity into our programs.