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

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

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

Начать курс

Введение: Почему SOLID — основа качественного C# кода

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

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

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

Теория

Класс должен иметь только одну причину для изменения. Другими словами, каждый класс отвечает ровно за одну функциональность или бизнес-логику. Если класс делает слишком много, его становится сложно тестировать, изменять и переиспользовать.

Практика: нарушение SRP

public class OrderProcessor
{
    public void ProcessOrder(Order order)
    {
        // 1. Валидация заказа
        if (order.Total <= 0) throw new Exception("Invalid order");
        
        // 2. Сохранение в базу данных
        using var db = new AppDbContext();
        db.Orders.Add(order);
        db.SaveChanges();
        
        // 3. Отправка email-уведомления
        SmtpClient client = new SmtpClient();
        client.Send("admin@shop.com", order.CustomerEmail, "Order confirmed", "Thank you!");
    }
}

Здесь OrderProcessor нарушает SRP: он отвечает за валидацию, работу с БД и отправку email. Изменение логики отправки уведомлений потребует изменения этого же класса.

Рефакторинг с SRP

public class OrderValidator
{
    public bool Validate(Order order) => order.Total > 0;
}

public class OrderRepository
{
    public void Save(Order order)
    {
        using var db = new AppDbContext();
        db.Orders.Add(order);
        db.SaveChanges();
    }
}

public class EmailService
{
    public void SendConfirmation(string email)
    {
        SmtpClient client = new SmtpClient();
        client.Send("admin@shop.com", email, "Order confirmed", "Thank you!");
    }
}

public class OrderProcessor
{
    private readonly OrderValidator _validator;
    private readonly OrderRepository _repository;
    private readonly EmailService _emailService;

    public OrderProcessor(OrderValidator validator, OrderRepository repository, EmailService emailService)
    {
        _validator = validator;
        _repository = repository;
        _emailService = emailService;
    }

    public void ProcessOrder(Order order)
    {
        if (!_validator.Validate(order)) throw new Exception("Invalid order");
        _repository.Save(order);
        _emailService.SendConfirmation(order.CustomerEmail);
    }
}

Теперь каждый класс имеет одну зону ответственности. Это упрощает тестирование и модификацию.

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

Теория

Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. То есть вы должны иметь возможность добавлять новую функциональность, не изменяя существующий код.

Практика: нарушение OCP

public class ShapeDrawer
{
    public void DrawShape(object shape)
    {
        if (shape is Circle circle)
            Console.WriteLine($"Drawing circle with radius {circle.Radius}");
        else if (shape is Rectangle rect)
            Console.WriteLine($"Drawing rectangle {rect.Width}x{rect.Height}");
        // При добавлении нового типа фигуры — нужно изменять этот метод!
    }
}

Рефакторинг с OCP через абстракцию

public abstract class Shape
{
    public abstract void Draw();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override void Draw() => Console.WriteLine($"Drawing circle with radius {Radius}");
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override void Draw() => Console.WriteLine($"Drawing rectangle {Width}x{Height}");
}

public class ShapeDrawer
{
    public void DrawShape(Shape shape) => shape.Draw(); // Закрыто для модификации
}

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

3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков

Теория

Объекты в программе должны быть заменяемыми экземплярами их подтипов без изменения корректности программы. Если класс B наследует класс A, то мы должны иметь возможность использовать B везде, где ожидается A, без неожиданных побочных эффектов.

Практика: нарушение LSP

public class Bird
{
    public virtual void Fly() => Console.WriteLine("Flying");
}

public class Penguin : Bird
{
    public override void Fly() => throw new Exception("Penguins can't fly!"); // Нарушение LSP
}

Если клиентский код ожидает, что все птицы могут летать, подстановка Penguin вызовет исключение.

Решение: правильная иерархия

public abstract class Bird { }

public interface IFlyable
{
    void Fly();
}

public class Sparrow : Bird, IFlyable
{
    public void Fly() => Console.WriteLine("Sparrow flying");
}

public class Penguin : Bird { } // Не реализует IFlyable — логично

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

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

Теория

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

Практика: нарушение ISP

public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
}

public class Robot : IWorker
{
    public void Work() => Console.WriteLine("Working");
    public void Eat() => throw new Exception("Robots don't eat"); // Ненужный метод
    public void Sleep() => throw new Exception("Robots don't sleep");
}

Рефакторинг с ISP

public interface IWorkable
{
    void Work();
}

public interface I                

Блоги

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