Single Responsibility Principle: Why Does it Matter?

One of my favorite tips for writing software comes from one of my favorite software role models, Sandi Metz. In her book Practical Object-Oriented Design in Ruby, she says that when describing what a class (or any other software entity) does in one sentence, if you use the words "and" or "or," you're most likely breaking the Single Responsibility Principle (SRP). If you use the word "and," your class has at least two responsibilities. If you use the word "or," the class likely has more than one responsibility, and there's a good chance the responsibilities aren't related to one another.

Let's try to describe this Board class for a Tic Tac Toe program written in Ruby:

class Board
  def generate_blank_board
    #generates an empty board state
  end

  def display_board_in_terminal
    #displays the board in the Terminal
  end

  def update_board
    #updates the board state
  end
end

How would we describe this class? It keeps track of the board's state and displays this state to the user. It looks like we have an SRP violation! But now that we've determined we have a violation, why should we care?

Including two separate responsibilities in one class puts us in danger of coupling pieces of functionality that have no reason to depend on one another. In our Board, we are coupling the ideas of the Board state with its display. If we ever want to change how the state is stored or how a board is displayed, we would be handling two very different responsibilities. Say our requirements change, and now instead of displaying our game in the Terminal, we also want to display it in a browser. While creating a browser UI, there's a chance that we could introduce a bug into the state-keeping section of our class.

How could we accomplish the display of the board in a browser? Let's dig back into our Tic Tac Toe example. Our first option could be to use the same Board class that we defined above, and to add the new functionality to it:

class Board
  def generate_blank_board
    #generates an empty board state
  end

  def display_board_in_terminal
    #displays the board in the Terminal
  end

  def display_board_in_browser
    #displays the board in a browser
  end

  def update_board
    #updates the board state
  end
end

Now we have a Board class that does exactly what we need it to do… Or does it? If we look at this code a little closer, we'll realize that this class, which is responsible for rendering the game in the browser, shouldn't need to know anything about how the game is displayed in the Terminal. But, the way that we've currently written Board, the code that creates the browser’s user interface is very tightly coupled with the Terminal’s user interface.

Every time we make a change to display_board_in_terminal, we'll be touching the Board class, and thus might introduce a bug or some other unintended side effect to any entity that uses Board. For example, since our browser game also uses Board, this side effect could potentially trickle down and also affect how the game behaves in the browser as well—even though it doesn't ever use display_board_in_terminal!

Our other option to reuse Board in its current state may be to duplicate it and create a BrowserBoard, which has a display method that is specifically for the browser game (display_board_in_browser):

class BrowserBoard
  def generate_blank_board
    #generates an empty board state
  end

  def display_board_in_browser
    #displays the board in a browser
  end

  def update_board
    #updates the board state
  end
end

This choice is flawed as well. If we ever need to change the way a Board behaves, like the logic it uses to update its state, we would need to change the update_board method in both BrowserBoard and our original Board. This makes us twice as vulnerable to introducing a bug or inconsistency to our system. Plus, it is just tedious to update the same method twice!

So, how do we fix our original Board class above so that it adheres to the SRP? Instead of including code to both display a board and keep its state in one class, let's split those two pieces of functionality into separate classes. First we’ll pull out the code dealing with the board’s state into its own class, aptly named BoardState:

class BoardState
  def generate_blank_board
    #generates an empty board state
  end

  def update_board
    #updates the board state
  end
end

Then, for each new user interface that we want to incorporate into our application, we can simply create a new class to handle the presentation of the board specific to that outlet. So, for our browser game, we can create a BrowserBoardPresenter:

class BrowserBoardPresenter
  def display_board
    #displays the board in the UI
  end
end

We can use this presenter in conjunction with our general purpose BoardState class to refactor our Tic Tac Toe game without changing the game’s overall behavior. We'll instantiate a new BoardState and a new BoardPresenter, and simply pass the board’s current state to the presenter every time we want to display it, in whichever outlet we choose!

board_state = BoardState.new
board_state.update_board

browser_board_presenter = BrowserBoardPresenter.new
browser_board_presenter.display_board(@board_state)

Our BoardPresenter will only ever need to change if and when we'd like the user interface in the browser to change, and these potential changes can be done completely independently of how we keep track of our board's state. Likewise, our BoardState will only change when its state-keeping logic needs to change, without affecting the board's presentation at all. By all accounts, it looks like we've corrected our SRP violation!

Just to be sure, let's run these two classes through our handy "Sandi Metz SRP Checker" by answering the question, "What do each of these classes do?" The BoardState class keeps the state of a Tic Tac Toe board. The BrowserBoardPresenter class displays a Tic Tac Toe board in a browser. It looks like we've successfully eliminated any "ands" and "ors" from each of our class's descriptions, and thereby are successfully adhering to the Single Responsibility Principle!


Sources:
Practical Object-Oriented Design in Ruby by Sandi Metz
Agile Software Development, Principles, Patterns, and Practices by Robert Martin

Elizabeth Engelman, Software Crafter

Elizabeth Engelman is a Software Crafter at 8th Light Chicago who enjoys the collaborative nature of creating software. She likes to tinker her way through problems in iterative ways, whether they're complex software interactions or finding the best recipe for chocolate chip cookies.

Interested in 8th Light's services? Let's talk.

Learn more about our Ruby services

Contact Us