🧩 The ISP Rule
“Clients should not be forced to depend on methods they don’t use.”Imagine a Swiss Army knife 🔪 vs specialized tools:
- Swiss Army knife: Has everything, but each tool is mediocre
- Specialized tools: Each does one thing perfectly
Simple Rule: Split big interfaces into small, focused ones. Classes only implement what they need!
🚨 The Problem: Fat Interfaces
❌ BAD: One Giant Interface
Copy
from abc import ABC, abstractmethod
class Worker(ABC):
"""Every worker must do ALL of these... 😰"""
@abstractmethod
def work(self):
pass
@abstractmethod
def eat_lunch(self):
pass
@abstractmethod
def attend_meeting(self):
pass
@abstractmethod
def code(self):
pass
@abstractmethod
def design(self):
pass
@abstractmethod
def manage_team(self):
pass
class Developer(Worker):
def work(self):
print("Writing code...")
def eat_lunch(self):
print("Eating pizza...")
def attend_meeting(self):
print("In standup...")
def code(self):
print("Coding Python...")
def design(self):
# 😰 Developer doesn't design UI!
raise NotImplementedError("I'm not a designer!")
def manage_team(self):
# 😰 Developer doesn't manage!
raise NotImplementedError("I'm not a manager!")
class Robot(Worker):
def work(self):
print("Processing...")
def eat_lunch(self):
# 😰 Robots don't eat!
raise NotImplementedError("I don't eat!")
def attend_meeting(self):
# 😰 Robots don't attend meetings!
raise NotImplementedError("I don't attend meetings!")
def code(self):
print("Auto-generating code...")
def design(self):
raise NotImplementedError("I don't design!")
def manage_team(self):
raise NotImplementedError("I don't manage!")
# 🚨 Most methods throw exceptions - BAD!
✅ GOOD: Small, Focused Interfaces
Copy
from abc import ABC, abstractmethod
# Split into small, focused interfaces
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Meetable(ABC):
@abstractmethod
def attend_meeting(self):
pass
class Codeable(ABC):
@abstractmethod
def code(self):
pass
class Designable(ABC):
@abstractmethod
def design(self):
pass
class Manageable(ABC):
@abstractmethod
def manage_team(self):
pass
# Now each class implements ONLY what it needs!
class Developer(Workable, Eatable, Meetable, Codeable):
def work(self):
print("💻 Working on features...")
def eat(self):
print("🍕 Eating pizza...")
def attend_meeting(self):
print("🗣️ In standup...")
def code(self):
print("🐍 Writing Python...")
class Designer(Workable, Eatable, Meetable, Designable):
def work(self):
print("🎨 Creating mockups...")
def eat(self):
print("🥗 Eating salad...")
def attend_meeting(self):
print("🗣️ In design review...")
def design(self):
print("🖌️ Designing UI...")
class Manager(Workable, Eatable, Meetable, Manageable):
def work(self):
print("📊 Planning sprints...")
def eat(self):
print("☕ Coffee meeting...")
def attend_meeting(self):
print("🗣️ Leading meeting...")
def manage_team(self):
print("👥 Managing team...")
class Robot(Workable, Codeable):
"""Robot only works and codes - no eating or meetings!"""
def work(self):
print("🤖 Processing tasks...")
def code(self):
print("🤖 Auto-generating code...")
# 🎉 No NotImplementedError needed!
# Each class only has methods it actually uses
🖨️ Classic Example: Printer Interfaces
❌ BAD: Multifunction Monster
Copy
class MultifunctionDevice(ABC):
"""All devices must do EVERYTHING!"""
@abstractmethod
def print_doc(self, document):
pass
@abstractmethod
def scan_doc(self, document):
pass
@abstractmethod
def fax_doc(self, document):
pass
@abstractmethod
def copy_doc(self, document):
pass
@abstractmethod
def email_doc(self, document, email):
pass
class OldPrinter(MultifunctionDevice):
"""Old printer can only print! 😢"""
def print_doc(self, document):
print(f"🖨️ Printing: {document}")
def scan_doc(self, document):
raise Exception("Can't scan!") # 😰
def fax_doc(self, document):
raise Exception("Can't fax!") # 😰
def copy_doc(self, document):
raise Exception("Can't copy!") # 😰
def email_doc(self, document, email):
raise Exception("Can't email!") # 😰
✅ GOOD: Segregated Interfaces
Copy
from abc import ABC, abstractmethod
class Printer(ABC):
@abstractmethod
def print_doc(self, document):
pass
class Scanner(ABC):
@abstractmethod
def scan_doc(self) -> str:
pass
class Fax(ABC):
@abstractmethod
def fax_doc(self, document, number):
pass
class Copier(ABC):
@abstractmethod
def copy_doc(self, document) -> str:
pass
class Emailer(ABC):
@abstractmethod
def email_doc(self, document, email):
pass
# Simple printer - only prints!
class BasicPrinter(Printer):
def print_doc(self, document):
print(f"🖨️ Printing: {document}")
# Scanner - only scans!
class BasicScanner(Scanner):
def scan_doc(self):
return "📄 Scanned document content"
# All-in-one for those who need everything
class AllInOnePrinter(Printer, Scanner, Copier, Fax, Emailer):
def print_doc(self, document):
print(f"🖨️ Printing: {document}")
def scan_doc(self):
print("📸 Scanning...")
return "Scanned content"
def copy_doc(self, document):
print("📋 Copying...")
return document
def fax_doc(self, document, number):
print(f"📠 Faxing to {number}...")
def email_doc(self, document, email):
print(f"📧 Emailing to {email}...")
# Functions request only what they need
def print_report(printer: Printer, report):
printer.print_doc(report)
def scan_and_save(scanner: Scanner):
content = scanner.scan_doc()
return content
# Works with any device that can print!
print_report(BasicPrinter(), "Sales Report")
print_report(AllInOnePrinter(), "Sales Report")
# Works with any device that can scan!
scan_and_save(AllInOnePrinter())
# scan_and_save(BasicPrinter()) # ❌ Type error - BasicPrinter can't scan
🎮 Real Example: Game Characters
Copy
from abc import ABC, abstractmethod
# Segregated ability interfaces
class Movable(ABC):
@abstractmethod
def move(self, direction):
pass
class Attackable(ABC):
@abstractmethod
def attack(self, target):
pass
class Healable(ABC):
@abstractmethod
def heal(self, target):
pass
class Flyable(ABC):
@abstractmethod
def fly(self):
pass
class Swimmable(ABC):
@abstractmethod
def swim(self):
pass
class Castable(ABC):
@abstractmethod
def cast_spell(self, spell, target):
pass
# Characters implement only what they can do!
class Warrior(Movable, Attackable):
def move(self, direction):
print(f"⚔️ Warrior walking {direction}")
def attack(self, target):
print(f"⚔️ Warrior slashes {target}!")
class Mage(Movable, Attackable, Castable, Healable):
def move(self, direction):
print(f"🧙 Mage walking {direction}")
def attack(self, target):
print(f"🧙 Mage hits {target} with staff!")
def cast_spell(self, spell, target):
print(f"✨ Mage casts {spell} on {target}!")
def heal(self, target):
print(f"💚 Mage heals {target}!")
class Dragon(Movable, Attackable, Flyable):
def move(self, direction):
print(f"🐉 Dragon stomping {direction}")
def attack(self, target):
print(f"🐉 Dragon breathes fire on {target}!")
def fly(self):
print("🐉 Dragon soars into the sky!")
class Fish(Movable, Swimmable):
def move(self, direction):
print(f"🐟 Fish swimming {direction}")
def swim(self):
print("🐟 Fish diving deep!")
class Turret(Attackable): # Can't move!
def attack(self, target):
print(f"🔫 Turret fires at {target}!")
# Game engine uses specific interfaces
def process_flying_units(units: list[Flyable]):
for unit in units:
unit.fly()
def process_attackers(units: list[Attackable]):
for unit in units:
unit.attack("enemy")
# Usage
flying_units = [Dragon()] # Only things that can fly
process_flying_units(flying_units)
attackers = [Warrior(), Mage(), Dragon(), Turret()] # All attackers
process_attackers(attackers)
📱 Real Example: Mobile App Features
Copy
from abc import ABC, abstractmethod
# Feature interfaces
class Photographable(ABC):
@abstractmethod
def take_photo(self):
pass
class Callable(ABC):
@abstractmethod
def make_call(self, number):
pass
class Textable(ABC):
@abstractmethod
def send_text(self, number, message):
pass
class GPSEnabled(ABC):
@abstractmethod
def get_location(self):
pass
class BluetoothEnabled(ABC):
@abstractmethod
def connect_bluetooth(self, device):
pass
class NFCEnabled(ABC):
@abstractmethod
def nfc_pay(self, amount):
pass
# Different devices implement different features
class BasicPhone(Callable, Textable):
"""Old flip phone - calls and texts only!"""
def make_call(self, number):
print(f"📞 Calling {number}...")
def send_text(self, number, message):
print(f"💬 Texting {number}: {message}")
class SmartPhone(Callable, Textable, Photographable, GPSEnabled, BluetoothEnabled, NFCEnabled):
"""Modern smartphone - has everything!"""
def make_call(self, number):
print(f"📱 Video calling {number}...")
def send_text(self, number, message):
print(f"💬 iMessaging {number}: {message}")
def take_photo(self):
print("📸 Taking high-res photo!")
def get_location(self):
return {"lat": 40.7128, "lng": -74.0060}
def connect_bluetooth(self, device):
print(f"🔵 Connecting to {device}...")
def nfc_pay(self, amount):
print(f"💳 Paying ${amount} via NFC...")
class Camera(Photographable):
"""Dedicated camera - only takes photos!"""
def take_photo(self):
print("📷 Taking professional photo!")
class GPSDevice(GPSEnabled):
"""Dedicated GPS - only navigation!"""
def get_location(self):
return {"lat": 40.7128, "lng": -74.0060}
# Functions request only what they need
def take_photos(devices: list[Photographable]):
for device in devices:
device.take_photo()
def navigate(device: GPSEnabled):
loc = device.get_location()
print(f"📍 You are at {loc}")
# All of these work!
take_photos([SmartPhone(), Camera()]) # ✅
navigate(SmartPhone()) # ✅
navigate(GPSDevice()) # ✅
# navigate(BasicPhone()) # ❌ Type error - BasicPhone has no GPS
🔄 How to Apply ISP
1
Identify the fat interface
Look for interfaces with many methods that some implementations don’t need
2
Group related methods
Which methods are always used together?
3
Create focused interfaces
One interface per group of related methods
4
Update implementations
Each class implements only the interfaces it needs
🧪 Practice Exercise
Challenge: Fix the Vehicle Interface
Challenge: Fix the Vehicle Interface
This interface is too fat. Split it!
Copy
class Vehicle(ABC):
@abstractmethod
def start_engine(self):
pass
@abstractmethod
def stop_engine(self):
pass
@abstractmethod
def accelerate(self):
pass
@abstractmethod
def brake(self):
pass
@abstractmethod
def fly(self):
pass
@abstractmethod
def sail(self):
pass
@abstractmethod
def submerge(self):
pass
@abstractmethod
def refuel(self):
pass
@abstractmethod
def recharge(self):
pass
class Car(Vehicle):
def fly(self):
raise NotImplementedError("Cars can't fly!")
def sail(self):
raise NotImplementedError("Cars can't sail!")
def submerge(self):
raise NotImplementedError("Cars can't submerge!")
def recharge(self):
raise NotImplementedError("Gas cars don't recharge!")
# ... lots of NotImplementedError
class Bicycle(Vehicle):
def start_engine(self):
raise NotImplementedError("Bicycles have no engine!")
# ... even more NotImplementedError! 😱
📝 Key Takeaways
Small > Big
Many small interfaces beat one big one
No Forced Methods
Classes shouldn’t implement what they don’t need
Combine as Needed
Classes can implement multiple interfaces
Client-Focused
Interfaces should fit what clients actually need
🏃 Next: Dependency Inversion Principle
Let’s learn the final SOLID principle - how to depend on abstractions!Continue to Dependency Inversion →
Learn how to make your code flexible by depending on abstractions!