Composition over Inheritance

Haunted — это библиотека для создания веб-компонентов на основе Web Components и функционального подхода к состоянию. Она строится на принципе composition over inheritance, что подразумевает использование функций и композиции вместо классической иерархии наследования. Такой подход облегчает повторное использование кода, тестирование и делает архитектуру компонентов более гибкой.


Функциональные компоненты

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

import { html, component, useState } from 'haunted';

function Counter() {
  const [count, setCount] = useState(0);

  return html`
    <div>
      <p>Счётчик: ${count}</p>
      <button @click=${() => setCount(count + 1)}>Увеличить</button>
    </div>
  `;
}

customElements.define('my-counter', component(Counter));

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


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

Haunted предоставляет хуки (useState, useEffect, useReducer и т.д.), которые позволяют комбинировать поведение и состояние без наследования.

function useToggle(initial = false) {
  const [on, setOn] = useState(initial);
  const toggle = () => setOn(!on);
  return [on, toggle];
}

function ToggleButton() {
  const [on, toggle] = useToggle();

  return html`
    <button @click=${toggle}>${on ? 'Включено' : 'Выключено'}</button>
  `;
}

customElements.define('toggle-button', component(ToggleButton));

Здесь useToggle — отдельная функция с поведением, которую можно многократно использовать в разных компонентах. Это композиция поведения вместо наследования классов.


Комбинирование нескольких хуков

В Haunted легко объединять несколько хуков, создавая сложное поведение из простых, независимых функций:

function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  return { count, increment, decrement };
}

function CounterControls() {
  const { count, increment, decrement } = useCounter();

  return html`
    <div>
      <p>Значение: ${count}</p>
      <button @click=${increment}>+</button>
      <button @click=${decrement}>-</button>
    </div>
  `;
}

customElements.define('counter-controls', component(CounterControls));

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


Higher-Order Components и декораторы

Haunted поддерживает концепцию Higher-Order Components (HOC) для повторного использования логики.

function withLogging(componentFn) {
  return function(props) {
    console.log('Рендер компонента', componentFn.name, props);
    return componentFn(props);
  };
}

function Hello({ name }) {
  return html`<p>Привет, ${name}</p>`;
}

customElements.define('hello-logged', component(withLogging(Hello)));

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


Взаимодействие компонентов через Props

В Haunted компоненты взаимодействуют через props, а не через наследование:

function UserCard({ user }) {
  return html`
    <div>
      <h2>${user.name}</h2>
      <p>${user.email}</p>
    </div>
  `;
}

function App() {
  const user = { name: 'Иван', email: 'ivan@example.com' };
  return html`<user-card .user=${user}></user-card>`;
}

customElements.define('app-root', component(App));

Преимущество: компоненты полностью изолированы и легко тестируются, а изменение поведения достигается через составление других компонентов, а не через наследование.


Составные компоненты (Composition)

Haunted поощряет вложенные компоненты, где каждый элемент выполняет свою функцию, а сложные интерфейсы строятся из простых:

function Modal({ title, content }) {
  return html`
    <div class="modal">
      <h1>${title}</h1>
      <div>${content}</div>
    </div>
  `;
}

function App() {
  return html`
    <modal
      .title=${'Приветствие'}
      .content=${html`<p>Добро пожаловать!</p>`}
    ></modal>
  `;
}

customElements.define('app-modal', component(App));

Здесь Modal — независимый компонент, который можно использовать в разных местах приложения, комбинируя с любым контентом.


Преимущества подхода Composition over Inheritance в Haunted

  1. Модульность — функции можно легко переиспользовать.
  2. Тестируемость — каждый хук и компонент можно тестировать отдельно.
  3. Гибкость — поведение компонентов можно изменять без изменения иерархий.
  4. Простота поддержки — меньше связанных классов, меньше сложности.

Итоговый паттерн

Основная идея Haunted — создавать компоненты как чистые функции с состоянием и поведением, комбинируя их через хуки, props и HOC. Наследование классов здесь используется лишь для регистрации элементов (customElements.define), но логика полностью строится через композицию. Такой подход облегчает масштабирование, переиспользование и поддержку сложных приложений.