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