Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

🧩 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 gym membership analogy: Imagine a gym that forces every member to sign up for swimming, weightlifting, yoga, boxing, AND rock climbing — even if you only want to lift weights. You are paying for (and contractually obligated to maintain eligibility for) services you never use. ISP says the gym should offer separate memberships: a weights pass, a swim pass, a yoga pass. You pick only what you need. In code, this means small, focused interfaces so that implementing classes are not burdened with methods that are irrelevant to them.

🚨 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

# DESIGN REASONING: Each interface represents one capability.
# Classes compose only the interfaces they genuinely support.
# No NotImplementedError, no dead methods, no lies in the API.

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

Why ISP Matters in Production

In real codebases, fat interfaces cause a subtle but painful problem: forced coupling. If UserService depends on a DataStore interface with 30 methods (read, write, delete, batch, stream, replicate…), but UserService only ever calls read() and write(), it is still coupled to all 30 methods. When someone adds a 31st method to DataStore, UserService’s tests might break even though it uses none of the new functionality. ISP prevents this — by depending on a narrow ReadWriteStore interface with just 2 methods, UserService is insulated from changes it does not care about. A senior engineer would say: “ISP is really about coupling management. The wider the interface you depend on, the more reasons your code has to change. Narrow interfaces minimize your exposure to other teams’ changes.”

Interview Insight

ISP shows up in interviews as the “what if we add a new feature?” question. When you design a system with a monolithic interface and the interviewer says “now support a device that can only do X but not Y,” your design either handles it gracefully or crumbles. If you split interfaces upfront, adding a printer-only device that does not scan is trivial: it implements Printable and ignores Scannable. If you used a fat MultiFunctionDevice interface, you are stuck with NotImplementedError. The ISP-aware design also connects to LSP — classes that implement an interface should actually support all its methods, and small interfaces make that achievable. When presenting your design, say: “I am keeping this interface narrow so implementors are not forced to support operations they cannot perform.”

🏃 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!