Что такое SOLID: Принципы Солид программирование

Что такое SOLID: Принципы Солид программирование

  • 23 января
  • читать 10 мин
Александра Донцова
Александра Донцова Front-end Developer в Sigma Software, Преподаватель Компьютерной школы Hillel.

Разберем в данной статье довольно популярный запрос в поиске или ChatGPT: solid что это.

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

Вот краткий обзор каждого принципа:

  • Single Responsibility Principle (Принцип единой ответственности): Каждый класс должен иметь только одну причину для изменения, что означает, что класс должен выполнять только одну задачу.
  • Open/Closed Principle (Принцип открытости/закрытости): Программные сущности (классы, модули, функции и т.д.) должны быть открытыми для расширения, но закрытыми для изменений. Это означает, что вы можете добавлять новые функции без изменения существующего кода.
  • Liskov Substitution Principle (Принцип подстановки Лисков): Объекты должны быть заменяемы их подтипами без изменения правильности программы. То есть, подклассы должны быть заменяемыми на их родительские классы.
  • Interface Segregation Principle (Принцип сегрегации интерфейса): Клиенты не должны быть вынуждены зависеть от интерфейсов, которые они не используют. Это стимулирует создание узких интерфейсов, которые более конкретно отвечают потребностям клиентов.
  • Dependency Inversion Principle (Принцип инверсии зависимостей): Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба типа модулей должны зависеть от абстракций.

solid что это

Рекомендуем публикацию по теме

Важность SOLID для Front-end разработчиков

  • Поддерживаемость и расширяемость: Front-end проекты часто быстро растут и эволюционируют, особенно в сложных приложениях. Следуя SOLID, разработчики могут легче адаптировать и расширять свой код без значительных переписываний.
  • Уменьшение сложности: Современные веб-приложения могут быть очень сложными. SOLID помогает управлять этой сложностью, разделяя код на меньшие, более управляемые части.
  • Содействие командной работе: Когда проект придерживается четко определенных принципов, новые члены команды могут быстрее разобраться в кодовой базе, а совместная работа становится более эффективной.
  • Повышение качества кода: SOLID способствует написанию более чистого, организованного и тестируемого кода, что важно для стабильности и надежности веб-приложений.
  • Адаптивность к изменениям: В условиях постоянного изменения технологий и требований пользователей, соблюдение SOLID позволяет легче адаптироваться к новым требованиям без полного перепроектирования системы.

Ниже рассмотрим детальнее принципы solid.

Принцип единой ответственности

Принцип единой ответственности — один из ключевых принципов в SOLID, который утверждает, что каждый класс или модуль в программном коде должен иметь только одну причину для изменения. Другими словами, это означает, что каждый класс должен отвечать только за одну область функциональности и иметь только одну задачу или ответственность. Этот принцип помогает избежать «божественных объектов» — классов, которые пытаются делать слишком много вещей одновременно, что приводит к сложности и затрудняет поддержку кода.

Примеры:

  • Компоненты пользовательского интерфейса: Представьте, что вы создаете компонент для веб-приложения, например, `UserProfile`. Если этот компонент отвечает и за отображение профиля пользователя, и за получение данных с сервера, и за валидацию данных, он нарушает SRP. Вместо этого вы должны разделить эти задачи: один компонент (`UserProfile`) для отображения интерфейса, другой модуль (например, `UserAPI`) для управления запросами на сервер и третий (например, `UserValidation`) для валидации данных.
  • Функции утилит: Рассмотрим функцию, которая вычисляет и форматирует дату. Если эта функция и вычисляет дату (например, добавляет дни к текущей дате), и форматирует ее для отображения (например, преобразует в формат «дд-мм-рррр»), то она нарушает SRP. Лучше иметь одну функцию для вычисления даты и другую для ее форматирования.
  • Реакция на события (Event Handling): Предположим, что у вас есть кнопка, которая выполняет две разные функции: сохранение данных формы и ее закрытие. Это нарушение SRP, поскольку обработчик событий кнопки имеет две причины для изменения (сохранение данных и закрытие формы). Лучше разделить эти функции на два отдельных обработчика событий.
function handleSubmit(formData) {
  if (validateFormData(formData)) {
    saveFormData(formData);
    displaySuccessMessage();
  } else {
    displayErrorMessage();
  }
}


function validateFormData(formData) {
  // Код для валідації даних форми
}


