The Dependency Inversion Principle

Share on facebook
Share on google
Share on twitter
Share on linkedin
Photo by Roman Kraft on Unsplash

This is a quick blog on my understanding of dependency inversion. There are still elements I am unsure about, so please feel free to leave feedback.

What is the Dependency Inversion Principle?

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

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

— Robert C. Martin

That’s great, but what does it mean? 🤷🏽‍♀️ I’ll demonstrate my understanding by using a class in my Tic Tac Toe application as an example.

Dependency Inversion in Tic Tac Toe

In the application, there’s a class called GameFactory. The purpose of GameFactory is to create an instance of the Game class with the specified players and a board.

Here’s a condensed version of the class:

class GameFactory
  def initialize(player_factory)
    @player_factory = player_factory
  end
  def new_game(squares)
board = Board.new(squares)
    player1 = @player_factory.create_player('x', 1)
    player2 = @player_factory.create_player('o', 2)
Game.new(board, player1, player2)
  end
end

In the new_game method, new instances of the Board and Game classes are created within it. However, this violates the Dependency Inversion Principle.

What’s wrong with it?

The high-level class GameFactory is dependent on the low level classesBoard and Game As a result, they are tightly coupled. A change in a low-level class will affect the high-level class.

If the name of the Board or Game class was changed, the new_game method within GameFactory wouldn’t work. As a result, it would need to be amended to accommodate the renamed classes.

If sub classes of Board and Game were to be used to create a new game, (for example, BestBoard and FunGame) the new_game method would need to be changed again to accommodate this.

A method to resolve the above issues is to pass the classes into GameFactory’s constructor:

class GameFactory
  def initialize(player_factory, board, game)
    @player_factory = player_factory
    @board = board
    @game = game
  end
  def new_game(squares)
board = @board.new(squares)
    player1 = @player_factory.create_player('x', 1)
    player2 = @player_factory.create_player('o', 2)
@game.new(board, player1, player2)
  end
end

Whatever is passed in as board and game during initialisation, becomes @board and @game within GameFactory.

If the names of the Board and Game classes were to change, initialising GameFactory with the renamed classes would not affect GameFactory.

If subclasses of Board and Game (
for example, BestBoard and FunGame) were used to initialise an instance of GameFactory, this would not affect how new_game functions.

In conclusion, my understanding is initialising a specific class, or classes within another results in tight coupling. Being able to inject the classes via the constructor, helps to make the classes loosely coupled.