SOLID принципы в C#: теория и практика от А до Я

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

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

Начать курс

Введение в SOLID принципы в C#

Разработка на C# в экосистеме .NET требует не только знания синтаксиса, но и умения проектировать гибкие, поддерживаемые и тестируемые системы. Именно для этого были созданы SOLID принципы — пять фундаментальных правил объектно-ориентированного программирования, сформулированных Робертом Мартином (дядюшкой Бобом).

Акроним SOLID расшифровывается как:

  • S — Single Responsibility Principle (Принцип единственной ответственности)
  • O — Open/Closed Principle (Принцип открытости/закрытости)
  • L — Liskov Substitution Principle (Принцип подстановки Лисков)
  • I — Interface Segregation Principle (Принцип разделения интерфейсов)
  • D — Dependency Inversion Principle (Принцип инверсии зависимостей)

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

1. Принцип единственной ответственности (SRP)

Суть: Класс должен иметь только одну причину для изменения. Другими словами, каждый класс отвечает за одну четко определенную задачу.

Нарушение SRP в C#

Рассмотрим типичный пример — класс OrderService, который делает слишком много:

public class OrderService
{
    public void ProcessOrder(Order order)
    {
        // 1. Сохранение в БД
        SaveToDatabase(order);
        
        // 2. Отправка email
        SendEmail(order.CustomerEmail, "Заказ оформлен");
        
        // 3. Логирование
        File.WriteAllText("log.txt", $"Заказ {order.Id} обработан");
    }
    
    private void SaveToDatabase(Order order) { /* ... */ }
    private void SendEmail(string email, string message) { /* ... */ }
}

Проблема: Изменение логики отправки email или способа логирования приведет к изменению класса OrderService. Это нарушает SRP.

Правильная реализация SRP

Разделяем ответственность на отдельные сервисы:

public class OrderProcessor
{
    private readonly IOrderRepository _repository;
    private readonly INotificationService _notificationService;
    private readonly ILogger _logger;
    
    public OrderProcessor(
        IOrderRepository repository,
        INotificationService notificationService,
        ILogger logger)
    {
        _repository = repository;
        _notificationService = notificationService;
        _logger = logger;
    }
    
    public void ProcessOrder(Order order)
    {
        _repository.Save(order);
        _notificationService.SendConfirmation(order.CustomerEmail);
        _logger.Log($"Заказ {order.Id} обработан");
    }
}

public interface IOrderRepository { void Save(Order order); }
public interface INotificationService { void SendConfirmation(string email); }
public interface ILogger { void Log(string message); }

Теперь каждый класс отвечает только за свою задачу. Изменение логики логирования не затрагивает обработку заказов.

2. Принцип открытости/закрытости (OCP)

Суть: Классы должны быть открыты для расширения, но закрыты для модификации. То есть вы должны иметь возможность добавлять новое поведение без изменения существующего кода.

Нарушение OCP в C#

Классический пример — калькулятор с условиями:

public class AreaCalculator
{
    public double CalculateArea(object shape)
    {
        if (shape is Rectangle rect)
            return rect.Width * rect.Height;
        else if (shape is Circle circle)
            return Math.PI * circle.Radius * circle.Radius;
        // Добавление новой фигуры требует изменения этого метода!
        throw new NotSupportedException();
    }
}

Проблема: При добавлении треугольника или квадрата придется модифицировать метод CalculateArea.

Правильная реализация OCP

Используем полиморфизм и абстракцию:

public abstract class Shape
{
    public abstract double CalculateArea();
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override double CalculateArea() => Width * Height;
}

public class Circle : Shape
{
    public double Radius { get; set; }
    
    public override double CalculateArea() => Math.PI * Radius * Radius;
}

// Клиентский код не меняется при добавлении новых фигур
public class AreaCalculator
{
    public double CalculateArea(Shape shape) => shape.CalculateArea();
}

Теперь для добавления новой фигуры достаточно создать новый класс, наследующий от Shape, без изменения существующего кода.

3. Принцип подстановки Лисков (LSP)

Суть: Объекты базового класса должны заменяться объектами производных классов без нарушения корректности программы. Проще говоря, наследник не должен ломать логику родителя.

Нарушение LSP в C#

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

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area() => Width * Height;
}

public class Square : Rectangle
{
    private int _side;
    
    public override int Width 
    { 
        set { _side = value; } 
    }
    
    public override int Height 
    { 
        set { _side = value; } 
    }
}

// Клиентский код:
void Test(Rectangle rect)
{
    rect.Width = 5;
    rect.Height = 10;
    Debug.Assert(rect.Area() == 50); // Для Square будет 100!
}

Проблема: Квадрат нарушает поведение прямоугольника — установка ширины меняет высоту.

Правильная реализация LSP

Лучше использовать общий интерфейс или абстрактный класс:

public interface IShape
{
    int Area();
}

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public int Area() => Width * Height;
}

public class Square : IShape
{
    public int Side { get; set; }
    public int Area() => Side * Side;
}

Теперь каждый класс реализует интерфейс без нарушения ожиданий клиентского кода.

4. Принцип разделения интерфейсов (ISP)

Суть: Клиенты не должны зависеть от методов, которые они не используют. Вместо одного "толстого" интерфейса лучше создать несколько специализированных.

Нарушение ISP в C#

                

Блоги

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