home / skills / yanko-belov / code-craft / interface-segregation

interface-segregation skill

/skills/interface-segregation

This skill helps you design focused interfaces by applying the Interface Segregation Principle, avoiding throw and no-op methods and promoting cohesive,

npx playbooks add skill yanko-belov/code-craft --skill interface-segregation

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.8 KB
---
name: interface-segregation-principle
description: Use when designing interfaces. Use when implementing interfaces with methods you don't need. Use when forced to implement throw/no-op for interface methods.
---

# Interface Segregation Principle (ISP)

## Overview

**Clients should not be forced to depend on interfaces they don't use.**

Many small, focused interfaces are better than one large "fat" interface. If an implementer must throw exceptions or provide no-ops for interface methods, the interface is too large.

## When to Use

- Designing a new interface
- Implementing an interface with unused methods
- Forced to implement methods that don't apply
- Interface has more than 5-7 methods
- Different implementers use different subsets of methods

## The Iron Rule

```
NEVER implement an interface method with throw or no-op.
```

**No exceptions:**
- Not for "it's what the interface requires"
- Not for "I'll provide both approaches"
- Not for "the caller can check capabilities"
- Not for "it's clearly documented as unsupported"

**Providing both the violation and the correct approach is still providing a violation.**

## Detection: The "Throw/No-op" Smell

If your implementation looks like this, the interface is wrong:

```typescript
// ❌ FAT INTERFACE
interface MultiFunctionDevice {
  print(doc: string): void;
  scan(): string;
  fax(doc: string): void;
}

// ❌ VIOLATION: Forced to implement unusable methods
class BasicPrinter implements MultiFunctionDevice {
  print(doc: string): void { /* works */ }
  scan(): string { throw new Error("Not supported"); }  // ← ISP violation
  fax(doc: string): void { /* no-op */ }                // ← ISP violation
}
```

## The Correct Pattern: Segregated Interfaces

Split the fat interface into focused capabilities:

```typescript
// ✅ CORRECT: Segregated interfaces
interface Printer {
  print(doc: string): void;
}

interface Scanner {
  scan(): string;
}

interface Fax {
  fax(doc: string): void;
}

// Implement only what you support
class BasicPrinter implements Printer {
  print(doc: string): void { /* works */ }
  // No scan or fax - doesn't promise what it can't deliver
}

class AllInOne implements Printer, Scanner, Fax {
  print(doc: string): void { /* works */ }
  scan(): string { /* works */ }
  fax(doc: string): void { /* works */ }
}

// Combined type for callers who need everything
type MultiFunctionDevice = Printer & Scanner & Fax;
```

## Pressure Resistance Protocol

### 1. "The Interface Already Exists"
**Pressure:** "Implement this existing interface, handle unsupported methods"

**Response:** The interface is wrong. Propose splitting it.

**Action:**
```
"This interface forces implementers to provide throw/no-op for methods they don't support.
I recommend splitting into: [list focused interfaces].
Should I refactor the interface, or document this as tech debt?"
```

### 2. "Just Throw an Error"
**Pressure:** "Handle unsupported methods by throwing"

**Response:** Runtime errors for expected interface methods is a design failure.

**Action:** Split the interface so implementers only promise what they can deliver.

### 3. "I'll Provide Both Options"
**Pressure:** "Here's the violation you asked for AND here's the better way"

**Response:** Providing the violation at all enables bad code to ship.

**Action:** Provide ONLY the correct approach. Don't implement the fat interface.

### 4. "Callers Can Check First"
**Pressure:** "Add a `supports(method)` check"

**Response:** This is a workaround for bad design. Type system should enforce capabilities.

**Action:** Split interfaces so the type system does the checking at compile time.

## Red Flags - STOP and Reconsider

If you notice ANY of these, the interface needs splitting:

- Implementing a method with `throw new Error`
- Implementing a method as no-op (empty body)
- Interface has 7+ methods
- Different implementers use different subsets
- Adding `supportsX()` capability checks
- Implementers have large blocks of unused methods

**All of these mean: Split the interface.**

