Композиция против наследования

Одной из ключевых концепций современного подхода к разработке пользовательских интерфейсов является выбор между композицией компонентов и использованием наследования. Preact, как облегчённая и высокопроизводительная альтернатива React, полностью поддерживает модель композиции, которая является основным паттерном построения UI.


Проблемы наследования в UI-компонентах

Наследование предполагает создание иерархий классов, где дочерние компоненты расширяют базовые. На практике это создаёт несколько ограничений:

  • Жёсткая связь между компонентами. Любые изменения в базовом классе могут непредсказуемо влиять на все наследники.
  • Сложность повторного использования. Повторное использование поведения базового компонента в другой иерархии может потребовать создания новых промежуточных классов.
  • Сложности тестирования и поддержки. Глубокие цепочки наследования усложняют отслеживание источника ошибок и увеличивают вероятность побочных эффектов.

В Preact компоненты обычно реализуются как функции или классы. Даже классовый подход не рекомендуется использовать для глубокого наследования из-за лёгкости и гибкости функциональных компонентов.


Преимущества композиции

Композиция компонентов заключается в том, что сложные интерфейсы строятся из более простых, без привязки к иерархии классов:

  • Гибкость. Компоненты можно комбинировать любыми способами, создавая новые функциональные блоки без изменения исходного кода.
  • Повторное использование. Малые компоненты можно включать в несколько разных интерфейсов без необходимости наследования.
  • Изоляция состояния. Каждый компонент управляет своим локальным состоянием, что облегчает отладку и тестирование.

В Preact композиция реализуется через вложенные компоненты, children, props и контексты.

Пример функциональной композиции:

import { h } from 'preact';

function Button({ label, onClick }) {
  return <button onCl ick={onClick}>{label}</button>;
}

function Toolbar() {
  return (
    <div>
      <Button label="Сохранить" onCl ick={() => console.log('Сохранено')} />
      <Button label="Отмена" onCl ick={() => console.log('Отменено')} />
    </div>
  );
}

В этом примере Toolbar компонует несколько кнопок без необходимости наследовать их функциональность.


Использование children для композиции

Свойство children позволяет передавать внутрь компонента любые элементы, обеспечивая динамическую композицию:

function Card({ children, title }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-content">{children}</div>
    </div>
  );
}

function App() {
  return (
    <Card title="Новости">
      <p>Сегодня произошло важное событие.</p>
      <p>Подробности внутри.</p>
    </Card>
  );
}

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


Контексты как инструмент композиции

Preact поддерживает контекст (Context API) для передачи данных через дерево компонентов без необходимости прокидывания props через все уровни. Это особенно важно для больших приложений:

import { createContext } from 'preact';
import { useContext } from 'preact/hooks';

const ThemeContext = createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Кнопка</button>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

Контексты позволяют разделять ответственность между компонентами, сохраняя модульность и композиционную структуру.


Высшие компоненты и рендер-пропсы

Higher-Order Components (HOC) и Render Props — это способы композиции, которые заменяют наследование:

  • HOC — функция, принимающая компонент и возвращающая новый компонент с дополнительной функциональностью.
  • Render Props — паттерн, где компонент принимает функцию как prop и использует её для рендеринга.

Пример HOC:

function withLogger(Component) {
  return function Wrapped(props) {
    console.log('Рендер компонента', Component.name);
    return <Component {...props} />;
  };
}

const LoggedButton = withLogger(Button);

Пример Render Props:

function DataFetcher({ render }) {
  const data = ['a', 'b', 'c'];
  return render(data);
}

function App() {
  return (
    <DataFetcher render={(items) => <ul>{items.map(i => <li>{i}</li>)}</ul>} />
  );
}

Оба подхода позволяют добавлять поведение без наследования, что делает архитектуру более гибкой.


Заключение по архитектурным решениям

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

Основная идея — строить интерфейсы как набор независимых блоков, комбинируя их через props, children, контексты и функциональные паттерны вроде HOC и Render Props. Это обеспечивает максимальную гибкость и масштабируемость приложения.