RU

Паттерны проектирования

Паттерны проектирования

Порождающие паттерны

Одиночка

Следит за тем, чтобы в приложении существовал только один экземпляр объекта. Достаточно хранить общий экземпляр в области модуля и возвращать его через функцию.

class Database {
  static #_instance: Database;
  
  private constructor() {
    // делаем конструктор приватным, чтобы запретить вызов через new 
  }
  
  //делаем логику в статическом методе, а не в конструкторе, чтобы не вводить в заблуждение,
  //что создание нового инстанса new Database() может возвращать старый объект
  static getInstance() {
    if (!Database.#_instance) {
      Database.#_instance = new Database();
    }
    return Database.#_instance
  }
}

Еще проще в случае с ES-модулями - просто экспортировать готовый инстанс из модуля. В данном примере:

export default Database.getInstance();

Фабрика

Помогает создавать объекты, не раскрывая детали их устройства. Функция-фабрика сама решает, какой объект вернуть.

function createNotifier(type: "email" | "sms") {
  if (type === "email") {
    return {
      send: (message) => console.log(`Письмо: ${message}`),
    };
  }

  return {
    send: (message) => console.log(`SMS: ${message}`),
  };
}

const notifier = createNotifier("sms");
notifier.send("Order is ready");

Структурные паттерны

Декоратор

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

function withLog<T extends (...args: unknown[]) => unknown>(fn: T): T {
  return ((...args: unknown[]) => {
    console.log("Вызов с аргументами:", args);
    const result = fn(...args);
    console.log("Результат:", result);
    return result;
  }) as T;
}

const multiply = (a: number, b: number) => a * b;
const multiplyWithLog = withLog(multiply);

multiplyWithLog(2, 3);
// Вызов с аргументами: [2, 3]
// Результат: 6

В React декоратор часто реализуют через HOC (Higher-Order Component), который принимает компонент и возвращает новый с добавленным поведением.

type WithLoaderProps = { isLoading: boolean };

function withLoader<P>(Component: React.ComponentType<P>) {
  return function WithLoader(props: P & WithLoaderProps) {
    if (props.isLoading) {
      return <div>Загрузка...</div>;
    }

    const { isLoading, ...restProps } = props;
    return <Component {...(restProps as P)} />;
  };
}

function UsersList({ users }: { users: string[] }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user}>{user}</li>
      ))}
    </ul>
  );
}

const UsersListWithLoader = withLoader(UsersList);

// <UsersListWithLoader users={["Аня", "Петр"]} isLoading={false} />

Поведенческие паттерны

Наблюдатель

Описывает зависимость "один ко многим": когда источник события изменяется, все подписчики получают уведомление.

type Listener = () => void;

const listeners: Listener[] = [];

export function subscribe(listener: Listener) {
  listeners.push(listener);
}

export function notify() {
  listeners.forEach((listener) => listener());
}

subscribe(() => console.log("Пользователь получил уведомление"));
subscribe(() => console.log("Логгер записал событие"));

notify();
// Пользователь получил уведомление
// Логгер записал событие

Стратегия

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

type PriceStrategy = (value: number) => string;

const short: PriceStrategy = (value) => `$${value}`;
const detailed: PriceStrategy = (value) => `${value.toFixed(2)} USD`;

function formatPrice(value: number, strategy: PriceStrategy) {
  return strategy(value);
}

const products = [199, 49.9, 12];
const strategy = detailed;

products.map((price) => console.log(formatPrice(price, strategy)));
// 199.00 USD
// 49.90 USD
// 12.00 USD