SOLID principles
SOLID is an acronym for the five object-oriented design (OOD) principles by Robert C. Martin (also known as Uncle Bob). They provide a guide to writing maintainable, scalable, and robust software. They are high-level principles.
Single responsibility principle
A class should have a single responsibility.
Bad example
class Hero:
lp = 50
x = 2
y = 3
vx = 0
vy = 0
ax = 0
ay = 0
Drawbacks:
- The class is huge so difficult to understand
- Difficult to test: any modification of the class would need to test the whole class again.
- Difficult to extend/modify.
Good
- Seperate the responsabilities into different classes
class Life:
lp = 50
def is_dead(self):
return self.lp <= 0
class PhysicalPoint:
x = 2
y = 3
vx = 0
vy = 0
ax = 0
ay = 0
class Hero:
def __init__(self):
self.life = Life
self.point = PhysicalPoint()
Open/closed principle
Prefer extensions via adding new classes than modification of existing classes.
Bad example
It is bad to modify existing classes, add/remove fields, modify methods etc.
Drawbacks:
- Need for retest code that was already tested
- Need to check that the modified class is still usable from other classes
For example, suppose you want to add a new type of ennemy would need to modify existing code.
def get_damage(enemy):
if enemy.type == DRAGON:
return enemy.force + 3
elif enemy.type == WIZARD:
return enemy.magic ** 2
elif enemy.type == HUMAN:
return 2
else:
raise NotImplementedError
Good example
- do new classes that inherits from abstract classes, assign a behavior to a class by modifying a field/method which is abstract
class Enemy:
def get_damage(self):
raise NotImplementedError
class Dragon(Enemy):
def get_damage(self):
return self.force + 3
class Wizard(Enemy):
def get_damage(self):
return self.magic ** 2
class Human(Enemy):
def get_damage(self):
return 2
class SuperHuman(Enemy):
def get_damage(self):
return 10000
Liskov substitution principle
Barbara Liskov - 2008 Turing Award
Wrong example
Suppose there exists a class Square
. We might be tempted to create a class Rectangle
as a subclass of Square
because we reuse the field width
, and just add a field height
.
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("Rectangle", "Square")])
dot
from dataclasses import dataclass
@dataclass
class Square():
width: float
def area(self):
return self.width * self.width
@dataclass
class Rectangle(Square):
height: float
def area(self):
return self.width * self.height
L = [Square(2), Rectangle(2, 3)]
for shape in L:
print(shape.area())
4
6
This causes problems. Indeed, the developer reads "Any rectangle is a square" which is false. Some invariants written in the class Square
may no longer be true for a Rectangle
.
Solution
Please do:
from graphviz import Digraph
dot = Digraph(graph_attr=dict(rankdir="BT"), edge_attr={'arrowhead':'empty'})
dot.edges([("Square", "Rectangle")])
dot
from dataclasses import dataclass
@dataclass
class Rectangle():
width: float
height: float
def area(self):
"""_summary_
Returns:
_type_: _description_
"""
return self.width * self.height
class Square(Rectangle):
def __init__(self, width):
super().__init__(width, width)
L = [Square(2), Rectangle(2, 3)]
for shape in L:
print(shape.area())
4
6
Interface segregation principle
Clients should not be forced to depend on methods that they do not use.
- Build small devoted abstract classes specific to some clients
Instead of implementing Dijkstra's algorithm that takes a GridGraph
, please implement it that takes an abstract Graph
. Dijkstra's algorithm does not care about method like G.grid_width
or G.grid_height
.
Dependency inversion principle
Please always depend on abstraction, not on concrete classes.
Bad example
For instance, the class Hero
here depends on the class Sound
. The class Sound
is specific and you create an object of type Sound
.
class Hero:
def __init__(self):
self.soundAttack = Sound("attack.ogg")
def attack(self):
self.soundAttack.play()
Drawbacks
- If the implementation if Sound changes (for instance, it does not take .ogg files anymore but .wav)
- handling deaf persons would require to traverse all the classes using
Sound
and make the modification
Solution
Decide an abstract interface for the game first and stick to it (except if it is bad!).
class IGame:
sound: ISound
graphics: IGraphics
class Hero:
def __init__(self, game: IGame):
self.game = game
def attack(self):
self.game.sound.play("attack")
Advantages:
- Robust to the change of a library/framework. If the implemntation of the very concrete
Sound
class changes, we just change the implementation ofISound
. The modification are confined. - Robust for adding new feature. Suppose you want to be inclusive and target deaf persons. Just add a new subclass to
ISound
that also displays a subtitle.