SOLID принципы в TypeScript: теория и практика с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему SOLID важен в TypeScript

SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного проектирования, сформулированных Робертом Мартином (дядюшка Боб). В контексте TypeScript эти принципы приобретают особую ценность благодаря строгой системе типов и поддержке современных парадигм ООП. Применение SOLID делает код более гибким, тестируемым и поддерживаемым, что критически важно для масштабируемых проектов.

В этой статье мы разберём каждый принцип SOLID на практических примерах TypeScript, покажем антипаттерны и их правильные реализации. Вы узнаете, как писать код, который не ломается при добавлении новой функциональности.

1. Single Responsibility Principle (SRP) — Принцип единственной ответственности

Теория

Каждый класс должен иметь только одну причину для изменения. Это означает, что класс должен отвечать только за одну часть функциональности программы. Если класс выполняет несколько задач, его сложнее поддерживать и тестировать.

Антипаттерн (нарушение SRP)

class UserService {
    saveUser(user: User): void {
        // Сохранение в БД
        console.log('User saved');
    }

    sendEmail(user: User, message: string): void {
        // Отправка email
        console.log(`Email sent to ${user.email}`);
    }

    generateReport(user: User): string {
        // Генерация отчёта
        return `Report for ${user.name}`;
    }
}

Правильная реализация (SRP)

class UserRepository {
    save(user: User): void {
        // Только сохранение
    }
}

class EmailService {
    send(user: User, message: string): void {
        // Только отправка email
    }
}

class ReportGenerator {
    generate(user: User): string {
        // Только генерация отчётов
    }
}

2. Open-Closed Principle (OCP) — Принцип открытости/закрытости

Теория

Классы должны быть открыты для расширения, но закрыты для модификации. Новую функциональность следует добавлять через наследование или композицию, а не изменением существующего кода.

Антипаттерн (нарушение OCP)

class PaymentProcessor {
    processPayment(type: string, amount: number): void {
        if (type === 'card') {
            // Обработка карты
        } else if (type === 'paypal') {
            // Обработка PayPal
        } else if (type === 'crypto') {
            // Обработка криптовалюты
        }
        // При добавлении нового типа — нужно менять класс!
    }
}

Правильная реализация (OCP)

interface PaymentMethod {
    process(amount: number): void;
}

class CardPayment implements PaymentMethod {
    process(amount: number): void {
        console.log(`Processing card payment: $${amount}`);
    }
}

class PayPalPayment implements PaymentMethod {
    process(amount: number): void {
        console.log(`Processing PayPal payment: $${amount}`);
    }
}

class PaymentProcessor {
    constructor(private paymentMethod: PaymentMethod) {}

    process(amount: number): void {
        this.paymentMethod.process(amount);
    }
}

3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков

Теория

Объекты базового класса должны заменяться объектами его подклассов без нарушения работы программы. Подклассы не должны изменять ожидаемое поведение базового класса.

Антипаттерн (нарушение LSP)

class Rectangle {
    constructor(public width: number, public height: number) {}

    setWidth(width: number): void {
        this.width = width;
    }

    setHeight(height: number): void {
        this.height = height;
    }

    getArea(): number {
        return this.width * this.height;
    }
}

class Square extends Rectangle {
    setWidth(width: number): void {
        this.width = width;
        this.height = width; // Нарушение!
    }

    setHeight(height: number): void {
        this.height = height;
        this.width = height; // Нарушение!
    }
}

Правильная реализация (LSP)

interface Shape {
    getArea(): number;
}

class Rectangle implements Shape {
    constructor(public width: number, public height: number) {}

    getArea(): number {
        return this.width * this.height;
    }
}

class Square implements Shape {
    constructor(public side: number) {}

    getArea(): number {
        return this.side * this.side;
    }
}

function printArea(shape: Shape): void {
    console.log(`Area: ${shape.getArea()}`);
}

4. Interface Segregation Principle (ISP) — Принцип разделения интерфейсов

Теория

Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько специализированных интерфейсов, чем один универсальный.

Антипаттерн (нарушение 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('Robot cannot eat!'); }
    sleep(): void { throw new Error('Robot cannot sleep!'); }
}

Правильная реализация (ISP)

interface Workable {
    work(): void;
}

interface Eatable {
    eat(): void;
}

interface Sleepable {
    sleep(): void;
}

class HumanWorker implements Workable, Eatable, Sleepable {
    work(): void { /* работает */ }
    eat(): void { /* ест */ }
    sleep(): void { /* спит */ }
}

class RobotWorker implements Workable {
    work(): void { /* работает */ }
}

5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Теория

Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали должны зависеть от абстракций.

Антипаттерн (нарушение DIP)

class MySQLDatabase {
    connect(): void { /* подключение к MySQL */ }
    query(sql: string): any { /* выполнение запроса */ }
}

class UserService {
    private database: MySQLDatabase;

    constructor() {
        this.database = new MySQLDatabase(); // Жёсткая связь
    }

    getUser(id: number): User {
        return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
    }
}

Правильная реализация (DIP)

interface Database {
    connect(): void;
    query(sql: string): any;
}

class MySQLDatabase implements Database {
    connect(): void {                

Блоги

Рекомендаций по книгам