Архитектурные паттерны в C#: превращаем хаос в порядок

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Архитектурные паттерны в C#: не дайте вашему коду превратиться в спагетти

Каждый разработчик C# рано или поздно сталкивается с моментом, когда его проект начинает напоминать тарелку переваренных макарон. Методы на 500 строк, классы, которые «знают всё», и баги, которые плодятся быстрее, чем кролики. Знакомо? Самое время поговорить об архитектурных паттернах в C#.

В этой статье мы не будем читать скучные лекции. Мы разберём, какие архитектурные паттерны C# реально спасают проекты, покажем примеры кода C# и дадим чёткие критерии выбора. Поехали!

Паттерн MVC: классика, которая не стареет

Model-View-Controller (MVC) — это, пожалуй, самый популярный архитектурный паттерн в C# для веб-приложений. Его главная идея — разделить ответственность на три кита: данные (Model), отображение (View) и логику управления (Controller).

Как это работает на практике?

Представьте интернет-магазин. Пользователь нажал кнопку «Добавить в корзину» — это сигнал для Controller. Controller просит Model обновить данные. Model говорит: «Готово!». Controller выбирает подходящую View и отправляет ей свежие данные. View просто рисует то, что получила.

Вот минимальный пример кода C# для ASP.NET Core MVC:

// Model
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// Controller
public class ProductController : Controller
{
    private readonly AppDbContext _context;

    public ProductController(AppDbContext context)
    {
        _context = context;
    }

    public IActionResult Index()
    {
        var products = _context.Products.ToList();
        return View(products); // View сама решит, как отрисовать список
    }
}

Когда использовать: веб-приложения с богатым UI, где нужно чётко разделить логику и представление. Идеально для архитектуры приложений C# на ASP.NET Core.

Паттерн MVP: для тех, кто дружит с Windows Forms

Model-View-Presenter (MVP) — старший брат MVC, который особенно хорош для десктопных приложений на WinForms или WPF. Главное отличие: View — это пассивный «болванчик», а Presenter управляет всем процессом.

Пример для WinForms

// Интерфейс View (пассивная)
public interface ILoginView
{
    string UserName { get; }
    string Password { get; }
    event EventHandler LoginClicked;
    void ShowError(string message);
}

// Presenter (активный дирижёр)
public class LoginPresenter
{
    private readonly ILoginView _view;
    private readonly IAuthService _authService;

    public LoginPresenter(ILoginView view, IAuthService authService)
    {
        _view = view;
        _authService = authService;
        _view.LoginClicked += OnLoginClicked;
    }

    private void OnLoginClicked(object sender, EventArgs e)
    {
        if (_authService.Authenticate(_view.UserName, _view.Password))
        {
            // Переход на главную форму
        }
        else
        {
            _view.ShowError("Неверный логин или пароль");
        }
    }
}

Плюс: View можно легко тестировать, подменяя её mock-объектом. Минус: кода становится больше, но он становится предсказуемым.

Паттерн Repository: скрываем сложность базы данных

Паттерн Repository C# — это прослойка между бизнес-логикой и хранилищем данных. Вместо того чтобы разбрасывать SQL-запросы по всему коду, вы создаёте единый «репозиторий» для работы с сущностями.

Пример с Entity Framework Core

public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
    Task<IEnumerable<Product>> GetAllAsync();
    Task AddAsync(Product product);
    Task UpdateAsync(Product product);
    Task DeleteAsync(int id);
}

public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;

    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        return await _context.Products.FindAsync(id);
    }

    public async Task AddAsync(Product product)
    {
        await _context.Products.AddAsync(product);
        await _context.SaveChangesAsync();
    }
    // ... остальные методы
}

Совет: Не делайте «универсальный репозиторий» на все случаи жизни. Лучше создавайте отдельные интерфейсы для каждой агрегированной сущности. Это один из ключевых паттернов проектирования C#, который реально упрощает жизнь.

CQRS: разделяем чтение и запись

CQRS C# ASP.NET — это паттерн, который разделяет команды (изменение данных) и запросы (чтение данных). Зачем? Потому что в сложных системах требования к чтению и записи часто отличаются.

Как это выглядит

Допустим, у нас есть система заказов. Для команды «Создать заказ» нам нужно валидировать корзину, проверить кредитный лимит, зарезервировать товар. Для запроса «Показать историю заказов» — просто дёрнуть одну таблицу.

// Команда
public class CreateOrderCommand : IRequest<int>
{
    public int UserId { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

// Обработчик команды
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly IOrderRepository _orderRepo;
    private readonly IUserCreditService _creditService;

    public async Task<int> Handle(CreateOrderCommand request, 
        CancellationToken cancellationToken)
    {
        // Сложная бизнес-логика
        var order = new Order { UserId = request.UserId };
        foreach (var item in request.Items)
        {
            order.AddItem(item.ProductId, item.Quantity);
        }
        await _orderRepo.AddAsync(order);
        return order.Id;
    }
}

// Запрос
public class GetOrderHistoryQuery : IRequest<List<OrderHistoryDto>>
{
    public int UserId { get; set; }
}

// Обработчик запроса
public class GetOrderHistoryHandler : IRequestHandler<GetOrderHistoryQuery, 
    List<OrderHistoryDto>>
{
    private readonly IDbConnection _db;

    public async Task<List<OrderHistoryDto>> Handle(
        GetOrderHistoryQuery request, CancellationToken ct)
    {
        // Простой и быстрый SQL
        var sql = "SELECT Id, Total, Date FROM Orders WHERE UserId = @UserId";
        return (await _db.QueryAsync<OrderHistoryDto>(sql, new { 
            request.UserId })).ToList();
    }
}

Этот подход часто используется в Clean Architecture C# и onion architecture C#. Он делает код невероятно тестируемым и масштабируемым.

Onion Architecture и Clean Architecture: слои, которые защищают

Эти два понятия часто путают, но суть одна: код должен быть организован слоями, где внутренние слои (

Блоги

Рекомендаций по книгам