Перейти к содержимому

5. SOLID: Interface Segregation

Клиенты не должны зависеть от методов, которые они не используют.

Лучше много маленьких специализированных интерфейсов, чем один большой «толстый».


// ❌ Один большой интерфейс принуждает реализовывать всё
interface Animal {
eat(): void;
sleep(): void;
fly(): void; // Не все животные летают
swim(): void; // Не все животные плавают
bark(): void; // Только собаки лают
meow(): void; // Только кошки мяукают
}
class Dog implements Animal {
eat(): void { console.log('Eating'); }
sleep(): void { console.log('Sleeping'); }
fly(): void { throw new Error('Dogs cannot fly!'); } // ← Нарушение!
swim(): void { console.log('Swimming'); }
bark(): void { console.log('Woof!'); }
meow(): void { throw new Error('Dogs do not meow!'); } // ← Нарушение!
}

// ✅ Маленькие, специализированные интерфейсы
interface CanEat {
eat(): void;
}
interface CanSleep {
sleep(): void;
}
interface CanFly {
fly(): void;
}
interface CanSwim {
swim(): void;
}
interface CanBark {
bark(): void;
}
// Каждый класс реализует только нужные интерфейсы
class Dog implements CanEat, CanSleep, CanSwim, CanBark {
eat(): void { console.log('Eating'); }
sleep(): void { console.log('Sleeping'); }
swim(): void { console.log('Swimming'); }
bark(): void { console.log('Woof!'); }
}
class Eagle implements CanEat, CanSleep, CanFly {
eat(): void { console.log('Eating'); }
sleep(): void { console.log('Sleeping'); }
fly(): void { console.log('Flying high!'); }
}
class Duck implements CanEat, CanSleep, CanFly, CanSwim {
eat(): void { console.log('Eating'); }
sleep(): void { console.log('Sleeping'); }
fly(): void { console.log('Flying low'); }
swim(): void { console.log('Quack-swimming'); }
}

// ❌ Один интерфейс для всех операций
interface UserRepository {
findById(id: string): Promise<User>;
findAll(): Promise<User[]>;
save(user: User): Promise<User>;
update(user: User): Promise<User>;
delete(id: string): Promise<void>;
findByEmail(email: string): Promise<User>;
countAll(): Promise<number>;
exportToCsv(): Promise<string>; // Это вообще репозиторий?
sendPasswordReset(userId: string): Promise<void>; // А это?
}
// ✅ Разделённые интерфейсы
interface UserReader {
findById(id: string): Promise<User>;
findAll(): Promise<User[]>;
findByEmail(email: string): Promise<User>;
count(): Promise<number>;
}
interface UserWriter {
save(user: User): Promise<User>;
update(user: User): Promise<User>;
delete(id: string): Promise<void>;
}
interface UserExporter {
exportToCsv(users: User[]): Promise<string>;
}
// Реализуй только то, что нужно
class ReadOnlyUserRepository implements UserReader {
async findById(id: string): Promise<User> { ... }
async findAll(): Promise<User[]> { ... }
async findByEmail(email: string): Promise<User> { ... }
async count(): Promise<number> { ... }
}
class FullUserRepository implements UserReader, UserWriter {
async findById(id: string): Promise<User> { ... }
// ... все методы
}

ISP активно применяется при проектировании компонентов:

// ❌ Один большой интерфейс пропсов
interface ButtonProps {
label: string;
onClick?: () => void;
onHover?: () => void;
onFocus?: () => void;
icon?: ReactNode;
iconPosition?: 'left' | 'right';
loadingText?: string;
isLoading?: boolean;
isDisabled?: boolean;
tooltipText?: string;
badgeCount?: number;
// ... ещё 20 опциональных пропсов
}
// ✅ Разделённые интерфейсы через composition
interface BaseButtonProps {
label: string;
onClick?: () => void;
isDisabled?: boolean;
}
interface IconButtonProps extends BaseButtonProps {
icon: ReactNode;
iconPosition?: 'left' | 'right';
}
interface LoadingButtonProps extends BaseButtonProps {
isLoading: boolean;
loadingText?: string;
}
// Каждый компонент получает только нужные пропсы
function Button({ label, onClick, isDisabled }: BaseButtonProps) { ... }
function IconButton({ label, icon, iconPosition, ...rest }: IconButtonProps) { ... }
function LoadingButton({ label, isLoading, loadingText, ...rest }: LoadingButtonProps) { ... }

Признаки нарушения:

  • Классы реализуют методы через throw new Error('Not implemented')
  • Методы в интерфейсе, которые нужны только одному из 10 клиентов
  • Интерфейс с 15+ методами (скорее всего, его нужно разбить)
  • Клиент импортирует интерфейс, но использует 2 из 10 его методов

  1. Разбей следующий интерфейс на несколько специализированных:
interface MediaPlayer {
play(): void;
pause(): void;
stop(): void;
rewind(): void;
skipForward(seconds: number): void;
setVolume(level: number): void;
mute(): void;
enableSubtitles(language: string): void;
changeVideoQuality(quality: '4K' | '1080p' | '720p'): void;
downloadMedia(): Promise<void>;
shareMedia(platform: 'twitter' | 'facebook'): void;
}
  1. Создай систему авторизации с интерфейсами CanRead, CanWrite, CanDelete, CanAdmin и реализуй роли: ReadonlyUser, Editor, Admin.

  2. Посмотри на TypeScript utility types (Partial<T>, Pick<T,K>, Omit<T,K>) — как они помогают соблюдать ISP?

  3. Как ISP связан с паттерном «Composition over Inheritance»?

  4. Найди нарушение ISP в публичной npm библиотеке (можно в lodash, express или другой).