function saveFormData(formData) {
  // Код для збереження даних форми
}


function displaySuccessMessage() {
  alert('Дані успішно збережено!');
}


function displayErrorMessage() {
  alert('Помилка у введених даних!');
}

Принцип открытости/закрытости (OCP)

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

Представим, что мы разрабатываем веб-приложение, где нужно отображать различные типы сообщений пользователям. Мы можем создать базовый класс для сообщения и расширить его для конкретных типов сообщений.

Перед изменением (без применения OCP):

class Alert {
  constructor(message) {
      this.message = message;
  }


  display() {
      // Логіка відображення звичайного повідомлення
      console.log(`Alert: ${this.message}`);
  }
}


// Використання
const alert = new Alert("Помилка!");
alert.display();

В этом случае, если нам нужно добавить другие типы уведомлений (например, успех, предупреждение), нам придется изменять класс `Alert`.

После изменения (с применением OCP):

class Alert {
  constructor(message) {
      this.message = message;
  }


  display() {
      // Базова логіка відображення повідомлення
      console.log(this.message);
  }
}


class SuccessAlert extends Alert {
  display() {
      // Логіка відображення повідомлення про успіх
      console.log(`Success: ${this.message}`);
  }
}


class ErrorAlert extends Alert {
  display() {
      // Логіка відображення повідомлення про помилку
      console.log(`Error: ${this.message}`);
  }
}


// Використання
const successAlert = new SuccessAlert("Операція успішна!");
const errorAlert = new ErrorAlert("Помилка при операції!");


successAlert.display();
errorAlert.display();

В этом примере базовый класс `Alert` остается неизменным, а функциональность для различных типов сообщений реализована в подклассах. Таким образом, если нам нужно добавить новый тип сообщения, мы можем просто создать новый подкласс без изменения существующего кода.

Принцип подстановки Лисков (LSP)

Принцип подстановки Лисков утверждает, что если класс S является подтипом класса T, тогда объекты типа T можно заменить объектами типа S без изменения желаемой правильности выполнения программы. Это означает, что подклассы должны быть заменяемыми на их родительские классы.

Этот принцип особенно важен в полиморфизме и наследовании. В Front-end разработке, где мы часто работаем с компонентными иерархиями (например, в React или Vue), это означает, что потомки компонентов должны иметь возможность заменить родительские компоненты без нарушения функциональности.

Представим, что мы разрабатываем приложение, в котором есть базовый класс `Button`, и различные специализированные кнопки, такие как `IconButton` или `SubmitButton`.

Без применения LSP:

class Button {
  click() {
      console.log('Клік по кнопці');
  }
}


class IconButton extends Button {
  click() {
      throw new Error("Ця кнопка не підтримує клік");
  }


  // специфічна логіка для IconButton
  iconClick() {
      console.log('Клік по іконці');
  }
}


function performClick(button) {
  button.click();
}


const regularButton = new Button();
const iconButton = new IconButton();


performClick(regularButton); // працює правильно
performClick(iconButton); // викине виняток

В этом примере, когда мы пытаемся использовать `IconButton` вместо `Button`, мы получаем ошибку, что является нарушением LSP.

С применением LSP:

class Button {
  click() {
      console.log('Клік по кнопці');
  }
}


class IconButton extends Button {
  // IconButton розширює функціональність, але не змінює базову поведінку
  iconClick() {
      console.log('Клік по іконці');
  }
}


function performClick(button) {
  button.click();
}


const regularButton = new Button();
const iconButton = new IconButton();


performClick(regularButton); // працює правильно
performClick(iconButton); // також працює правильно

В этом примере `IconButton` расширяет функциональность `Button`, но не изменяет его базового поведения, поэтому `IconButton` может безопасно заменять `Button` без возникновения ошибок.

Использование LSP позволяет создавать более гибкие и масштабируемые иерархии классов, которые легко поддерживать и расширять.

Принцип сегрегации интерфейса (ISP)

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

ISP помогает создать модульную архитектуру в приложениях, где каждый модуль или компонент имеет узко определенную ответственность. Это облегчает управление зависимостями, упрощает тестирование и поддержку, а также повышает переиспользование кода.

Перед применением ISP:

// Більший інтерфейс, який визначає методи для різних типів дій
interface UserActions {
  createPost(content: string): void;
  sendMessage(user: User, message: string): void;
  createSchedule(date: Date, task: string): void;
}


