Полиморфизм и инкапсуляция на практике
↑ 1.4 ООП в C♯ · ← Предыдущая · → Следующая
Зачем это на собесе
«Расскажите про четыре принципа ООП» — вопрос-разминка. Интервьюер ждёт не определений из учебника, а примеров: где в вашем коде полиморфизм убрал
if/else, как инкапсуляция защитила инварианты, чем отличаются виды полиморфизма.
Объяснение
Четыре принципа ООП одной строкой
- Инкапсуляция — данные и правила их изменения живут вместе; наружу торчит минимум.
- Наследование — повторное использование и специализация через отношение «является».
- Полиморфизм — один интерфейс, много реализаций; вызывающий не знает конкретный тип.
- Абстракция — работа с существенным, скрытие деталей за контрактом.
Полиморфизм
Виды в C#:
- Подтипов (runtime, динамический):
virtual/override, интерфейсы. Вызываемый метод определяется реальным типом объекта. - Параметрический: generics — один алгоритм для разных типов (
List<T>). - Ad hoc (compile-time): перегрузка методов и операторов; выбор реализации по типам аргументов на этапе компиляции.
Практический эффект: убираем ветвления по типу.
// Плохо: каждый новый тип требует правки метода
decimal Fee(Payment p) => p.Kind switch
{
"card" => p.Amount * 0.02m,
"sbp" => 0m,
"cash" => 0m,
_ => throw new NotSupportedException(),
};
// Хорошо: поведение внутри типов, вызывающий не знает деталей
public interface IPaymentMethod { decimal Fee(decimal amount); }
public sealed class CardPayment : IPaymentMethod { public decimal Fee(decimal a) => a * 0.02m; }
public sealed class SbpPayment : IPaymentMethod { public decimal Fee(decimal a) => 0m; }
decimal Total(IPaymentMethod m, decimal amount) => amount + m.Fee(amount);Новый способ оплаты — новый класс, существующий код не меняется (принцип открытости/закрытости).
Инкапсуляция на практике
Цель — защитить инварианты: объект нельзя перевести в некорректное состояние снаружи.
public sealed class Appointment
{
private readonly List<Attachment> _attachments = [];
public AppointmentStatus Status { get; private set; } = AppointmentStatus.Booked;
public IReadOnlyList<Attachment> Attachments => _attachments;
public void Cancel(string reason)
{
if (Status == AppointmentStatus.Completed)
throw new InvalidOperationException("Нельзя отменить завершённый приём");
Status = AppointmentStatus.Cancelled;
}
public void AddAttachment(Attachment a)
{
if (_attachments.Count >= 10) throw new InvalidOperationException("Не больше 10 файлов");
_attachments.Add(a);
}
}- Состояние меняется только методами, которые проверяют правила.
- Коллекции наружу — только как
IReadOnlyList/IReadOnlyCollection. - «Tell, don’t ask»: вместо
if (x.Status == ...) x.Status = ...вызывающий просит объект выполнить действие. - Минимальная видимость по умолчанию:
private→internal→publicпо необходимости.
Абстракция и границы
Интерфейс должен отражать потребность вызывающего, а не устройство реализации. IPaymentGateway.Charge(...) — хорошо, IPaymentGateway.GetHttpClient() — протечка.
Нюансы и подводные камни
- «Полиморфизм ради полиморфизма»: интерфейс с единственной реализацией без причин — лишняя сложность.
- Публичные сеттеры и публичные коллекции разрушают инкапсуляцию: инварианты проверять негде.
- Наследование ради переиспользования кода даёт хрупкие иерархии; для переиспользования чаще подходит композиция.
- Проверки типа (
is/switchпо типам) в нескольких местах — признак, что поведение стоит перенести в типы. В границах слоёв (маппинг, сериализация) переключение по типу нормально. - Нарушение LSP: наследник, который сужает допустимые входы или бросает
NotSupportedException, ломает полиморфизм.
Практика
- Убрать
switchпо строковому виду доставки, заменив его на набор стратегий. - Сделать класс
Orderс инвариантом «нельзя добавить позицию после оплаты» и закрытой коллекцией позиций. - Найти в своём проекте публичный
List<T>со сеттером и переделать на метод.
Вопросы с ответами
Расскажите про принципы ООП на примере.
Инкапсуляция:
Orderсам следит за инвариантами (статус, сумма). Полиморфизм: способы оплаты за общим интерфейсом. Наследование: базовыйReportBaseс Template Method. Абстракция: клиент работает сIPaymentGateway, не зная про HTTP.
Какие виды полиморфизма бывают в C#?
Подтипов (virtual/override, интерфейсы), параметрический (generics), ad hoc (перегрузка методов и операторов).
Как полиморфизм помогает избавиться от
if/elseиswitch?Поведение выносится в реализации общего интерфейса. Вызывающий код обращается к интерфейсу, а выбор реализации делает DI или фабрика. Новое поведение добавляется без правки старого кода.
Что такое инвариант и как его защищает инкапсуляция?
Инвариант — правило, которое всегда должно выполняться для объекта. Инкапсуляция закрывает состояние и разрешает менять его только методами, которые проверяют правила.
Чем отличается статический полиморфизм от динамического?
Статический (перегрузка, generics) определяется компилятором по типам во время компиляции. Динамический (virtual, интерфейсы) — по реальному типу объекта во время выполнения.
Связанные темы
- Предыдущая: Интерфейсы и абстрактные классы, default interface methods · Следующая: Композиция вместо наследования
- Наследование и virtual: Наследование, virtual, override, new
- Generics: Generics: обобщённые типы и методы
- SOLID: [[be-8-1-1-solid-v-csharp-s-primerami|SOLID в C# с примерами]]
- Strategy: Поведенческие паттерны: Strategy, Chain of Responsibility, Command, Observer, Template Method, Mediator