Interface Segregation Principle
Small, focused surfaces.
No caller depends on operations it never uses. Several narrow types beat one wide “role” interface. The fat interface is a coupling smell with a respectable pseudonym.
Opinion
I have spent enough time refactoring God-interfaces to know the pattern is the same every time. Two consumers grow over a year. One of them needs three methods; the other needs four; the union is seven. Someone wires both consumers to the seven-method interface because the type-system wants it, and now changing any of those seven methods forces a recompile, a retest, and a careful re-read of code that genuinely does not care. ISP is the rule that says: stop. Two narrow interfaces beat the union; the calls are smaller; the rebuilds are smaller; the bugs are smaller.
Modern TypeScript has the structural-typing equivalent built in. Pick<User, "id" | "name"> replaces User; a function parameter declares the smallest object shape it actually reads. That is ISP done at the call site, every time the call site is written. The discipline is not Java-shaped overhead; it is the modern type-system's default when the team uses it.
Pair ISP with A11 Dependency Inversion: the abstraction the high level depends on must be narrow enough to satisfy without leaking detail. Pair it with F6 Encapsulation: the public surface is the only surface, and ISP says that surface is per-client. Pair it with T1 Domain-Driven Types: the type encodes the design, and the design includes the consumer's actual needs. The four rules together make a codebase whose interfaces grow by intention rather than by accumulation.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
No caller depends on operations it never uses. Several narrow types beat one wide role-interface. In structural-typing languages, prefer per-callsite narrowing — `Pick<User, 'id'>` over `User` — when the function only reads the smaller shape. /tenet/interface-segregation-principle/A10
AI eyes only
Rule: small, focused surfaces. Clients depend on the slice they use, never the whole.
Reject: function parameters typed as the full domain object when only one or two fields are read. Reject: God-interfaces with a dozen methods. Reject: forcing a caller to satisfy members it does not use.
Generate: parameters typed as Pick<Type, "field"> of the fields actually read. Interfaces split by client need, not by source class.
Diagnostic: list the members the function actually accesses. If the parameter type declares more than that list, narrow the type.
Why?
- Smaller blast radius on every refactor. Touching a method that lives on a narrow interface forces a recompile of three files, not thirty. The build graph shrinks where it matters most.
- Smaller mocks in tests. A mock of a three-method interface is three lines; a mock of a seven-method God-interface is seven lines plus the maintenance cost of the four methods the test never calls.
- The interface is shaped by the consumer's needs, not the implementation's accidents. Two consumers with disjoint needs get two interfaces; the type system tells the truth instead of accumulating a union.
- TypeScript's
Pick, Go's structural interfaces and Rust's trait bounds make compliance close to free. Per-callsite interface narrowing is a one-liner in every modern type system. - Pairs with F6 Encapsulation: the public surface is the only surface, and ISP says that surface is per-client. Smaller surfaces are easier to honour and easier to change.
- Easier onboarding. A new contributor reading a narrow interface knows what the consumer needs in under a minute; a wide interface tells them what every consumer might use, which is a different question.
- Plays well with discriminated unions. When the “capability” is a discriminator (printable + stapleable + faxable), the union is the type and the consumer asks the type system which capability is present.
- Agents reach for the wide type by default because autocomplete prefers high-coverage types. With ISP loaded, the model picks the smallest object shape its function actually reads — a discipline a senior reviewer applies in code review.
Origins
The principle came out of Robert C. Martin's consulting work for Xerox in the mid-1990s. The Xerox printer engineering team had a single Job class that represented every operation the multifunction device could perform — print, staple, fax. Every client module had to depend on every method of Job; touching the stapling logic forced a rebuild of the print and fax modules. Martin proposed splitting Job into role-specific interfaces — Printable, Stapleable, Faxable — so each client saw only what it used. He published the case study in The Interface Segregation Principle, C++ Report August 1996.1“The Interface Segregation Principle,” C++ Report, August 1996. The Xerox case study with the fat `Job` class. The original published statement.
The principle landed in book form in Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002) as the “I” of SOLID, and was restated in Clean Architecture (2017), Ch. 10.2Clean Architecture (Pearson, 2017), Ch. 10 “The Interface Segregation Principle.” Modern restatement; emphasises the architectural reading where the segregation operates at the package boundary. Martin's framing remained Java-shaped: classes, interfaces, implements keywords, and the explicit-interface tradition that comes with nominal type systems. The structural-typing world (Go, TypeScript, OCaml) gets ISP almost for free — ask for the smallest object shape your function reads, and you have done it.
Bertrand Meyer's 1988 Object-Oriented Software Construction was the parallel tradition. Meyer treated client-supplier contracts as the primary unit of design; the contract is what the client requires, and the supplier is built to satisfy it. ISP is the same instinct expressed at the interface scale instead of the method scale.3Object-Oriented Software Construction, 1st ed. (Prentice Hall, 1988). Client-supplier contracts as the primary unit of design; the contract is what the client requires. The principle's name is Martin's; the underlying instinct is older.
Modern languages took the principle further. Go's structural interfaces (“an interface is satisfied by any type that has the methods”) make ISP the path of least resistance — the language idiom is “the bigger the interface, the weaker the abstraction” (Rob Pike).6“The bigger the interface, the weaker the abstraction.” Go Proverbs (2015). The Go community's verdict; ISP as the language idiom rather than the optional principle. Rust's trait coherence rules and trait-bound generic constraints land in the same place from the type-theory direction. TypeScript's Pick, Omit and structural intersection types make per-callsite interface narrowing a one-liner. The principle outlived the C++ Report; the languages followed it.
Quotes
Clients should not be forced to depend upon interfaces that they do not use.
The bigger the interface, the weaker the abstraction.
If appropriate interface types exist, then parameters, return values, variables, and fields should all be declared using interface types.
We design role interfaces from the perspective of the calling object: the methods are precisely those that the caller needs.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01The original Xerox case study. A fat `Job` class forces every client to depend on every method; ISP is the fix.
- 02Clean Architecture, Ch. 10SupportsModern restatement; emphasises the architectural reading where segregation operates at the package boundary.
- 03First book-length treatment. ISP as the I of SOLID; worked examples in C++ and Java.
- 04Client-supplier contracts as the primary unit of design. ISP is the same instinct expressed at the interface scale.
- 05Effective Java, 3rd ed.SupportsItem 64 “Refer to objects by their interfaces.” The pragmatic Java-shaped operationalisation: depend on the smallest interface that satisfies the call.
Sixteen sources spanning Martin's 1996 piece and the SOLID canon, with Meyer and Bloch operationalising the rule for OO and Java. The modern structural-typing traditions (TypeScript, Go, Rust) and test-doubles literature sit further down. The qualifiers carry the “sometimes a wide interface is cheaper” reading; the opposers carry the “ISP is just narrow types” reading.
Examples
// Before: a feeder depends on twelve methods to use two.interface HedgehogPlatform { feed(id: HedgehogId, grams: number): void; weigh(id: HedgehogId): number; track(id: HedgehogId): GpsFix; release(id: HedgehogId, site: Site): void; breed(pair: BreedingPair): void; medicate(id: HedgehogId, drug: Drug): void; // ... vaccinate, triage, microchip, quarantine, report, ... audit(): AuditReport;}function feedShift(platform: HedgehogPlatform, id: HedgehogId) { platform.feed(id, 50); platform.weigh(id);}
// After: three role interfaces. Each consumer depends on what it uses.interface Feeder { feed(id: HedgehogId, grams: number): void; weigh(id: HedgehogId): number; }interface Tracker { track(id: HedgehogId): GpsFix; release(id: HedgehogId, site: Site): void; }interface Veterinarian { medicate(id: HedgehogId, drug: Drug): void; }function feedShift(feeder: Feeder, id: HedgehogId) { feeder.feed(id, 50); feeder.weigh(id);}
// Before: a feeder depends on twelve methods to use two.interface HedgehogPlatform { feed(id: HedgehogId, grams: number): void; weigh(id: HedgehogId): number; track(id: HedgehogId): GpsFix; release(id: HedgehogId, site: Site): void; breed(pair: BreedingPair): void; medicate(id: HedgehogId, drug: Drug): void; // ... vaccinate, triage, microchip, quarantine, report, ... audit(): AuditReport;}function feedShift(platform: HedgehogPlatform, id: HedgehogId) { platform.feed(id, 50); platform.weigh(id);}
// After: three role interfaces. Each consumer depends on what it uses.interface Feeder { feed(id: HedgehogId, grams: number): void; weigh(id: HedgehogId): number; }interface Tracker { track(id: HedgehogId): GpsFix; release(id: HedgehogId, site: Site): void; }interface Veterinarian { medicate(id: HedgehogId, drug: Drug): void; }function feedShift(feeder: Feeder, id: HedgehogId) { feeder.feed(id, 50); feeder.weigh(id);}
// Before: a feeder depends on twelve methods to use two./** @typedef {{ feed, weigh, track, release, breed, medicate, audit }} HedgehogPlatform */ // ... vaccinate, triage, microchip, quarantine, report, ...function feedShift(platform, id) { platform.feed(id, 50); platform.weigh(id);}
// After: three role typedefs. Each consumer depends on what it uses./** @typedef {{ feed: (id, grams) => void, weigh: (id) => number }} Feeder *//** @typedef {{ track: (id) => GpsFix, release: (id, site) => void }} Tracker *//** @typedef {{ medicate: (id, drug) => void }} Veterinarian */function feedShift(feeder, id) { feeder.feed(id, 50); feeder.weigh(id);}
# Before: a feeder depends on twelve methods to use two.from typing import Protocolclass HedgehogPlatform(Protocol): def feed(self, id: HedgehogId, grams: int) -> None: ... def weigh(self, id: HedgehogId) -> int: ... def track(self, id: HedgehogId) -> GpsFix: ... def release(self, id: HedgehogId, site: Site) -> None: ... def medicate(self, id: HedgehogId, drug: Drug) -> None: ... # ... breed, audit, vaccinate, triage, microchip, quarantine, ...def feed_shift(platform: HedgehogPlatform, id: HedgehogId) -> None: platform.feed(id, 50) platform.weigh(id)
# After: three Protocol classes. Each consumer depends on what it uses.class Feeder(Protocol): def feed(self, id: HedgehogId, grams: int) -> None: ... def weigh(self, id: HedgehogId) -> int: ...class Tracker(Protocol): def track(self, id: HedgehogId) -> GpsFix: ... def release(self, id: HedgehogId, site: Site) -> None: ...class Veterinarian(Protocol): def medicate(self, id: HedgehogId, drug: Drug) -> None: ...def feed_shift(feeder: Feeder, id: HedgehogId) -> None: feeder.feed(id, 50) feeder.weigh(id)
// Before: a feeder depends on twelve methods to use two.interface HedgehogPlatform { void feed(HedgehogId id, int grams); int weigh(HedgehogId id); GpsFix track(HedgehogId id); void release(HedgehogId id, Site site); void medicate(HedgehogId id, Drug drug); // ... breed, audit, vaccinate, triage, microchip, quarantine, ...}void feedShift(HedgehogPlatform platform, HedgehogId id) { platform.feed(id, 50); platform.weigh(id);}
// After: three role interfaces. Each consumer depends on what it uses.interface Feeder { void feed(HedgehogId id, int grams); int weigh(HedgehogId id);}interface Tracker { GpsFix track(HedgehogId id); void release(HedgehogId id, Site site);}interface Veterinarian { void medicate(HedgehogId id, Drug drug);}void feedShift(Feeder feeder, HedgehogId id) { feeder.feed(id, 50); feeder.weigh(id);}
// Before: a feeder depends on twelve methods to use two.interface HedgehogPlatform { public function feed(HedgehogId $id, int $grams): void; public function weigh(HedgehogId $id): int; public function track(HedgehogId $id): GpsFix; public function release(HedgehogId $id, Site $site): void; public function medicate(HedgehogId $id, Drug $drug): void; // ... breed, audit, vaccinate, triage, microchip, quarantine, ...}function feedShift(HedgehogPlatform $platform, HedgehogId $id): void { $platform->feed($id, 50); $platform->weigh($id);}
// After: three role interfaces. Each consumer depends on what it uses.interface Feeder { public function feed(HedgehogId $id, int $grams): void; public function weigh(HedgehogId $id): int;}interface Tracker { public function track(HedgehogId $id): GpsFix; public function release(HedgehogId $id, Site $site): void;}interface Veterinarian { public function medicate(HedgehogId $id, Drug $drug): void;}function feedShift(Feeder $feeder, HedgehogId $id): void { $feeder->feed($id, 50); $feeder->weigh($id);}
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| @typescript-eslint/no-unused-vars | typescript-eslint | function parameters the body never reads. The cheapest possible signal that the parameter type is wider than it needs to be. |
| @typescript-eslint/no-empty-interface | typescript-eslint | empty interfaces. Either the interface should be a `Pick`-shaped narrowing, or the consumer didn't actually need an interface at all. |
| @typescript-eslint/prefer-function-type | typescript-eslint | interfaces that wrap a single call signature. A function type is the narrower expression; the interface is over-spec. |
| sonarjs/cognitive-complexity | eslint-plugin-sonarjs | functions doing too much. Often the symptom of a parameter type carrying too much; narrow it and complexity drops. |
| sonarjs/no-useless-intersection | eslint-plugin-sonarjs | intersection types where one operand is `unknown` or `any`. The intersection adds a name without narrowing; the consumer still gets the wide shape. |
| max-classes-per-file | ESLint core | files with multiple classes. Often the symptom of one fat class that should have been split into role-specific types. |
| max-params | ESLint core | long parameter lists. The function-level shape of the same problem; the function is asking for too much from too many sources. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { sonarjs },
rules: {
'@typescript-eslint/no-unused-vars': ['error', { args: 'all', argsIgnorePattern: '^_' }],
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'sonarjs/cognitive-complexity': ['error', 15],
'sonarjs/no-useless-intersection': 'error',
'max-classes-per-file': ['error', 1],
'max-params': ['error', 3],
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| @typescript-eslint/no-unused-vars | typescript-eslint | function parameters the body never reads. The cheapest possible signal that the parameter type is wider than it needs to be. |
| @typescript-eslint/no-empty-interface | typescript-eslint | empty interfaces. Either the interface should be a `Pick`-shaped narrowing, or the consumer didn't actually need an interface at all. |
| @typescript-eslint/prefer-function-type | typescript-eslint | interfaces that wrap a single call signature. A function type is the narrower expression; the interface is over-spec. |
| sonarjs/cognitive-complexity | eslint-plugin-sonarjs | functions doing too much. Often the symptom of a parameter type carrying too much; narrow it and complexity drops. |
| sonarjs/no-useless-intersection | eslint-plugin-sonarjs | intersection types where one operand is `unknown` or `any`. The intersection adds a name without narrowing; the consumer still gets the wide shape. |
| max-classes-per-file | ESLint core | files with multiple classes. Often the symptom of one fat class that should have been split into role-specific types. |
| max-params | ESLint core | long parameter lists. The function-level shape of the same problem; the function is asking for too much from too many sources. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { sonarjs },
rules: {
'@typescript-eslint/no-unused-vars': ['error', { args: 'all', argsIgnorePattern: '^_' }],
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'sonarjs/cognitive-complexity': ['error', 15],
'sonarjs/no-useless-intersection': 'error',
'max-classes-per-file': ['error', 1],
'max-params': ['error', 3],
}
});Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| no-unused-vars | ESLint core | function parameters the body never reads. Same signal as in TypeScript: the parameter is too wide. |
| max-classes-per-file | ESLint core | files with multiple classes. The God-class smell at the file level. |
| max-params | ESLint core | long parameter lists; the function-level expression of ISP failure. |
| sonarjs/cognitive-complexity | eslint-plugin-sonarjs | functions doing too much. Often a downstream symptom of accepting too wide a parameter type. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
export default tseslint.config({
files: ['**/*.{ts,tsx}'],
plugins: { sonarjs },
rules: {
'@typescript-eslint/no-unused-vars': ['error', { args: 'all', argsIgnorePattern: '^_' }],
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'sonarjs/cognitive-complexity': ['error', 15],
'sonarjs/no-useless-intersection': 'error',
'max-classes-per-file': ['error', 1],
'max-params': ['error', 3],
}
});Apply these rules in pyproject.toml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| PLR0913 (too-many-arguments) | Ruff (pylint) | long parameter lists. The function-level expression of ISP failure; weaken with a Protocol or a dataclass. |
| PLR0904 (too-many-public-methods) | Ruff (pylint) | classes with too many public methods. The class-level God-object smell ISP is designed to prevent. |
| B008 (function-call-in-default-argument) | Ruff (flake8-bugbear) | default-argument calls — often the symptom of a parameter that should have been a separate Protocol or callable. |
| C400-C418 (unnecessary-comprehensions) | Ruff (flake8-comprehensions) | comprehensions that should have been generator expressions or simpler builtins. Smaller scope at the call-site is the same instinct as ISP at the type-site. |
| Protocol classes (PEP 544) | mypy | wide concrete types where a structural Protocol would do. The Pythonic shape of ISP; mypy enforces the narrowing. |
| C901 (complex-structure) | Ruff (mccabe) | complex functions. Often a downstream symptom of taking too wide a parameter type; narrow the type and the complexity drops. |
pyproject.tomlconfiguration snippet
[tool.ruff.lint]
select = [
"PLR0913",
"PLR0904",
"B",
"C4",
]
[tool.mypy]
strict = true
disallow_subclassing_any = true
extra_checks = trueApply these rules in pmd-ruleset.xml. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| InterfaceIsType | Checkstyle | interfaces used only to hold constants. The classic anti-pattern; the interface should describe a role, not declare statics. |
| TooManyMethods | PMD | classes or interfaces with too many methods. The structural shape of ISP failure; configurable threshold. |
| ExcessivePublicCount | PMD | classes with too many public attributes / methods. The God-class smell ISP prevents at the implementation level. |
| AbstractClassWithoutAbstractMethod | PMD | abstract classes that aren't actually abstract. The smell that the type is a category, not a contract — usually a sign the contract should have been a narrow interface. |
| S1448 (too many methods) | SonarQube Java | method-count violations on classes or interfaces. The class-cohesion side of ISP; pair with the cohesion-metric rules. |
| ArchUnit interface-shape rules | ArchUnit | package-level constraints on interface count or method count. Architecture-test enforcement of the rule the lint catches per-class. |
pmd-ruleset.xmlconfiguration snippet
<?xml version="1.0"?>
<ruleset name="InterfaceSegregation"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0">
<description>A10 — narrow, role-based interfaces.</description>
<rule ref="category/java/design.xml/ExcessivePublicCount"/>
<rule ref="category/java/design.xml/CouplingBetweenObjects"/>
<rule ref="category/java/design.xml/TooManyMethods">
<properties>
<property name="maxmethods" value="10"/>
</properties>
</rule>
<rule ref="category/java/bestpractices.xml/AbstractClassWithoutAbstractMethod"/>
</ruleset>Apply these rules in phpstan.neon. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| ExcessiveClassComplexity | PHPMD | classes that exceed a complexity threshold — usually the symptom of a class doing too many jobs because its interface accepted them all. |
| ExcessivePublicCount | PHPMD | classes with too many public methods or fields. The God-object surface ISP exists to break apart. |
| TooManyPublicMethods | PHPMD | method count on a single class. Configurable threshold; pair with PHPStan strict to surface the unused-by-some-callers methods. |
| level: max | PHPStan | wide types at call sites. PHPStan max enforces narrowed parameter types; combined with strict-rules, it catches the same violations TS catches structurally. |
| PossiblyUndefinedMethod | Psalm | method calls on union types where one branch lacks the method. The shape that says: you split this interface but kept the union too wide. |
| Architecture rules | PHPArkitect | package-level constraints on interface shape. Architecture-test enforcement of the rule the lint catches per-class. |
phpstan.neonconfiguration snippet
includes:
- vendor/phpstan/phpstan-strict-rules/rules.neon
parameters:
level: max
paths:
- src
checkExplicitMixedMissingReturn: true
checkFunctionNameCase: true
checkInternalClassCaseSensitivity: trueAI rules
.cursor/rules/a10-interface-segregation.mdc---
description: Prickles A10 — Interface Segregation Principle
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles A10 — Interface Segregation Principle
Build interfaces around clients, not implementations. No caller depends on operations it never uses.
When two consumers of one interface have disjoint method needs, split the interface into two role-specific interfaces. Several narrow types beat one wide role.
In structural-typing languages, prefer per-callsite narrowing — `Pick<User, 'id' | 'name'>` over `User` — when the function only reads the smaller shape.
Refuse a parameter type wider than the function reads. Refuse a base class with methods only some subclasses honour. Refuse a God-interface that two consumers reach into for opposite reasons.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The strongest steelman is the proliferation cost.5“Interface Segregation Principle” discussion. Catalogues the proliferation cost — too many narrow interfaces become noise when consumer needs are small or stable. Splitting one interface into many means more types to name, more files to navigate, more import lines per consumer, and the canonical “which interface does X implement?” question every time the type is extended. Some practitioners argue that for small teams with stable domains, a wider interface is genuinely cheaper to maintain than several narrow ones. The implication for ISP: the rule is right when interfaces have many consumers with disjoint needs, and over-applied when they do not. The rule is correct; the discipline is knowing when it earns its weight.
Counter-argument retort
The proliferation-cost counter is correct in the small and wrong in the large.5“Interface Segregation Principle” discussion. Catalogues the proliferation cost — too many narrow interfaces become noise when consumer needs are small or stable. For a single team with a stable domain and three implementations of one interface, splitting saves nothing and costs naming overhead. The rule is right when interfaces have many consumers with disjoint needs; the discipline is knowing when that condition holds. The reply: the structural-typing path makes the cost of compliance close to zero. Pick<User, "id" | "name"> is not a separate named interface to maintain; it is a shape that lives at the call site. Apply ISP at the smallest scale the language supports, and the proliferation problem dissolves.
The composition-first counter — “don't use interfaces; use composition; ISP becomes irrelevant” — lands in the same place as the parallel counter to A9 Liskov Substitution. ISP is not a recommendation to use interfaces; it is the rule for when you do. If you are composing rather than implementing, you have already satisfied the rule by construction. The principle is the discipline at the decision point, not the recommendation to choose interfaces.
The harder objection is the test-doubles one. A narrow interface with three methods needs a mock with three methods; a wider interface with seven methods needs a mock with seven. ISP makes the test-double surface smaller, which is good in isolation but adds setup overhead across many test files. The reply: the small-mock burden is a Liskov problem in disguise. A mock that satisfies a narrow interface is closer to behavioural substitutability than a mock of a wide interface; the test that uses the narrow mock is closer to a behaviour test than a coupling test.9Growing Object-Oriented Software, Guided by Tests (Addison-Wesley, 2010). The role-based-interface design that pairs with ISP — narrow interfaces drive easier mocks and easier behaviour-focused tests. Smaller surface is the same property in the test suite that it is in the production code.
The genuine residue is naming. Many narrow interfaces need many names, and naming is the hardest problem in programming. Pair ISP with F2 Intention-Revealing Names: the name should describe what the consumer needs, not what the implementation provides. Renderable is a capability; UserService is a class; the first survives ISP and the second doesn't.
Notes
- [1]Robert C. Martin — “The Interface Segregation Principle,” C++ Report, August 1996. The Xerox case study with the fat `Job` class. The original published statement.
- [2]Robert C. Martin — Clean Architecture (Pearson, 2017), Ch. 10 “The Interface Segregation Principle.” Modern restatement; emphasises the architectural reading where the segregation operates at the package boundary.
- [3]Bertrand Meyer — Object-Oriented Software Construction, 1st ed. (Prentice Hall, 1988). Client-supplier contracts as the primary unit of design; the contract is what the client requires.