Understanding SOLID Principles
1. Single Responsibility Principle (SRP)
By keeping classes focused on a specific task, we enhance code clarity, reduce dependencies, and make our code easier to maintain.
-- Tip
Example
class Player:
def __init__(self):
# Player-specific attributes and initialization
def move(self):
# Code for moving the player
def attack(self):
# Code for player attacks
def keep_score(self):
# Code for keeping track of the player's score
class AssetLoader:
def load_assets(self):
# Code for loading game assets
class NetworkManager:
def connect(self):
# Code for establishing network connection
def disconnect(self):
# Code for closing network connection
# Usage
player = Player()
player.move()
player.attack()
player.keep_score()
asset_loader = AssetLoader()
asset_loader.load_assets()
network_manager = NetworkManager()
network_manager.connect()
network_manager.disconnect()
Warning
2. Open/Closed Principle (OCP)
By relying on abstraction, interfaces, and inheritance, we can achieve code that is flexible and easily extensible
-- Tip
Example
class OrderCalculator:
def calculate_total(self, items):
total = 0
for item in items:
total += item.price
return total
class DiscountedOrderCalculator(OrderCalculator):
def calculate_total(self, items):
total = super().calculate_total(items)
discount = 0.1 # 10% discount
return total - (total * discount)
# Usage
items = [Item("Toy", 10), Item("Book", 5), Item("Game", 20)]
order_calculator = OrderCalculator()
total_cost = order_calculator.calculate_total(items)
discount_calculator = DiscountCalculator()
discounted_cost = discount_calculator.apply_discount(total_cost, 5)
print("Total cost:", total_cost)
print("Discounted cost:", discounted_cost)
3. Liskov Substitution Principle (LSP)
Example
class Animal:
def makeSound(self):
pass
class Dog(Animal):
def makeSound(self):
return "Woof!"
class Cat(Animal):
def makeSound(self):
return "Meow!"
def animal_sounds(animals):
for animal in animals:
print(animal.makeSound())
# Usage
animals = [Dog(), Cat()]
animal_sounds(animals)
The Liskov Substitution Principle helps us build code that is flexible, reliable, and avoids surprises. It's like having a rulebook that ensures our code's behavior remains consistent and predictable, no matter which class we use.
-- Tip
4. Interface Segregation Principle (ISP)
It is like having a rule that says, "Don't make things more complicated than they need to be."
Example
# Bad design violating ISP
class Notification:
def sendEmail(self, recipient, message):
pass
def sendSMS(self, recipient, message):
pass
def sendPushNotification(self, recipient, message):
pass
# Good design following ISP
class EmailNotification:
def sendEmail(self, recipient, message):
pass
class SMSNotification:
def sendSMS(self, recipient, message):
pass
# Usage
class EmailService:
def __init__(self, emailNotification):
self.emailNotification = emailNotification
def sendWelcomeEmail(self, recipient):
message = "Welcome to our platform!"
self.emailNotification.sendEmail(recipient, message)
emailNotification = EmailNotification()
emailService = EmailService(emailNotification)
emailService.sendWelcomeEmail("john@example.com")
Check out!
5. Dependency Inversion Principle (DIP)
Example
# Notification Sender Interface
class NotificationSender:
def send_notification(self, message):
pass
# Email Sender Class
class EmailSender(NotificationSender):
def send_notification(self, message):
print(f"Sending email notification: {message}")
# SMS Sender Class
class SMSSender(NotificationSender):
def send_notification(self, message):
print(f"Sending SMS notification: {message}")
# Push Notification Sender Class
class PushNotificationSender(NotificationSender):
def send_notification(self, message):
print(f"Sending push notification: {message}")
# High-level Class
class NotificationService:
def __init__(self, sender: NotificationSender):
self.sender = sender
def send_notification(self, message):
self.sender.send_notification(message)
# Creating instances and using the NotificationService
email_sender = EmailSender()
notification_service = NotificationService(email_sender)
notification_service.send_notification("Hello, this is an email notification!")
sms_sender = SMSSender()
notification_service = NotificationService(sms_sender)
notification_service.send_notification("Hello, this is an SMS notification!")
push_notification_sender = PushNotificationSender()
notification_service = NotificationService(push_notification_sender)
notification_service.send_notification("Hello, this is a push notification!")
By using the Dependency Inversion Principle, we achieve decoupling between the high-level and low-level classes. The high-level class doesn't need to know the specifics of each sender implementation, allowing for flexibility, testability, and easier maintenance.
-- Tip
Benefits of Applying SOLID Principles
Real-World Examples
1. SOLID in Action: Single Responsibility Principle
class Authentication:
def signup(self, username, password):
# Handle user signup process
pass
def login(self, username, password):
# Handle user login process
pass
def logout(self):
# Handle user logout process
pass
class UserDataStorage:
def saveUserData(self, user):
# Store user data in the database
pass
def getUserData(self, username):
# Retrieve user data from the database
pass
def updateUserData(self, user):
# Update user data in the database
pass
class PasswordEncryption:
def encryptPassword(self, password):
# Encrypt the password
pass
def validatePassword(self, password, hashedPassword):
# Validate the provided password against the hashed password
pass
# Usage
authentication = Authentication()
userStorage = UserDataStorage()
passwordEncryption = PasswordEncryption()
authentication.signup("john", "password123")
user = userStorage.getUserData("john")
hashedPassword = passwordEncryption.encryptPassword("password123")
if passwordEncryption.validatePassword("password123", hashedPassword):
authentication.login("john", "password123")
# Perform additional actions for the logged-in user
authentication.logout()
So, to put it simply, the Single Responsibility Principle tells us to break down complex tasks into separate classes, with each class focusing on one specific job.
Main Idea
2. SOLID in Action: Open/Closed Principle
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def calculate_total_price(self):
total_price = 0
for item in self.items:
total_price += item.price
return total_price
class DiscountCoupon:
def apply_discount(self, cart):
total_price = cart.calculate_total_price()
discount = total_price * 0.1 # Assuming a 10% discount
total_price -= discount
return total_price
class PromotionalOffer:
def apply_offer(self, cart):
total_price = cart.calculate_total_price()
if total_price >= 100: # Assuming a promotional offer for orders over $100
total_price -= 20 # Apply a $20 discount
return total_price
# Usage example
cart = ShoppingCart()
cart.add_item(Item("Shirt", 25))
cart.add_item(Item("Jeans", 50))
discount_coupon = DiscountCoupon()
promotional_offer = PromotionalOffer()
# Applying the discount coupon
discounted_price = discount_coupon.apply_discount(cart)
print(f"Discounted price: ${discounted_price}")
# Applying the promotional offer
final_price = promotional_offer.apply_offer(cart)
print(f"Final price after promotional offer: ${final_price}")
3. SOLID in Action: Liskov Substitution Principle
class Shape:
def draw(self):
raise NotImplementedError("Subclasses must implement the draw method.")
def calculate_area(self):
raise NotImplementedError("Subclasses must implement the calculate_area method.")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def draw(self):
print(f"Drawing a rectangle with width {self.width} and height {self.height}.")
def calculate_area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def draw(self):
print(f"Drawing a circle with radius {self.radius}.")
def calculate_area(self):
return 3.14 * self.radius * self.radius
# Usage example
shapes = [Rectangle(5, 3), Circle(2)]
for shape in shapes:
shape.draw()
area = shape.calculate_area()
print(f"Area: {area}")
4. SOLID in Action: Interface Segregation Principle
By applying the Interface Segregation Principle, you can create separate interfaces for each mode, ensuring that classes only depend on the interfaces they actually use. This keeps the codebase clean and avoids unnecessary dependencies.
class StreamingMode:
def play(self, song):
raise NotImplementedError("Subclasses must implement the play method for streaming.")
def pause(self):
raise NotImplementedError("Subclasses must implement the pause method for streaming.")
def next(self):
raise NotImplementedError("Subclasses must implement the next method for streaming.")
class LocalPlaybackMode:
def play(self, song):
raise NotImplementedError("Subclasses must implement the play method for local playback.")
def pause(self):
raise NotImplementedError("Subclasses must implement the pause method for local playback.")
def previous(self):
raise NotImplementedError("Subclasses must implement the previous method for local playback.")
class PlaylistManagementMode:
def create_playlist(self, name):
raise NotImplementedError("Subclasses must implement the create_playlist method for playlist management.")
def add_song_to_playlist(self, playlist, song):
raise NotImplementedError("Subclasses must implement the add_song_to_playlist method for playlist management.")
def remove_song_from_playlist(self, playlist, song):
raise NotImplementedError("Subclasses must implement the remove_song_from_playlist method for playlist management.")
class MusicPlayer(StreamingMode, LocalPlaybackMode):
def play(self, song):
# Implementation for playing a song in the music player.
pass
def pause(self):
# Implementation for pausing the music player.
pass
def next(self):
# Implementation for playing the next song.
pass
def previous(self):
# Implementation for playing the previous song.
pass
class Playlist(PlaylistManagementMode):
def create_playlist(self, name):
# Implementation for creating a new playlist.
pass
def add_song_to_playlist(self, playlist, song):
# Implementation for adding a song to a playlist.
pass
def remove_song_from_playlist(self, playlist, song):
# Implementation for removing a song from a playlist.
pass
By applying the Interface Segregation Principle, we ensure that classes depend only on the interfaces relevant to their specific functionality.
Main Idea
5. SOLID in Action: Dependency Inversion Principle
By applying the Dependency Inversion Principle, we can introduce an abstraction layer, such as an interface, between the two modules. This allows the high-level module to depend on the abstraction rather than the concrete implementation, providing flexibility, testability, and decoupling.
class DatabaseInterface:
def connect(self):
pass
def execute_query(self, query):
pass
def disconnect(self):
pass
class MySQLDatabase(DatabaseInterface):
def connect(self):
# Connect to MySQL database.
def execute_query(self, query):
# Execute the query in MySQL database.
def disconnect(self):
# Disconnect from MySQL database.
class PostgreSQLDatabase(DatabaseInterface):
def connect(self):
# Connect to PostgreSQL database.
def execute_query(self, query):
# Execute the query in PostgreSQL database.
def disconnect(self):
# Disconnect from PostgreSQL database.
class HighLevelModule:
"""depend on the `DatabaseInterface` abstraction."""
def __init__(self, database: DatabaseInterface):
self.database = database
def perform_database_operation(self):
self.database.connect()
# Perform the database operation using the interface methods.
self.database.disconnect()