5. Паттерны проектирования¶
Паттерны в практике используются не ради формальности, а чтобы разделить ответственность между частями приложения.
Паттерн — это повторяемое решение типовой проблемы. Его не нужно добавлять механически в каждую строку проекта. Важно понимать, какую боль он снимает.
В этой практике паттерны помогают ответить на вопросы:
- где хранить запросы к базе;
- где размещать бизнес-логику;
- как создавать разные типы событий;
- как выбирать алгоритм аналитики;
- как уведомлять части системы о новом событии.
Repository¶
Repository скрывает EF Core от сервисов.
public interface IActivityEventRepository
{
Task<ActivityEvent> AddAsync(ActivityEvent entity, CancellationToken ct);
Task<IReadOnlyList<ActivityEvent>> GetAsync(ActivityEventFilter filter, CancellationToken ct);
Task<ActivityEvent?> GetByIdAsync(int id, CancellationToken ct);
Task<bool> DeleteAsync(int id, CancellationToken ct);
}
Service Layer¶
Сервис содержит сценарий: проверка входных данных, создание сущности, вызов репозитория, преобразование в DTO.
Если контроллер отвечает на вопрос “какой HTTP-запрос пришёл?”, то сервис отвечает на вопрос “что должна сделать система?”. Поэтому в сервисе появляются проверки, логирование бизнес-операции и вызовы репозитория.
public class ActivityEventService : IActivityEventService
{
private readonly IActivityEventRepository _repository;
public ActivityEventService(IActivityEventRepository repository)
{
_repository = repository;
}
public async Task<ActivityEventDto> CreateAsync(CreateActivityEventRequest request, int userId, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.Type))
throw new ArgumentException("Тип события обязателен.");
var entity = new ActivityEvent
{
UserId = userId,
Type = request.Type,
Source = request.Source,
Description = request.Description,
Severity = request.Severity,
CreatedAt = DateTime.UtcNow
};
var saved = await _repository.AddAsync(entity, ct);
return saved.ToDto();
}
}
Factory¶
Factory создаёт разные события по типу.
Фабрика полезна, когда объект можно создать несколькими стандартными способами. Например, событие входа всегда имеет тип Login, уровень Info и понятное описание. Чтобы не повторять эти значения в разных местах, можно вынести создание в фабрику.
public static class ActivityEventFactory
{
public static ActivityEvent CreateLoginEvent(int userId, string source)
{
return new ActivityEvent
{
UserId = userId,
Type = "Login",
Source = source,
Description = "Пользователь вошёл в систему",
Severity = "Info",
CreatedAt = DateTime.UtcNow
};
}
}
Strategy¶
Strategy позволяет выбрать алгоритм аналитики.
Представьте, что в системе есть несколько аналитических отчётов: по типам событий, по пользователям, по ошибкам. У каждого отчёта свой алгоритм расчёта, но запускать их хочется одинаково. Strategy позволяет оформить каждый алгоритм отдельным классом.
public interface IAnalyticsStrategy
{
string Name { get; }
Task<object> CalculateAsync(DateTime from, DateTime to, CancellationToken ct);
}
public class EventTypeAnalyticsStrategy : IAnalyticsStrategy
{
private readonly AppDbContext _db;
public string Name => "event-types";
public EventTypeAnalyticsStrategy(AppDbContext db)
{
_db = db;
}
public async Task<object> CalculateAsync(DateTime from, DateTime to, CancellationToken ct)
{
return await _db.ActivityEvents
.Where(x => x.CreatedAt >= from && x.CreatedAt <= to)
.GroupBy(x => x.Type)
.Select(g => new { Type = g.Key, Count = g.Count() })
.ToListAsync(ct);
}
}
Singleton¶
В ASP.NET Core Singleton регистрируют для объектов без состояния или с потокобезопасным состоянием.
Важно: DbContext нельзя регистрировать как Singleton, потому что он создаётся на запрос.
Singleton живёт всё время работы приложения. Если положить туда изменяемые данные пользователя или DbContext, разные запросы начнут мешать друг другу. Поэтому Singleton подходит для фабрик токенов, генераторов конфигурации и объектов без пользовательского состояния.
Observer / Event Bus¶
Простой Event Bus уведомляет подписчиков о новом событии.
public interface IEventBus
{
void Publish<T>(T message);
void Subscribe<T>(Action<T> handler);
}
public class InMemoryEventBus : IEventBus
{
private readonly Dictionary<Type, List<Delegate>> _handlers = new();
public void Subscribe<T>(Action<T> handler)
{
var type = typeof(T);
if (!_handlers.ContainsKey(type))
_handlers[type] = new List<Delegate>();
_handlers[type].Add(handler);
}
public void Publish<T>(T message)
{
var type = typeof(T);
if (!_handlers.TryGetValue(type, out var handlers))
return;
foreach (var handler in handlers.Cast<Action<T>>())
handler(message);
}
}
В практике можно публиковать сообщение ActivityEventCreated, чтобы сбрасывать кэш аналитики или писать дополнительный лог.
Как не переусложнить проект¶
Паттерн стоит применять, если он делает код понятнее. Если ради паттерна появляется пять новых файлов, а задача оставалась простой, значит решение стало тяжелее самой проблемы. В этой практике обязательны Repository и Service Layer, потому что они формируют архитектуру приложения. Factory, Strategy и Event Bus можно расширять в индивидуальной части проекта.