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 связывает |