5. SOLID: Interface Segregation
Design Patterns. Урок: SOLID — Interface Segregation Principle
Заголовок раздела «Design Patterns. Урок: SOLID — Interface Segregation Principle»Клиенты не должны зависеть от методов, которые они не используют.
Лучше много маленьких специализированных интерфейсов, чем один большой «толстый».
Проблема «жирного» интерфейса
Заголовок раздела «Проблема «жирного» интерфейса»// ❌ Один большой интерфейс принуждает реализовывать всё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'); }}Реальный пример: CRUD репозиторий
Заголовок раздела «Реальный пример: CRUD репозиторий»// ❌ Один интерфейс для всех операций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 в React: пропсы компонентов
Заголовок раздела «ISP в React: пропсы компонентов»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 опциональных пропсов}
// ✅ Разделённые интерфейсы через compositioninterface 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) { ... }Как найти нарушения ISP
Заголовок раздела «Как найти нарушения ISP»Признаки нарушения:
- Классы реализуют методы через
throw new Error('Not implemented') - Методы в интерфейсе, которые нужны только одному из 10 клиентов
- Интерфейс с 15+ методами (скорее всего, его нужно разбить)
- Клиент импортирует интерфейс, но использует 2 из 10 его методов
Практические задания
Заголовок раздела «Практические задания»- Разбей следующий интерфейс на несколько специализированных:
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;}-
Создай систему авторизации с интерфейсами
CanRead,CanWrite,CanDelete,CanAdminи реализуй роли: ReadonlyUser, Editor, Admin. -
Посмотри на TypeScript utility types (
Partial<T>,Pick<T,K>,Omit<T,K>) — как они помогают соблюдать ISP? -
Как ISP связан с паттерном «Composition over Inheritance»?
-
Найди нарушение ISP в публичной npm библиотеке (можно в lodash, express или другой).