Опануйте принципи SOLID всього за 8 хвилин!

Опануйте принципи SOLID всього за 8 хвилин!
Переклад 14 хв. читання
17 серпня 2023

У цьому блозі я продемонструю реалізацію принципів SOLID у застосунку React. До кінця цієї статті ви будете повністю розуміти принципи SOLID. Перш ніж ми почнемо, дозвольте мені дати вам короткий вступ до цих принципів.

Що таке принципи SOLID?

Принципи SOLID - це п'ять принципів проєктування, які допомагають нам зробити нашу програму придатною для повторного використання, підтримуваною, масштабованою та слабко зв'язаною. Принципи SOLID такі: *

  • Принцип єдиної відповідальності
  • Принцип відкритості-закритості (Open-Closed)
  • Принцип підстановки Лісков
  • Принцип розділення інтерфейсів
  • Принцип інверсії залежностей

Гаразд, розгляньмо кожен з цих принципів окремо. Я використовую React як приклад, але основні концепції схожі з іншими мовами програмування та фреймворками.

Принцип єдиної відповідальності (SRP)

"Модуль повинен бути відповідальним лише перед одним актором". - Вікіпедія.

Принцип єдиної відповідальності стверджує, що компонент повинен мати одну чітку мету або відповідальність. Він повинен зосереджуватися на конкретній функціональності або поведінці та уникати виконання не пов'язаних з нею завдань. Дотримання SRP робить компоненти чіткішими, модульними та простими для розуміння та модифікації. Розглянемо реальну реалізацію.

// ❌ Погана практика: Компонент з декількома обов'язками

const Products = () => {
    return (
        <div className="products">
            {products.map((product) => (
                <div key={product?.id} className="product">
                    <h3>{product?.name}</h3>
                    <p>${product?.price}</p>
                </div>
            ))}
        </div>
    );
};

У вищенаведеному прикладі компонент Products порушує принцип єдиної відповідальності, беручи на себе кілька обов'язків. Він керує ітераціями продуктів і обробляє рендеринг інтерфейсу для кожного продукту. Це може зробити компонент складним для розуміння, підтримки та тестування в майбутньому.

Замість цього, щоб дотримуватися SRP, зробіть так:

// ✅ Хороша практика: Розподіл обов'язків на менші складові

import Product from './Product';
import products from '../../data/products.json';

const Products = () => {
    return (
        <div className="products">
            {products.map((product) => (
                <Product key={product?.id} product={product} />
            ))}
        </div>
    );
};

// Product.js
// Виділіть окремий компонент, що відповідає за відображення інформації про товар
const Product = ({ product }) => {
    return (
        <div className="product">
            <h3>{product?.name}</h3>
            <p>${product?.price}</p>
        </div>
    );
};

Такий поділ гарантує, що кожен компонент має єдину відповідальність, що полегшує його розуміння, тестування та підтримку.

Принцип відкритості-закритості

"Програмні елементи (класи, модулі, функції тощо) повинні бути відкритими для розширення, але закритими для модифікації". - Вікіпедія.

Принцип відкритості-закритості підкреслює, що компоненти повинні бути відкритими для розширення (можуть додавати нову поведінку або функціональність), але закритими для модифікації (наявний код повинен залишатися незмінним). Цей принцип заохочує створення коду, який є стійким до змін, модульним та легко підтримуваним. Розглянемо реальну реалізацію.

// ❌ Погана практика: Порушення принципу відкритості-закритості

// Button.js
// Existing Button component
const Button = ({ text, onClick }) => {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}

// Button.js
// Модифіковано існуючий компонент кнопки з додатковим проппом іконки (модифікація)
const Button = ({ text, onClick, icon }) => {
  return (
    <button onClick={onClick}>
      <i className={icon} />
      <span>{text}</span>
    </button>
  );
}

// Home.js
// 👇 Уникайте: Модифікований існуючий проп компонента
const Home = () => {
  const handleClick= () => {};

  return (
    <div>
      {/* ❌ Avoid this */}
      <Button text="Submit" onClick={handleClick} icon="fas fa-arrow-right" /> 
    </div>
  );
}

У наведеному вище прикладі ми модифікуємо існуючий компонент Button, додавши проп іконки. Зміна існуючого компонента для пристосування до нових вимог порушує принцип відкритості-закритості. Ці зміни роблять компонент крихкішим і підвищують ризик небажаних побічних ефектів при використанні в різних контекстах.

Замість цього зробіть так:

// ✅ Хороша практика: Принцип "відкритий-закритий"

// Button.js
// Existing Button functional component
const Button = ({ text, onClick }) => {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}

