Гид по технологиям

Наследование в объектно-ориентированном программировании

6 min read Программирование Обновлено 29 Dec 2025
Наследование в OOP: понятие, примеры и лучшие практики
Наследование в OOP: понятие, примеры и лучшие практики

семья японских макак купающихся в горячем источнике

Нередко в объектно-ориентированном программировании (ООП) хочется переиспользовать код. Классы нужны, чтобы создавать объекты без повторной прописки одних и тех же полей и методов. Но что если похожи не объекты, а сами классы?

Например, дерево имеет корни, ствол и ветви. Это справедливо для вязов, дубов и сосен. Вместо копирования одинаковых частей для каждой конкретной породы удобнее сделать базовый класс “Дерево” и дать всем остальным наследоваться от него.

Почему наследование важно

Правило DRY — “Не повторяйся” — часто становится аргументом в пользу наследования. Если копировать и вставлять один и тот же код, можно легко внести ошибку в нескольких местах при изменении логики.

Рассмотрим наглядный пример: вы пишете Tamagotchi-игру. Первый питомец — белый медведь. Создаёте класс PolarBear, добавляете методы для звуков, питания, сна и передвижения.

        `    class PolarBear {
  private _weight: number = 990;

  constructor(weight: number = 0) {
    this._weight = weight
  }

  makeNoise() {
    console.log("made a roar");
  }

  eat() {
    console.log("eats whatever it wants");
  }

  sleep() {
    console.log("got a good night's sleep");
  }

  roam() {
    console.log("wandered about aimlessly");
  }
}
`
    

Затем менеджмент решает добавить всех медведей. Вы начинаете копировать класс и изменять по одному месту. Потом просят добавлять происхождение. Опять правки по всему коду. В итоге если менеджер попросит добавить ещё грызунов, обезьян или жирафов, поддерживать всё это будет тяжело.

Наследование решает эту проблему: выделите общую логику в базовый класс, а специфические детали оставьте в дочерних классах.

Наследование на практике

улыбающийся белый медведь, машущий лапой к камере

Чтобы упорядочить питомниковый код, введём родительский класс Animal, от которого унаследуются Bears, Rodents и Monkeys.

диаграмма, показывающая иерархические связи между классами

Вот фрагмент кода, который иллюстрирует идею:

        `    class Animal {
  private _weight: number;
  private _origin: string;

  constructor(weight: number = 0, origin: string = "") {
    this._weight = weight;
    this._origin = origin;
  }

  makeNoise(noise: string = "") {
    console.log("made a noise that sounded like: " + noise);
  }

  eat(food: string = "") {
    console.log("eats " + food);
  }

  sleep() {
    console.log("got a good night's sleep");
  }

  roam() {
    console.log("wandered about aimlessly");
  }
}

class Bear extends Animal {
  constructor(weight: number, origin: string) {
    super(weight, origin);
  }

  makeNoise(noise: string = "roar") {
    super.makeNoise(noise);
  }

  eat(food: string = "whatever it wants") {
    super.eat(food);
  }
}

class GrizzlyBear extends Bear {
  constructor(weight: number = 600, origin: string = "North America") {
    super(weight, origin);
  }
}

class Panda extends Bear {
  constructor(weight: number = 230, origin: string = "China") {
    super(weight, origin);
  }

  makeNoise() {
    super.makeNoise("squeek");
  }

  eat() {
    super.eat("shoots and leaves");
  }
}

`
    

В этом примере все классы наследуются от Animal. Bear расширяет Animal и задаёт стандартные реализации для звуков и питания. GrizzlyBear использует поведение по умолчанию, а Panda переопределяет методы makeNoise и eat.

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

Техника “is-a” и “has-a” — как найти отношения

Чтобы понять, должен ли класс наследовать другой, задайте вопрос: это отношение “is-a” (является ли) или “has-a” (имеет)?

  • Лемур “is-a” обезьяна. Значит, лемур может наследовать класс Monkey.
  • Кенгуру “is-a” сумчатое.
  • Лапа кролика не является кроликом: лапа — это “has-a” (свойство) кролика.

Эта техника хорошо работает в реальных моделях, но требует здравого смысла: не всё, что похоже, должно наследоваться.

носач, задумчиво смотрящий в камеру

Когда наследование не подходит (примеры и контрпримеры)

  • Когда классы похожи лишь частично — наследование может повысить связанность кода и затруднить изменения.
  • Когда вам нужна гибкая комбинация поведений (тогда лучше использовать композицию или миксины).
  • Когда поведение меняется в рантайме — наследование задаёт структуру на этапе разработки.

Контрпример: если у вас есть классы “ЛетающийКорабль” и “КрылатыйАвтомобиль”, которые разделяют способность летать и способность ехать, наследование от общего “Транспорт” может привести к раздутой иерархии. Лучше выделить интерфейс ILiftable или компонент FlightComponent и внедрять его через композицию.

