Программа курса← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF

Наследование

Введение

Классы редко живут поодиночке. Стоит написать несколько, и обнаруживается, что половина кода в них одинаковая: у сотрудника, клиента и курьера есть имя и телефон, у всех документов – автор и дата создания, у всех фигур на холсте – координаты и метод draw. Копировать это в каждый класс – значит потом искать по всему проекту, где ещё лежит та же логика.

Отсюда естественное желание – выстроить иерархию: описать общее один раз, а различия добавить сверху. В интернет-магазине это Товар, от которого идут Книга, Одежда и Электроника: цена, название и скидка описаны в одном месте, а размер, автор и гарантия – каждый у своего вида товара. Так и появляется наследование.

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

Класс, от которого наследуют, называют родителем (родительский, базовый класс), а тот, который наследует, – ребёнком (дочерний класс, наследник). В примере с магазином Товар – родитель, а Книга, Одежда и Электроника – его дети.

  • Автомобиль: ехать, открывать двери, включать фары.
  • Грузовик: добавляет загрузку щебня, отцепку прицепа, но наследует всё от Автомобиля.

Пример

using System;

public class Vehicle
{
    public int Weight { get; set; }
    public int Length { get; set; }
    public bool IsHeadlightsOn { get; set; }

    public Vehicle()
    {
        Weight = 100;
        Length = 120;
        IsHeadlightsOn = false;
    }

    public void TurnOnHeadlights()
    {
        IsHeadlightsOn = true;
    }
}

public class Truck : Vehicle
{
    public bool IsLoaded { get; set; }

    public Truck()
    {
        IsLoaded = true;
    }

    public void Unload()
    {
        IsLoaded = false;
    }
}

class Program
{
    static void Main()
    {
        Truck car = new Truck();

        // Метод описан в Vehicle, но грузовик тоже умеет включать фары
        car.TurnOnHeadlights();
        Console.WriteLine(car.IsHeadlightsOn);

        // А это уже собственный метод грузовика
        car.Unload();
        Console.WriteLine(car.IsLoaded);
    }
}
#include <iostream>

class Vehicle {
public:
    int weight = 100;
    int length = 120;
    bool is_headlights_on = false;

    void TurnOnHeadlights() { is_headlights_on = true; }
};

// Наследование в C++ бывает public, protected и private –
// public означает отношение «является»
class Truck : public Vehicle {
public:
    bool is_loaded = true;

    void Unload() { is_loaded = false; }
};

int main() {
    Truck car;

    // Метод описан в Vehicle, но грузовик тоже умеет включать фары
    car.TurnOnHeadlights();
    std::cout << car.is_headlights_on << std::endl;

    // А это уже собственный метод грузовика
    car.Unload();
    std::cout << car.is_loaded << std::endl;

    return 0;
}
package main

import "fmt"

// Наследования в Go нет – вместо него встраивание.
// Truck получает поля и методы Vehicle, но это композиция, а не иерархия.
type Vehicle struct {
	Weight         int
	Length         int
	IsHeadlightsOn bool
}

func NewVehicle() Vehicle {
	return Vehicle{Weight: 100, Length: 120}
}

func (v *Vehicle) TurnOnHeadlights() {
	v.IsHeadlightsOn = true
}

type Truck struct {
	Vehicle
	IsLoaded bool
}

func NewTruck() *Truck {
	return &Truck{Vehicle: NewVehicle(), IsLoaded: true}
}

func (t *Truck) Unload() {
	t.IsLoaded = false
}

func main() {
	car := NewTruck()

	// Метод описан у Vehicle, но вызывается на грузовике: встраивание
	// поднимает его наверх, как если бы он был объявлен у Truck
	car.TurnOnHeadlights()
	fmt.Println(car.IsHeadlightsOn)

	// А это уже собственный метод грузовика
	car.Unload()
	fmt.Println(car.IsLoaded)
}
class Vehicle:
    def __init__(self):
        self.weight = 100
        self.length = 120
        self.is_headlights_on = False

    def turn_on_headlights(self):
        self.is_headlights_on = True

class Truck(Vehicle):
    def __init__(self):
        super().__init__()
        self.is_loaded = True

    def unload(self):
        self.is_loaded = False

if __name__ == "__main__":
    car = Truck()

    # Метод описан в Vehicle, но грузовик тоже умеет включать фары
    car.turn_on_headlights()
    print(car.is_headlights_on)

    # А это уже собственный метод грузовика
    car.unload()
    print(car.is_loaded)
class Vehicle {
    weight = 100;
    length = 120;
    isHeadlightsOn = false;

    turnOnHeadlights(): void {
        this.isHeadlightsOn = true;
    }
}

class Truck extends Vehicle {
    isLoaded = true;

    unload(): void {
        this.isLoaded = false;
    }
}

const car = new Truck();

// Метод описан в Vehicle, но грузовик тоже умеет включать фары
car.turnOnHeadlights();
console.log(car.isHeadlightsOn);

// А это уже собственный метод грузовика
car.unload();
console.log(car.isLoaded);
public class Vehicle {
    private int weight;
    private int length;
    private boolean isHeadlightsOn;

    public Vehicle() {
        weight = 100;
        length = 120;
        isHeadlightsOn = false;
    }

    public boolean isHeadlightsOn() {
        return isHeadlightsOn;
    }

    public void turnOnHeadlights() {
        isHeadlightsOn = true;
    }
}

public class Truck extends Vehicle {
    private boolean isLoaded;

    public Truck() {
        isLoaded = true;
    }

    public boolean isLoaded() {
        return isLoaded;
    }

    public void unload() {
        isLoaded = false;
    }
}

public class Program {
    public static void main(String[] args) {
        Truck car = new Truck();

        // Метод описан в Vehicle, но грузовик тоже умеет включать фары
        car.turnOnHeadlights();
        System.out.println(car.isHeadlightsOn());

        // А это уже собственный метод грузовика
        car.unload();
        System.out.println(car.isLoaded());
    }
}
// В Kotlin классы закрыты для наследования по умолчанию:
// чтобы от Vehicle можно было наследоваться, нужен модификатор open.
open class Vehicle {
    var weight = 100
    var length = 120
    var isHeadlightsOn = false

    fun turnOnHeadlights() {
        isHeadlightsOn = true
    }
}

class Truck : Vehicle() {
    var isLoaded = true

    fun unload() {
        isLoaded = false
    }
}

fun main() {
    val car = Truck()

    // Метод описан в Vehicle, но грузовик тоже умеет включать фары
    car.turnOnHeadlights()
    println(car.isHeadlightsOn)

    // А это уже собственный метод грузовика
    car.unload()
    println(car.isLoaded)
}

Что даёт наследование

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

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

Звучит так, будто механизм безусловно хорош. Но посмотрите на пример выше ещё раз: в Go наследования нет вовсе – там встраивание, то есть композиция. И Go тут не исключение, языки помоложе от наследования классов отказываются один за другим. Почему настолько удобная на вид вещь оказалась настолько спорной – в следующем уроке.