"SOLID principles are the compass that navigates developers through the complexities of software architecture"
- Definition
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (
DIP) - Summarise
- Let's Refactor!
- Quiz
- Homework
SOLID principles are a set of design principles in software engineering that, when followed properly, make the software more understandable, flexible, and maintainable. The acronym SOLID stands for five design principles:
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
A class should have one, and only one, reason to change. This means a class should only have one job or responsibility.
Consider a Report class that generates a report and then prints it. According to SRP, these two actions should be separated into different classes.
# Violating SRP
class Report:
def generate_report(self, data):
# Code to generate the report
pass
def print_report(self, report):
# Code to print the report
pass
# Adhering to SRP
class ReportGenerator:
def generate_report(self, data):
# Code to generate the report
pass
class ReportPrinter:
def print_report(self, report):
# Code to print the report
passAgain, each class should have its own purpose, so the best to write down functionality/architecture of the project on the paper and stick to that in advance.
Consider a UserManagement class that handles user-related operations such as
- Adding a user
- Sending a welcome email
- Save (logging) the activity of the user
class UserManagement:
def add_user(self, user):
# Code to add the user to the system
pass
def send_welcome_email(self, user):
# Code to send a welcome email to the user
pass
def log_activity(self, activity):
# Code to log user activity
passThink about how we can refactor this, does this class serve its purpose? Is it correct that it handles lots of different operations? Can we refactor it to improve maintainability/readability and scalability?
The answer is that each method is not really related to the UserManagment activity, it has 3 different purposes which could be separated into other classes:
class UserRegistry:
def add_user(self, user):
# Code to add the user to the system
pass
class EmailService:
def send_welcome_email(self, user):
# Code to send a welcome email to the user
pass
class ActivityLogger:
def log_activity(self, activity):
# Code to log user activity
passThe code above ensures that each class has a single reason to change. Which means that we applied SRP successfully.
Software entities (like classes, modules, functions) should be open for extension but closed for modification.
This means that the behavior of a module/class/function can be extended without modifying its source code.
Let's consider an example of a TaxCalculator class that calculates tax based on the type of product. Initially, the class is not following OCP, because every time a new tax type is introduced, the class has to be modified.
class TaxCalculator:
def calculate_tax(self, product_type, price):
if product_type == "book":
return price * 0.05 # 5% tax for books
elif product_type == "food":
return price * 0.10 # 10% tax for food
# More conditions for other product typesTo follow the OCP, we can define a generic TaxCalculator class and then extend it for each specific tax type.
class TaxCalculator:
def calculate_tax(self, price):
pass
class BookTaxCalculator(TaxCalculator):
def calculate_tax(self, price):
return price * 0.05
class FoodTaxCalculator(TaxCalculator):
def calculate_tax(self, price):
return price * 0.10
# More classes for other product types
...Let's consider a reporting system where reports can be generated in different formats (e.g., PDF, CSV). Initially, the system is not following OCP because adding a new report format requires modifying existing code.
class ReportGenerator:
def generate_report(self, data, format_type):
if format_type == "PDF":
# Generate PDF report
pass
elif format_type == "CSV":
# Generate CSV report
pass
# More conditions for other formatsHere, we define a generic ReportGenerator class and then extend it for each specific report format.
class ReportGenerator:
def generate_report(self, data):
pass
class PDFReportGenerator(ReportGenerator):
def generate_report(self, data):
# Generate PDF report logic
pass
class CSVReportGenerator(ReportGenerator):
def generate_report(self, data):
# Generate CSV report logic
pass
# More classes for other formatsSometimes people tend to over-engineer the solution by introducing too many layers of abstraction or making the system too generic.
Yes, SOLID principles tend to be a great foundation but please, don't overthink the design of your application, the majority of code can easily be refactored and updated throughout your journey, JUST CODE!
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
Consider a system with different types of shapes where each shape can calculate its area.
A violation of LSP would occur if a subclass of Shape does not correctly implement the area() method.
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):
# Incorrect implementation or not implemented
passIt can lead to incorrect behavior when a Circle is used in place of a Shape.
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 * self.radiusUsing this principle can be a challenge in the begging of your application design, but if you use it within a correct approach it can be very predictable and robust for further development
Let's consider a system with different types of birds. Initially, each bird has a fly() method.
A violation of LSP would occur if a subclass of Bird (like Penguin) cannot correctly implement the fly() method.
class Bird:
def fly(self):
# Default fly behavior
pass
class Sparrow(Bird):
def fly(self):
# Implementation for flying
pass
class Penguin(Bird):
def fly(self):
# Penguins can't fly!
raise Exception("Can't fly")In complex systems fly() would lead to incorrect behavior or runtime errors which potentially could be hard to debug.
This can be refactored by creating separate interfaces for FlyingBird and NonFlyingBird to ensure consistency throughout the code and correct implementation of overriden methods.
class Bird:
# Common bird behavior (if any)
pass
class FlyingBird(Bird):
def fly(self):
# Implementation for flying
pass
class NonFlyingBird(Bird):
# Other behaviors specific to non-flying birds
pass
class Sparrow(FlyingBird):
def fly(self):
# Sparrow-specific flying behavior
pass
class Penguin(NonFlyingBird):
# Penguin-specific behaviors
passPenguin objects are not expected to fly, and thus, the system does not assume that capability, adhering to LSP.
There are lots of rabbit holes for LSP but generally some static linters as mypy handle all of SOLID this for developers, but don't forget to turn on the brain while designing your application ;)
The Interface Segregation Principle (ISP) states that no client should be forced to depend on methods it does not use.
This principle aims to split larger interfaces into smaller, more specific ones so that clients only need to know about the methods that are interesting to them.
Let's consider a printing system where the initial design forces the Printer class to implement functions that are not essential to all types of printers, such as faxing or scanning.
class AllInOnePrinter:
def print_document(self, document):
# Print the document
pass
def scan_document(self, document):
# Scan the document
pass
def fax_document(self, document):
# Fax the document
passIn this example, even simple printers without scanning or faxing capability have to implement the scan_document and fax_document methods, which violates ISP.
We refactor the example by creating separate interfaces for each responsibility.
class Printer:
def print_document(self, document):
pass
class Scanner:
def scan_document(self, document):
pass
class FaxMachine:
def fax_document(self, document):
pass
# Yes! That's where multiple inheritacne comes in use
class AllInOneMachine(Printer, Scanner, FaxMachine):
def print_document(self, document):
# Print the document
pass
def scan_document(self, document):
# Scan the document
pass
def fax_document(self, document):
# Fax the document
pass
# Create instances
simple_printer = Printer()
scanner = Scanner()
fax_machine = FaxMachine()
# Using individual component for each class
document = "This is a document."
simple_printer.print_document(document) # ``Printer``: Printing document
scanner.scan_document(document) # ``Scanner``: Scanning document
fax_machine.fax_document(document) # ``FaxMachine``: Faxing document
# All in one can handle three different operations in case we need to use all of them
all_in_one = AllInOneMachine()
all_in_one.print_document(document)
all_in_one.scan_document(document)
all_in_one.fax_document(document)It might be hard to understand but the simple explanation will be the following:
Printer,Scanner, andFaxMachineare interfaces (or abstract classes) that define specific functionalities. (That is a great example ofSRPas well)AllInOneMachineimplements all these interfaces, providing the functionality for printing, scanning, and faxing.
Note: Clients that only need a printer can depend on the Printer interface without being forced to know about scanning or faxing and same applies to Scanner and FaxMachine.
If you need to print, you use Printer, to fax FaxMachine , and to scan Scanner is the way to go!
Consider a vehicle control system where the initial design forces all vehicle types to implement functionalities that are not essential for all of them, such as launchMissiles for military vehicles or playMusic for civilian vehicles.
We don't want to have the ability to launch missiles in our car we use day to day for commuting to work, do we?
class VehicleControl:
def steerLeft(self):
# Steer the vehicle left
pass
def steerRight(self):
# Steer the vehicle right
pass
def launchMissiles(self):
# Launch missiles (mainly for military vehicles)
pass
def playMusic(self):
# Play music (mainly for civilian vehicles)
passWe refactor the example above by creating separate interfaces for each category of functionalities.
# Parental Inerfaces (Base classes)
class BasicVehicleOperations:
def steer(self):
# Steer the vehicle
pass
class MilitaryOperations:
def launchMissiles(self):
# Launch missiles
pass
class EntertainmentOperations:
def playMusic(self):
# Play music
pass
class CivilianVehicle(BasicVehicleOperations, EntertainmentOperations):
def steer(self):
# Implementation for steering
pass
def playMusic(self):
# Implementation to play music
pass
class MilitaryVehicle(BasicVehicleOperations, MilitaryOperations):
def steer(self):
# Implementation for steering
pass
def launchMissiles(self):
# Implementation to launch missiles
passSometimes applying ISP can might result in a large number of interfaces, each with only a few methods. And this can become a headache for developers, but on the other hand, smaller and more specific interfaces lead to more modular and understandable code.
The Dependency Inversion Principle (DIP) suggests that high-level modules should not depend on low-level modules.
Abstractions should not depend on details, but details should depend on abstractions.
Imagine you have a news reporting system where a NewsReporter class is responsible for reporting news. Initially, it directly uses a RadioChannel class to broadcast news. We want to adhere to DIP to make our NewsReporter more flexible and not tightly coupled to the RadioChannel.
class RadioChannel:
def broadcast_news(self, news):
print(f"Broadcasting news on the radio: {news}")
class NewsReporter:
def __init__(self):
self.radio_channel = RadioChannel()
def report_news(self, news):
self.radio_channel.broadcast_news(news) In this example, NewsReporter is directly dependent on RadioChannel, meaning if we want to broadcast news on a different medium, like TV, we'd have to modify the NewsReporter class, violating DIP.
Let's introduce an abstraction (interface) named BroadcastMedium and make NewsReporter dependent on this interface rather than a concrete class.
class BroadcastMedium:
def broadcast_news(self, news):
pass
class RadioChannel(BroadcastMedium):
def broadcast_news(self, news):
print(f"Broadcasting news on the radio: {news}")
class TVChannel(BroadcastMedium):
def broadcast_news(self, news):
print(f"Broadcasting news on TV: {news}")
class NewsReporter:
def __init__(self, broadcast_medium: BroadcastMedium):
self.broadcast_medium = broadcast_medium
def report_news(self, news):
self.broadcast_medium.broadcast_news(news)Now, NewsReporter relies on the BroadcastMedium interface. We can easily broadcast news on the radio, TV, or any other medium that implements the BroadcastMedium interface without changing the NewsReporter class.
Since the main idea is that both high and low level modules should depend on abstractions. It is a great when example we don't care of the implementation details of broadcast_news in BroadcastMedium class, we just call it.
Let's say we have a book reading app where a BookReader class is responsible for reading books. Initially, it's directly using a PDFReader class. We'll apply DIP to make BookReader flexible and not dependent on the PDFReader.
class PDFReader:
def read_book(self, book):
print(f"Reading {book} in PDF format.")
class BookReader:
def __init__(self):
self.reader = PDFReader()
def read_book(self, book):
self.reader.read_book(book)In this example, BookReader is directly dependent on PDFReader. If we want to read books in a different format, we'd need to change BookReader, violating DIP.
Introduce an abstraction (interface) named BookFormatReader and make BookReader dependent on this interface.
class BookFormatReader:
def read_book(self, book):
pass
class PDFReader(BookFormatReader):
def read_book(self, book):
print(f"Reading {book} in PDF format.")
class EpubReader(BookFormatReader):
def read_book(self, book):
print(f"Reading {book} in EPUB format.")
class BookReader:
def __init__(self, format_reader: BookFormatReader):
self.format_reader = format_reader
def read_book(self, book):
self.format_reader.read_book(book) # Use the specific format of the readerNow, BookReader depends on the BookFormatReader interface. We can easily read books in PDF, EPUB, or any other format that implements the BookFormatReader interface without changing the BookReader class.
I want to share a table with you to which you could refer to during designing your application. It might be not extremly readable, but helpful anyway to bear in mind summarising the key principles of SOLID and all rabit holes you can encounter.
| Principle | Purpose | Advantages | Potential Disadvantages |
|---|---|---|---|
| Single Responsibility Principle (SRP) | Ensure a class has only one reason to change. | - Easier maintenance - Increased modularity - Improved readability |
- May lead to many small, tightly coupled classes |
| Open/Closed Principle (OCP) | Allow entities to be open for extension but closed for modification. | - Flexibility in extension - Protection against changes - Reduced risk of bugs |
- May introduce abstract layers - Can lead to over-engineering |
| Liskov Substitution Principle (LSP) | Subtypes must be substitutable for their base types. | - Enhanced reliability - Promotes consistency - Better code reusability |
- Restricts how inheritance is used - Can make hierarchy design complex |
| Interface Segregation Principle (ISP) | No client should be forced to depend on methods it doesn't use. | - Decoupled system - Increased cohesion - Easier to understand interfaces |
- May increase the number of interfaces - Potential for duplicate methods |
| Dependency Inversion Principle (DIP) | High-level modules should not depend on low-level modules. | - Decoupled architecture - Easier to refactor and test - Promotes flexible system |
- Increased complexity - Indirect relations between components |
Suppose we want to create an app that manages and displays messages in various formats.
class MessageManager:
def __init__(self, content):
self.content = content
def format_message(self, format_type):
if format_type == "JSON":
return f"{{'message': '{self.content}'}}"
elif format_type == "XML":
return f"<message>{self.content}</message>"
def display_message(self, format_type):
formatted_message = self.format_message(format_type)
print(formatted_message)
# Usage
message_manager = MessageManager("Hello, SOLID!")
message_manager.display_message("JSON")In order to refactor our application we should use the following steps:
Step 1: Go throughout the code and identify which principle is violated?
class MessageManager:
def __init__(self, content):
self.content = content
# SRP Violation: This class handles multiple responsibilities
def format_message(self, format_type):
if format_type == "JSON":
return f"{{'message': '{self.content}'}}"
elif format_type == "XML":
return f"<message>{self.content}</message>"
# OCP Violation: Modifying the class to add a new format
# LSP Violation: Subclasses would find it hard to support all format types
# ISP Violation: Clients that need only message formatting are forced to depend on display functionality too
def display_message(self, format_type):
formatted_message = self.format_message(format_type)
print(formatted_message)
message_manager = MessageManager("Hello, SOLID!")
message_manager.display_message("JSON")- Single Responsibility Principle (SRP) Violation: The
MessageManagerclass handles multiple responsibilities, including formatting and displaying messages. - Open/Closed Principle (OCP) Violation: To support a new message format, the
format_messagemethod needs to be modified, violating the principle of open for extension, closed for modification. - Interface Segregation Principle (ISP) Violation: Clients that only want message formatting are forced to depend on the display functionality.
- Dependency Inversion Principle (DIP) Violation: The
MessageManagerclass has a concrete dependency on message formatting and display, making it inflexible.
Step 2: Address each issue separately
A) Separate Responsibilities (SRP)
class Message:
def __init__(self, content):
self.content = contentSRP: Single responsibility for managing message content.
B) Create Formatter Interface and Implementations (OCP, LSP, ISP)
from abc import ABC, abstractmethod
class MessageFormatter(ABC):
@abstractmethod
def format(self, message):
pass
class JSONFormatter(MessageFormatter):
def format(self, message):
return f"{{'message': '{message.content}'}}"
class XMLFormatter(MessageFormatter):
def format(self, message):
return f"<message>{message.content}</message>"OCP: Open for extension, closed for modificationLSP: Subtypes can be substituted without altering the correctness of the programISP: Clients can choose specific formatters they need without depending on unused methods
C) Implement Display Functionality
class MessageDisplayer:
def __init__(self, formatter: MessageFormatter):
self.formatter = formatter
def display(self, message: Message):
formatted_message = self.formatter.format(message)
print(formatted_message)DIP: High-level MessageDisplayer depends on abstraction MessageFormatter, not on concrete implementations!
Step 3: Put the code altogether
from abc import ABC, abstractmethod
class Message:
def __init__(self, content):
self.content = content
class MessageFormatter(ABC):
@abstractmethod
def format(self, message):
pass
class JSONFormatter(MessageFormatter):
def format(self, message):
return f"{{'message': '{message.content}'}}"
class XMLFormatter(MessageFormatter):
def format(self, message):
return f"<message>{message.content}</message>"
class MessageDisplayer:
def __init__(self, formatter: MessageFormatter):
self.formatter = formatter
def display(self, message: Message):
formatted_message = self.formatter.format(message)
print(formatted_message)
message = Message("Hello, SOLID!")
json_formatter = JSONFormatter()
message_displayer = MessageDisplayer(json_formatter)
message_displayer.display(message)
xml_formatter = XMLFormatter()
message_displayer = MessageDisplayer(xml_formatter)
message_displayer.display(message){'message': 'Hello, SOLID!'}
<message>Hello, SOLID!</message>
-
Single Responsibility Principle (SRP): Separated the concerns into different classes:
Messagefor managing message content,MessageFormatterfor formatting messages, andMessageDisplayerfor displaying messages. -
Open/Closed Principle (OCP): Introduced the
MessageFormatterinterface. New formatters can be added without modifying existing code, adhering to OCP. -
Liskov Substitution Principle (LSP): Clients can use instances of derived classes (
JSONFormatter,XMLFormatter) through theMessageFormatterinterface without affecting the correctness of the program. -
Interface Segregation Principle (ISP): Clients that only want to format messages can depend on
MessageFormatterwithout being forced to depend on the display functionality. -
Dependency Inversion Principle (DIP):
MessageDisplayerdepends on theMessageFormatterabstraction, not the concrete implementations. It inverts the traditional dependency from high-level modules to low-level modules.
This refactoring makes the application more maintainable, extensible, and robust by adhering to the SOLID principles.
Suppose we are developing a system for a bookstore that handles different types of book transactions such as selling, renting, and exchanging books.
class BookstoreManager:
def __init__(self, books):
self.books = books
def process_transaction(self, book_id, transaction_type):
if transaction_type == "SELL":
# process selling transaction
print(f"Selling book with ID: {book_id}")
elif transaction_type == "RENT":
# process renting transaction
print(f"Renting book with ID: {book_id}")
elif transaction_type == "EXCHANGE":
# process exchange transaction
print(f"Exchanging book with ID: {book_id}")
# Usage
books = {"001": "Book 1", "002": "Book 2"}
bookstore_manager = BookstoreManager(books)
bookstore_manager.process_transaction("001", "SELL")The main skill a software engineer should have is thinking and ability to solve real-world problems using programming. Try to reproduce steps described above and understand which concepts were used in order to refactor our bookstore system.
from abc import ABC, abstractmethod
class Book:
def __init__(self, book_id, title):
self.book_id = book_id
self.title = title
class Transaction(ABC):
@abstractmethod
def execute(self, book: Book):
pass
class SellTransaction(Transaction):
def execute(self, book: Book):
print(f"Selling book with ID: {book.book_id}")
class RentTransaction(Transaction):
def execute(self, book: Book):
print(f"Renting book with ID: {book.book_id}")
class ExchangeTransaction(Transaction):
def execute(self, book: Book):
print(f"Exchanging book with ID: {book.book_id}")
class BookstoreManager:
def __init__(self, books):
self.books = books
def process_transaction(self, book_id, transaction: Transaction):
if book_id in self.books:
book = self.books[book_id]
transaction.execute(book)
else:
print(f"Book with ID: {book_id} not found.")
# Usage
books = {"001": Book("001", "Book 1"), "002": Book("002", "Book 2")}
bookstore_manager = BookstoreManager(books)
sell_transaction = SellTransaction()
bookstore_manager.process_transaction("001", sell_transaction)Which SOLID principle is violated in the
Reportclass?
class Report:
def generate_pdf_report(self, data):
pass
def generate_csv_report(self, data):
pass
def print_report(self, report):
passA) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Interface Segregation Principle (ISP)
Which SOLID principle is violated in the
Vehicleclass?
class Vehicle:
def start_engine(self):
pass
def stop_engine(self):
pass
def fly(self):
# Hint: Logic for flying (applicable only for certain vehicles)
passA) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Interface Segregation Principle (ISP)
Which SOLID principle is violated in the
NewsPublisherclass?
class NewsPublisher:
def publish_news(self, news):
if self.platform == "Facebook":
# Publish news to Facebook
pass
elif self.platform == "Twitter":
# Publish news to Twitter
passA) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Dependency Inversion Principle (DIP)
Which SOLID principle is violated in the code above?
class Rectangle:
def set_dimensions(self, width, height):
self.width = width
self.height = height
class Square(Rectangle):
def set_dimensions(self, side):
self.width = side
self.height = sideA) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Dependency Inversion Principle (DIP)
Which SOLID principle is most likely to be violated if the
EmailSenderclass is used directly in high-level modules?
class EmailSender:
def send_email(self, content, smtp_server):
# Logic to send email using ``SMTP`` server
passA) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Interface Segregation Principle (ISP)
D) Dependency Inversion Principle (DIP)
Which SOLID principle is violated in the
UserInterfaceclass?
class UserInterface:
def display_text(self, text):
pass
def play_audio(self, audio):
# Play audio (not needed for text-based interfaces)
passA) Single Responsibility Principle (SRP)
B) Liskov Substitution Principle (LSP)
C) Interface Segregation Principle (ISP)
D) Dependency Inversion Principle (DIP)
Which SOLID principle is violated in the
MediaPlayerclass?
class MediaPlayer:
def play_media(self, file):
if file.type == "audio":
# Play audio
pass
elif file.type == "video":
# Play video
passA) Single Responsibility Principle (SRP)
B) Open/Closed Principle (OCP)
C) Liskov Substitution Principle (LSP)
D) Dependency Inversion Principle (DIP)
Objective: Update an application below to manage a variety of recipes that allow users to add new recipes, store them, and display them in an organized manner. The application should be adaptable to different types of recipes and dietary requirements.
- Users should be able to create new recipes and specify if they are for a special diet.
- The application should be able to save recipes to a file or database.
- Users should be able to retrieve a list of all recipes or search for recipes by various criteria.
- Users should be able to update and delete recipes.
class RecipeOrganizer:
def __init__(self):
self.recipes = []
def add_recipe(self, title, ingredients, instructions):
self.recipes.append({
'title': title,
'ingredients': ingredients,
'instructions': instructions
})
def display_recipes(self):
for recipe in self.recipes:
print(recipe['title'])
print('Ingredients:', recipe['ingredients'])
print('Instructions:', recipe['instructions'])
def save_to_file(self):
with open('recipes.txt', 'w') as file:
for recipe in self.recipes:
file.write(f"{recipe['title']}\n")
file.write(f"{recipe['ingredients']}\n")
file.write(f"{recipe['instructions']}\n\n")
# Usage
organizer = RecipeOrganizer()
organizer.add_recipe('Pasta', ['Pasta', 'Tomato'], 'Boil pasta, add tomato sauce')
organizer.display_recipes()
organizer.save_to_file()Try to identify all violated SOLID principles and re-write this application.
Rewrite your apps you have created already to match SOLID. Don't forget to split logic into different modules as well.