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

Ответ

Принцип открытости/закрытости (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): Позволяет подписываться на события, не изменяя код издателя.

Ответ 18+ 🔞

Ну вот, опять про эти ваши принципы SOLID, да? Слушай, а принцип открытости-закрытости — это вообще, блядь, гениальная штука, если её правильно понять, а не так, как обычно — через жопу.

Представь себе, ты написал класс, который считает площадь. И всё у тебя работает, тесты зелёные, заказчик доволен. И тут приходит менеджер и говорит: «А добавь-ка нам треугольничек, он тоже площадь имеет». И если ты — мудак, то полезешь в свой AreaCalculator и начнёшь там пихать if (shape is Triangle t).... А потом придёт менеджер и скажет: «А теперь шестиугольник». И ты опять туда же. И так до тех пор, пока твой метод не превратится в пиздец-монстра на сотню строк, который боишься трогать.

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

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

Смотри, как это выглядит в жизни. Вот пример от мудака, который не в курсе:

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. Пиздец, а не жизнь.
        throw new ArgumentException("Unknown shape");
    }
}

Чувствуешь? Каждый новый тип фигуры — это нож в твою спину и плевок в твой же код. Терпения ебать ноль на такое.

А теперь смотри, как надо делать по-человечески:

Шаг первый: создаём абстракцию. То есть, договорённость, что у любой фигуры будет метод посчитать площадь.

public interface IShape
{
    double CalculateArea();
}

Шаг второй: реализуем конкретные штуки. Каждая фигура сама знает, как посчитать свою площадь. Прямо как в жизни — круг знает про πR², а прямоугольник про ширину и высоту.

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;
}

Шаг третий: делаем универсальный калькулятор. Он нихуя не знает про конкретные фигуры. Его дело — взять список чего угодно, что умеет в CalculateArea(), и просуммировать.

public class AreaCalculator
{
    // Этот метод теперь можно заколотить гвоздями и больше не открывать.
    // Он закрыт для изменений. Идилия, ёпта!
    public double TotalArea(IEnumerable<IShape> shapes)
    {
        return shapes.Sum(shape => shape.CalculateArea());
    }
}

И вот теперь, когда менеджер приползёт с криком «ТРЕУГОЛЬНИК НАДО БЛЯТЬ СРОЧНО!», ты делаешь вот что:

public class Triangle : IShape
{
    public double Base { get; set; }
    public double Height { get; set; }
    public double CalculateArea() => 0.5 * Base * Height;
}

И всё. Ты добавил новый класс. Ты не полез в AreaCalculator и не сломал там ничего. Он как работал с IShape, так и работает. Ты его расширил, но не модифицировал. Вот и весь принцип, в рот меня чих-пых.

Для особо одарённых, кто хочет выглядеть умно на собеседовании, можно блеснуть словами: «Это достигается через паттерны, например, Стратегия, Декоратор или Наблюдатель». Но суть-то от этого не меняется — выноси мозги в абстракции, а не таскай их с собой в виде кучи if-else по всему коду.