Aaron Proctor

The incoherent braindump of a software developer

Single Responsibility Principle

This is part 1 of a series of blog posts on the SOLID principles – five fundamental principles of object-oriented software design.

Single Responsibility Principle

The Single Responsibility Principle states:

Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class.

Self-explanatory, but how can you tell how many responsibilities a class has? Well Robert Martin, author of the SOLID principles, defines a responsibility as a reason to change. In other words, there should only ever be one reason for a given class to change.

This principle ensures that all classes are smaller, cohesive and tightly focused on providing one single service. This makes it easier to modify the software as any given change is tied to one single class.

Consider the following example showing a Player class in a simple game:

This class has multiple reasons to change. What if the requirements change so that killing an enemy is worth more than one point? What if the critical health threshold of 25 needs to change? What if the method of outputting the health warning changes? What if movement changes?

If any one of these requirements changes, then the entire Player functionality and any of its dependencies will be affected. This code should be refactored so that it has a more clearly defined purpose.

Let’s strip out the Health property and replace it with a class, HealthCounter, that’s responsible for detecting the critical health threshold:

Now this only needs to change if the IsCritical functionality changes.

Displaying the health warning should also be distinct from the Player class, so we can extract a class for this functionality too:

Now if we want to do something more complex with the health alert than just writing to the console, we don’t need to affect unrelated functionality.

Another issue with this class is that it’s responsible for handling player movement and tracking its position. Let’s delegate this responsibility to a specific GameBoard class. That way, the Move method doesn’t need to worry about these intricacies.

The GameBoard class tracks the location of objects in the game world:

Now the Player class is no longer knows the detail of how to move around the board:

Finally, the attacking logic is encapsulated in the player object when it really should be separate. Let’s delegate this to a PlayerAttack class:

And the attack method on the Player object becomes simpler:

Now the PlayerAttack class knows how to perform the attack (which entails retrieving the enemy on the next tile and calling the TakeDamage() method) and so this logic is encapsulated.

Note also here that we’re passing in a way to track the score rather than using control statements in the Playe.Attack() method. This approach is useful to delegate control of the flow of execution (“if (!enemy.Kill())”).

This way, PlayerAttack now contains both the condition (enemy was killed) and statement to execute if the condition holds (increase the score) which encompasses a single responsibility.

Now our final Player class is a higher-level class with a defined responsibility, which is to delegate control of Player aspects to the more specialised classes. However, we are still violating other SOLID principles.

Post a comment