The SOLID principles are five fundamental design principles that guide developers in creating software that is easy to maintain, extend, and understand. These principles, introduced by Robert Martin (Uncle Bob), form the foundation of good object-oriented design and help prevent common software design problems.
Overview of SOLID 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)
Each principle addresses specific aspects of software design:
Single Responsibility Principle ensures that each class has only one reason to change, making code more focused and easier to maintain.
Open/Closed Principle promotes extending functionality through addition rather than modification, reducing the risk of breaking existing code.
Liskov Substitution Principle ensures that derived classes can replace their base classes without altering program correctness.
Interface Segregation Principle prevents classes from depending on interfaces they don’t use, reducing coupling and improving modularity.
Dependency Inversion Principle decouples high-level modules from low-level implementation details by depending on abstractions.
Benefits of Following SOLID Principles
Following these principles leads to several key benefits in software development:
Maintainability improves significantly as code becomes more organized and changes are localized to specific areas. When requirements change, developers can modify code with confidence, knowing that changes won’t have unexpected ripple effects throughout the system.
Testability increases because well-designed classes with single responsibilities are easier to test in isolation. Dependencies can be easily mocked or stubbed, enabling comprehensive unit testing strategies.
Flexibility and Extensibility become natural characteristics of the codebase. New features can be added through extension rather than modification, and different implementations can be swapped without affecting client code.
Code Reusability improves as classes become more focused and loosely coupled. Components can be reused in different contexts without bringing along unnecessary dependencies.
I’ll explain each SOLID principle separately with detailed explanations and code examples.
Single Responsibility Principle (SRP)
The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have only one job or responsibility. This principle helps create more maintainable and testable code by ensuring that each class has a clear, focused purpose.
When a class has multiple responsibilities, changes to one responsibility can affect the other responsibilities, making the code fragile and harder to maintain. By separating concerns, we make our code more modular and easier to understand.
The principle applies not just to classes but also to functions and modules. Each unit of code should do one thing well and have a single, well-defined purpose.## Open/Closed Principle (OCP)
The Open/Closed Principle states that software entities should be open for extension but closed for modification. This means you should be able to add new functionality without changing existing code that already works and has been tested.
This principle encourages the use of abstraction and polymorphism to allow new behaviors to be added through inheritance, composition, or dependency injection. It helps prevent bugs that can be introduced when modifying existing code and makes the system more maintainable and flexible.
The key is to design your code so that when requirements change, you extend the system by adding new code rather than changing existing code.## Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In other words, if class B is a subtype of class A, then objects of type A should be replaceable with objects of type B without altering the correctness of the program.
This principle ensures that inheritance is used correctly and that subclasses truly represent a “is-a” relationship with their parent class. It prevents the creation of subclasses that violate the expected behavior of their parent class.
The key aspects of LSP include maintaining the same method signatures, not strengthening preconditions, not weakening postconditions, and preserving the behavioral contracts of the parent class.
from typing import List, Dict, Optional
from dataclasses import dataclass
from datetime import datetime
import json
# VIOLATION of Single Responsibility Principle
class BadUserManager:
"""
This class violates SRP by having multiple responsibilities:
1. User data management
2. Data validation
3. Database operations
4. Report generation
5. Email notifications
"""
def __init__(self):
self.users: List[Dict] = []
def add_user(self, name: str, email: str, age: int) -> bool:
# Responsibility 1: Data validation
if not name or not email or age < 0:
return False
if "@" not in email:
return False
# Responsibility 2: User data management
user = {
"id": len(self.users) + 1,
"name": name,
"email": email,
"age": age,
"created_at": datetime.now()
}
self.users.append(user)
# Responsibility 3: Database operations (simulated)
self._save_to_database(user)
# Responsibility 4: Email notifications
self._send_welcome_email(email, name)
return True
def _save_to_database(self, user: Dict) -> None:
"""Simulated database save operation"""
print(f"Saving user {user['name']} to database...")
def _send_welcome_email(self, email: str, name: str) -> None:
"""Email sending logic"""
print(f"Sending welcome email to {email}")
def generate_user_report(self) -> str:
"""Responsibility 5: Report generation"""
report = "User Report\n"
report += "=" * 20 + "\n"
for user in self.users:
report += f"ID: {user['id']}, Name: {user['name']}, Email: {user['email']}\n"
return report
# FOLLOWING Single Responsibility Principle
@dataclass
class User:
"""Data class responsible only for representing user data"""
id: int
name: str
email: str
age: int
created_at: datetime
class UserValidator:
"""Class responsible only for user data validation"""
@staticmethod
def validate_user_data(name: str, email: str, age: int) -> tuple[bool, str]:
"""Validate user input data and return validation result with message"""
if not name or not name.strip():
return False, "Name cannot be empty"
if not email or "@" not in email:
return False, "Invalid email format"
if age < 0 or age > 150:
return False, "Invalid age"
return True, "Valid"
class UserRepository:
"""Class responsible only for user data persistence"""
def __init__(self):
self._users: List[User] = []
self._next_id = 1
def save_user(self, user: User) -> None:
"""Save user to storage (simulated database operation)"""
print(f"Saving user {user.name} to database...")
self._users.append(user)
def get_all_users(self) -> List[User]:
"""Retrieve all users from storage"""
return self._users.copy()
def find_user_by_id(self, user_id: int) -> Optional[User]:
"""Find user by ID"""
for user in self._users:
if user.id == user_id:
return user
return None
def get_next_id(self) -> int:
"""Get next available user ID"""
current_id = self._next_id
self._next_id += 1
return current_id
class EmailService:
"""Class responsible only for email operations"""
@staticmethod
def send_welcome_email(email: str, name: str) -> bool:
"""Send welcome email to new user"""
print(f"Sending welcome email to {email}")
print(f"Subject: Welcome {name}!")
print(f"Body: Thank you for joining us, {name}!")
return True
@staticmethod
def send_notification_email(email: str, subject: str, body: str) -> bool:
"""Send general notification email"""
print(f"Sending email to {email}")
print(f"Subject: {subject}")
print(f"Body: {body}")
return True
class UserReportGenerator:
"""Class responsible only for generating user reports"""
def __init__(self, user_repository: UserRepository):
self._user_repository = user_repository
def generate_summary_report(self) -> str:
"""Generate a summary report of all users"""
users = self._user_repository.get_all_users()
report = "User Summary Report\n"
report += "=" * 30 + "\n"
report += f"Total Users: {len(users)}\n"
report += "-" * 30 + "\n"
for user in users:
report += f"ID: {user.id:3d} | Name: {user.name:20s} | Email: {user.email:25s} | Age: {user.age:3d}\n"
return report
def generate_json_report(self) -> str:
"""Generate a JSON report of all users"""
users = self._user_repository.get_all_users()
user_dicts = []
for user in users:
user_dicts.append({
"id": user.id,
"name": user.name,
"email": user.email,
"age": user.age,
"created_at": user.created_at.isoformat()
})
return json.dumps(user_dicts, indent=2)
class UserService:
"""
Service class that orchestrates user operations
Responsible only for coordinating between different components
"""
def __init__(self, user_repository: UserRepository, email_service: EmailService):
self._user_repository = user_repository
self._email_service = email_service
self._validator = UserValidator()
def create_user(self, name: str, email: str, age: int) -> tuple[bool, str]:
"""Create a new user using all the specialized components"""
# Validate input using validator
is_valid, message = self._validator.validate_user_data(name, email, age)
if not is_valid:
return False, f"Validation failed: {message}"
# Create user object
user = User(
id=self._user_repository.get_next_id(),
name=name,
email=email,
age=age,
created_at=datetime.now()
)
# Save user using repository
self._user_repository.save_user(user)
# Send welcome email using email service
self._email_service.send_welcome_email(email, name)
return True, f"User {name} created successfully"
def get_all_users(self) -> List[User]:
"""Get all users from repository"""
return self._user_repository.get_all_users()
# Usage demonstration
def demonstrate_srp():
print("=== Single Responsibility Principle Demo ===\n")
# Create dependencies
user_repository = UserRepository()
email_service = EmailService()
user_service = UserService(user_repository, email_service)
report_generator = UserReportGenerator(user_repository)
# Create some users
print("Creating users...")
success, message = user_service.create_user("Alice Johnson", "alice@example.com", 28)
print(f"Result: {message}\n")
success, message = user_service.create_user("Bob Smith", "bob@example.com", 35)
print(f"Result: {message}\n")
success, message = user_service.create_user("", "invalid-email", -5)
print(f"Result: {message}\n")
# Generate reports
print("Generating reports...")
print(report_generator.generate_summary_report())
print("\nJSON Report:")
print(report_generator.generate_json_report())
if __name__ == "__main__":
demonstrate_srp()
Single Responsibility Principle Benefits Demonstrated:
MAINTAINABILITY:
- Each class has a single, well-defined purpose
- Changes to validation logic only affect UserValidator
- Changes to email templates only affect EmailService
- Database schema changes only affect UserRepository
TESTABILITY:
- Each class can be tested in isolation
- Mock dependencies can be easily injected
- Test cases are focused and specific
REUSABILITY:
- UserValidator can be used in other contexts
- EmailService can send different types of emails
- UserRepository can be extended for different storage backends
FLEXIBILITY:
- Easy to swap implementations (e.g., different email providers)
- Easy to add new validation rules
- Easy to change report formats
The SRP violation (BadUserManager) makes the code:
- Hard to test (too many dependencies)
- Fragile to change (multiple reasons to modify)
- Difficult to reuse (tightly coupled responsibilities)
- Hard to understand (mixed concerns)
Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In other words, if class B is a subtype of class A, then objects of type A should be replaceable with objects of type B without altering the correctness of the program.
This principle ensures that inheritance is used correctly and that subclasses truly represent a “is-a” relationship with their parent class. It prevents the creation of subclasses that violate the expected behavior of their parent class.
The key aspects of LSP include maintaining the same method signatures, not strengthening preconditions, not weakening postconditions, and preserving the behavioral contracts of the parent class.
from abc import ABC, abstractmethod
from typing import List, Optional
from dataclasses import dataclass
import math
# VIOLATION of Liskov Substitution Principle
class BadBird:
"""Base bird class that assumes all birds can fly - LSP violation setup"""
def __init__(self, name: str, species: str):
self.name = name
self.species = species
def fly(self) -> str:
"""All birds should be able to fly according to this design"""
return f"{self.name} is flying high in the sky!"
def make_sound(self) -> str:
"""All birds make some sound"""
return f"{self.name} makes a sound"
class BadEagle(BadBird):
"""Eagle implementation - works fine with base class contract"""
def fly(self) -> str:
return f"{self.name} soars majestically through the clouds!"
def make_sound(self) -> str:
return f"{self.name} screeches loudly!"
class BadPenguin(BadBird):
"""
Penguin implementation - VIOLATES LSP!
Penguins can't fly, so they break the expected behavior of Bird
"""
def fly(self) -> str:
# This violates LSP - changes expected behavior
raise NotImplementedError("Penguins cannot fly!")
def make_sound(self) -> str:
return f"{self.name} trumpets and brays!"
def bad_bird_sanctuary(birds: List[BadBird]) -> None:
"""
This function breaks when BadPenguin is passed
because it violates LSP expectations
"""
print("Making all birds fly:")
for bird in birds:
try:
print(f" {bird.fly()}")
except NotImplementedError as e:
print(f" ERROR: {e}")
# FOLLOWING Liskov Substitution Principle
@dataclass
class BirdStats:
"""Data class for bird statistics"""
wingspan: float # in meters
weight: float # in kilograms
max_speed: float # in km/h
class Bird(ABC):
"""
Abstract base class for birds with proper LSP design
Only defines behaviors that ALL birds can perform
"""
def __init__(self, name: str, species: str, stats: BirdStats):
self.name = name
self.species = species
self.stats = stats
@abstractmethod
def make_sound(self) -> str:
"""All birds can make sounds"""
pass
@abstractmethod
def eat(self) -> str:
"""All birds need to eat"""
pass
def get_info(self) -> str:
"""Get basic information about the bird"""
return f"{self.name} is a {self.species}"
class FlyingBird(Bird):
"""
Intermediate class for birds that can fly
This preserves LSP by only including flying behavior for birds that can actually fly
"""
@abstractmethod
def fly(self) -> str:
"""Flying birds can fly"""
pass
def get_flight_altitude(self) -> float:
"""Get typical flight altitude in meters"""
# Default implementation based on wingspan
return self.stats.wingspan * 100
class SwimmingBird(Bird):
"""
Intermediate class for birds that can swim
This allows swimming behavior without violating LSP for non-swimming birds
"""
@abstractmethod
def swim(self) -> str:
"""Swimming birds can swim"""
pass
def get_dive_depth(self) -> float:
"""Get maximum dive depth in meters"""
# Default implementation based on weight (heavier birds dive deeper)
return self.stats.weight * 2
class Eagle(FlyingBird):
"""Eagle - can fly but cannot swim"""
def fly(self) -> str:
altitude = self.get_flight_altitude()
return f"{self.name} soars at {altitude:.0f}m altitude, hunting for prey!"
def make_sound(self) -> str:
return f"{self.name} lets out a piercing screech!"
def eat(self) -> str:
return f"{self.name} catches fish with its sharp talons"
class Penguin(SwimmingBird):
"""
Penguin - can swim but cannot fly
This does NOT violate LSP because it doesn't inherit fly() method
"""
def swim(self) -> str:
depth = self.get_dive_depth()
return f"{self.name} dives to {depth:.1f}m depth, chasing fish!"
def make_sound(self) -> str:
return f"{self.name} trumpets and calls to its colony!"
def eat(self) -> str:
return f"{self.name} catches krill and small fish while swimming"
class Duck(FlyingBird, SwimmingBird):
"""
Duck - can both fly and swim
Multiple inheritance is okay here because both interfaces are compatible
"""
def fly(self) -> str:
return f"{self.name} flies in formation with other ducks!"
def swim(self) -> str:
return f"{self.name} paddles gracefully on the water surface!"
def make_sound(self) -> str:
return f"{self.name} quacks loudly!"
def eat(self) -> str:
return f"{self.name} dabbles for aquatic plants and insects"
class Ostrich(Bird):
"""
Ostrich - cannot fly or swim, only inherits basic Bird behavior
This preserves LSP by not inheriting inappropriate methods
"""
def make_sound(self) -> str:
return f"{self.name} makes deep booming calls!"
def eat(self) -> str:
return f"{self.name} pecks at seeds, fruits, and small animals"
def run(self) -> str:
"""Ostriches have their own special ability"""
return f"{self.name} runs at speeds up to {self.stats.max_speed} km/h!"
# LSP-compliant functions that work with any Bird subtype
class BirdSanctuary:
"""Bird sanctuary that respects LSP by working with appropriate bird types"""
def __init__(self):
self.all_birds: List[Bird] = []
self.flying_birds: List[FlyingBird] = []
self.swimming_birds: List[SwimmingBird] = []
def add_bird(self, bird: Bird) -> None:
"""Add a bird to the sanctuary and categorize it properly"""
self.all_birds.append(bird)
# LSP compliance: we can safely check types and add to appropriate collections
if isinstance(bird, FlyingBird):
self.flying_birds.append(bird)
if isinstance(bird, SwimmingBird):
self.swimming_birds.append(bird)
def make_all_birds_vocalize(self) -> List[str]:
"""Make all birds make their sounds - works with ANY Bird subtype (LSP compliant)"""
sounds = []
for bird in self.all_birds:
sounds.append(bird.make_sound())
return sounds
def feed_all_birds(self) -> List[str]:
"""Feed all birds - works with ANY Bird subtype (LSP compliant)"""
feeding_results = []
for bird in self.all_birds:
feeding_results.append(bird.eat())
return feeding_results
def organize_flight_show(self) -> List[str]:
"""Organize flight show - only works with birds that can actually fly"""
flight_results = []
for bird in self.flying_birds:
# LSP compliance: all FlyingBird subtypes can safely call fly()
flight_results.append(bird.fly())
return flight_results
def organize_swimming_exhibition(self) -> List[str]:
"""Organize swimming exhibition - only works with birds that can swim"""
swimming_results = []
for bird in self.swimming_birds:
# LSP compliance: all SwimmingBird subtypes can safely call swim()
swimming_results.append(bird.swim())
return swimming_results
def get_sanctuary_report(self) -> str:
"""Generate a comprehensive report about all birds"""
report = "Bird Sanctuary Report\n"
report += "=" * 40 + "\n"
report += f"Total Birds: {len(self.all_birds)}\n"
report += f"Flying Birds: {len(self.flying_birds)}\n"
report += f"Swimming Birds: {len(self.swimming_birds)}\n"
report += "-" * 40 + "\n"
for bird in self.all_birds:
report += f"\n{bird.get_info()}\n"
report += f" Stats: {bird.stats.wingspan}m wingspan, {bird.stats.weight}kg, max speed {bird.stats.max_speed}km/h\n"
# Check capabilities without violating LSP
capabilities = []
if isinstance(bird, FlyingBird):
capabilities.append("Flying")
if isinstance(bird, SwimmingBird):
capabilities.append("Swimming")
if hasattr(bird, 'run'):
capabilities.append("Running")
report += f" Capabilities: {', '.join(capabilities) if capabilities else 'Basic bird behaviors'}\n"
return report
# Demonstration of LSP compliance
class BirdBehaviorAnalyzer:
"""
Analyzer that works with Bird objects and respects LSP
Can work with any Bird subtype without knowing the specific implementation
"""
@staticmethod
def analyze_bird_sounds(birds: List[Bird]) -> dict[str, int]:
"""Analyze bird sounds - works with ANY Bird subtype"""
sound_analysis = {}
for bird in birds:
# LSP compliance: we can call make_sound() on any Bird
sound = bird.make_sound()
# Simple analysis based on sound characteristics
if "screech" in sound.lower() or "piercing" in sound.lower():
sound_type = "Predator Call"
elif "quack" in sound.lower():
sound_type = "Water Bird Call"
elif "trumpet" in sound.lower() or "call" in sound.lower():
sound_type = "Social Call"
elif "boom" in sound.lower():
sound_type = "Deep Call"
else:
sound_type = "General Call"
sound_analysis[sound_type] = sound_analysis.get(sound_type, 0) + 1
return sound_analysis
@staticmethod
def calculate_average_stats(birds: List[Bird]) -> dict[str, float]:
"""Calculate average statistics - works with ANY Bird subtype"""
if not birds:
return {}
total_wingspan = sum(bird.stats.wingspan for bird in birds)
total_weight = sum(bird.stats.weight for bird in birds)
total_speed = sum(bird.stats.max_speed for bird in birds)
count = len(birds)
return {
"average_wingspan": round(total_wingspan / count, 2),
"average_weight": round(total_weight / count, 2),
"average_max_speed": round(total_speed / count, 2)
}
# Usage demonstration
def demonstrate_lsp():
print("=== Liskov Substitution Principle Demo ===\n")
# First, show the LSP violation
print("1. LSP VIOLATION Example:")
print("-------------------------")
bad_eagle = BadEagle("Storm", "Bald Eagle")
bad_penguin = BadPenguin("Wadsworth", "Emperor Penguin")
print("Individual bird behaviors:")
print(f" {bad_eagle.fly()}")
print(f" Trying penguin: ", end="")
try:
print(bad_penguin.fly())
except NotImplementedError as e:
print(f"ERROR - {e}")
print("\nTrying to use birds polymorphically (this breaks):")
bad_bird_sanctuary([bad_eagle, bad_penguin])
print("\n" + "="*60 + "\n")
# Now show LSP compliance
print("2. LSP COMPLIANT Example:")
print("-------------------------")
# Create birds with proper stats
eagle_stats = BirdStats(wingspan=2.3, weight=4.5, max_speed=120)
penguin_stats = BirdStats(wingspan=0.0, weight=25.0, max_speed=8) # Swimming speed
duck_stats = BirdStats(wingspan=1.0, weight=1.2, max_speed=65)
ostrich_stats = BirdStats(wingspan=0.0, weight=120.0, max_speed=70)
# Create bird instances
eagle = Eagle("Storm", "Bald Eagle", eagle_stats)
penguin = Penguin("Wadsworth", "Emperor Penguin", penguin_stats)
duck = Duck("Quackers", "Mallard Duck", duck_stats)
ostrich = Ostrich("Speedy", "Common Ostrich", ostrich_stats)
# Create sanctuary and add birds
sanctuary = BirdSanctuary()
birds = [eagle, penguin, duck, ostrich]
for bird in birds:
sanctuary.add_bird(bird)
print("All birds can perform basic behaviors (LSP compliance):")
sounds = sanctuary.make_all_birds_vocalize()
for sound in sounds:
print(f" {sound}")
print("\nFeeding time (works with all Bird subtypes):")
feeding = sanctuary.feed_all_birds()
for feed_result in feeding:
print(f" {feed_result}")
print("\nFlight show (only flying birds participate):")
flight_show = sanctuary.organize_flight_show()
for flight in flight_show:
print(f" {flight}")
print("\nSwimming exhibition (only swimming birds participate):")
swimming_show = sanctuary.organize_swimming_exhibition()
for swim in swimming_show:
print(f" {swim}")
# Demonstrate polymorphic behavior analysis
print("\n" + "-"*40)
analyzer = BirdBehaviorAnalyzer()
print("Sound Analysis (works with any Bird subtype):")
sound_analysis = analyzer.analyze_bird_sounds(birds)
for sound_type, count in sound_analysis.items():
print(f" {sound_type}: {count}")
print("\nStatistical Analysis (works with any Bird subtype):")
stats = analyzer.calculate_average_stats(birds)
for stat_name, value in stats.items():
print(f" {stat_name}: {value}")
print("\n" + "-"*40)
print("Sanctuary Report:")
print(sanctuary.get_sanctuary_report())
# Demonstrate special behaviors that don't violate LSP
print("Special Behaviors (type-specific, but doesn't violate LSP):")
if hasattr(ostrich, 'run'):
print(f" {ostrich.run()}")
if __name__ == "__main__":
demonstrate_lsp()
Liskov Substitution Principle Benefits Demonstrated:
BEHAVIORAL CONSISTENCY:
- All Bird subtypes can be used interchangeably for common behaviors
- make_sound() and eat() work consistently across all bird types
- No unexpected exceptions or broken contracts
PROPER INHERITANCE HIERARCHY:
- FlyingBird and SwimmingBird only include birds that actually have those capabilities
- No forced implementation of inappropriate methods
- Each subclass truly “is-a” valid substitute for its parent
POLYMORPHISM WITHOUT SURPRISES:
- BirdSanctuary methods work with any Bird subtype
- BirdBehaviorAnalyzer functions work with any Bird collection
- Client code doesn’t need to know specific bird types
TYPE SAFETY:
- Compile-time and runtime type safety maintained
- No need for try-catch blocks to handle inappropriate method calls
- Clear contracts that all subtypes must honor
The LSP violation (BadPenguin) demonstrates problems:
- Throws exceptions for inherited methods (violates contract)
- Cannot be used polymorphically with other birds
- Forces client code to handle special cases
- Breaks the “is-a” relationship expectation
The LSP-compliant design ensures:
- All subtypes honor their parent’s contracts
- Substitutability without breaking functionality
- Clear separation of capabilities through proper inheritance
- Robust polymorphic behavior throughout the system
Key LSP Guidelines Followed:
- Preconditions cannot be strengthened by subtypes
- Postconditions cannot be weakened by subtypes
- Invariants of the supertype must be preserved
- History constraint (new methods may be added, but existing behavior preserved)
Interface Segregation Principle (ISP)
The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. This means that larger interfaces should be split into smaller, more specific ones so that clients only need to know about the methods that are relevant to them.
This principle helps reduce coupling between classes and makes the system more modular and flexible. It prevents the creation of “fat” interfaces that force implementing classes to provide implementations for methods they don’t actually need.
ISP encourages the creation of role-based interfaces that are focused on specific client needs rather than general-purpose interfaces that try to serve all possible clients.## Dependency Inversion Principle (DIP)
from abc import ABC, abstractmethod
from typing import List, Optional, Protocol
from dataclasses import dataclass
from enum import Enum
# VIOLATION of Interface Segregation Principle
class BadMultiFunctionDevice(ABC):
"""
Fat interface that violates ISP by forcing all devices to implement
all methods, even if they don't support all functionalities
"""
@abstractmethod
def print_document(self, document: str) -> bool:
"""Print a document"""
pass
@abstractmethod
def scan_document(self) -> str:
"""Scan a document"""
pass
@abstractmethod
def fax_document(self, document: str, number: str) -> bool:
"""Send a fax"""
pass
@abstractmethod
def copy_document(self) -> str:
"""Copy a document"""
pass
@abstractmethod
def email_document(self, document: str, recipient: str) -> bool:
"""Email a document"""
pass
class BadAllInOnePrinter(BadMultiFunctionDevice):
"""All-in-one printer that can do everything"""
def print_document(self, document: str) -> bool:
print(f"All-in-One: Printing document: {document}")
return True
def scan_document(self) -> str:
print("All-in-One: Scanning document...")
return "Scanned document content"
def fax_document(self, document: str, number: str) -> bool:
print(f"All-in-One: Faxing '{document}' to {number}")
return True
def copy_document(self) -> str:
print("All-in-One: Copying document...")
return "Copied document"
def email_document(self, document: str, recipient: str) -> bool:
print(f"All-in-One: Emailing '{document}' to {recipient}")
return True
class BadBasicPrinter(BadMultiFunctionDevice):
"""
Basic printer that only supports printing - VIOLATES ISP!
Forced to implement methods it doesn't support
"""
def print_document(self, document: str) -> bool:
print(f"Basic Printer: Printing document: {document}")
return True
def scan_document(self) -> str:
# ISP Violation: Forced to implement unsupported functionality
raise NotImplementedError("Basic printer cannot scan")
def fax_document(self, document: str, number: str) -> bool:
# ISP Violation: Forced to implement unsupported functionality
raise NotImplementedError("Basic printer cannot fax")
def copy_document(self) -> str:
# ISP Violation: Forced to implement unsupported functionality
raise NotImplementedError("Basic printer cannot copy")
def email_document(self, document: str, recipient: str) -> bool:
# ISP Violation: Forced to implement unsupported functionality
raise NotImplementedError("Basic printer cannot email")
def bad_device_manager(devices: List[BadMultiFunctionDevice]) -> None:
"""
This function breaks when it tries to use methods that some devices don't support
"""
print("Trying to use all devices for all functions:")
for device in devices:
try:
device.print_document("Test document")
device.scan_document()
device.fax_document("Test fax", "555-1234")
except NotImplementedError as e:
print(f" ERROR: {e}")
# FOLLOWING Interface Segregation Principle
@dataclass
class Document:
"""Simple document representation"""
title: str
content: str
pages: int = 1
class Printer(Protocol):
"""Focused interface for printing functionality only"""
def print_document(self, document: Document) -> bool:
"""Print a document"""
...
class Scanner(Protocol):
"""Focused interface for scanning functionality only"""
def scan_document(self) -> Document:
"""Scan a document and return it"""
...
class FaxMachine(Protocol):
"""Focused interface for fax functionality only"""
def send_fax(self, document: Document, phone_number: str) -> bool:
"""Send a fax"""
...
class Copier(Protocol):
"""Focused interface for copying functionality only"""
def copy_document(self, document: Document, copies: int = 1) -> List[Document]:
"""Copy a document"""
...
class EmailSender(Protocol):
"""Focused interface for email functionality only"""
def send_email(self, document: Document, recipient: str, subject: str) -> bool:
"""Send document via email"""
...
# Concrete implementations that only implement what they actually support
class BasicPrinter:
"""Basic printer that only implements Printer interface"""
def __init__(self, name: str):
self.name = name
def print_document(self, document: Document) -> bool:
"""Print a document"""
print(f"{self.name}: Printing '{document.title}' ({document.pages} pages)")
print(f" Content preview: {document.content[:50]}...")
return True
class DocumentScanner:
"""Scanner device that only implements Scanner interface"""
def __init__(self, name: str, resolution: int = 300):
self.name = name
self.resolution = resolution
def scan_document(self) -> Document:
"""Scan a document"""
print(f"{self.name}: Scanning document at {self.resolution} DPI...")
return Document(
title="Scanned Document",
content="This is the scanned content of the document",
pages=1
)
class NetworkFax:
"""Fax machine that only implements FaxMachine interface"""
def __init__(self, name: str, fax_number: str):
self.name = name
self.fax_number = fax_number
def send_fax(self, document: Document, phone_number: str) -> bool:
"""Send a fax"""
print(f"{self.name}: Sending fax '{document.title}' to {phone_number}")
print(f" From: {self.fax_number}")
print(f" Pages: {document.pages}")
return True
class DigitalCopier:
"""Copier that only implements Copier interface"""
def __init__(self, name: str):
self.name = name
def copy_document(self, document: Document, copies: int = 1) -> List[Document]:
"""Copy a document"""
print(f"{self.name}: Making {copies} copies of '{document.title}'")
copied_documents = []
for i in range(copies):
copy = Document(
title=f"{document.title} (Copy {i+1})",
content=document.content,
pages=document.pages
)
copied_documents.append(copy)
return copied_documents
class EmailGateway:
"""Email service that only implements EmailSender interface"""
def __init__(self, smtp_server: str):
self.smtp_server = smtp_server
def send_email(self, document: Document, recipient: str, subject: str) -> bool:
"""Send document via email"""
print(f"Email Gateway: Sending '{document.title}' to {recipient}")
print(f" Subject: {subject}")
print(f" Server: {self.smtp_server}")
print(f" Attachment: {document.pages} pages")
return True
# Multi-function devices that implement multiple focused interfaces
class AllInOneDevice:
"""
All-in-one device that implements multiple interfaces
ISP Compliant: Each interface is focused and cohesive
"""
def __init__(self, name: str):
self.name = name
# Implements Printer interface
def print_document(self, document: Document) -> bool:
print(f"{self.name} (All-in-One): Printing '{document.title}'")
return True
# Implements Scanner interface
def scan_document(self) -> Document:
print(f"{self.name} (All-in-One): Scanning document...")
return Document(
title="All-in-One Scanned Doc",
content="Content scanned by all-in-one device",
pages=1
)
# Implements Copier interface
def copy_document(self, document: Document, copies: int = 1) -> List[Document]:
print(f"{self.name} (All-in-One): Copying '{document.title}' x{copies}")
return [Document(
title=f"{document.title} (AIO Copy {i+1})",
content=document.content,
pages=document.pages
) for i in range(copies)]
# Implements FaxMachine interface
def send_fax(self, document: Document, phone_number: str) -> bool:
print(f"{self.name} (All-in-One): Faxing '{document.title}' to {phone_number}")
return True
class PrinterScannerCombo:
"""
Printer-Scanner combo that only implements what it supports
ISP Compliant: Only implements Printer and Scanner interfaces
"""
def __init__(self, name: str):
self.name = name
def print_document(self, document: Document) -> bool:
print(f"{self.name} (Printer-Scanner): Printing '{document.title}'")
return True
def scan_document(self) -> Document:
print(f"{self.name} (Printer-Scanner): Scanning document...")
return Document(
title="Combo Scanned Doc",
content="Content from printer-scanner combo",
pages=1
)
# Client classes that depend only on the interfaces they need
class DocumentPrintingService:
"""Service that only needs printing capability - depends only on Printer"""
def __init__(self, printer: Printer):
self.printer = printer
def print_documents(self, documents: List[Document]) -> List[bool]:
"""Print multiple documents"""
results = []
print("Document Printing Service:")
for doc in documents:
result = self.printer.print_document(doc)
results.append(result)
return results
class DocumentDigitizationService:
"""Service that only needs scanning capability - depends only on Scanner"""
def __init__(self, scanner: Scanner):
self.scanner = scanner
def digitize_documents(self, count: int) -> List[Document]:
"""Scan multiple documents"""
documents = []
print("Document Digitization Service:")
for i in range(count):
doc = self.scanner.scan_document()
documents.append(doc)
return documents
class DocumentDistributionService:
"""
Service that handles document distribution
Depends only on the interfaces it actually uses
"""
def __init__(self,
fax_machine: Optional[FaxMachine] = None,
email_sender: Optional[EmailSender] = None):
self.fax_machine = fax_machine
self.email_sender = email_sender
def distribute_document(self, document: Document,
fax_number: Optional[str] = None,
email_recipient: Optional[str] = None) -> dict[str, bool]:
"""Distribute document via available channels"""
results = {}
print("Document Distribution Service:")
if fax_number and self.fax_machine:
results['fax'] = self.fax_machine.send_fax(document, fax_number)
if email_recipient and self.email_sender:
subject = f"Document: {document.title}"
results['email'] = self.email_sender.send_email(document, email_recipient, subject)
return results
class DocumentWorkflowManager:
"""
Workflow manager that coordinates different services
Each service only depends on interfaces it needs (ISP compliant)
"""
def __init__(self):
self.services = {}
def register_printer(self, printer: Printer) -> None:
"""Register a printer service"""
self.services['printing'] = DocumentPrintingService(printer)
def register_scanner(self, scanner: Scanner) -> None:
"""Register a scanner service"""
self.services['scanning'] = DocumentDigitizationService(scanner)
def register_copier(self, copier: Copier) -> None:
"""Register a copier (stores directly since we'll use it directly)"""
self.services['copier'] = copier
def register_distribution(self, fax: Optional[FaxMachine] = None,
email: Optional[EmailSender] = None) -> None:
"""Register distribution services"""
self.services['distribution'] = DocumentDistributionService(fax, email)
def process_workflow(self, original_doc: Document) -> dict[str, any]:
"""Process a complete document workflow"""
results = {}
print("Starting Document Workflow:")
print("=" * 40)
# Print original document
if 'printing' in self.services:
print_results = self.services['printing'].print_documents([original_doc])
results['printing'] = print_results
# Make copies
if 'copier' in self.services:
copies = self.services['copier'].copy_document(original_doc, 2)
results['copies'] = len(copies)
print(f"Created {len(copies)} copies")
# Distribute via fax and email
if 'distribution' in self.services:
dist_results = self.services['distribution'].distribute_document(
original_doc,
fax_number="555-FAX-DOC",
email_recipient="recipient@example.com"
)
results['distribution'] = dist_results
return results
# Usage demonstration
def demonstrate_isp():
print("=== Interface Segregation Principle Demo ===\n")
# First show ISP violation
print("1. ISP VIOLATION Example:")
print("-------------------------")
bad_all_in_one = BadAllInOnePrinter()
bad_basic = BadBasicPrinter()
print("All-in-one printer (works fine):")
bad_all_in_one.print_document("Test document")
bad_all_in_one.scan_document()
print("\nBasic printer (forced to implement unsupported methods):")
bad_device_manager([bad_basic])
print("\n" + "="*60 + "\n")
# Now show ISP compliance
print("2. ISP COMPLIANT Example:")
print("-------------------------")
# Create test document
test_doc = Document(
title="Important Report",
content="This is a very important business report with crucial information.",
pages=3
)
# Create devices that only implement what they support
basic_printer = BasicPrinter("HP LaserJet")
scanner = DocumentScanner("Canon Scanner", 600)
fax_machine = NetworkFax("Brother Fax", "555-FAX-1234")
copier = DigitalCopier("Xerox Copier")
email_gateway = EmailGateway("smtp.company.com")
# Create multi-function devices
all_in_one = AllInOneDevice("Canon All-in-One")
printer_scanner = PrinterScannerCombo("HP OfficeJet")
print("Testing individual devices (each implements only what it supports):")
# Test basic printer (only printing)
basic_printer.print_document(test_doc)
# Test scanner (only scanning)
scanned_doc = scanner.scan_document()
# Test fax machine (only faxing)
fax_machine.send_fax(test_doc, "555-DEST-FAX")
print("\n" + "-"*40)
print("Testing services that depend only on needed interfaces:")
# Create services that only depend on what they need
print_service = DocumentPrintingService(basic_printer)
scan_service = DocumentDigitizationService(scanner)
distribution_service = DocumentDistributionService(fax_machine, email_gateway)
# Test services
print_service.print_documents([test_doc])
print()
digitized_docs = scan_service.digitize_documents(2)
print()
dist_results = distribution_service.distribute_document(
test_doc,
fax_number="555-RECIPIENT",
email_recipient="boss@company.com"
)
print(f"Distribution results: {dist_results}")
print("\n" + "-"*40)
print("Testing workflow with multi-function device:")
# Create workflow manager and register services
workflow = DocumentWorkflowManager()
workflow.register_printer(all_in_one) # All-in-one as printer
workflow.register_scanner(all_in_one) # Same device as scanner
workflow.register_copier(all_in_one) # Same device as copier
workflow.register_distribution(all_in_one, email_gateway) # Mixed devices
# Process complete workflow
workflow_results = workflow.process_workflow(test_doc)
print(f"\nWorkflow completed with results: {workflow_results}")
print("\n" + "-"*40)
print("ISP Benefits Demonstrated:")
print("• Each interface is focused and cohesive")
print("• Clients depend only on methods they use")
print("• Devices implement only supported functionality")
print("• Easy to compose services from different devices")
print("• No forced implementation of unsupported methods")
if __name__ == "__main__":
demonstrate_isp()
Interface Segregation Principle Benefits Demonstrated:
FOCUSED INTERFACES:
- Printer, Scanner, FaxMachine, etc. are small, focused interfaces
- Each interface has a single, well-defined responsibility
- Clients can depend on exactly what they need
NO FORCED DEPENDENCIES:
- BasicPrinter only implements Printer interface
- DocumentScanner only implements Scanner interface
- No device is forced to implement unsupported functionality
FLEXIBLE COMPOSITION:
- Services can be composed from different devices
- AllInOneDevice can serve multiple roles
- Easy to mix and match different implementations
EASIER TESTING:
- Each interface can be mocked independently
- Tests focus only on relevant functionality
- No need to mock unused methods
BETTER MODULARITY:
- Changes to one interface don’t affect others
- New device types can implement any combination of interfaces
- Services are loosely coupled to specific implementations
The ISP violation (BadMultiFunctionDevice) problems:
- Forces all devices to implement all methods
- Creates unnecessary dependencies
- Leads to NotImplementedError exceptions
- Makes testing more complex
- Violates single responsibility at interface level
The ISP-compliant design ensures:
- Each interface serves a specific client need
- No client depends on methods it doesn’t use
- Easy to implement partial functionality
- Clean separation of concerns
- Robust composition patterns
Key ISP Guidelines Followed:
- Keep interfaces small and focused
- Group related methods that change together
- Prefer many client-specific interfaces over one general-purpose interface
- Avoid “fat” interfaces that serve multiple client types
Dependency Inversion Principle (DIP)
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.
This principle helps create flexible, maintainable code by inverting the traditional dependency structure. Instead of high-level classes depending directly on low-level implementation classes, both depend on abstract interfaces or protocols.
DIP enables loose coupling, makes testing easier through dependency injection, and allows for easy swapping of implementations without affecting