SOLID принципы в JavaScript: теория и практика

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

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

Начать курс

Введение в SOLID принципы в JavaScript

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

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

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

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

Нарушение SRP

Рассмотрим класс UserService, который одновременно управляет пользователями, отправляет email и логирует действия:

class UserService {
  constructor() {
    this.users = [];
  }

  addUser(name) {
    this.users.push({ name });
    console.log('User added'); // Логирование
    this.sendEmail(name); // Отправка email
  }

  sendEmail(name) {
    console.log(`Email sent to ${name}`);
  }

  getUsers() {
    return this.users;
  }
}

Здесь класс имеет три обязанности: управление данными, логирование и отправка уведомлений. Изменение логики логирования или отправки email приведет к изменению класса UserService.

Соблюдение SRP

Разделим обязанности на отдельные классы:

class Logger {
  log(message) {
    console.log(`[LOG]: ${message}`);
  }
}

class EmailService {
  send(name) {
    console.log(`Email sent to ${name}`);
  }
}

class UserService {
  constructor(logger, emailService) {
    this.users = [];
    this.logger = logger;
    this.emailService = emailService;
  }

  addUser(name) {
    this.users.push({ name });
    this.logger.log(`User ${name} added`);
    this.emailService.send(name);
  }

  getUsers() {
    return this.users;
  }
}

Теперь каждый класс отвечает за свою задачу, и изменения в одном сервисе не затрагивают другие.

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

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

Нарушение OCP

Предположим, у нас есть функция расчета площади фигур:

function calculateArea(shape) {
  if (shape.type === 'circle') {
    return Math.PI * shape.radius ** 2;
  } else if (shape.type === 'rectangle') {
    return shape.width * shape.height;
  }
  // При добавлении новой фигуры придется модифицировать эту функцию
}

Соблюдение OCP

Используем полиморфизм через классы:

class Shape {
  area() {
    throw new Error('Method "area()" must be implemented');
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }
  area() {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  area() {
    return this.width * this.height;
  }
}

// Добавляем новую фигуру без изменения существующего кода
class Triangle extends Shape {
  constructor(base, height) {
    super();
    this.base = base;
    this.height = height;
  }
  area() {
    return (this.base * this.height) / 2;
  }
}

function calculateArea(shape) {
  return shape.area();
}

Теперь добавление новой фигуры не требует изменения функции calculateArea.

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

Принцип гласит: объекты в программе должны быть заменяемы экземплярами их подтипов без нарушения корректности программы. Проще говоря, если у вас есть базовый класс, то любой его подкласс должен корректно работать везде, где используется базовый класс.

Нарушение LSP

Классический пример с прямоугольником и квадратом:

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  setWidth(width) {
    this.width = width;
  }

  setHeight(height) {
    this.height = height;
  }

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

class Square extends Rectangle {
  setWidth(width) {
    this.width = width;
    this.height = width; // Нарушение: изменяем высоту
  }

  setHeight(height) {
    this.height = height;
    this.width = height; // Нарушение: изменяем ширину
  }
}

function increaseRectangleWidth(rectangle) {
  rectangle.setWidth(rectangle.width + 1);
  return rectangle.getArea();
}

// Для квадрата это сломает логику
const square = new Square(5, 5);
console.log(increaseRectangleWidth(square)); // Ожидаем 30, получаем 36

Соблюдение LSP

Лучше сделать классы независимыми или использовать общий интерфейс:

class Shape {
  getArea() {
    throw new Error('Method "getArea()" must be implemented');
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  getArea() {
    return this.width * this.height;
  }
}

class Square extends Shape {
  constructor(side) {
    super();
    this.side = side;
  }
  getArea() {
    return this.side * this.side;
  }
}

Теперь и прямоугольник, и квадрат корректно заменяют базовый класс Shape.

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

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

В JavaScript нет строгих интерфейсов, но принцип применяется через композицию и проверку методов.

Нарушение ISP

class Worker {
  work() { console.log('Working...'); }
  eat() { console.log('Eating...'); }
  sleep() { console.log('Sleeping...'); }
}

class Robot extends Worker {
  eat() { throw new Error('Rob                

Блоги

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