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

В JavaScript-фреймворке Lit построение компонентов возможно через два основных подхода: наследование и композицию. Каждый из них имеет свои преимущества и ограничения, и понимание различий критически важно для построения масштабируемых интерфейсов.


Наследование

Наследование в Lit осуществляется через стандартные механизмы классов JavaScript. Компонент создается как класс, который расширяет базовый класс LitElement или другой пользовательский компонент.

import { LitElement, html, css } from 'lit';

class BaseButton extends LitElement {
  static styles = css`
    button {
      padding: 8px 16px;
      border-radius: 4px;
      border: none;
      cursor: pointer;
    }
  `;

  render() {
    return html``;
  }
}

class PrimaryButton extends BaseButton {
  static styles = [
    BaseButton.styles,
    css`
      button {
        background-color: blue;
        color: white;
      }
    `
  ];
}
customElements.define('primary-button', PrimaryButton);

Особенности наследования:

  • Позволяет повторно использовать логику и стили базового класса.
  • Поддерживает расширение методов и свойств базового компонента.
  • Сложность растет при глубокой иерархии: легко столкнуться с неявной зависимостью между компонентами.
  • Любое изменение в базовом компоненте может непредсказуемо повлиять на все наследники.

Композиция

Композиция предполагает создание компонентов как независимых единиц, которые могут включать друг друга через разметку. В Lit это делается с помощью включения одного компонента внутрь другого.

import { LitElement, html, css } from 'lit';
import './primary-button.js';

class DialogBox extends LitElement {
  static styles = css`
    .dialog {
      border: 1px solid #ccc;
      padding: 16px;
      border-radius: 8px;
      width: 300px;
    }
  `;

  render() {
    return html`
      
Ок
`; } } customElements.define('dialog-box', DialogBox);

Особенности композиции:

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

Сравнение подходов

Характеристика Наследование Композиция
Повторное использование Да, через базовые классы Да, через включение компонентов
Изоляция логики Низкая (зависимость от базового класса) Высокая (каждый компонент автономен)
Гибкость Ограничена иерархией Высокая, можно комбинировать как угодно
Масштабируемость Сложнее при глубокой иерархии Легче, благодаря модульности

Практические рекомендации

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

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

В Lit композиция тесно связана с передачей данных через свойства (@property) и обработку событий через CustomEvent:

class CounterButton extends LitElement {
  static properties = {
    count: { type: Number }
  };

  constructor() {
    super();
    this.count = 0;
  }

  _increment() {
    this.count++;
    this.dispatchEvent(new CustomEvent('count-changed', { detail: this.count }));
  }

  render() {
    return html``;
  }
}
customElements.define('counter-button', CounterButton);

Другой компонент может использовать этот элемент:

class Dashboard extends LitElement {
  render() {
    return html`
       console.log(e.detail)}>
    `;
  }
}
customElements.define('dashboard-view', Dashboard);

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