SOLID принципы в JavaScript: продвинутая реализация и примеры кода

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

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

Начать курс

Введение: SOLID в экосистеме JavaScript

SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного проектирования, сформулированных Робертом Мартином. В контексте JavaScript, особенно при разработке на Node.js с использованием TypeScript и современных фреймворков (NestJS, Express), эти принципы становятся критически важными для построения масштабируемых и поддерживаемых приложений. На продвинутом уровне SOLID выходит за рамки простых классов и интерфейсов, затрагивая архитектуру модулей, внедрение зависимостей и тестирование.

В этой статье мы рассмотрим каждый принцип SOLID на примерах кода для Node.js, акцентируя внимание на практических нюансах и типичных ошибках. Вы узнаете, как применять SOLID в реальных проектах, используя возможности JavaScript (ES6+) и TypeScript.

1. Принцип единственной ответственности (SRP) в JavaScript

Теория: Класс или модуль должен иметь только одну причину для изменения. В JavaScript это часто нарушается, когда в одном файле смешиваются бизнес-логика, работа с БД и форматирование ответов.

Практика (Node.js): Разделяем ответственность на уровне сервисов и репозиториев.

// ❌ Нарушение SRP: класс обрабатывает запросы, валидирует и сохраняет
class UserService {
    createUser(data) {
        if (!data.email) throw new Error('Email required');
        const db = new Database();
        return db.save('users', data);
    }
}

// ✅ Соблюдение SRP: разделение на валидатор, репозиторий и сервис
class UserValidator {
    validate(data) {
        if (!data.email) throw new Error('Email required');
    }
}

class UserRepository {
    constructor(db) {
        this.db = db;
    }
    save(data) {
        return this.db.save('users', data);
    }
}

class UserService {
    constructor(validator, repository) {
        this.validator = validator;
        this.repository = repository;
    }
    createUser(data) {
        this.validator.validate(data);
        return this.repository.save(data);
    }
}

Используйте внедрение зависимостей (DI) для передачи валидатора и репозитория в сервис. Это упрощает тестирование и замену компонентов.

2. Принцип открытости/закрытости (OCP) через полиморфизм

Теория: Классы должны быть открыты для расширения, но закрыты для модификации. В JavaScript это достигается через наследование, композицию или стратегии.

Практика (Node.js с TypeScript): Используем абстрактные классы или интерфейсы для определения контракта.

// Абстрактный класс для отчётов
abstract class ReportGenerator {
    abstract generate(data: any): string;
}

class PDFReport extends ReportGenerator {
    generate(data: any): string {
        return `PDF: ${JSON.stringify(data)}`;
    }
}

class CSVReport extends ReportGenerator {
    generate(data: any): string {
        return `CSV: ${data.join(',')}`;
    }
}

// Клиентский код не меняется при добавлении новых форматов
class ReportService {
    constructor(private generator: ReportGenerator) {}
    createReport(data: any) {
        return this.generator.generate(data);
    }
}

Для чистого JavaScript используйте фабрики или стратегии (паттерн Strategy), чтобы избежать модификации существующего кода.

3. Принцип подстановки Лисков (LSP) в асинхронном коде

Теория: Объекты базового класса должны быть заменяемы объектами производного класса без нарушения работы программы. В JavaScript LSP часто нарушается при переопределении методов с разными контрактами (например, возвращаемые типы или исключения).

Практика (Node.js с async/await): Следите за тем, чтобы подклассы не ослабляли предусловия и не усиливали постусловия.

class Database {
    async connect() { return 'connected'; }
    async query(sql) { return []; }
}

class MySQLDatabase extends Database {
    async connect() { 
        // OK: возвращает тот же тип
        return 'mysql_connected'; 
    }
    async query(sql) {
        // ❌ Нарушение: выбрасывает ошибку, если sql не строка
        if (typeof sql !== 'string') throw new Error('Invalid SQL');
        return super.query(sql);
    }
}

// ✅ Соблюдение LSP: подкласс не добавляет новых проверок
class PostgresDatabase extends Database {
    async connect() { return 'postgres_connected'; }
    async query(sql) {
        // Используем те же предусловия
        return await super.query(sql);
    }
}

Избегайте усложнения контракта в подклассах. Используйте TypeScript для явного описания типов возвращаемых значений.

4. Принцип разделения интерфейсов (ISP) для микросервисов

Теория: Клиенты не должны зависеть от интерфейсов, которые они не используют. В JavaScript это актуально для API-клиентов и модулей.

Практика (Node.js): Разделяем большие интерфейсы на специализированные.

// ❌ Нарушение ISP: один интерфейс для всех операций
class FileManager {
    read(path) { /* ... */ }
    write(path, data) { /* ... */ }
    delete(path) { /* ... */ }
    execute(path) { /* ... */ } // Не нужен для чтения
}

// ✅ Соблюдение ISP: выделяем отдельные интерфейсы
class ReadOnlyFileManager {
    read(path) { /* ... */ }
}

class WriteFileManager {
    write(path, data) { /* ... */ }
}

class DeleteFileManager {
    delete(path) { /* ... */ }
}

// Клиент использует только нужный интерфейс
class ReportReader {
    constructor(private fileManager: ReadOnlyFileManager) {}
    getReport(path) {
        return this.fileManager.read(path);
    }
}

В Node.js это особенно полезно при работе с адаптерами для внешних API (например, разделение на IEmailSender и ISMSSender).

5. Принцип инверсии зависимостей (DIP) с DI-контейнерами

Теория: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. В JavaScript это реализуется через внедрение зависимостей и контейнеры.

Практика (Node.js с Inversify или Awilix): Используем DI-контейнер для управления зависимостями.

// Абстракция (интерфейс)
class Logger {
    log(message) { throw new Error('Not implemented'); }
}

// Конкретная реализация
class ConsoleLogger extends Logger {
    log(message) { console.log(message); }
}

// Модуль верхнего уровня зависит от абстракции
class OrderService {
    constructor(private logger: Logger) {}
    createOrder(order) {                

Блоги

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