Что такое принципы SOLID?

«Что такое принципы SOLID?» — вопрос из категории Архитектура, который задают на 58% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного проектирования и программирования, направленных на создание понятного, гибкого и поддерживаемого кода.

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

Один класс должен иметь одну и только одну причину для изменения.

Класс должен решать строго одну задачу или отвечать за одну функциональность. Это упрощает тестирование, понимание и снижает связность.

// НАРУШЕНИЕ: Класс отвечает и за логику заказа, и за его сохранение, и за логирование.
public class OrderProcessor
{
    public void Process(Order order)
    {
        // Валидация заказа...
        // Применение бизнес-правил...
        this.SaveToDatabase(order); // Ответственность за персистентность
        this.Log($"Order {order.Id} processed"); // Ответственность за логирование
    }
    private void SaveToDatabase(Order order) { /* ... */ }
    private void Log(string message) { /* ... */ }
}

// СОБЛЮДЕНИЕ: Разделение ответственностей.
public class OrderProcessor // Отвечает только за бизнес-логику обработки
{
    private readonly IOrderRepository _repository;
    private readonly ILogger _logger;
    // Зависимости внедряются извне (DIP)
    public OrderProcessor(IOrderRepository repo, ILogger logger)
    {
        _repository = repo;
        _logger = logger;
    }

    public void Process(Order order)
    {
        // Валидация и бизнес-правила...
        _repository.Save(order); // Делегируем сохранение
        _logger.Log($"Order {order.Id} processed"); // Делегируем логирование
    }
}
public interface IOrderRepository { void Save(Order order); } // Ответственность за данные
public interface ILogger { void Log(string message); } // Ответственность за логи

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

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

Новую функциональность следует добавлять через создание новых классов (наследование, композиция), а не изменяя код существующих, уже протестированных.

// НАРУШЕНИЕ: Для добавления новой фигуры нужно менять метод AreaCalculator.
public class AreaCalculator
{
    public double CalculateArea(object shape)
    {
        if (shape is Rectangle r) return r.Width * r.Height;
        if (shape is Circle c) return Math.PI * c.Radius * c.Radius;
        // Добавляем новый `if` для Triangle -> ИЗМЕНЕНИЕ КЛАССА!
        throw new ArgumentException("Unknown shape");
    }
}

// СОБЛЮДЕНИЕ: Используем абстракцию. Новые фигуры расширяют систему, не изменяя калькулятор.
public abstract class Shape
{
    public abstract double CalculateArea(); // Закрыт для изменений (абстрактный)
}
public class Rectangle : Shape { /* ... */ public override double CalculateArea() => Width * Height; }
public class Circle : Shape { /* ... */ public override double CalculateArea() => Math.PI * Radius * Radius; }
public class Triangle : Shape { /* ... */ public override double CalculateArea() => Base * Height / 2; } // РАСШИРЕНИЕ

public class AreaCalculator
{
    // Метод теперь зависит от абстракции Shape. Он ЗАКРЫТ для изменений.
    public double CalculateArea(Shape shape) => shape.CalculateArea();
}

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

Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности программы.

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

// НАРУШЕНИЕ: Класс Square нарушает контракт класса Rectangle (можно менять ширину и высоту независимо).
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 = base.Height = value; } }
    public override int Height { set { base.Width = base.Height = value; } }
}
// Клиентский код ломается:
void TestArea(Rectangle r)
{
    r.Width = 5;
    r.Height = 4;
    Console.WriteLine(r.Width * r.Height); // Ожидает 20, но для Square получит 16.
}

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

Много специализированных интерфейсов лучше, чем один универсальный.

Клиенты не должны зависеть от методов, которые они не используют. Большие "жирные" интерфейсы приводят к тому, что классы вынуждены реализовывать ненужные методы (пустыми или с исключениями).

// НАРУШЕНИЕ: Принтер-сканер вынужден реализовывать метод Fax, который ему не нужен.
public interface IMultiFunctionDevice
{
    void Print(Document d);
    void Scan(Document d);
    void Fax(Document d); // Не всем устройствам нужен факс!
}
public class OldPrinter : IMultiFunctionDevice
{
    public void Print(Document d) { /* OK */ }
    public void Scan(Document d) { throw new NotImplementedException(); } // НЕ НУЖЕН!
    public void Fax(Document d) { throw new NotImplementedException(); } // НЕ НУЖЕН!
}

// СОБЛЮДЕНИЕ: Разделяем на мелкие интерфейсы.
public interface IPrinter { void Print(Document d); }
public interface IScanner { void Scan(Document d); }
public interface IFax { void Fax(Document d); }

public class OldPrinter : IPrinter { public void Print(Document d) { /* ... */ } } // Только то, что нужно
public class Photocopier : IPrinter, IScanner { /* ... */ } // Композиция интерфейсов

5. Принцип инверсии зависимостей (Dependency Inversion Principle - DIP)

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

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Этот принцип лежит в основе внедрения зависимостей (Dependency Injection) и использования IoC-контейнеров.

// НАРУШЕНИЕ: Класс высокоуровневой логики напрямую зависит от конкретной реализации низкоуровневой (базы данных).
public class OrderService
{
    private readonly SqlServerOrderRepository _repository; // Зависимость от детали
    public OrderService()
    {
        _repository = new SqlServerOrderRepository(); // Жёсткое создание зависимости
    }
    public void ProcessOrder(Order order) => _repository.Save(order);
}

// СОБЛЮДЕНИЕ: Зависим от абстракции (интерфейса). Конкретная реализация внедряется извне.
public class OrderService // Модуль верхнего уровня (бизнес-логика)
{
    private readonly IOrderRepository _repository; // Зависимость от абстракции
    // Внедрение зависимости через конструктор (Constructor Injection)
    public OrderService(IOrderRepository repository) // Абстракция, а не деталь
    {
        _repository = repository; // Может быть SqlServer, PostgreSQL, InMemory репозиторий
    }
    public void ProcessOrder(Order order) => _repository.Save(order);
}
// Абстракция, от которой зависят и OrderService, и все репозитории
public interface IOrderRepository { void Save(Order order); }
// Деталь (низкоуровневый модуль) зависит от абстракции
public class SqlServerOrderRepository : IOrderRepository { public void Save(Order order) { /* ... */ } }
public class PostgreSQLOrderRepository : IOrderRepository { public void Save(Order order) { /* ... */ } }

Итог: Следование SOLID приводит к созданию кода с низкой связностью (loose coupling) и высокой связностью (high cohesion), что значительно упрощает его тестирование, расширение и поддержку в долгосрочной перспективе.

Видео-ответы