// IconButton.js
// IconButton component
// ✅ Добре: Ви нічого тут не змінили.
const IconButton = ({ text, icon, onClick }) => {
  return (
    <button onClick={onClick}>
      <i className={icon} />
      <span>{text}</span>
    </button>
  );
}

const Home = () => {
  const handleClick = () => {
    // Handle button click event
  }

  return (
    <div>
      <Button text="Submit" onClick={handleClick} />
      {/* 
      <IconButton text="Submit" icon="fas fa-heart" onClick={handleClick} />
    </div>
  );
}

У наведеному вище прикладі ми створюємо окремий функціональний компонент IconButton. Компонент IconButton інкапсулює рендеринг кнопки-піктограми без модифікації існуючого компонента Button. Він дотримується принципу відкритості-закритості, розширюючи функціональність за допомогою композиції, а не модифікації.

Принцип підстановки Лісков

"Об'єкти підтипу повинні бути замінними для об'єктів супертипу" - Вікіпедія.

Принцип підстановки Лісков (LSP) - це фундаментальний принцип об'єктно орієнтованого програмування, який підкреслює необхідність взаємозамінності об'єктів в межах ієрархії. В контексті React-компонентів LSP просуває ідею, що похідні компоненти повинні мати можливість замінювати свої базові компоненти, не впливаючи на коректність або поведінку програми. Розглянемо реальну реалізацію.

// ⚠️ Погана практика
// Такий підхід порушує принцип підстановки Лісков, оскільки він змінює 
// поведінку похідного компонента, що може призвести до непередбачуваних 
// проблем при заміні базового компонента Select на похідний.
const BadCustomSelect = ({ value, iconClassName, handleChange }) => {
  return (
    <div>
      <i className={iconClassName}></i>
      <select value={value} onChange={handleChange}>
        <options value={1}>One</options>
        <options value={2}>Two</options>
        <options value={3}>Three</options>
      </select>
    </div>
  );
};

const LiskovSubstitutionPrinciple = () => {
  const [value, setValue] = useState(1);

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  return (
    <div>
      {/** ❌ Уникайте цього */}
      {/** Нижче кастомний селект не має характеристик базового елемента `select */}
      <BadCustomSelect value={value} handleChange={handleChange} />
    </div>
  );
};

У наведеному вище прикладі ми маємо компонент BadCustomSelect, призначений для використання в ролі нестандартного елемента вибору в React. Однак він порушує принцип підстановки Лісков (LSP), оскільки обмежує поведінку базового елемента select.

Замість цього зробіть так:

// ✅ Хороша практика
// Цей компонент слідує принципу підстановки Ліскова і дозволяє використовувати характеристики select.

const CustomSelect = ({ value, iconClassName, handleChange, ...props }) => {
  return (
    <div>
      <i className={iconClassName}></i>
      <select value={value} onChange={handleChange} {...props}>
        <options value={1}>One</options>
        <options value={2}>Two</options>
        <options value={3}>Three</options>
      </select>
    </div>
  );
};

const LiskovSubstitutionPrinciple = () => {
  const [value, setValue] = useState(1);

  const handleChange = (event) => {
    setValue(event.target.value);
  };

  return (
    <div>
      {/* ✅ Цей компонент CustomSelect працює за принципом підстановки Лісков */}
      <CustomSelect
        value={value}
        handleChange={handleChange}
        defaultValue={1}
      />
    </div>
  );
};

У переглянутому коді з'явився компонент CustomSelect, призначений для розширення функціональності стандартного елемента select у React. Компонент приймає такі пропси, як value, iconClassName, handleChange та додаткові пропси за допомогою оператора поширення ...props. Дозволяючи використовувати характеристики елемента select та приймати додаткові пропси, компонент CustomSelect слідує принципу підстановки Лісков (LSP).

Принцип розділення інтерфейсів

"Жоден код не повинен залежати від методів, які він не використовує". - Вікіпедія.

Принцип розділення інтерфейсів (Interface Segregation Principle, ISP) передбачає, що інтерфейси повинні бути сфокусовані та пристосовані до конкретних вимог клієнта, а не бути надмірно широкими та змушувати клієнтів реалізовувати непотрібну функціональність. Розглянемо, як це реалізовано на практиці.

// ❌ Уникайте: розкривати непотрібну інформацію для цього компонента
// Це створює зайві залежності та складність для компонента
const ProductThumbnailURL = ({ product }) => {
  return (
    <div>
      <img src={product.imageURL} alt={product.name} />
    </div>
  );
};

