Какие шаблоны проектирования используешь для backend?

Ответ

Выбор паттернов зависит от масштаба и сложности приложения. Вот ключевые, которые я применяю на практике:

  1. 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);
    }
  2. Dependency Injection (Внедрение зависимостей). Фундаментальный паттерн в ASP.NET Core для управления зависимостями и повышения тестируемости.

    // Регистрация в Startup.cs / Program.cs
    services.AddScoped<IUserService, UserService>();
    services.AddSingleton<ICacheService, RedisCacheService>();
  3. CQRS (Command Query Responsibility Segregation). Разделение моделей для операций записи (Commands) и чтения (Queries). Особенно полезен в высоконагруженных системах, где требования к чтению и записи разные.

    • Command: Изменяет состояние системы (CreateOrderCommand).
    • Query: Возвращает данные без изменения состояния (GetUserOrdersQuery). Часто реализуется с помощью библиотеки MediatR.
  4. 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;
        }
    }
  5. 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 и т.д.
  6. 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+ 🔞

А, паттерны проектирования, ну классика же! Слушай, тут всё зависит от того, какой у тебя проект — маленькая лавочка или монстр на миллион пользователей. Как говорится, не надо из пушки по воробьям, но и зубочисткой танк не прошибешь.

Вот на чём я обычно строю, чтобы потом не было мучительно больно:

  1. 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(...) — и попробуй это всё отмокать или поменять.

  2. Dependency Injection (Внедрение зависимостей). В ASP.NET Core это из коробки, святое дело. Просто пишешь интерфейс, пишешь класс, регистрируешь — и система сама тебе всё подсовывает куда надо. Тестируемость зашкаливает.

    services.AddScoped<IUserService, UserService>();
    services.AddSingleton<ICacheService, RedisCacheService>();

    Главное — не увлекаться и не создавать циклических зависимостей, а то приложение просто не встанет, и будешь сидеть, ебать колотить, искать, где же эта сука закольцевалась.

  3. CQRS (Command Query Responsibility Segregation). Вот это уже для серьёзных ребят. Идея простая, как валенок: то, что читает данные, и то, что их пишет — это две большие разницы. В простом CRUD это overkill, конечно. Но когда у тебя читателей в сто раз больше, чем писателей, или когда запросы на чтение — это адские джойны десяти таблиц, а команды на запись — простые инсерты, вот тут CQRS выручает. Часто идёт в паре с MediatR, чтобы не париться с ручной диспетчеризацией.

    • Command — меняет что-то (СоздатьЗаказ).
    • Query — только смотрит (ПолучитьЗаказыПользователя).
  4. 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;
        }
    }

    Красота же! Сервис не знает, как создаётся пользователь, он только команду отправил.

  5. 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
  6. 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 — это как специи, добавляются по вкусу в любом проекте, где есть изменчивая логика или сквозная функциональность.

Главное — не применять всё и сразу, а то получится архитектурный космический корабль для поездки за хлебом. Сначала проблема, потом паттерн, а не наоборот.