Как расширить класс в контексте принципа открытости-закрытости
Что проверяют
Понимаете ли вы OCP как способ добавлять поведение новым кодом, не переписывая уже проверенный, и можете ли показать это на C#, а не пересказать определение. Следом спрашивают, где граница: когда абстракция оправдана, а когда это лишняя сложность.
Как отвечать
Сформулируйте суть: класс закрыт для изменений, если новый сценарий не требует править его код, и открыт для расширения, если в нём есть точка, куда подключается новое поведение. Эта точка почти всегда абстракция.
Покажите на примере. Расчёт скидки со switch по типу клиента при каждом новом типе требует правки метода. Выносим правило в IDiscountRule с методами IsApplicable и Calculate, калькулятор получает IEnumerable<IDiscountRule> из DI и проходит по ним. Новая скидка это новый класс и строка регистрации, калькулятор не меняется.
Механизмы в C#:
- интерфейсы и стратегия: основной способ, хорошо дружит с DI и моками в тестах;
- декоратор: обернуть
IOrderServiceлогированием или кэшем, не трогая исходный класс. Библиотека Scrutor регистрирует декораторы черезDecorate; - абстрактный класс и шаблонный метод: общий алгоритм фиксирован, шаги переопределяются через
abstractилиvirtual; - keyed services (с .NET 8): выбрать реализацию по ключу через
[FromKeyedServices]вместоswitch; - события и делегаты: подписчики добавляются снаружи.
Extension-методы тоже упоминают, но честно оговорите: они добавляют удобный синтаксис, а не новое полиморфное поведение, и доступа к закрытому состоянию у них нет.
Где спотыкаются
Считают наследование главным инструментом OCP. Глубокие иерархии дают хрупкий базовый класс: правка родителя ломает наследников, то есть закрытость как раз теряется.
Заводят интерфейс на каждый класс «на будущее». Точку расширения стоит делать там, где изменения реально повторяются, а не везде.
Трактуют принцип как «код нельзя менять никогда». Исправление бага и рефакторинг это нормальные правки, OCP про то, чтобы новый вариант поведения не требовал вскрывать старый.