This is part 2 of a series of blog posts on the SOLID principles – five fundamental principles of object-oriented software design.
- SOLID: Five principles of good software design
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
The Open/Closed Principle
The Open/Closed Principle states:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
This suggests that a class should facilitate new functionality by extension rather than modification. It has obvious advantages when it comes to changing requirements – by leaving existing functionality untouched, it reduces the likelihood of new development causing ripple effects.
But how do we design our object model to achieve this?
Let’s suppose we add a new type of Enemy to the game, the Boss, which is more difficult to kill. We could do this with an Enum:
1 2 3 4 5 |
public enum EnemyType { Standard, Boss } |
And then modify our Enemy to have a hit points property and PlayerAttack method to utilise the new type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public void Attack(IScoreCounter counter) { if (_enemy.EnemyType == EnemyType.Standard) { _enemy.Kill(); counter.Add(1); } else { _enemy.HitPoints--; if (_enemy.HitPoints > 0) counter.Add(1); } } |
Not only have we made the code more complex and error-prone, but we’ve also modified the Enemy class and PlayerAttack class despite those concepts not having changed in the requirements.
Instead, let’s try extending the Enemy class to create a new type, Boss:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class Boss : Enemy { public Boss() { HitPoints = 3; } public int HitPoints { get; set; } public override void TakeDamage() { if (IsAlive) { HitPoints--; CheckForDeath(); } } private void CheckForDeath() { if (HitPoints <= 0) IsAlive = false; } } |
Now we have encapsulated the new requirements for a Boss without touching the Enemy class. Using inheritance or abstractions in place of enums is good practice – certainly any time you’re using a ‘switch’ or ‘if’ statement with an enum it should be treated with suspicion.
Post a comment