ООП и проектирование: вопросы на собеседовании

Принципы ООП, SOLID, паттерны, композиция против наследования, тестируемый код. Спрашивают у любого джуна после алгоритмов.

#1Принципы ООПjunior

Назовите принципы ООП и объясните каждый своими словами.

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

Наследование — переиспользование и расширение поведения родителя. Самый опасный из принципов: жёстко связывает классы. Правило — «наследование для отношения *является*, композиция для *имеет*».

Полиморфизм — единый интерфейс для разных реализаций. Код работает с абстракцией Shape и не знает про круги и квадраты. Это то, ради чего ООП вообще существует: добавление новой фигуры не требует правок существующего кода.

Абстракция — выделение существенного и отбрасывание деталей. Часто объединяют с инкапсуляцией, но акцент разный: абстракция — про то, что мы моделируем, инкапсуляция — про то, как прячем.

Как ответить сильно: привести к каждому принципу пример из своего проекта. «В курсовой по расписанию у меня был интерфейс Exporter с реализациями для PDF и Excel — это полиморфизм» звучит убедительнее определения из учебника.

  • Чем абстракция отличается от инкапсуляции?
  • Почему говорят «композиция вместо наследования»?
#2SOLIDmiddle

Расшифруйте SOLID с примерами.

S — Single Responsibility. У класса одна причина для изменения. Класс Report, который и считает данные, и рендерит HTML, и шлёт почту, придётся менять при изменении любого из трёх — разделяем.

O — Open/Closed. Открыт для расширения, закрыт для изменения. Вместо растущего switch по типу платежа — интерфейс PaymentMethod с новыми реализациями.

L — Liskov Substitution. Наследник должен подставляться вместо родителя без сюрпризов. Классический контрпример: Square extends Rectangle — установка ширины у квадрата меняет и высоту, ломая ожидания кода, работающего с прямоугольником.

I — Interface Segregation. Много узких интерфейсов лучше одного широкого. Не заставляйте класс реализовывать методы, которые ему не нужны и в которых он бросает NotSupportedException.

D — Dependency Inversion. Модули зависят от абстракций, а не от конкретных реализаций. Сервис принимает интерфейс UserRepository, а не класс PostgresUserRepository — это и делает код тестируемым через подмену зависимости.

Важная оговорка, которая понравится интервьюеру: SOLID — набор ориентиров, а не законов. Буквальное следование им в маленьком проекте порождает десятки интерфейсов с одной реализацией. Принципы решают проблему изменяемости кода, и применять их стоит там, где изменения действительно ожидаются.

  • Приведите нарушение LSP из своей практики
  • Чем DI-принцип отличается от DI-контейнера?
#3Наследованиеmiddle

Почему композицию предпочитают наследованию?

Проблемы наследования:

  • Жёсткая связанность. Наследник зависит от деталей реализации родителя; изменение базового класса ломает потомков — «проблема хрупкого базового класса».
  • Наследуется всё, включая ненужное. Интерфейс потомка раздувается.
  • Иерархия фиксируется на этапе компиляции и не меняется в рантайме.
  • Комбинаторный взрыв. Кофе с молоком, с сиропом, с молоком и сиропом — для каждой комбинации нужен класс.

Композиция — объект содержит другие объекты и делегирует им работу:

class Logger {
  constructor(private sink: Sink) {}          // подставляем файл, консоль или сеть
  log(msg: string) { this.sink.write(msg); }
}

Поведение собирается из частей, заменяется в рантайме и тестируется изолированно. На композиции построены паттерны «Стратегия», «Декоратор», «Адаптер» — и тот же кофе с добавками решается декоратором без взрыва классов.

Когда наследование уместно: настоящее отношение «является», стабильная и неглубокая иерархия, общий контракт (чаще — наследование интерфейса, а не реализации). Глубина больше двух-трёх уровней — почти всегда признак проблемы.

  • Что такое проблема ромбовидного наследования?
  • Как работает паттерн Декоратор?
#4Паттерныmiddle

Какие паттерны проектирования вы применяли? Расскажите про три.

Отвечать нужно своими примерами, иначе вопрос читается как заученный.

Стратегия — семейство взаимозаменяемых алгоритмов за одним интерфейсом. Разные способы сортировки, разные платёжные провайдеры, разные форматы экспорта. Убирает большие switch.

Фабрика / Фабричный метод — создание объектов без привязки к конкретным классам. Полезен, когда тип определяется конфигурацией или данными: по строке 'pdf' получить нужный экспортёр.

Наблюдатель — объект уведомляет подписчиков об изменениях. Лежит в основе событийных моделей, реактивности, EventEmitter, подписок в UI-фреймворках.

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

Главный тезис: паттерн — это ответ на конкретную проблему, а не украшение. Если в решении есть паттерн, он должен убирать дублирование или облегчать изменение. Насаждать паттерны заранее — распространённая ошибка.

  • Чем Стратегия отличается от Состояния?
  • Почему Синглтон называют антипаттерном?
#5Основыjunior

Чем абстрактный класс отличается от интерфейса? Когда использовать каждый?

Абстрактный класс может содержать состояние (поля), конструктор и готовые реализации методов. Класс наследует только один абстрактный класс.

Интерфейс — контракт без состояния. Класс может реализовать сколько угодно интерфейсов. В современных языках интерфейсы получили реализации по умолчанию (default-методы в Java, расширения протоколов в Swift), поэтому граница размылась — но состояния в интерфейсе по-прежнему нет.

Как выбирать:

  • Интерфейс — когда описываете способность: Comparable, Serializable, Repository. Позволяет множественную реализацию и не навязывает иерархию. По умолчанию выбирайте его.
  • Абстрактный класс — когда у группы классов есть общее состояние и код, который иначе придётся дублировать, и они действительно образуют одно семейство.

Частый шаблон: интерфейс задаёт контракт, абстрактный класс даёт базовую реализацию, конкретные классы наследуют её — так устроены коллекции в Java (List → AbstractList → ArrayList).

В JavaScript/TypeScript интерфейсы существуют только в типах и исчезают при компиляции, абстрактные классы — реальные сущности в рантайме.

  • Что такое множественное наследование и почему его нет в Java?
  • Зачем нужны дефолтные методы в интерфейсах?

Ещё 7 вопросов в этом треке

  1. Что такое полиморфизм? Чем перегрузка отличается от переопределения?
  2. Чем передача по значению отличается от передачи по ссылке? Как это работает с объектами?
  3. Как правильно работать с исключениями?
  4. Что делает код тестируемым? Что такое внедрение зависимостей?
  5. Как работает сборщик мусора? Может ли в языке со сборкой мусора быть утечка памяти?
  6. Что такое состояние гонки и как его предотвратить?
  7. Что такое технический долг и рефакторинг? Как вы поддерживаете качество кода?