SOLID принципы в TypeScript: теория и практика от А до Я

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

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

Начать курс

SOLID принципы в TypeScript: теория и практика от А до Я

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

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

Определение: Каждый класс должен иметь только одну причину для изменения. Иными словами, класс должен отвечать за одну чётко определённую задачу.

Проблема: Когда класс берёт на себя несколько обязанностей, его становится сложно тестировать, расширять и поддерживать. Изменение одной функции может поломать другую.

Пример нарушения SRP:

// ❌ Нарушение SRP: класс занимается и хранением, и форматированием, и отправкой
class UserService {
    saveUser(user: User): void {
        // Сохранение в БД
        console.log('User saved');
    }

    formatUserData(user: User): string {
        return `Name: ${user.name}, Email: ${user.email}`;
    }

    sendWelcomeEmail(user: User): void {
        // Отправка email
        console.log('Email sent');
    }
}

Решение с SRP:

// ✅ Каждый класс отвечает за свою задачу
class UserRepository {
    save(user: User): void {
        console.log('User saved');
    }
}

class UserFormatter {
    format(user: User): string {
        return `Name: ${user.name}, Email: ${user.email}`;
    }
}

class EmailService {
    sendWelcome(user: User): void {
        console.log('Email sent');
    }
}

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

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

Пример нарушения OCP:

// ❌ При добавлении нового типа фигуры придётся менять метод calculateArea
class AreaCalculator {
    calculateArea(shape: any): number {
        if (shape.type === 'circle') {
            return Math.PI * shape.radius ** 2;
        } else if (shape.type === 'rectangle') {
            return shape.width * shape.height;
        }
        throw new Error('Unknown shape');
    }
}

Решение с OCP (используем полиморфизм):

// ✅ Используем интерфейс и наследование
interface Shape {
    area(): number;
}

class Circle implements Shape {
    constructor(private radius: number) {}
    area(): number {
        return Math.PI * this.radius ** 2;
    }
}

class Rectangle implements Shape {
    constructor(private width: number, private height: number) {}
    area(): number {
        return this.width * this.height;
    }
}

class AreaCalculator {
    calculateArea(shape: Shape): number {
        return shape.area(); // Не требует изменений при добавлении новых фигур
    }
}

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

Определение: Объекты в программе должны быть заменяемы экземплярами их подтипов без изменения правильности выполнения программы. Дочерний класс не должен нарушать контракт родительского класса.

Пример нарушения LSP:

// ❌ Нарушение: подкласс изменяет поведение предка
class Bird {
    fly(): void {
        console.log('Flying');
    }
}

class Penguin extends Bird {
    fly(): void {
        throw new Error('Penguins cannot fly!'); // Нарушение контракта
    }
}

Решение с LSP:

// ✅ Разделяем иерархию: не все птицы летают
interface Flyable {
    fly(): void;
}

class Bird {
    // Общие свойства птиц
}

class Sparrow extends Bird implements Flyable {
    fly(): void {
        console.log('Sparrow flying');
    }
}

class Penguin extends Bird {
    // Пингвины не летают, но могут плавать
    swim(): void {
        console.log('Penguin swimming');
    }
}

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 does not eat'); } // ❌
    sleep(): void { throw new Error('Robot does not 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 {
    save(data: string): void {
        console.log(`Saving to MySQL: ${data}`);
    }
}

class UserService {
    private db: MySQLDatabase;

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

    saveUser(name: string): void {
        this.db.save(name);
    }
}

Решение с DIP (инверсия зависимостей):

// ✅ Зависимость от абстракции (интерфейса)
interface Database {
    save(data: string): void;
}

class MySQLDatabase implements Database {
    save(data: string): void {
        console.log(`Saving to MySQL: ${data}`);
    }
}

class PostgreSQLDatabase implements Database {
    save(data: string): void {
        console.log(`Saving to PostgreSQL: ${data}`);
    }
}

class UserService {
    constructor(private db: Database                

Блоги

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