Встречается на 38% собеседований по C#

Что такое SOLID

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

Как это работает

Каждая буква аббревиатуры SOLID обозначает один принцип:

  1. S — Single Responsibility (Принцип единственной ответственности). Класс должен иметь только одну причину для изменения, то есть решать только одну задачу. Это упрощает понимание кода и уменьшает риск побочных эффектов при модификации.

  2. O — Open-Closed (Принцип открытости/закрытости). Программные сущности должны быть открыты для расширения, но закрыты для изменения. Новую функциональность следует добавлять через наследование или композицию, не меняя существующий код.

  3. L — Liskov Substitution (Принцип подстановки Лисков). Объекты наследующего класса должны без проблем заменять объекты базового класса, не нарушая корректность программы. Это означает, что наследник не должен изменять контракт базового класса, например, выбрасывать исключения там, где их не было.

  4. I — Interface Segregation (Принцип разделения интерфейсов). Клиенты не должны зависеть от методов, которые они не используют. Лучше иметь несколько узкоспециализированных интерфейсов, чем один общий.

  1. D — Dependency Inversion (Принцип инверсии зависимостей). Модули верхнего уровня не должны зависеть от модулей нижнего уровня; оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали должны зависеть от абстракций.

Примеры на C#

Рассмотрим каждый принцип на практическом примере.

Single Responsibility — класс UserManager не должен одновременно сохранять данные и логировать:

csharp
// Плохо: класс и логирует, и сохраняет данные
class UserManager {
    void Save(User user) { /* сохранение */ }
    void Log(string message) { /* запись в лог */ }
}

// Хорошо: разделяем ответственность
class UserRepository {
    void Save(User user) { /* сохранение */ }
}

class Logger {
    void Log(string message) { /* запись в лог */ }
}

Open-Closed — добавление новых фигур не требует изменения существующего кода:

csharp
abstract class Shape {
    public abstract double Area();
}

class Circle : Shape {
    private double _radius;
    public override double Area() => Math.PI * _radius * _radius;
}

class Rectangle : Shape {
    private double _width, _height;
    public override double Area() => _width * _height;
}

Liskov Substitution — наследник не должен нарушать ожидания базового класса:

csharp
class Bird {
    public virtual void Fly() { /* полет */ }
}

// Плохо: пингвин не умеет летать, но наследует метод
class Penguin : Bird {
    public override void Fly() => throw new NotSupportedException();
}

// Хорошо: разделяем на интерфейсы
interface IFlyable { void Fly(); }
class Sparrow : IFlyable { public void Fly() { /* полет */ } }
class Penguin { /* не реализует IFlyable */ }

Interface Segregation — вместо одного интерфейса с множеством методов — несколько узких:

csharp
// Плохо: класс, который только печатает, вынужден реализовывать Scan()
interface IPrinterScanner {
    void Print();
    void Scan();
}

// Хорошо: раздельные интерфейсы
interface IPrinter { void Print(); }
interface IScanner { void Scan(); }

class Printer : IPrinter {
    public void Print() { /* печать */ }
}

Dependency Inversion — зависимость от абстракции, а не от конкретной реализации:

csharp
interface ILogger { void Log(string message); }

class ConsoleLogger : ILogger {
    public void Log(string message) => Console.WriteLine(message);
}

class OrderService {
    private readonly ILogger _logger;
    public OrderService(ILogger logger) {
        _logger = logger; // внедрение зависимости
    }
}

Подводные камни

  • Чрезмерное дробление — слишком много маленьких классов и интерфейсов усложняют код. Применяйте принципы разумно, ориентируясь на реальные потребности.
  • Нарушение LSP — часто встречается, когда наследник сужает поведение базового класса. Проверяйте, что наследник действительно является подтипом базового.
  • Жёсткая привязка к конкретным классам — без Dependency Inversion сложно тестировать и заменять компоненты.
  • Игнорирование Open-Closed — изменение существующего кода вместо расширения приводит к регрессиям и конфликтам.

Когда использовать

SOLID применим практически в любом объектно-ориентированном проекте, но особенно полезен в больших системах, где важна поддерживаемость. На собеседовании важно не только перечислить принципы, но и показать понимание их практической ценности на примерах.

Коротко

  • SOLID — пять принципов ООП: единственная ответственность, открытость/закрытость, подстановка Лисков, разделение интерфейсов, инверсия зависимостей.
  • Цель — гибкий, тестируемый и расширяемый код.
  • Каждый принцип решает конкретную проблему: от дублирования кода до жёсткой связанности.
  • На собеседовании приводите примеры на C#, показывающие нарушение и соблюдение принципов.
как отвечать на вопрос
пример собеседования
фреймворки на собеседовании
типичные вопросы junior
интервью вопросы и ответы