The SOLID Principles
Five ideas for keeping object-oriented code understandable.
The SOLID principles are five core principles of object-oriented programming:
- Single Responsibility Principle (SRP): a class should have responsibility for only one functional area — in other words, it should have only one reason to change.
- Open-Closed Principle (OCP): a class should be open for extension but closed for modification. That is, a class's behavior can be changed by extending it rather than editing its source code.
- Liskov Substitution Principle (LSP): subclasses must be able to replace their parent classes without affecting the correctness of the program. In other words, an instance of a parent class can be swapped for an instance of one of its subclasses and the program's behavior will not change.
- Interface Segregation Principle (ISP): clients should not depend on interfaces they don't use. In other words, a class should not force its clients to implement methods they don't need.
- Dependency Inversion Principle (DIP): high-level modules should not depend on low-level modules; both should depend on abstractions. In other words, a class should depend on an abstraction rather than on a concrete implementation.
These principles help us design object-oriented systems that are more flexible, extensible, and maintainable.
- Single Responsibility Principle (SRP):
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def get_name(self):
return self.name
def get_email(self):
return self.email
class UserDatabase:
def __init__(self):
self.users = []
def add_user(self, user):
self.users.append(user)
def get_user_by_email(self, email):
for user in self.users:
if user.get_email() == email:
return user
return NoneIn the code above, the User class is only responsible for representing a user's information, while the UserDatabase class is only responsible for managing user data. Each class therefore has a single responsibility, which satisfies the SRP.
- Open-Closed Principle (OCP):
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2In the code above, Shape is an abstract class that defines an area method without implementing it. Rectangle and Circle inherit from Shape and implement area. So to add a new shape, you only need to create a new class that inherits from Shape — no need to modify Shape's source code, which satisfies the OCP.
- Liskov Substitution Principle (LSP):
class Animal:
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
def make_animal_sound(animal):
print(animal.make_sound())
dog = Dog()
cat = Cat()
make_animal_sound(dog)
make_animal_sound(cat)In the code above, Dog and Cat both inherit from Animal and implement make_sound. The make_animal_sound function takes a parameter of type Animal and calls its make_sound method. Whether a Dog object or a Cat object is passed in, it works correctly — which satisfies the LSP.
- Interface Segregation Principle (ISP):
class Printable:
def print(self):
pass
class Document:
def __init__(self, content):
self.content = content
def get_content(self):
return self.content
class Printer:
def print_document(self, document):
document.print()
class SimpleDocument(Document, Printable):
def print(self):
print(self.get_content())
class FancyDocument(Document, Printable):
def print(self):
print("*****")
print(self.get_content())
print("*****")In the code above, the Printable interface defines only a single print method, the Document class represents a document, and the Printer class represents a printer. SimpleDocument and FancyDocument both inherit from Document and implement the Printable interface's print method. The Printer class depends only on the Printable interface, not on any concrete document class, which satisfies the ISP.
- Dependency Inversion Principle (DIP):
class Database:
def __init__(self):
self.data = []
def add(self, item):
self.data.append(item)
class Logger:
def log(self, message):
print(message)
class Service:
def __init__(self, database, logger):
self.database = database
self.logger = logger
def add_item(self, item):
self.database.add(item)
self.logger.log("Item added: " + str(item))In the code above, the Service class depends on the Database and Logger classes, but it depends on their abstractions rather than their concrete implementations. So if you need to swap out the database or the logger, you just create a new class implementing the corresponding abstraction — no need to modify Service's source code, which satisfies the DIP.