If you need a refresher on Object-Oriented Programming before reading this article, here is all you need:
- Classes and Objects in Python
- Object-Oriented Programming: Encapsulation in Python
- Inheritance in Python
- Object-Oriented Programming: Polymorphism in Python
The Single Responsibility Principle (SRP) is about making a class focus on its primary responsibility.
Other responsibilities must be avoided.
Letting your objects take too many responsibilities is the formula for future headaches and a bunch of code smells.
This can be better explained with code, so let’s see an example of this principle.
Python Code Example
Consider a class called Vehicle
like the one below.
We can initialize a vehicle with some attributes like model and year.
We also have some methods like move and accelerate, which are actions a vehicle does.
We also have a __str__(self)
to make it easy to print the object in a human-readable format.
class Vehicle:
def __init__(self, year, model, plate_number, current_speed = 0):
self.year = year
self.model = model
self.plate_number = plate_number
self.current_speed = current_speed
def move(self):
self.current_speed += 1
def accelerate(self, value):
self.current_speed += value
def stop(self):
self.current_speed = 0
def __str__(self):
return f'{self.model}-{self.year}-{self.plate_number}'
my_car = Vehicle(2009, 'F8', 'ABC1234', 100)
my_car.move()
print(my_car.current_speed)
my_car.accelerate(10)
print(my_car.current_speed)
my_car.stop()
print(my_car)
The output of the test above will be:
101
111
F8-2009-ABC1234
The class above follows the Single Responsibility Principle.
It only handles attributes and methods that concern itself, a Vehicle.
Breaking the Single Responsibility Principle
Let’s break the SRP.
Say you want to save the object in a file, to store the information in a persistent way.
Naively, a programmer can just add a save(self, filename)
method.
This method will take the object it belongs to and save it in a file.
class Vehicle:
def __init__(self, year, model, plate_number, current_speed = 0):
self.year = year
self.model = model
self.plate_number = plate_number
self.current_speed = current_speed
def move(self):
self.current_speed += 1
def accelerate(self, value):
self.current_speed += value
def stop(self):
self.current_speed = 0
def __str__(self):
return f'{self.model}-{self.year}-{self.plate_number}'
def save(self, filename):
file = open(filename, "w")
file.write(str(self))
file.close()
my_car = Vehicle(2009, 'F8', 'ABC1234', 100)
print(my_car)
my_car.save("my_car.txt")
with open("my_car.txt") as f:
print(f.read())
The output for the code above is:
F8-2009-ABC1234
F8-2009-ABC1234
You can test the code and check it works.
But should a Vehicle
class be able to write data to a file?
What does storing info have to do with a Vehicle
?
Think about it in terms of a massive system with hundreds or thousands of classes.
Are you going to write a "save file" method for each class?
What happens when you need to make a change to the way your files are stored?
Maybe you want to check whether the path of the file exists to avoid errors and print a message for the user.
In this case, you would need to change every single file containing the "save file" method, which is prone to errors and it is bad practice.
How do we solve this then?
Fixing the class
The fixing, in this case, is as in the code below.
I created a new class called DataService
and moved the save
method from Vehicle to DataService
.
Since DataService
is a utility class meant to only save objects to a file, it doesn’t make sense to save itself.
So I annotated the save
method with @staticmethod
.
If you execute the code below you will notice the behavior is the same and the code still runs.
The difference is that now I can use DataService
and save(my_object, filename)
to store any kind of object.
And if I want to change the way I save my objects from files to a database, for instance, I just need to make a change in a single place.
Later I cans also implement methods to retrieve the data, update the data, or delete it, among other actions related to data management that are very common in any real-world system.
class Vehicle:
def __init__(self, year, model, plate_number, current_speed = 0):
self.year = year
self.model = model
self.plate_number = plate_number
self.current_speed = current_speed
def move(self):
self.current_speed += 1
def accelerate(self, value):
self.current_speed += value
def stop(self):
self.current_speed = 0
def __str__(self):
return f'{self.model}-{self.year}-{self.plate_number}'
class DataService:
@staticmethod
def save(my_object, filename):
file = open(filename, "w")
file.write(str(my_object))
file.close()
my_car = Vehicle(2009, 'F8', 'ABC1234', 100)
print(my_car)
data_service = DataService()
data_service.save(my_car, "my_car.txt")
with open("my_car.txt") as f:
print(f.read())
The output will be:
F8-2009-ABC1234
F8-2009-ABC1234
Anti-Pattern: God Classes (God Object)
For every pattern, there is an anti-pattern.
Classes with loads of responsibility are called God Classes.
God is omnipotent, omnipresent, and omniscient, and so it is a God Object.
It is everywhere, it can do everything and it knows everything.
This creates massive classes with thousands of lines of code that no one wants to touch in fear of breaking something.
Keep your classes cohesive, focus on their primary responsibility and avoid this bad practice.
Your future self (and your workmates) will thank you.