Ответ
Выбор паттернов зависит от масштаба и сложности приложения. Вот ключевые, которые я применяю на практике:
-
Repository & Unit of Work. Абстрагируют доступ к данным, что упрощает тестирование и смену источника данных (например, с EF Core на Dapper).
public interface IRepository<T> where T : class { Task<T?> GetByIdAsync(int id); void Add(T entity); void Remove(T entity); } public interface IUnitOfWork : IDisposable { IRepository<User> Users { get; } IRepository<Order> Orders { get; } Task<int> SaveChangesAsync(CancellationToken ct = default); } -
Dependency Injection (Внедрение зависимостей). Фундаментальный паттерн в ASP.NET Core для управления зависимостями и повышения тестируемости.
// Регистрация в Startup.cs / Program.cs services.AddScoped<IUserService, UserService>(); services.AddSingleton<ICacheService, RedisCacheService>(); -
CQRS (Command Query Responsibility Segregation). Разделение моделей для операций записи (Commands) и чтения (Queries). Особенно полезен в высоконагруженных системах, где требования к чтению и записи разные.
- Command: Изменяет состояние системы (
CreateOrderCommand). - Query: Возвращает данные без изменения состояния (
GetUserOrdersQuery). Часто реализуется с помощью библиотеки MediatR.
- Command: Изменяет состояние системы (
-
Mediator. Позволяет уменьшить связанность между компонентами. Компоненты общаются не напрямую, а через посредника.
// С MediatR public record CreateUserCommand(string Email, string Name) : IRequest<int>; public class CreateUserHandler : IRequestHandler<CreateUserCommand, int> { private readonly AppDbContext _context; public CreateUserHandler(AppDbContext context) => _context = context; public async Task<int> Handle(CreateUserCommand request, CancellationToken ct) { var user = new User { Email = request.Email, Name = request.Name }; _context.Users.Add(user); await _context.SaveChangesAsync(ct); return user.Id; } } -
Strategy. Используется, когда необходимо выбирать алгоритм во время выполнения.
public interface IPaymentStrategy { Task<PaymentResult> ProcessAsync(decimal amount); } public class PaymentProcessor { private readonly IPaymentStrategy _strategy; public PaymentProcessor(IPaymentStrategy strategy) => _strategy = strategy; public Task<PaymentResult> ExecutePayment(decimal amount) => _strategy.ProcessAsync(amount); } // Внедряем конкретную стратегию: CreditCardStrategy, PayPalStrategy и т.д. -
Decorator. Для динамического добавления поведения объекту (логирование, кэширование, валидация) без изменения его класса.
public class LoggingUserServiceDecorator : IUserService { private readonly IUserService _innerService; private readonly ILogger _logger; public LoggingUserServiceDecorator(IUserService innerService, ILogger logger) { _innerService = innerService; _logger = logger; } public async Task<User> GetUserAsync(int id) { _logger.LogInformation("Fetching user {UserId}", id); return await _innerService.GetUserAsync(id); } }
Для простого CRUD-приложения часто достаточно Repository + DI. Для сложных, масштабируемых систем добавляются CQRS, Mediator и стратегии.
Ответ 18+ 🔞
А, паттерны проектирования, ну классика же! Слушай, тут всё зависит от того, какой у тебя проект — маленькая лавочка или монстр на миллион пользователей. Как говорится, не надо из пушки по воробьям, но и зубочисткой танк не прошибешь.
Вот на чём я обычно строю, чтобы потом не было мучительно больно:
-
Repository и Unit of Work. Это как бы база, фундамент. Чтобы не привязываться намертво к Entity Framework или какой другой хуйне. Захотел завтра с EF на Dapper перелезть — да не вопрос, блядь. И тестить удобно.
public interface IRepository<T> where T : class { Task<T?> GetByIdAsync(int id); void Add(T entity); void Remove(T entity); } public interface IUnitOfWork : IDisposable { IRepository<User> Users { get; } IRepository<Order> Orders { get; } Task<int> SaveChangesAsync(CancellationToken ct = default); }Без этого потом такой пиздец начинается, когда в каждом сервисе
_context.Users.Where(...)— и попробуй это всё отмокать или поменять. -
Dependency Injection (Внедрение зависимостей). В ASP.NET Core это из коробки, святое дело. Просто пишешь интерфейс, пишешь класс, регистрируешь — и система сама тебе всё подсовывает куда надо. Тестируемость зашкаливает.
services.AddScoped<IUserService, UserService>(); services.AddSingleton<ICacheService, RedisCacheService>();Главное — не увлекаться и не создавать циклических зависимостей, а то приложение просто не встанет, и будешь сидеть, ебать колотить, искать, где же эта сука закольцевалась.
-
CQRS (Command Query Responsibility Segregation). Вот это уже для серьёзных ребят. Идея простая, как валенок: то, что читает данные, и то, что их пишет — это две большие разницы. В простом CRUD это overkill, конечно. Но когда у тебя читателей в сто раз больше, чем писателей, или когда запросы на чтение — это адские джойны десяти таблиц, а команды на запись — простые инсерты, вот тут CQRS выручает. Часто идёт в паре с MediatR, чтобы не париться с ручной диспетчеризацией.
- Command — меняет что-то (
СоздатьЗаказ). - Query — только смотрит (
ПолучитьЗаказыПользователя).
- Command — меняет что-то (
-
Mediator (Посредник). Библиотека MediatR — это просто песня, если правильно использовать. Все компоненты перестают знать друг о друге. Хочешь выполнить действие — кидаешь команду в шину. Обработчик где-то там, на другом конце вселенной, её ловит и выполняет. Связность падает ниже плинтуса, но появляется новая хуйня — надо следить за тем, чтобы не создавать монструозных команд-хендлеров на тысячу строк.
public record CreateUserCommand(string Email, string Name) : IRequest<int>; public class CreateUserHandler : IRequestHandler<CreateUserCommand, int> { private readonly AppDbContext _context; public CreateUserHandler(AppDbContext context) => _context = context; public async Task<int> Handle(CreateUserCommand request, CancellationToken ct) { var user = new User { Email = request.Email, Name = request.Name }; _context.Users.Add(user); await _context.SaveChangesAsync(ct); return user.Id; } }Красота же! Сервис не знает, как создаётся пользователь, он только команду отправил.
-
Strategy (Стратегия). О, это люблю. Когда у тебя есть несколько способов сделать одно и то же (например, оплата: карта, PayPal, крипта), и надо выбирать на лету. Вместо гигантского
switchили кучиif— делаешь интерфейс стратегии и набор реализаций. Потом просто подсовываешь нужную в рантайме. Элегантно и расширяемо.public interface IPaymentStrategy { Task<PaymentResult> ProcessAsync(decimal amount); } public class PaymentProcessor { private readonly IPaymentStrategy _strategy; public PaymentProcessor(IPaymentStrategy strategy) => _strategy = strategy; public Task<PaymentResult> ExecutePayment(decimal amount) => _strategy.ProcessAsync(amount); } // Подсовываем что надо: CreditCardStrategy, PayPalStrategy, CryptoStrategy -
Decorator (Декоратор). Это когда нужно добавить функциональность (логирование, кэширование, валидацию) не лезу в сам класс, а оборачиваю его, как капустный лист голубцы. В мире DI это делается вообще в одну строку регистрации. Главное — соблюдать порядок декораторов, а то закэшируешь невалидные данные и будешь потом охуевать.
public class LoggingUserServiceDecorator : IUserService { private readonly IUserService _innerService; private readonly ILogger _logger; public LoggingUserServiceDecorator(IUserService innerService, ILogger logger) { _innerService = innerService; _logger = logger; } public async Task<User> GetUserAsync(int id) { _logger.LogInformation("Fetching user {UserId}", id); return await _innerService.GetUserAsync(id); } }
Итог, как я это вижу: Для простого админского CRUD — хватит Repository + Unit of Work, и не парься. DI и так везде. Как только логика начинает распухать, появляются сценарии вроде «создать заказ, списать деньги, отправить уведомление, обновить склад» — это звоночек, что пора смотреть в сторону Mediator/CQRS, чтобы не получить спагетти-код в сервисах. А Strategy и Decorator — это как специи, добавляются по вкусу в любом проекте, где есть изменчивая логика или сквозная функциональность.
Главное — не применять всё и сразу, а то получится архитектурный космический корабль для поездки за хлебом. Сначала проблема, потом паттерн, а не наоборот.