SOLID принципы в TypeScript: теория и практика продвинутого уровня
SOLID — это пять фундаментальных принципов объектно-ориентированного проектирования, которые позволяют создавать гибкий, поддерживаемый и масштабируемый код. В TypeScript, благодаря строгой типизации, интерфейсам и абстрактным классам, реализация этих принципов становится особенно эффективной. В этой статье мы разберем каждый принцип на продвинутом уровне с реальными примерами на TypeScript.
1. Single Responsibility Principle (SRP) — Принцип единственной ответственности
Теория: Каждый класс должен иметь только одну причину для изменения. В TypeScript это означает, что модуль отвечает ровно за одну функциональность.
Продвинутая практика: Часто нарушение SRP проявляется в "божественных объектах" (God Objects). Используйте композицию и делегирование.
// ❌ Нарушение SRP: класс отвечает и за данные, и за валидацию, и за сохранение
class UserService {
constructor(private user: User) {}
validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
saveToDatabase(): void {
// логика сохранения
}
sendWelcomeEmail(): void {
// логика отправки
}
}
// ✅ SRP: разделяем ответственность
interface IEmailValidator {
isValid(email: string): boolean;
}
interface IUserRepository {
save(user: User): Promise<void>;
}
interface INotificationService {
sendWelcome(user: User): Promise<void>;
}
class UserRegistrationService {
constructor(
private validator: IEmailValidator,
private repository: IUserRepository,
private notifier: INotificationService
) {}
async register(user: User): Promise<void> {
if (!this.validator.isValid(user.email)) {
throw new Error('Invalid email');
}
await this.repository.save(user);
await this.notifier.sendWelcome(user);
}
}
2. Open/Closed Principle (OCP) — Принцип открытости/закрытости
Теория: Классы должны быть открыты для расширения, но закрыты для модификации. В TypeScript это достигается через абстрактные классы и интерфейсы.
Продвинутая практика: Используйте паттерн "Стратегия" (Strategy) для реализации OCP.
// ❌ Нарушение OCP: при добавлении нового типа отчета нужно менять класс
class ReportGenerator {
generate(data: any[], type: string): string {
if (type === 'csv') {
return data.map(row => row.join(',')).join('\n');
} else if (type === 'json') {
return JSON.stringify(data);
}
// при добавлении 'xml' — модифицируем этот метод
throw new Error('Unsupported format');
}
}
// ✅ OCP: используем полиморфизм
interface IReportFormatter {
format(data: any[]): string;
}
class CsvFormatter implements IReportFormatter {
format(data: any[]): string {
return data.map(row => row.join(',')).join('\n');
}
}
class JsonFormatter implements IReportFormatter {
format(data: any[]): string {
return JSON.stringify(data);
}
}
class XmlFormatter implements IReportFormatter {
format(data: any[]): string {
// реализация для XML
return '<data>...</data>';
}
}
class ReportGeneratorOCP {
constructor(private formatter: IReportFormatter) {}
generate(data: any[]): string {
return this.formatter.format(data);
}
}
// Использование: расширяем без модификации
const csvReport = new ReportGeneratorOCP(new CsvFormatter());
const jsonReport = new ReportGeneratorOCP(new JsonFormatter());
3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков
Теория: Объекты в программе должны быть заменяемыми экземплярами их подтипов без изменения корректности программы. В TypeScript это особенно критично из-за номинативной типизации.
Продвинутая практика: Избегайте нарушения контрактов базового класса. Используйте шаблон "Шаблонный метод" (Template Method).
// ❌ Нарушение LSP: подкласс усиливает предусловия
abstract class Bird {
abstract fly(): void;
}
class Sparrow extends Bird {
fly(): void {
console.log('Sparrow flying');
}
}
class Penguin extends Bird {
fly(): void {
throw new Error('Penguins cannot fly!'); // ❌ Нарушение контракта
}
}
// ✅ LSP: корректная иерархия
interface IFlyable {
fly(): void;
}
interface ISwimmable {
swim(): void;
}
abstract class BirdLSP {
abstract eat(): void;
}
class SparrowLSP extends BirdLSP implements IFlyable {
eat(): void { /* ... */ }
fly(): void { console.log('Sparrow flying'); }
}
class PenguinLSP extends BirdLSP implements ISwimmable {
eat(): void { /* ... */ }
swim(): void { console.log('Penguin swimming'); }
}
4. Interface Segregation Principle (ISP) — Принцип разделения интерфейсов
Теория: Клиенты не должны зависеть от интерфейсов, которые они не используют. В TypeScript это означает создание тонких, специализированных интерфейсов.
Продвинутая практика: Используйте discriminated unions и utility types для гибких интерфейсов.
// ❌ Нарушение ISP: "толстый" интерфейс
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
class HumanWorker implements Worker {
work(): void { /* ... */ }
eat(): void { /* ... */ }
sleep(): void { /* ... */ }
}
class RobotWorker implements Worker {
work(): void { /* ... */ }
eat(): void { throw new Error('Robots don\'t eat'); } // ❌
sleep(): void { throw new Error('Robots don\'t sleep'); } // ❌
}
// ✅ ISP: разделяем интерфейсы
interface IWorkable {
work(): void;
}
interface IEatable {
eat(): void;
}
interface ISleepable {
sleep(): void;
}
type HumanCapabilities = IWorkable & IEatable & ISleepable;
type RobotCapabilities = IWorkable;
class HumanWorkerISP implements HumanCapabilities {
work(): void { /* ... */ }
eat(): void { /* ... */ }
sleep(): void { /* ... */ }
}
class RobotWorkerISP implements RobotCapabilities {
work(): void { /* ... */ }
}
5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей
Теория: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. В TypeScript — это интерфейсы и абстрактные классы.
Продвинутая практика: Используйте DI-контейнеры (например, InversifyJS) или простую фабрику для инверсии.
// ❌ Нарушение DIP: