Strategy паттерн для вариативности поведения

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


Инкапсуляция поведения

В Lit компоненты обычно наследуют от LitElement и описывают своё состояние через свойства (@property). Strategy паттерн позволяет вынести динамическое поведение из компонента в отдельные классы или функции, которые могут быть подключены к компоненту в зависимости от условий.

Пример структуры Strategy:

class BehaviorA {
  execute() {
    return 'Выполняется поведение A';
  }
}

class BehaviorB {
  execute() {
    return 'Выполняется поведение B';
  }
}

class BehaviorContext {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  run() {
    return this.strategy.execute();
  }
}

В Lit такой контекст можно интегрировать через свойства компонента, что позволяет изменять поведение на лету.


Интеграция Strategy в LitElement

Компонент Lit может хранить ссылку на стратегию и вызывать её методы при обновлении или событиях:

import { LitElement, html, css } from 'lit';
import { property } from 'lit/decorators.js';

class DynamicBehaviorComponent extends LitElement {
  @property({ type: Object }) behavior;

  static styles = css`
    div {
      padding: 16px;
      border: 1px solid #ccc;
      margin: 8px 0;
    }
  `;

  render() {
    return html`
      <div>${this.behavior?.execute() ?? 'Поведение не задано'}</div>
      <button @click=${() => this.changeBehavior()}>Сменить поведение</button>
    `;
  }

  changeBehavior() {
    if (this.behavior instanceof BehaviorA) {
      this.beh * avior = new BehaviorB();
    } else {
      this.beh * avior = new BehaviorA();
    }
  }
}

customElements.define('dynamic-behavior', DynamicBehaviorComponent);

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


Применение Strategy для разных состояний компонента

В веб-приложениях часто требуется, чтобы один компонент мог по-разному реагировать на события или данные. Например, карточка товара может отображать цену в разных валютах, применять разные стили подсветки или изменять текстовые сообщения. Для каждого такого варианта можно создать отдельную стратегию:

class USDPriceStrategy {
  constructor(price) {
    this.price = price;
  }
  execute() {
    return `$${this.price.toFixed(2)}`;
  }
}

class EURPriceStrategy {
  constructor(price) {
    this.price = price;
  }
  execute() {
    return `€${(this.price * 0.93).toFixed(2)}`;
  }
}

Компонент может менять стратегию динамически в зависимости от пользовательских настроек или контекста:

this.beh * avior = new USDPriceStrategy(100);
this.beh * avior = new EURPriceStrategy(100);

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

LitElement реагирует на изменения свойств и автоматически перерисовывает компонент. Это позволяет связать Strategy паттерн с реактивным состоянием без дополнительных слушателей или ручного обновления DOM.

@property({ type: Object })
behavior;

updated(changedProperties) {
  if (changedProperties.has('behavior')) {
    console.log('Стратегия изменилась:', this.behavior.execute());
  }
}

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


Плюсы использования Strategy в Lit

  • Разделение ответственности: компонент отвечает за рендеринг, стратегии — за логику.
  • Расширяемость: новые варианты поведения добавляются без изменения основного кода компонента.
  • Тестируемость: стратегии легко тестировать отдельно.
  • Динамичность: поведение можно менять во время работы приложения, не перезагружая компонент.

Практический пример: переключение темы компонента

class LightTheme {
  apply(element) {
    element.style.backgroundColor = '#fff';
    element.style.color = '#000';
  }
}

class DarkTheme {
  apply(element) {
    element.style.backgroundColor = '#222';
    element.style.color = '#eee';
  }
}

class ThemedComponent extends LitElement {
  @property({ type: Object }) theme;

  render() {
    return html`<div>Тема применена</div>`;
  }

  updated() {
    this.theme?.apply(this.shadowRoot.querySelector('div'));
  }

  toggleTheme() {
    this.theme = this.theme instanceof LightTheme ? new DarkTheme() : new LightTheme();
  }
}

customElements.define('themed-component', ThemedComponent);

Компонент делегирует всю логику изменения стиля объектам стратегий. Это упрощает добавление новых тем и делает код более чистым.


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