// ❌ Bad Practice
const Product = ({ product }) => {
  return (
    <div>
      <ProductThumbnailURL product={product} />
      <h4>{product?.name}</h4>
      <p>{product?.description}</p>
      <p>{product?.price}</p>
    </div>
  );
};

const Products = () => {
  return (
    <div>
      {products.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
}

У наведеному вище прикладі ми передаємо всю інформацію про продукт компоненту ProductThumbnailURL, хоча він цього не вимагає. Це додає непотрібні ризики та складність компоненту і порушує принцип розділення інтерфейсів (Interface Segregation Principle, ISP).

Проведемо рефакторинг, щоб дотримуватися ISP:

// ✅ Добре: зменшення непотрібних залежностей та покращення 
// кодова база стає легшою в обслуговуванні та масштабуванні.
const ProductThumbnailURL = ({ imageURL, alt }) => {
  return (
    <div>
      <img src={imageURL} alt={alt} />
    </div>
  );
};

// ✅ Good Practice
const Product = ({ product }) => {
  return (
    <div>
      <ProductThumbnailURL imageURL={product.imageURL} alt={product.name} />
      <h4>{product?.name}</h4>
      <p>{product?.description}</p>
      <p>{product?.price}</p>
    </div>
  );
};

const Products = () => {
  return (
    <div>
      {products.map((product) => (
        <Product key={product.id} product={product} />
      ))}
    </div>
  );
};

В оновленому коді компонент ProductThumbnailURL отримує лише необхідну інформацію, а не всю інформацію про продукт. Це запобігає непотрібним ризикам і сприяє дотриманню принципу розділення інтерфейсів (ISP).

Принцип інверсії залежності

"Одна сутність повинна залежати від абстракцій, а не від конкретики" - Вікіпедія.

Принцип інверсії залежностей (Dependency Inversion Principle, DIP) підкреслює, що високорівневі компоненти не повинні залежати від низькорівневих компонентів. Цей принцип сприяє вільному зв'язку та модульності, а також полегшує обслуговування програмних систем. Розглянемо, як це реалізовано на практиці.

// ❌ Погана практика
// Цей компонент слідує за конкретизацією замість абстракції і 
// порушує принцип інверсії залежностей

const CustomForm = ({ children }) => {
  const handleSubmit = () => {
    // submit operations
  };
  return <form onSubmit={handleSubmit}>{children}</form>;
};

const DependencyInversionPrinciple = () => {
  const [email, setEmail] = useState();

  const handleChange = (event) => {
    setEmail(event.target.value);
  };

  const handleFormSubmit = (event) => {
    // submit business logic here
  };

  return (
    <div>
      {/** ❌ Уникайте: міцно з'єднані та важко змінювані */}
      <CustomForm>
        <input
          type="email"
          value={email}
          onChange={handleChange}
          name="email"
        />
      </CustomForm>
    </div>
  );
};

Компонент CustomForm тісно пов'язаний зі своїми дочірніми елементами, що перешкоджає гнучкості та ускладнює зміну або розширення його поведінки.

Замість цього зробіть так:

// ✅ Належна практика
// Цей компонент слідує за абстракцією та просуває принцип інверсії залежностей

const AbstractForm = ({ children, onSubmit }) => {
  const handleSubmit = (event) => {
    event.preventDefault();
    onSubmit();
  };

  return <form onSubmit={handleSubmit}>{children}</form>;
};

const DependencyInversionPrinciple = () => {
  const [email, setEmail] = useState();

  const handleChange = (event) => {
    setEmail(event.target.value);
  };

  const handleFormSubmit = () => {
    // submit business logic here
  };

  return (
    <div>
      {/** ✅ Натомість використовуйте абстракцію */}
      <AbstractForm onSubmit={handleFormSubmit}>
        <input
          type="email"
          value={email}
          onChange={handleChange}
          name="email"
        />
        <button type="submit">Submit</button>
      </AbstractForm>
    </div>
  );
};

У переглянутому коді ми ввели компонент AbstractForm, який діє як абстракція для форми. Він отримує функцію onSubmit як проп і обробляє надсилання форми. Такий підхід дозволяє нам легко замінити або розширити поведінку форми, не змінюючи компонент вищого рівня.

Висновок

Принципи SOLID дають рекомендації, які дозволяють розробникам створювати добре продумані, підтримувані та розширювані програмні рішення. Дотримуючись цих принципів, розробники можуть досягти модульності, повторного використання коду, гнучкості та зменшення складності коду.

Я сподіваюся, що цей блог надав вам цінну інформацію та надихнув вас на застосування цих принципів у ваших поточних або майбутніх проєктах на React.

Джерело: Mastering SOLID Principles in Just 8 Minutes!
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація