Полиморфизм и инкапсуляция на практике

↑ 1.4 ООП в C♯ · ← Предыдущая · → Следующая

Обязательно~6 мин чтенияУровень: junior

Зачем это на собесе

«Расскажите про четыре принципа ООП» — вопрос-разминка. Интервьюер ждёт не определений из учебника, а примеров: где в вашем коде полиморфизм убрал 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> со сеттером и переделать на метод.

Вопросы с ответами

Связанные темы