Альтернативные подходы

  • Композиция: делегируйте поведение другим объектам (has-a). Это снижает связанность и упрощает тестирование.
  • Интерфейсы и абстрактные классы: задают контракт без конкретной реализации.
  • Миксины и декораторы: добавить поведение к конкретному объекту без жёсткой иерархии.

Пример простой композиции (псевдокод):

  • Класс Animal содержит ссылку на BehaviorComponent (например, EatingBehavior).
  • Для разных животных создаются разные реализации EatingBehavior.

Это позволяет менять поведение на лету и комбинировать различные способности.

Эвристики и ментальные модели

  • Правило 1: Если вы можете описать отношение словом “является”, скорее всего, это наследование (is-a).
  • Правило 2: Если один объект “имеет” другой как часть себя — это композиция (has-a).
  • Правило 3: Чистое наследование хорошо для расширения общего поведения; композиция лучше для комбинирования сильных/слабых сторон.
  • Правило 4: Часто полезно начать с композиции — при необходимости можно выделить общую абстракцию позже.

Мини-методология: как внедрять наследование в проекте

  1. Выделите повторяющийся код и ответственность.
  2. Сформулируйте поведение в терминах “is-a” или “has-a”.
  3. Создайте базовый класс с чистым интерфейсом и минимальной реализацией.
  4. Позвольте дочерним классам переопределять поведение только там, где это действительно необходимо.
  5. Пишите тесты на поведение базового класса и на отличающиеся реализации в наследниках.
  6. При росте требований рассматривайте миграцию к композиции, если наследование усложняет код.

Чеклист перед использованием наследования (для разработчика)

  • Я точно могу описать отношение “is-a” между классами?
  • Поведение, которое я выношу в базовый класс, действительно общие для всех потомков?
  • Я оставил минимально необходимую реализацию в базовом классе?
  • Есть ли тесты, покрывающие базовое поведение и специфические случаи для потомков?
  • Не проще ли внедрить поведение через компонент (composition) или интерфейс?

Диаграмма принятия решения

flowchart TD
  A[Нужно ли повторять поведение?] --> B{Это поведение описывается как 'является'?}
  B -- Да --> C[Использовать наследование]
  B -- Нет --> D[Рассмотреть композицию или интерфейсы]
  C --> E{Поведение часто меняется в рантайме?}
  E -- Да --> D
  E -- Нет --> F[Оставить наследование]

Роль-based чеклист (разработчик, архитектор, тестировщик)

  • Разработчик: определил связь is-a/has-a, написал тесты, минимизировал API базового класса.
  • Архитектор: проверил масштабируемость и влияние на поддерживаемость, предложил альтернативы (композиция).
  • Тестировщик: покрывает базовое поведение и все переопределённые случаи в наследниках.

Критерии приёмки

  • Все общие методы вынесены в базовый класс и имеют документацию.
  • Переопределения у дочерних классов ясны и ограничены по объёму.
  • Тесты проходят и демонстрируют, что полиморфизм работает (замена экземпляра родителем не ломает логику).
  • Нет дублирования логики между классами, которое было бы проще решить композицией.

Практическое задание

  • Откройте сэндбокс с TypeScript.
  • Допишите недостающие классы в примере выше.
  • Добавьте класс Monkey, а затем ProboscisMonkey, наследующий Monkey.
  • Попробуйте реализовать альтернативную версию через композицию и сравните сложность.

Что ещё полезно знать

  • Наследование — это инструмент, а не догма. Оно отлично подходит, когда иерархия естественна и стабильна.
  • Для языка TypeScript рекомендую читать официальную документацию по классам и наследованию для деталей реализации и особенностей компиляции.

Краткое резюме

Наследование помогает уменьшить дублирование кода и организовать модель объектов в виде иерархии. Применяйте технику “is-a/has-a” для выбора между наследованием и композицией. Всегда пишите тесты и задокументируйте решения об архитектуре — это сэкономит время при дальнейшем расширении функциональности.

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

Дополнительные шаги: реализуйте пример в своём проекте, сравните производительность поддержки и сложность изменений при расширении набора классов.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Google Voice как VoIP‑телефон по Wi‑Fi
Связь

Google Voice как VoIP‑телефон по Wi‑Fi

Настройки и управление Windows 10
Windows 10

Настройки и управление Windows 10

Famjama vs Cozi: Коротко о лучших семейных органайзерах
Семейная организация

Famjama vs Cozi: Коротко о лучших семейных органайзерах

Как научиться программировать — руководство для начинающих
Программирование

Как научиться программировать — руководство для начинающих

Unexpected Store Exception в Windows 10/11 — как устранить
Windows

Unexpected Store Exception в Windows 10/11 — как устранить

Управление торрентом с телефона
Сети

Управление торрентом с телефона