class User implements UserActions {
  createPost(content: string) {
      // Логіка створення посту
  }


  sendMessage(user: User, message: string) {
      // Логіка надсилання повідомлення
  }


  createSchedule(date: Date, task: string) {
      // Логіка створення розкладу
  }
}


// У цьому випадку, клас User змушений реалізовувати всі методи, хоча деякі з них можуть бути йому не потрібні

После применения ISP:

// Розділені інтерфейси для кожної конкретної дії
interface PostCreator {
  createPost(content: string): void;
}


interface MessageSender {
  sendMessage(user: User, message: string): void;
}


interface ScheduleCreator {
  createSchedule(date: Date, task: string): void;
}


class User implements PostCreator, MessageSender {
  createPost(content: string) {
      // Логіка створення посту
  }


  sendMessage(user: User, message: string) {
      // Логіка надсилання повідомлення
  }


  // Метод createSchedule видалений, оскільки він не потрібен для класу User
}

В этом примере, после разделения большого интерфейса `UserActions` на несколько меньших (`PostCreator`, `MessageSender`, `ScheduleCreator`), класс `User` теперь может выборочно реализовывать только те интерфейсы, которые ему нужны. Это уменьшает ненужные зависимости и делает код более модульным и гибким.

Принцип инверсии зависимостей (DIP)

Принцип инверсии зависимостей указывает, что модули высокого уровня (те, которые выполняют ключевые бизнес-операции) не должны зависеть от модулей низкого уровня (те, которые выполняют базовые операции, например доступ к данным). Вместо этого, оба типа модулей должны зависеть от абстракций (интерфейсов или абстрактных классов).

DIP позволяет создавать более гибкие, легкие для тестирования и поддержки Front-end системы, особенно важные при работе с большими приложениями и фреймворками, такими как React, Angular или Vue.

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

Без применения DIP:

// Клас для отримання даних користувача з сервера
class UserApi {
  getUser(userId) {
      // Логіка отримання даних користувача
  }
}


// Компонент високого рівня, який залежить від конкретної реалізації UserApi
class UserProfile {
  constructor(userId, api) {
      this.userId = userId;
      this.api = api;
  }


  showProfile() {
      const userData = this.api.getUser(this.userId);
      // Відображення даних користувача
  }
}


const api = new UserApi();
const userProfile = new UserProfile(1, api);
userProfile.showProfile();

В этом случае `UserProfile` напрямую зависит от конкретной реализации `UserApi`, что усложняет тестирование и рефакторинг.

С применением DIP:

// Абстракція, від якої залежить і компонент, і модуль доступу до даних
interface UserDataSource {
  getUser(userId): User;
}


// Реалізація інтерфейсу для отримання даних користувача з сервера
class UserApi implements UserDataSource {
  getUser(userId) {
      // Логіка отримання даних користувача
  }
}


// Компонент, який залежить від абстракції
class UserProfile {
  constructor(userId, dataSource) {
      this.userId = userId;
      this.dataSource = dataSource;
  }


  showProfile() {
      const userData = this.dataSource.getUser(this.userId);
      // Відображення даних користувача
  }
}


const api = new UserApi();
const userProfile = new UserProfile(1, api);
userProfile.showProfile();

Здесь `UserProfile` и `UserApi` зависят от абстракции `UserDataSource`. Это позволяет легко заменить реализацию получения данных без изменения `UserProfile`, упрощая тестирование и расширение функциональности.

SOLID принципы формируют фундамент для создания прочной архитектуры программного обеспечения, особенно в сфере Front-end разработки. Эти принципы направлены на повышение гибкости, поддерживаемости и масштабируемости кода, что является критически важным в быстро меняющемся мире современных вебтехнологий.

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

В мире, где технологии и требования постоянно меняются, важно, чтобы Front-end разработчики принимали и использовали SOLID принципы в своей работе. Это позволит создавать более адаптивные, стабильные веб-приложения, которые легко поддерживать. Использование этих принципов в рабочих процессах не только повысит эффективность и качество кода, но и сделает разработку более приятной.

Поэтому, как Front-end разработчики, мы должны стремиться к включению этих принципов в наши ежедневные практики кодирования, ведь они являются фундаментом для создания высококачественных, эффективных и длительных веб-приложений.

Рекомендуем курс по теме