← Назад к вопросам

Какая функциональность позволяет нарушать принцип Open–closed?

2.0 Middle🔥 121 комментариев
#Паттерны проектирования

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Возможности C#, позволяющие нарушить принцип Open/Closed

Принцип Open/Closed (Открыт для расширения, закрыт для изменения) декларирует, что сущности должны быть открыты для расширения, но закрыты для модификации. Однако в C# и Unity существуют мощные языковые и архитектурные возможности, которые при неаккуратном использовании легко нарушают этот принцип.

1. Ключевое слово virtual и наследование

Само по себе наследование — основной инструмент для соблюдения OCP, но неконтролируемое переопределение методов через virtual и override может привести к хрупкой иерархии, где изменение базового класса ломает поведение множества наследников.

public class Enemy
{
    public virtual void Attack()
    {
        // Базовая реализация атаки
        Debug.Log("Enemy attacks!");
    }
}

public class BossEnemy : Enemy
{
    public override void Attack()
    {
        // Прямая модификация поведения базового класса
        // Что если в базовом Attack() добавится важная логика?
        Debug.Log("Boss uses special attack!");
        // Наследник НЕ вызывает base.Attack() — нарушение LSP и OCP
    }
}

Если позже в Enemy.Attack() добавится, например, учет перезарядки, все наследники, не вызывающие base.Attack(), сломаются. Это вынуждает модифицировать все переопределяющие классы, нарушая "закрытость".

2. Операторы switch/if-else на основе типа объекта

Это классический антипаттерн, жестко фиксирующий логику. Добавление нового типа требует правки всех таких операторов.

public class DamageCalculator
{
    public float CalculateDamage(Entity entity)
    {
        // Нарушение OCP: при добавлении нового типа Entity нужно менять этот метод
        switch (entity)
        {
            case Player player:
                return player.BaseDamage * player.Multiplier;
            case Enemy enemy:
                return enemy.BaseDamage;
            // case NewEntity newEntity: <- При добавлении: МОДИФИКАЦИЯ!
            default:
                throw new ArgumentException();
        }
    }
}

3. Модификаторы доступа public и internal для полей/методов, не предназначенных для расширения

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

public class Inventory
{
    public List<Item> Items; // Публичное поле
    // Теперь любая логика, напрямую работающая с Items, завязана на конкретную коллекцию.
    // Замена List<Item> на, например, словарь, сломает всех внешних потребителей.
}

4. Использование конкретных классов вместо абстракций

Прямое создание экземпляров конкретных классов через new жестко связывает код.

public class WeaponManager
{
    private Pistol _pistol = new Pistol(); // Жесткая привязка к классу Pistol
    // Чтобы добавить Rifle, нужно ПЕРЕПИСЫВАТЬ код этого класса.
}

5. Статические классы и методы (static)

Статические утилиты, особенно содержащие глобальное состояние или сложную логику, часто становятся точками, требующими модификации при расширении функциональности.

public static class GameSettings
{
    public static Difficulty CurrentDifficulty = Difficulty.Normal;
}
// Множество классов по всей кодовой базе обращаются напрямую.
// Изменение способа хранения настроек (например, переход на ScriptableObject) потребует изменений везде.

6. Магические числа и строки, разбросанные по коду

Изменение такого константного значения требует поиска и правки всех мест использования, что является прямой модификацией.

if (player.Health < 0) // Магическое число 0
{
    Die();
}
// Лучше: константа или конфигурируемое поле.

Как следовать OCP в Unity?

Вместо нарушения принципа используйте:

  • Интерфейсы и абстрактные классы для определения контрактов.
  • Паттерн Стратегия для замены алгоритмов.
  • Паттерн Наблюдатель (события event, UnityEvent) для слабой связанности.
  • ScriptableObject для данных и конфигурации, отделяя код от значений.
  • Внедрение зависимостей (через конструктор, методы или DI-фреймворки).
// Пример соблюдения OCP через интерфейс
public interface IDamageable { void TakeDamage(float damage); }

public class DamageDealer
{
    public void ApplyDamage(IDamageable target, float damage)
    {
        target.TakeDamage(damage); // Закрыт для изменения, открыт для расширения новыми IDamageable
    }
}
// Добавление нового типа уроняемого объекта не требует изменений в DamageDealer.

Таким образом, сам язык C# предоставляет инструменты, которые могут как нарушать, так и поддерживать OCP. Ключ — в сознательном проектировании, когда расширение системы достигается за счет добавления нового кода (новых классов, реализующих интерфейсы), а не правки старого.