Как реализовать принцип открытости/закрытости (Open/Closed Principle)?

«Как реализовать принцип открытости/закрытости (Open/Closed Principle)?» — вопрос из категории ООП, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

Основной механизм — абстракция. Вместо того чтобы работать с конкретными классами, код должен зависеть от интерфейсов или абстрактных классов.

Плохой пример (нарушает OCP): Каждый раз при добавлении новой фигуры нужно менять класс AreaCalculator.

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;
        // Добавление нового `if` для Triangle нарушит принцип!
        throw new ArgumentException("Unknown shape");
    }
}

Хороший пример (соответствует OCP):

  1. Определяем абстракцию:
    public interface IShape
    {
    double CalculateArea();
    }
  2. Реализуем конкретные сущности:
    
    public class Rectangle : IShape
    {
    public double Width { get; set; }
    public double Height { get; set; }
    public double CalculateArea() => Width * Height;
    }

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

3.  **Создаем расширяемый калькулятор:**
```csharp
public class AreaCalculator
{
    // Этот метод теперь ЗАКРЫТ для модификаций.
    // Чтобы добавить поддержку новой фигуры, мы НЕ меняем этот код.
    public double TotalArea(IEnumerable<IShape> shapes)
    {
        return shapes.Sum(shape => shape.CalculateArea());
    }
}
  1. Расширяем систему (добавляем новую фигуру):
    public class Triangle : IShape
    {
    public double Base { get; set; }
    public double Height { get; set; }
    public double CalculateArea() => 0.5 * Base * Height;
    }
    // Класс `AreaCalculator` продолжает работать без изменений!

Паттерны, помогающие следовать OCP:

  • Стратегия (Strategy): Инкапсулирует семейство алгоритмов, делая их взаимозаменяемыми.
  • Декоратор (Decorator): Динамически добавляет новую функциональность объекту.
  • Наблюдатель (Observer): Позволяет подписываться на события, не изменяя код издателя.