MidТеория3 min

Принципы SOLID в C#

Все пять принципов объектно-ориентированного проектирования с примерами на C# .NET 10: SRP, OCP, LSP, ISP, DIP

SOLID -- пять принципов объектно-ориентированного проектирования, которые делают код гибким, понятным и поддерживаемым. Эти принципы были сформулированы Робертом Мартином и являются фундаментом для промышленной разработки.

S -- Single Responsibility Principle

Принцип единственной ответственности: класс должен иметь только одну причину для изменения.

Если класс одновременно создаёт заказы, отправляет email, генерирует PDF и сохраняет данные в БД -- у него четыре причины для изменения. Любое изменение в одной области может случайно сломать другую.

Нарушение SRP:

public class OrderService
{
    public Order CreateOrder(CreateOrderRequest request) { /* ... */ }
    public void SendEmailNotification(Order order) { /* ... */ }
    public void GenerateInvoicePdf(Order order) { /* ... */ }
    public void SaveToDatabase(Order order) { /* ... */ }
}

Соблюдение SRP -- каждый класс отвечает за одну операцию:

public class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, OrderDto>
{
    private readonly IOrderRepository _repository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<OrderDto> Handle(
        CreateOrderCommand command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Address);
        await _repository.AddAsync(order, ct);
        await _unitOfWork.SaveChangesAsync(ct);
        return order.ToDto();
    }
}

public class OrderConfirmedNotificationHandler
    : INotificationHandler<OrderConfirmedEvent>
{
    private readonly IEmailService _emailService;
    // Only responsible for sending notifications
}

В архитектуре с CQRS каждый Handler отвечает за одну операцию, что естественным образом соблюдает SRP.

O -- Open/Closed Principle

Принцип открытости/закрытости: класс открыт для расширения, но закрыт для модификации.

При добавлении новой функциональности не нужно менять существующий код -- достаточно добавить новый класс.

Нарушение OCP:

public class DiscountCalculator
{
    public decimal Calculate(Order order, string customerType)
    {
        switch (customerType)
        {
            case "Regular": return 0;
            case "Premium": return order.TotalAmount.Amount * 0.1m;
            case "VIP": return order.TotalAmount.Amount * 0.2m;
            default: return 0;
        }
    }
}

Каждый новый тип клиента требует изменения этого метода. Соблюдение OCP через паттерн «Стратегия»:

public interface IDiscountStrategy
{
    decimal CalculateDiscount(Order order);
}

public class RegularDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(Order order) => 0;
}

public class PremiumDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(Order order)
        => order.TotalAmount.Amount * 0.1m;
}

public class VipDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(Order order)
        => order.TotalAmount.Amount * 0.2m;
}

// New discount type = new class, no changes to existing code
public class SeasonalDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(Order order)
        => order.TotalAmount.Amount * 0.15m;
}

Добавление сезонной скидки -- это новый класс без модификации существующих.

L -- Liskov Substitution Principle

Принцип подстановки Лисков: объекты подкласса должны быть заменяемы объектами базового класса без нарушения корректности программы.

Если код работает с базовым типом, подстановка любой конкретной реализации не должна ломать поведение.

Нарушение LSP:

public class ReadOnlyRepository : IOrderRepository
{
    public Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
    {
        /* OK */
    }

    public Task AddAsync(Order order, CancellationToken ct)
    {
        throw new NotSupportedException("Read-only!");
        // LSP violation!
    }
}

Код, ожидающий IOrderRepository, вызовет AddAsync и получит неожиданное исключение. Соблюдение LSP -- разделить интерфейсы:

public interface IOrderReadRepository
{
    Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<IReadOnlyList<Order>> GetByCustomerIdAsync(
        string customerId, CancellationToken ct = default);
}

public interface IOrderWriteRepository
{
    Task AddAsync(Order order, CancellationToken ct = default);
    void Update(Order order);
}

public interface IOrderRepository
    : IOrderReadRepository, IOrderWriteRepository { }

Теперь ReadOnlyRepository реализует только IOrderReadRepository, и нет нарушения контракта.

I -- Interface Segregation Principle

Принцип разделения интерфейсов: клиенты не должны зависеть от методов, которые они не используют.

Нарушение ISP:

public interface IOrderService
{
    Task<Order> CreateOrder(CreateOrderCommand cmd);
    Task<Order> GetOrder(Guid id);
    Task CancelOrder(Guid id);
    Task GenerateReport(DateTime from, DateTime to);
    Task ExportToExcel(Guid orderId);
    Task SendNotification(Guid orderId);
}

Контроллер, который только создаёт заказы, вынужден зависеть от методов экспорта и отчётов. CQRS с MediatR идеально соблюдает ISP:

public record CreateOrderCommand(
    string CustomerId, ShippingAddress Address)
    : IRequest<OrderDto>;

public record GetOrderQuery(Guid OrderId)
    : IRequest<OrderDto>;

public record CancelOrderCommand(Guid OrderId, string Reason)
    : IRequest;

Каждый Handler зависит только от тех интерфейсов, которые ему действительно нужны. CreateOrderHandler использует IOrderRepository и IUnitOfWork, но ничего не знает об IEmailService.

D -- Dependency Inversion Principle

Принцип инверсии зависимостей: зависимости должны быть направлены к абстракциям, а не к конкретным реализациям.

Нарушение DIP:

public class OrderService
{
    private readonly SqlOrderRepository _repository;
    private readonly SmtpEmailSender _emailSender;

    public OrderService()
    {
        _repository = new SqlOrderRepository();
        _emailSender = new SmtpEmailSender();
    }
}

Класс намертво привязан к SQL Server и SMTP. Замена на другую БД или провайдер email потребует переписывания. Соблюдение DIP:

public class CreateOrderHandler
    : IRequestHandler<CreateOrderCommand, OrderDto>
{
    private readonly IOrderRepository _repository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderHandler(
        IOrderRepository repository,
        IUnitOfWork unitOfWork)
    {
        _repository = repository;
        _unitOfWork = unitOfWork;
    }
}

// Registration in DI container (Program.cs)
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

Handler зависит от абстракций (IOrderRepository), а конкретные реализации подставляются через DI-контейнер. При переходе на другую БД меняется только одна строка регистрации.

SOLID в контексте проекта

Принцип Как применяется в проекте
SRP Каждый Command/Query Handler -- одна операция
OCP Стратегии, валидации через FluentValidation, новые Pipeline Behaviors
LSP Интерфейсы репозиториев, корректное наследование Entity
ISP CQRS разделяет чтение и запись, мелкие специализированные интерфейсы
DIP Domain определяет интерфейсы, Infrastructure реализует, DI связывает

Проверь себя

Какой принцип SOLID нарушается, если класс OrderService одновременно создаёт заказы, отправляет email и генерирует отчёты?

Какой принцип реализует паттерн CQRS с MediatR?

Что произойдёт при нарушении принципа подстановки Лисков (LSP)?

Как принцип инверсии зависимостей (DIP) реализуется в Clean Architecture?