Паттерн композиции

Паттерн композиции позволяет собирать сложное поведение из простых, независимых частей. В контексте timeago.js это означает создание составных функций форматирования из небольших специализированных функций.


Базовая композиция функций

import { format } from 'timeago.js';

type Formatter = (date: Date | string | number, locale: string) => string;

// Функция compose: применяет трансформации последовательно
function compose<T>(...fns: ((arg: T) => T)[]): (arg: T) => T {
  return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);
}

// pipe — то же, но слева направо
function pipe<T>(...fns: ((arg: T) => T)[]): (arg: T) => T {
  return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);
}

Составные форматтеры

type DateContext = {
  date:   Date | string | number | null;
  locale: string;
  result: string;
};

// Отдельные шаги форматирования
const validateDate = (ctx: DateContext): DateContext => {
  if (ctx.date == null) {
    return { ...ctx, result: '', date: null };
  }
  const d = new Date(ctx.date as any);
  if (isNaN(d.getTime())) {
    return { ...ctx, result: '', date: null };
  }
  return { ...ctx, date: d };
};

const applyFormat = (ctx: DateContext): DateContext => {
  if (!ctx.date) return ctx;
  return { ...ctx, result: format(ctx.date as Date, ctx.locale) };
};

const capitalize = (ctx: DateContext): DateContext => ({
  ...ctx,
  result: ctx.result
    ? ctx.result.charAt(0).toUpperCase() + ctx.result.slice(1)
    : ctx.result,
});

const addFallback = (fallback: string) =>
  (ctx: DateContext): DateContext => ({
    ...ctx,
    result: ctx.result || fallback,
  });

Сборка форматтера через pipe

const formatWithFallback = (date: unknown, locale = 'ru', fallback = 'давно'): string => {
  const initial: DateContext = { date: date as any, locale, result: '' };

  const process = pipe(
    validateDate,
    applyFormat,
    capitalize,
    addFallback(fallback)
  );

  return process(initial).result;
};

Компонент-фабрика

type FormatterConfig = {
  locale:     string;
  capitalize: boolean;
  fallback:   string;
};

function createFormatter(config: FormatterConfig): (date: unknown) => string {
  const steps: Array<(ctx: DateContext) => DateContext> = [
    validateDate,
    applyFormat,
  ];

  if (config.capitalize) steps.push(capitalize);
  steps.push(addFallback(config.fallback));

  return (date: unknown): string => {
    const ctx: DateContext = { date: date as any, locale: config.locale, result: '' };
    const process = pipe(...steps);
    return process(ctx).result;
  };
}

// Создание специализированных форматтеров
const ruFormatter = createFormatter({
  locale:     'ru',
  capitalize: true,
  fallback:   'Давно',
});

const deFormatter = createFormatter({
  locale:     'de',
  capitalize: false,
  fallback:   'Vor langer Zeit',
});

Композиция обёрток

// Higher-order функции — оборачивают форматтер дополнительной логикой
type FormatFn = (date: unknown, locale?: string) => string;

function withLogging(fn: FormatFn): FormatFn {
  return (date, locale = 'ru') => {
    const result = fn(date, locale);
    console.debug(`[format] ${date} → "${result}"`);
    return result;
  };
}

function withCache(fn: FormatFn, ttl = 30_000): FormatFn {
  const cache = new Map<string, { value: string; ts: number }>();
  return (date, locale = 'ru') => {
    const key = `${date}:${locale}`;
    const hit = cache.get(key);
    if (hit && Date.now() - hit.ts < ttl) return hit.value;
    const value = fn(date, locale);
    cache.set(key, { value, ts: Date.now() });
    return value;
  };
}

function withFallback(fn: FormatFn, fallback: string): FormatFn {
  return (date, locale = 'ru') => {
    try {
      return fn(date, locale) || fallback;
    } catch {
      return fallback;
    }
  };
}

// Сборка
const smartFormat = withCache(
  withLogging(
    withFallback(
      (date, locale) => format(new Date(date as any), locale!),
      'давно'
    )
  ),
  60_000
);

Composable React хуки

// Базовый хук форматирования
function useFormat(date: Date | string, locale = 'ru'): string {
  return useMemo(() => format(new Date(date), locale), [date, locale]);
}

// Хук с автообновлением
function useAutoFormat(date: Date | string, locale = 'ru'): string {
  const [value, setValue] = useState(() => format(new Date(date), locale));

  useEffect(() => {
    const id = setInterval(() => setValue(format(new Date(date), locale)), 30_000);
    return () => clearInterval(id);
  }, [date, locale]);

  return value;
}

// Хук с валидацией
function useSafeFormat(date: unknown, locale = 'ru'): string {
  return useMemo(() => {
    if (!date) return '';
    const d = new Date(date as any);
    return isNaN(d.getTime()) ? '' : format(d, locale);
  }, [date, locale]);
}

// Составной хук
function useRichTimeAgo(date: unknown, locale = 'ru'): {
  text:    string;
  isValid: boolean;
  date:    Date | null;
} {
  const isValid = useMemo(() => {
    if (!date) return false;
    return !isNaN(new Date(date as any).getTime());
  }, [date]);

  const parsedDate = useMemo(
    () => (isValid ? new Date(date as any) : null),
    [date, isValid]
  );

  const text = useAutoFormat(parsedDate ?? new Date(), locale);

  return { text: isValid ? text : '', isValid, date: parsedDate };
}

Выгоды паттерна композиции

Выгода Описание
Тестируемость Каждый шаг тестируется независимо
Переиспользование Шаги используются в разных комбинациях
Явность Цепочка шагов читается как список требований
Расширяемость Новый шаг добавляется без изменения существующих