SOLID Principles With Simple Examples — Part 1

Devlin Delves In
4 min readMay 31, 2018

--

Robert ‘Uncle Bob’ Martin in his 2000 paper Design Principles and Design Patterns was the first to group together and promote the SOLID principles. These ideas form a core philosophy for agile or adaptive software development. When followed, they can assist with developing well-constructed code which meets desirable design goals — code that is understandable, reusable and easy to maintain.

I have attempted to give a brief resume of each principle below with an example from my own coding experience (if possible) which helps to illustrate why it is useful. My examples are drawn from programs I have made in Ruby and Java.

1 — Single Responsibility Principle

An object should have only one responsibility — i.e. only one ‘reason to change’.

If your objects have one clear responsibility they will be more robust — so when changes need to be made, there is a greater protection from unexpected breaks. Classes which have unrelated dependencies and mix together potentially incoherent values are clearly harder to maintain and change. That all sounds very sensible, but the challenge often lies in determining exactly what that ‘one responsibility’ is. Uncle Bob emphasised that a responsibility should be defined as ‘a reason to change’ — if your object has more than one type of reason to change when change is required in the program then there is likely more than one responsibility.

The template sentence below is useful for assessing what the responsibility of your object is. If the ‘{its job}’ part requires an ‘and’ to work then this is usually indicative of more than one reason to change, or responsibility.

The {object/class} is responsible for {its job} by {action/behaviour}.

In my first ‘grownup’ program — a Battleships console game in Ruby — I had a Game class with various responsibilities. My completed responsibilities sentence would have been rather long:

The Game object is responsible for evaluating moves (legal or not, and hit or miss), sending and receiving console input and output, and running the game by {lots of different actions!}.

It is quite obvious to see that all three of these functions would require change at different times and for different reasons.

For example, the method determining whether a move is legal or not may require altering to reject any input which isn’t a grid coordinate — if I had an ‘input’ instance variable, this might be used both within console input and output methods and within the legal move methods, which may cause breaks throughout the class. Whereas, if console IO was handled by a separate object, this would not be affected when the representation of the input value changed within the original Game object. This shows that separating out responsibilities makes your program more robust.

2 — Open-Closed Principle

Software entities (classes, methods and so on) should be open for extension but closed for modification.

A class is open for extension when it does not depend directly on concrete implementations — instead it should rely on abstract base classes or interfaces and not care how such dependencies are implemented at runtime. As the interface it relies on is abstract enough to allow for new objects to be substituted in instead of requiring any change to the class itself, this means that the class would also be closed for modification. Following this idea should make your code more flexible, enabling you to easily add features without causing unexpected bugs.

In the first iteration of my TicTacToe game, I created a HumanPlayer class. The Game object would ask the HumanPlayer to use its getInput method to return an input which the Game is then able to play out on the board. The getInput method was a call to the consoleIO object to request an input from the user via the console.

This is fine when there are two human players. The next step, however, was to add a computer player which chooses its moves randomly — i.e. implemented using a random generator method without recourse to the console.

As my code base was, I would have had to change the HumanPlayer into a Player class and give it a new method: getComputerInput. The Game would also have to be modified, calling the human input method if the player was human and computer method if a computer. This infringes the OCP as classes have to be modified to accommodate a new feature.

Instead, the solution was to create a higher-level abstract Player class with two subclasses — HumanPlayer and ComputerPlayer. The Player class has an abstract getInput method which is not implemented. The subclasses have concrete versions of getInput. The main Game object is then able to call getInput on an abstract Player — at runtime, whichever player is the active player will utilise its own getInput method to return an input.

Now our source code should be able to satisfy the Open-Closed Principle. If our client asks for another type of player to be created — perhaps an UnbeatableComputerPlayer — then our Game class can be left as it is. We can create a new subclass of Player and give it a whatever implementation of the getInput method we like.

I continue with the other 3 principles in Part 2.

--

--