Skip to main content

🧩 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
ISP says: Don’t give a fish a bicycle interface! 🐟🚲
Simple Rule: Split big interfaces into small, focused ones. Classes only implement what they need!

🚨 The Problem: Fat Interfaces

❌ BAD: One Giant Interface

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

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

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

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

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

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

This interface is too fat. Split it!
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!