Паттерн стратегии позволяет выбирать алгоритм форматирования в runtime. Это полезно, когда логика форматирования зависит от контекста: типа пользователя, настроек приложения или типа контента.
import { format } from 'timeago.js';
import { formatDistanceToNow } from 'date-fns';
import { ru } from 'date-fns/locale';
// Интерфейс стратегии
interface TimeFormatStrategy {
format(date: Date | string | number): string;
}
// Стратегия timeago.js
class TimeagoStrategy implements TimeFormatStrategy {
constructor(private locale: string = 'ru') {}
format(date: Date | string | number): string {
return format(date, this.locale);
}
}
// Стратегия date-fns
class DateFnsStrategy implements TimeFormatStrategy {
format(date: Date | string | number): string {
return formatDistanceToNow(new Date(date as any), {
locale: ru,
addSuffix: true,
});
}
}
// Стратегия Intl.RelativeTimeFormat
class IntlStrategy implements TimeFormatStrategy {
private rtf: Intl.RelativeTimeFormat;
constructor(locale = 'ru') {
this.rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
}
format(date: Date | string | number): string {
const diffSec = (new Date(date as any).getTime() - Date.now()) / 1000;
const absSec = Math.abs(diffSec);
const sign = diffSec < 0 ? -1 : 1;
if (absSec < 60) return this.rtf.format(sign * Math.round(absSec), 'second');
if (absSec < 3600) return this.rtf.format(sign * Math.round(absSec / 60), 'minute');
return this.rtf.format(sign * Math.round(absSec / 3600), 'hour');
}
}
// Стратегия абсолютного времени
class AbsoluteStrategy implements TimeFormatStrategy {
constructor(private locale = 'ru-RU') {}
format(date: Date | string | number): string {
return new Date(date as any).toLocaleDateString(this.locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
}
class TimeFormatter {
private strategy: TimeFormatStrategy;
constructor(strategy: TimeFormatStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: TimeFormatStrategy): void {
this.strategy = strategy;
}
format(date: Date | string | number): string {
return this.strategy.format(date);
}
}
type DisplayContext = 'feed' | 'notification' | 'admin' | 'email';
function getStrategy(context: DisplayContext, locale = 'ru'): TimeFormatStrategy {
switch (context) {
case 'feed':
return new TimeagoStrategy(locale); // Относительное, живое
case 'notification':
return new IntlStrategy(locale); // Нативный браузерный
case 'admin':
return new AbsoluteStrategy('ru-RU'); // Точная дата
case 'email':
return new DateFnsStrategy(); // Без автообновления
default:
return new TimeagoStrategy(locale);
}
}
const formatter = new TimeFormatter(getStrategy('feed'));
console.log(formatter.format(new Date()));
class AgeBasedStrategy implements TimeFormatStrategy {
private recentStrategy: TimeFormatStrategy;
private oldStrategy: TimeFormatStrategy;
private thresholdMs: number;
constructor(thresholdMs = 7 * 86400_000) {
this.recentStrategy = new TimeagoStrategy(); // Свежие — относительное
this.oldStrategy = new AbsoluteStrategy(); // Старые — абсолютное
this.thresholdMs = thresholdMs;
}
format(date: Date | string | number): string {
const age = Date.now() - new Date(date as any).getTime();
if (age < this.thresholdMs) {
return this.recentStrategy.format(date);
}
return this.oldStrategy.format(date);
}
}
const smart = new TimeFormatter(new AgeBasedStrategy(7 * 86400_000));
smart.format(new Date(Date.now() - 3600_000)); // "час назад"
smart.format(new Date('2020-01-01')); // "1 января 2020 г."
class FallbackStrategy implements TimeFormatStrategy {
private primary: TimeFormatStrategy;
private secondary: TimeFormatStrategy;
constructor(primary: TimeFormatStrategy, secondary: TimeFormatStrategy) {
this.primary = primary;
this.secondary = secondary;
}
format(date: Date | string | number): string {
try {
const result = this.primary.format(date);
if (!result) throw new Error('Empty result');
return result;
} catch {
return this.secondary.format(date);
}
}
}
const resilient = new FallbackStrategy(
new TimeagoStrategy('ru'),
new AbsoluteStrategy('ru-RU')
);
const TimeFormatContext = React.createContext<TimeFormatStrategy>(
new TimeagoStrategy('ru')
);
function TimeFormatProvider({
strategy,
children,
}: {
strategy: TimeFormatStrategy;
children: React.ReactNode;
}) {
return (
<TimeFormatContext.Provider value={strategy}>
{children}
</TimeFormatContext.Provider>
);
}
function TimeDisplay({ date }: { date: Date }) {
const strategy = useContext(TimeFormatContext);
return <time>{strategy.format(date)}</time>;
}
// Использование
<TimeFormatProvider strategy={new AgeBasedStrategy()}>
<TimeDisplay date={new Date('2020-01-01')} /> {/* Абсолютная */}
<TimeDisplay date={new Date(Date.now() - 3600_000)} /> {/* Относительная */}
</TimeFormatProvider>
| Стратегия | Когда использовать |
|---|---|
| TimeagoStrategy | Живые ленты, комментарии, чаты |
| IntlStrategy | Один язык, только современные браузеры |
| DateFnsStrategy | date-fns уже в проекте |
| AbsoluteStrategy | Admin-интерфейсы, логи, email |
| AgeBasedStrategy | Смешанный контент разного возраста |
| FallbackStrategy | Отказоустойчивость |