SOLID принципы в C#: продвинутое применение и практические кейсы

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

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

Начать курс

Введение: когда SOLID перестаёт быть теорией

Большинство разработчиков знакомы с аббревиатурой SOLID на уровне "класс должен отвечать за одну вещь". Однако в реальных проектах на C# (.NET) с Dependency Injection, микросервисами и асинхронными паттернами наивное следование принципам приводит к обратному эффекту — переусложнению, раздуванию интерфейсов и нарушению инкапсуляции.

В этой статье мы рассмотрим SOLID не как набор догм, а как инструмент для принятия архитектурных решений. Вы узнаете, как правильно применять принципы в продвинутых сценариях: работа с IoC-контейнерами, композиция вместо наследования, обработка ошибок через интерфейсы и тестирование саг.

1. Single Responsibility: границы ответственности в DDD

Принцип единственной ответственности (SRP) часто путают с "один класс — одна функция". На самом деле речь идёт о причине изменения (reason to change). В контексте C# и Domain-Driven Design это означает, что класс должен изменяться только по одной причине, связанной с одним актором или бизнес-правилом.

Продвинутый антипаттерн: "Божественный Entity"

Рассмотрим типичный пример нарушения SRP в .NET — класс OrderEntity, который одновременно управляет состоянием заказа, рассчитывает налоги, отправляет email и сохраняет данные в БД через Entity Framework.

// ❌ Нарушение SRP
public class OrderEntity
{
    public int Id { get; set; }
    public decimal Total { get; set; }
    public string Email { get; set; }

    public void CalculateTax() { /* логика налогов */ }
    public void SendConfirmation() { /* отправка email */ }
    public void SaveToDatabase() { /* EF Core SaveChanges */ }
}

Проблема: изменение бизнес-правила по налогам (например, новый закон) приведёт к изменению класса, который отвечает ещё и за отправку уведомлений. Это повышает риск регрессии.

Решение: выделение сервисов через DI

// ✅ SRP через сервисы
public class Order
{
    public int Id { get; set; }
    public decimal Total { get; set; }
    // Только состояние, никакой логики
}

public class TaxCalculator : ITaxCalculator
{
    public decimal Calculate(Order order) { /* ... */ }
}

public class EmailService : INotificationService
{
    public Task SendConfirmation(Order order) { /* ... */ }
}

public class OrderRepository : IRepository<Order>
{
    public Task SaveAsync(Order order) { /* EF Core */ }
}

Теперь каждый класс имеет ровно одну причину для изменения. Контейнер DI (Microsoft.Extensions.DependencyInjection) связывает их через конструкторы.

2. Open/Closed: расширение через стратегию и декоратор

Принцип открытости/закрытости (OCP) гласит: класс должен быть открыт для расширения, но закрыт для модификации. В .NET это достигается не только через наследование, но и через композицию с паттернами Strategy и Decorator.

Продвинутый кейс: валидация с ростом правил

Представьте, что у вас есть система валидации заказов. Начальная версия проверяла только сумму и адрес. Через месяц добавилось правило "нельзя заказывать товары на сумму больше лимита". Вместо изменения класса OrderValidator, используем композицию.

// ❌ Нарушение OCP — модификация существующего класса
public class OrderValidator
{
    public bool Validate(Order order)
    {
        if (order.Total < 0) return false;
        if (string.IsNullOrEmpty(order.Address)) return false;
        // Новое правило — модифицируем класс
        if (order.Total > 10000) return false;
        return true;
    }
}
// ✅ OCP через композицию валидаторов
public interface IValidator<T>
{
    bool Validate(T entity);
}

public class TotalValidator : IValidator<Order>
{
    public bool Validate(Order order) => order.Total > 0;
}

public class AddressValidator : IValidator<Order>
{
    public bool Validate(Order order) => !string.IsNullOrEmpty(order.Address);
}

public class CompositeValidator<T> : IValidator<T>
{
    private readonly IEnumerable<IValidator<T>> _validators;
    public CompositeValidator(IEnumerable<IValidator<T>> validators)
    {
        _validators = validators;
    }
    public bool Validate(T entity) => _validators.All(v => v.Validate(entity));
}

Теперь для добавления нового правила (например, проверка лимита) достаточно создать новый класс LimitValidator и зарегистрировать его в DI. Исходный код CompositeValidator не меняется.

3. Liskov Substitution: контракты и ковариантность в generics

Принцип подстановки Лисков (LSP) — один из самых сложных для понимания. На продвинутом уровне речь идёт о контрактном программировании: подкласс не должен нарушать предусловия, постусловия и инварианты базового класса.

Типичная ошибка: нарушение через ковариантность

В C# есть поддержка ковариантности для интерфейсов (ключевое слово out). Однако её неправильное использование может нарушить LSP.

// ❌ Нарушение LSP через ковариантность
public interface IRepository<out T> where T : Entity
{
    T GetById(int id);
}

public class OrderRepository : IRepository<Order>
{
    public Order GetById(int id) => new Order();
}

// Клиентский код ожидает Entity, но получает Order — корректно
IRepository<Entity> repo = new OrderRepository();
var entity = repo.GetById(1); // Работает

Проблема возникает, если IRepository содержит метод void Save(T item) — ковариантность запрещает параметры. Но если вы используете ковариантность только для чтения, LSP не нарушается. Однако если подкласс возвращает null там, где базовый класс гарантирует не-null, это нарушение.

Правильный подход: контракты через Code Contracts или FluentValidation

// ✅ LSP через контракты
public abstract class RepositoryBase<T> where T : Entity
{
    public abstract T GetById(int id);
    // Постусловие: результат не null, если id > 0
}

public class OrderRepository : RepositoryBase<Order>
{
    public override Order GetById(int id)
    {
        if (id <= 0) throw new ArgumentException("Id must                

Блоги

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