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

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

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

Начать курс

Введение: зачем нужны SOLID принципы в C#?

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

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

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

Теория

Класс должен иметь только одну причину для изменения. Иными словами, каждый класс отвечает только за одну часть функциональности программы. Нарушение SRP приводит к тому, что изменение одной операции (например, сохранение в БД) затрагивает логику расчётов или отображения.

Практика на C#

Рассмотрим типичный пример нарушения SRP — класс, который одновременно вычисляет зарплату и сохраняет сотрудника в базу:

// ❌ Нарушение SRP: класс делает слишком много
public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }

    public decimal CalculateBonus()
    {
        return Salary * 0.1m;
    }

    public void SaveToDatabase()
    {
        // Логика сохранения в БД
        Console.WriteLine("Сотрудник сохранён в БД");
    }
}

Исправим ситуацию, разделив ответственности:

// ✅ SRP: каждый класс отвечает за свою задачу
public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
}

public class BonusCalculator
{
    public decimal Calculate(Employee employee)
    {
        return employee.Salary * 0.1m;
    }
}

public class EmployeeRepository
{
    public void Save(Employee employee)
    {
        Console.WriteLine("Сотрудник сохранён в БД");
    }
}

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

Теория

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

Практика на C#

Допустим, нам нужно рассчитывать площадь разных фигур. Нарушение OCP — использование if-else или switch для каждого нового типа фигуры:

// ❌ Нарушение OCP: при добавлении новой фигуры нужно менять код
public class AreaCalculator
{
    public double Calculate(object shape)
    {
        if (shape is Circle circle)
            return Math.PI * circle.Radius * circle.Radius;
        else if (shape is Rectangle rect)
            return rect.Width * rect.Height;
        // При добавлении Triangle придётся дописывать ещё один if
        return 0;
    }
}

Решение — использовать абстракцию и полиморфизм:

// ✅ OCP: расширяем через наследование, не трогая существующий код
public abstract class Shape
{
    public abstract double GetArea();
}

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

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

public class AreaCalculator
{
    public double Calculate(Shape shape)
    {
        return shape.GetArea(); // Работает для любой фигуры
    }
}

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

Теория

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

Практика на C#

Классическое нарушение LSP — наследование Square от Rectangle. Квадрат не должен быть прямоугольником с точки зрения поведения:

// ❌ Нарушение LSP: квадрат не может быть полноценным прямоугольником
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
}

public class Square : Rectangle
{
    public override int Width
    {
        set { base.Width = value; base.Height = value; }
    }
    public override int Height
    {
        set { base.Width = value; base.Height = value; }
    }
}

// Тест: ожидаем, что площадь не изменится, но это не так
Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 10;
// Ожидаемая площадь: 50, реальная: 100 — нарушение!

Правильное решение — не наследовать квадрат от прямоугольника, а создать общий интерфейс или абстрактный класс:

// ✅ LSP: разделяем на независимые классы
public interface IShape
{
    int GetArea();
}

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

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

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

Теория

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

Практика на C#

Пример нарушения — интерфейс IWorker, который заставляет всех работников реализовывать методы, которые им не нужны:

// ❌ Нарушение ISP: интерфейс слишком «толстый»
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
}

public class HumanWorker : IWorker
{
    public void Work() { /* работает */ }
    public void Eat() { /* ест */ }
    public void Sleep() { /* спит */ }
}

public class RobotWorker : IWorker
{
    public void Work() { /* работает */ }
    public void Eat() { throw new NotImplementedException(); } // Робот не ест!
    public void Sleep() { throw new NotImplementedException(); } // Робот не спит!
}

Исправляем через разделение интерфейсов:

// ✅ ISP: узкие интерфейсы для разных ролей
public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface ISleepable
{
    void                

Блоги

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