Strategy, Strategies, Pattern, Comet, StrategyPattern, TemplateMethodPattern
https://www.perplexity.ai/search/db6e2fc1-20a3-411a-823f-379f4d79ea73
Много стратегий — это нормально, если они независимы
Пример: ваш сложный объект рассматривается с разных сторон:
· Поведение (behavior)
· Расписание (scheduler)
· Метрики (metrics)
· Тунинг (tuner)
· Авторизация (auth)
· Логирование (logging)
· Кэширование (cache)
· Retry-политика (retry)
· Сериализация (serializer)
· Сжатие (compressor)
· и т.д.
Если у вас будет:
public class BackgroundService<
TBehavior, TScheduler, TMetrics, TTuner,
TAuth, TLogging, TCache, TRetry, TSer, TCompress>
: BackgroundService
where TBehavior : IBehaviorStrategy
where TScheduler : ISchedulerStrategy
where TMetrics : IMetricsStrategy
where TTuner : ITunerStrategy
where TAuth : IAuthStrategy
where TLogging : ILoggingStrategy
where TCache : ICacheStrategy
where TRetry : IRetryStrategy
where TSer : ISerializerStrategy
where TCompress : ICompressorStrategy
{ ... }
он станет неудобным:
· Много типов параметров.
· Конструктор с 10+ аргументами.
· Сложность чтения и поддержки.
Поэтому при 10–20 стратегиях лучше не делать один класс с 10+ дженериками, а:
1. Разбить на несколько компонентов (например, ProcessingService, SchedulingService, LoggingService).
2. Или использовать композицию через интерфейс — собирать сложный сервис из нескольких небольших сервисов, каждый со своей стратегией.
Но принцип «не смешивать стратегии» остается: каждый аспект — отдельный интерфейс и отдельный набор стратегий.
Не смешивать отдельные стратегии — это правильно
Вы спросили:
Если у меня есть сложный объект, который рассматривается с разных сторон,
то мы все равно не должны смешивать наши отдельные стратегии.
Да, не должны смешивать, и это правильно.
Почему:
1. Разная ответственность
o Scheduler отвечает за расписание, а Bridge не должен знать, как оно работает.
o Metrics отвечает за сбор метрик, а Behavior не должен за них заботиться.
2. Разная динамика изменений
o Те, кто меняет логику поведения, не должны дергать код метрик.
o Те, кто меняет способ кэширования, не должны менять код scheduler.
3. Принцип единой ответственности (SRP)
Каждый интерфейс стратегии — одна ответственность.
Смешивание нарушает SRP и усложняет тестирование.
4. Чистая архитектура
В чистой архитекттуре интерфейсы (включая стратегии) должны быть маленькими и специализированными, а зависимости направлены от инфраструктуры к бизнес-логике.[41]
Как практично организовать 10–20 стратегий
Вариант 1: Несколько небольших сервисов
Не один класс с 20 дженериками, а несколько:
public class ProcessingService<TBehavior, TAuth, TRetry>
: IHostedService
where TBehavior : IBehaviorStrategy
where TAuth : IAuthStrategy
where TRetry : IRetryStrategy
{ ... }
public class SchedulingService<TScheduler>
: IHostedService
where TScheduler : ISchedulerStrategy
{ ... }
public class LoggingService<TLogging>
: IHostedService
where TLogging : ILoggingStrategy
{ ... }
public class MetricsService<TMetrics>
: IHostedService
where TMetrics : IMetricsStrategy
{ ... }
Тогда:
· Каждый сервис имеет мало стратегий (2–4).
· Конструкторы небольшие.
· Логика разделена по доменам.
В DI вы регистрируйте их независимо:
services.AddHostedService<ProcessingService<MyBehavior, JwtAuth, DefaultRetry>>();
services.AddHostedService<SchedulingService<IntervalScheduler>>();
services.AddHostedService<LoggingService<FileLogger>>();
Вариант 2: Композитный сервис с «под-сервисами»
Если нужен один «главный» сервис, пусть он собирает другие:
public class MasterBackgroundService : BackgroundService
{
private readonly ProcessingService _processingService;
private readonly SchedulingService _schedulingService;
private readonly MetricsPoller _metricsPoller;
public MasterBackgroundService(
ProcessingService processingService,
SchedulingService schedulingService,
MetricsPoller metricsPoller)
{
_processingService = processingService;
_schedulingService = schedulingService;
_metricsPoller = metricsPoller;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// запускаем под-сервисы
await Task.WhenAll(
_processingService.StartAsync(stoppingToken),
_schedulingService.StartAsync(stoppingToken),
_metricsPoller.StartAsync(stoppingToken));
}
}
Здесь:
· Универсальный BackgroundService не перегружен 20 дженериками.
· Каждый под-сервис — отдельная стратегия своей области.
Вариант 3: Функциональные стратегии (делегаты)
Для маленьких стратегий (логирование, метрики, retry) можно использовать делегаты:
public class ProcessingService
{
private readonly Func<Task> _behavior;
private readonly Action<string> _log;
private readonly Action<string, double> _metric;
public ProcessingService(
Func<Task> behavior,
Action<string> log,
Action<string, double> metric)
{
_behavior = behavior;
_log = log;
_metric = metric;
}
}
Тогда:
· 10–20 стратегий не требуют 10–20 дженериков.
· Вы просто ¿подсовываете делегаты.
Это тоже «стратегия + делегирование», просто более легковесная.
· 10–20 стратегий — это нормально, если они разделены по доменам и не смешаны в один интерфейс.
· Сложный объект, рассматриваемый с разных сторон, не должен иметь одну «монолитную» стратегию, а должен использовать композицию нескольких стратегий.
· Подход вида:
BackgroundService<TBehavior, TScheduler, TMetrics, TTuner>
— хороший паттерн, но при 10–20 стратегиях лучше:
o Разбить на несколько сервисов, или
o Использовать делегаты для маленьких стратегий, или
o Собрать их через композицию через под-сервисы.
Это сохраняет модульность, тестируемость и читаемость и соответствует принципам чистой архитектуры.[41]