## Interface Design Guidelines

### Size
- **Ideal:** 1-3 methods per interface
- **Acceptable:** 4-5 methods if highly cohesive
- **Too large:** 6+ methods - look for split opportunities

### Cohesion Test
Ask: "Do ALL implementers need ALL these methods?"
- Yes → Keep together
- No → Split

### Common Splits

| Fat Interface | Segregated Interfaces |
|---------------|----------------------|
| `Repository<T>` | `Readable<T>`, `Writable<T>` |
| `Worker` | `Workable`, `Eatable`, `Meetable` |
| `MultiFunctionDevice` | `Printer`, `Scanner`, `Fax` |
| `FileSystem` | `FileReader`, `FileWriter`, `FileDeleter` |
| `UserService` | `UserReader`, `UserWriter`, `UserAuth` |

## Quick Reference

| Symptom | Action |
|---------|--------|
| Method implemented as throw | Split interface |
| Method implemented as no-op | Split interface |
| 7+ methods in interface | Look for split |
| `supports()` capability checks | Split interface |
| Implementers ignore methods | Split interface |

## Common Rationalizations (All Invalid)

| Excuse | Reality |
|--------|---------|
| "The interface already exists" | Interfaces can be refactored. |
| "Throwing makes it explicit" | Compile errors are better than runtime errors. |
| "I provided both approaches" | Providing the violation enables bad code. |
| "It's documented as unsupported" | Documentation doesn't fix design flaws. |
| "Many interfaces is complex" | Many small interfaces is simpler than one broken one. |
| "Callers can check capabilities" | Type system should do this, not runtime checks. |

## The Bottom Line

**No client should be forced to depend on methods it doesn't use.**

When asked to implement a fat interface:
1. Identify which methods are actually needed
2. Propose segregated interfaces
3. Implement only the focused interfaces

Never provide throw/no-op implementations. Never provide "both options." The fat interface is the problem - fix it.

Overview

This skill helps designers and implementers apply the Interface Segregation Principle (ISP) in TypeScript projects. It identifies when an interface is too large, shows how to split fat interfaces into focused capabilities, and explains why throw/no-op implementations are a design smell. The goal is to ensure types express capabilities so implementations only promise what they can deliver.

How this skill works

The skill inspects interfaces and implementations to detect the "throw/no-op" smell and other red flags (large interfaces, supports() checks, unused methods in implementers). It recommends splitting a fat interface into small, cohesive interfaces (1–3 methods ideally) and shows patterns for composing capabilities. It also offers suggested responses and actions when pressured to accept or ship violations.

When to use it

  • Designing a new interface or API surface in TypeScript
  • Implementing an interface and forced to throw or no-op for some methods
  • Refactoring a codebase where different implementers use different subsets of an interface
  • Reviewing interfaces with more than ~5–7 methods
  • When callers use runtime capability checks like supportsX()

Best practices

  • Prefer many small, focused interfaces over one fat interface
  • Aim for 1–3 methods per interface; 4–5 only if highly cohesive
  • Never implement an interface method with throw or an empty body
  • Use type composition (A & B) for callers that need multiple capabilities
  • Propose refactoring existing fat interfaces rather than adding runtime checks

Example use cases

  • Splitting a MultiFunctionDevice into Printer, Scanner, Fax so BasicPrinter doesn't implement unused methods
  • Refactoring a Repository<T> into Readable<T> and Writable<T> when some services only read
  • Rejecting a PR that implements unsupported methods by throwing and requesting segregated interfaces
  • Replacing supportsX() runtime checks with distinct interfaces so the compiler enforces capabilities
  • Designing a new service API and modeling individual responsibilities as separate interfaces

FAQ

What if the interface already exists and many modules depend on it?

Treat the existing interface as technical debt. Propose a phased refactor: introduce focused interfaces, update implementers and callers incrementally, and keep the old combined type as a composition if callers need everything.

Is throwing an error ever acceptable for an unsupported method?

No. Throwing or no-oping hides design issues and shifts failures to runtime. Split the interface so implementers only expose methods they support.