Архитектурные паттерны в 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: слои, которые защищают
Эти два понятия часто путают, но суть одна: код должен быть организован слоями, где внутренние слои (