← Назад к вопросам

Приведи пример проекта где использовал шаблон проектирования

1.7 Middle🔥 221 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный вопрос. Я действительно много раз применял различные шаблоны проектирования (Design Patterns) на практике. В качестве наиболее показательного примера приведу компонент модального окна/диалога в большом SPA-приложении на React (или Vue/Angular), который мы разрабатывали. Этот компонент идеально иллюстрирует целый ряд паттернов.

Контекст проекта и проблема

В приложении для управления проектами (аналог Jira или Trello) существовала острая необходимость в единой, надежной и гибкой системе отображения модальных окон, диалогов подтверждения, форм создания/редактирования задач, боковых панелей и т.д. Перед командой стояли задачи:

  • Избежать дублирования логики открытия/закрытия.
  • Централизовать управление состоянием видимости и историей (например, для стека модальных окон).
  • Сделать API для вызова модалок из любого места приложения (компонентов, хуков, сторов, не только по клику на кнопку).
  • Обеспечить легкую замену UI-обертки (скругления, тени, анимации) для всех диалогов.
  • Упростить тестирование.

Для решения мы применили комбинацию нескольких классических паттернов.

Реализация и использованные паттерны

1. Singleton (Одиночка) — для сервиса управления модалками

Нам нужна была единая точка управления. Мы создали класс ModalService, экземпляр которого был доступен глобально (через контекст React или DI в Vue). Это гарантировало, что состояние всех модалок координируется из одного места.

// Типизированный сервис, реализующий паттерн Singleton через модуль или контекст
class ModalService {
  private static instance: ModalService;
  private modals: Map<string, IModalComponent> = new Map();
  private listeners: Set<(modals: IModalData[]) => void> = new Set();

  private constructor() {}

  static getInstance(): ModalService {
    if (!ModalService.instance) {
      ModalService.instance = new ModalService();
    }
    return ModalService.instance;
  }

  open(modalId: string, component: React.ComponentType, props: any) {
    this.modals.set(modalId, { component, props });
    this.notifyListeners();
  }

  close(modalId: string) {
    this.modals.delete(modalId);
    this.notifyListeners();
  }

  // ... методы подписки (subscribe/notify)
}

2. Factory Method (Фабричный метод) — для создания конфигураций модалок

Вместо того чтобы вручную прописывать type, title, buttons для каждого диалога подтверждения, мы создали фабричные методы.

// Фабрика предопределенных диалогов
class DialogFactory {
  static createConfirmDialog(
    message: string,
    onConfirm: () => void
  ): IModalConfig {
    return {
      component: ConfirmDialog,
      props: {
        message,
        onConfirm,
        variant: 'danger'
      },
      overlayClose: false
    };
  }

  static createFormDialog<T>(formComponent: React.ComponentType<T>, initialData: T): IModalConfig {
    return {
      component: formComponent,
      props: { initialData },
      overlayClose: true,
      size: 'large'
    };
  }
}

// Использование в компоненте:
const handleDelete = () => {
  const config = DialogFactory.createConfirmDialog(
    "Удалить задачу безвозвратно?",
    () => api.deleteTask(taskId)
  );
  ModalService.getInstance().open('confirm-delete', config.component, config.props);
};

3. Observer (Наблюдатель) — для реактивности

Компонент-контейнер ModalRoot, который рендерит все активные модалки, должен был реагировать на изменения в ModalService. Это классический Observer: сервис (Subject) уведомляет подписчиков (Observers, в нашем случае ModalRoot) об изменениях в коллекции активных модалок.

// Компонент-наблюдатель (React)
const ModalRoot: React.FC = () => {
  const [activeModals, setActiveModals] = useState<IModalData[]>([]);

  useEffect(() => {
    const handleUpdate = (modals: IModalData[]) => setActiveModals(modals);
    const subscriptionId = ModalService.subscribe(handleUpdate);
    return () => ModalService.unsubscribe(subscriptionId);
  }, []);

  return (
    <Portal> {/* Используется Portal для рендера вне основного дерева */}
      {activeModals.map(({ id, component: Component, props }) => (
        <ModalWrapper key={id}> {/* Единая обертка */}
          <Component {...props} />
        </ModalWrapper>
      ))}
    </Portal>
  );
};

4. Compound Components (Составные компоненты) — для гибкости UI

Для построения сложных модальных окон (например, многошаговая форма) мы использовали подход Compound Components (паттерн, популярный в React-экосистеме). Это позволило гибко комбинировать содержимое.

// Использование составного компонента
<ComplexModal>
  <Modal.Header title="Создание проекта" />
  <Modal.Body>
    <FormStepper steps={['Основное', 'Настройки', 'Команда']} />
  </Modal.Body>
  <Modal.Footer>
    <Button variant="secondary">Назад</Button>
    <Button variant="primary">Далее</Button>
  </Modal.Footer>
</ComplexModal>

5. Strategy (Стратегия) — для различных анимаций открытия/закрытия

Анимации (fade, slide-up, scale) были инкапсулированы в стратегии. Компонент ModalWrapper принимал animationStrategy, что позволяло легко добавлять новые типы анимаций.

interface AnimationStrategy {
  enter: (element: HTMLElement) => Promise<void>;
  leave: (element: HTMLElement) => Promise<void>;
}

const fadeStrategy: AnimationStrategy = {
  enter: (el) => { /* реализация fade-in */ },
  leave: (el) => { /* реализация fade-out */ }
};

const slideUpStrategy: AnimationStrategy = { /* ... */ };

Итоги и выгоды

Использование этой связки паттернов привело к значительным преимуществам:

  • Снижение связности: Компоненты бизнес-логики больше не знали, как рендерится модалка, они только запрашивали ее через сервис.
  • Повторное использование: Фабричные методы и единая обертка (ModalWrapper) устранили дублирование кода.
  • Удобство тестирования: ModalService и фабрики можно было тестировать изолированно, мокая их в юнит-тестах компонентов.
  • Гибкость и масштабируемость: Добавление новой модалки сводилось к созданию компонента содержимого и вызову фабрики или сервиса. Легко было внедрить логику "предотвращения потери данных при закрытии".
  • Согласованность UI: Все диалоги имели одинаковое поведение (закрытие по ESC, клику на оверлей) и внешний вид благодаря единому корневому компоненту.

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