I Interface Segregation Principle ISP · How it works
1 min readRapid overview
How it works
I — Interface Segregation Principle (ISP)
"Clients should not be forced to depend on methods they do not use."
Split large interfaces into smaller, focused ones so implementations only need to provide what's actually used.
❌ Bad example (React/TypeScript):
// Fat interface forces all implementations to have all methods
interface MediaPlayer {
play(): void;
pause(): void;
stop(): void;
seekTo(time: number): void;
setVolume(level: number): void;
toggleFullscreen(): void;
enableSubtitles(): void;
adjustPlaybackSpeed(speed: number): void;
}
// Audio player doesn't need fullscreen or subtitles
class AudioPlayer implements MediaPlayer {
play() { /* ... */ }
pause() { /* ... */ }
stop() { /* ... */ }
seekTo(time: number) { /* ... */ }
setVolume(level: number) { /* ... */ }
// Forced to implement unnecessary methods
toggleFullscreen() {
throw new Error('Not supported'); // ❌
}
enableSubtitles() {
throw new Error('Not supported'); // ❌
}
adjustPlaybackSpeed(speed: number) {
throw new Error('Not supported'); // ❌
}
}
Each implementation is forced to implement everything, even if it doesn't need to. Violates both ISP and LSP.
✅ Good example (React/TypeScript):
// Split into focused interfaces
interface Playable {
play(): void;
pause(): void;
stop(): void;
}
interface Seekable {
seekTo(time: number): void;
}
interface VolumeControl {
setVolume(level: number): void;
}
interface VideoControls {
toggleFullscreen(): void;
enableSubtitles(): void;
}
interface PlaybackSpeedControl {
adjustPlaybackSpeed(speed: number): void;
}
// Audio player implements only what it needs
class AudioPlayer implements Playable, Seekable, VolumeControl, PlaybackSpeedControl {
play() { /* ... */ }
pause() { /* ... */ }
stop() { /* ... */ }
seekTo(time: number) { /* ... */ }
setVolume(level: number) { /* ... */ }
adjustPlaybackSpeed(speed: number) { /* ... */ }
}
// Video player implements all controls
class VideoPlayer implements Playable, Seekable, VolumeControl, VideoControls, PlaybackSpeedControl {
play() { /* ... */ }
pause() { /* ... */ }
stop() { /* ... */ }
seekTo(time: number) { /* ... */ }
setVolume(level: number) { /* ... */ }
toggleFullscreen() { /* ... */ }
enableSubtitles() { /* ... */ }
adjustPlaybackSpeed(speed: number) { /* ... */ }
}
// Simple player with minimal controls
class SimplePlayer implements Playable {
play() { /* ... */ }
pause() { /* ... */ }
stop() { /* ... */ }
}
// Components depend only on what they need
function PlayControls({ player }: { player: Playable }) {
return (
<div>
<button onClick={() => player.play()}>Play</button>
<button onClick={() => player.pause()}>Pause</button>
<button onClick={() => player.stop()}>Stop</button>
</div>
);
}
function VolumeControls({ player }: { player: VolumeControl }) {
return <input type="range" onChange={(e) => player.setVolume(+e.target.value)} />;
}
💡 Now each player only implements what it actually supports, and components depend on minimal interfaces.
✅ Good example (Angular services):
// Bad - fat service interface
interface DataService {
fetch(): Observable<Data[]>;
create(data: Data): Observable<Data>;
update(id: string, data: Data): Observable<Data>;
delete(id: string): Observable<void>;
export(): Observable<Blob>;
import(file: File): Observable<void>;
subscribe(callback: (data: Data[]) => void): void;
unsubscribe(): void;
}
// Good - segregated interfaces
interface ReadableDataService {
fetch(): Observable<Data[]>;
}
interface WritableDataService {
create(data: Data): Observable<Data>;
update(id: string, data: Data): Observable<Data>;
delete(id: string): Observable<void>;
}
interface ExportableDataService {
export(): Observable<Blob>;
import(file: File): Observable<void>;
}
interface RealtimeDataService {
subscribe(callback: (data: Data[]) => void): void;
unsubscribe(): void;
}
// Implementations choose what they support
@Injectable()
export class ApiDataService implements ReadableDataService, WritableDataService {
constructor(private http: HttpClient) {}
fetch(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data');
}
create(data: Data): Observable<Data> {
return this.http.post<Data>('/api/data', data);
}
update(id: string, data: Data): Observable<Data> {
return this.http.put<Data>(`/api/data/${id}`, data);
}
delete(id: string): Observable<void> {
return this.http.delete<void>(`/api/data/${id}`);
}
}
@Injectable()
export class ReadOnlyDataService implements ReadableDataService {
constructor(private http: HttpClient) {}
fetch(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data');
}
}
// Components depend only on what they need
@Component({...})
export class DataListComponent {
constructor(private dataService: ReadableDataService) {
// Only needs read access
}
ngOnInit() {
this.dataService.fetch().subscribe(data => this.data = data);
}
}
@Component({...})
export class DataEditorComponent {
constructor(
private readService: ReadableDataService,
private writeService: WritableDataService
) {
// Needs both read and write
}
}
✅ Good example (React form handling):
// Bad - single fat interface
interface FormHandler {
validate(): ValidationErrors;
submit(): Promise<void>;
reset(): void;
autosave(): void;
export(): string;
import(data: string): void;
}
// Good - segregated by responsibility
interface Validatable {
validate(): ValidationErrors;
}
interface Submittable {
submit(): Promise<void>;
}
interface Resetable {
reset(): void;
}
interface Autosaveable {
autosave(): void;
}
interface Exportable {
export(): string;
import(data: string): void;
}
// Simple form only needs basic operations
class ContactForm implements Validatable, Submittable, Resetable {
validate(): ValidationErrors {
// Validation logic
return {};
}
submit(): Promise<void> {
// Submit logic
return Promise.resolve();
}
reset(): void {
// Reset logic
}
}
// Complex form needs all features
class SurveyForm implements Validatable, Submittable, Resetable, Autosaveable, Exportable {
validate(): ValidationErrors { return {}; }
submit(): Promise<void> { return Promise.resolve(); }
reset(): void {}
autosave(): void {}
export(): string { return '{}'; }
import(data: string): void {}
}
// Components use focused interfaces
function FormValidator({ form }: { form: Validatable }) {
const errors = form.validate();
return <ErrorDisplay errors={errors} />;
}
function SubmitButton({ form }: { form: Submittable }) {
return <button onClick={() => form.submit()}>Submit</button>;
}
function AutosaveIndicator({ form }: { form: Autosaveable }) {
useEffect(() => {
const interval = setInterval(() => form.autosave(), 30000);
return () => clearInterval(interval);
}, [form]);
return <span>Autosaving...</span>;
}
✅ Good example (Event system):
// Bad - single notification interface
interface NotificationService {
sendEmail(to: string, message: string): void;
sendSMS(to: string, message: string): void;
sendPush(to: string, message: string): void;
logEvent(event: string): void;
}
// Good - segregated by channel
interface EmailNotifier {
sendEmail(to: string, message: string): void;
}
interface SMSNotifier {
sendSMS(to: string, message: string): void;
}
interface PushNotifier {
sendPush(to: string, message: string): void;
}
interface EventLogger {
logEvent(event: string): void;
}
// Services implement what they support
class EmailService implements EmailNotifier {
sendEmail(to: string, message: string): void {
// Email implementation
}
}
class SMSService implements SMSNotifier {
sendSMS(to: string, message: string): void {
// SMS implementation
}
}
class FullNotificationService implements EmailNotifier, SMSNotifier, PushNotifier, EventLogger {
sendEmail(to: string, message: string): void {}
sendSMS(to: string, message: string): void {}
sendPush(to: string, message: string): void {}
logEvent(event: string): void {}
}
// Consumers depend only on what they need
function useEmailNotifications(notifier: EmailNotifier) {
const notify = (to: string, msg: string) => {
notifier.sendEmail(to, msg);
};
return { notify };
}
💡 In frontend applications:
- Component props: Don't force components to accept props they don't use. Split large prop interfaces.
- Services: Separate read/write, sync/async, and different feature concerns into focused interfaces.
- Hooks: Custom hooks should accept minimal dependencies, not fat service objects.
- State management: Actions/reducers for different domains should use separate interfaces.
- API clients: Split into focused clients (UserAPI, ProductAPI) instead of one massive API interface.