Skip to content
IRC-CodingIRC-Coding
ООПНаследованиеInheritanceПолиморфизмLiskovКомпозицияOverrideInterfaceЯзык программирования

Основы Inheritance и Polymorphism в OOP

Наследование, полиморфизм, Liskov Substitution и композиция. Примеры на Java для отношений является-элементом.

S

schutzgeist

13 min read
Основы Inheritance и Polymorphism в OOP

Основы OOP наследования: Inheritance и Polymorphie

Наследование (Inheritance) — это центральное понятие объектно-ориентированного программирования, которое позволяет определить общие свойства и поведение в базовом классе и переиспользовать их в производных классах.

Что такое наследование?

Наследование описывает отношение “является” (is-a) между типами: производный класс наследует все публичные и защищённые члены базового класса и может их расширять или переопределять.

Ключевые концепции наследования

  • Отношение “является”: производный класс — это специализация базового класса
  • Переиспользование кода: общее поведение определяется один раз в центральном месте
  • Полиморфизм: объекты можно использовать как объекты базового типа
  • Расширяемость: новую функциональность легко добавлять в существующие иерархии

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

Простая иерархия наследования

// Абстрактный базовый класс
public abstract class Shape {
    protected String color;
    protected double x, y; // Position
    
    public Shape(String color, double x, double y) {
        this.color = Objects.requireNonNull(color);
        this.x = x;
        this.y = y;
    }
    
    // Абстрактные методы — должны быть реализованы
    public abstract double area();
    public abstract double perimeter();
    
    // Конкретный метод — может быть переопределён
    public void move(double dx, double dy) {
        this.x += dx;
        this.y += dy;
        System.out.println("Form verschoben nach (" + x + ", " + y + ")");
    }
    
    // Getters
    public String getColor() { return color; }
    public double getX() { return x; }
    public double getY() { return y; }
    
    @Override
    public String toString() {
        return "Shape{color='" + color + "', x=" + x + ", y=" + y + "}";
    }
}

// Конкретный производный класс
public class Rectangle extends Shape {
    private double width, height;
    
    public Rectangle(String color, double x, double y, double width, double height) {
        super(color, x, y); // Вызов конструктора базового класса
        setDimensions(width, height);
    }
    
    @Override
    public double area() {
        return width * height;
    }
    
    @Override
    public double perimeter() {
        return 2 * (width + height);
    }
    
    @Override
    public void move(double dx, double dy) {
        super.move(dx, dy); // Вызов метода базового класса
        System.out.println("Rechteck bewegt");
    }
    
    // Дополнительные методы
    public void setDimensions(double width, double height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("Maße müssen positiv sein");
        }
        this.width = width;
        this.height = height;
    }
    
    public double getWidth() { return width; }
    public double getHeight() { return height; }
    
    @Override
    public String toString() {
        return "Rectangle{" + super.toString() + 
               ", width=" + width + ", height=" + height + "}";
    }
}

// Ещё один производный класс
public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double x, double y, double radius) {
        super(color, x, y);
        setRadius(radius);
    }
    
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
    
    public void setRadius(double radius) {
        if (radius <= 0) {
            throw new IllegalArgumentException("Radius muss positiv sein");
        }
        this.radius = radius;
    }
    
    public double getRadius() { return radius; }
    
    @Override
    public String toString() {
        return "Circle{" + super.toString() + ", radius=" + radius + "}";
    }
}

Полиморфное использование

public class ShapeDemo {
    public static void main(String[] args) {
        // Полиморфные ссылки
        List<Shape> shapes = new ArrayList<>();
        
        shapes.add(new Rectangle("rot", 0, 0, 5, 3));
        shapes.add(new Circle("blau", 10, 10, 2));
        shapes.add(new Rectangle("grün", 5, 5, 2, 2));
        
        // Полиморфная обработка
        double totalArea = 0;
        for (Shape shape : shapes) {
            System.out.println(shape);
            System.out.println("Fläche: " + shape.area());
            System.out.println("Umfang: " + shape.perimeter());
            totalArea += shape.area();
            System.out.println("---");
        }
        
        System.out.println("Gesamtfläche: " + totalArea);
        
        // Динамические вызовы методов
        processShapes(shapes);
    }
    
    public static void processShapes(List<Shape> shapes) {
        for (Shape shape : shapes) {
            // Динамическая диспетчеризация — в зависимости от типа объекта
            shape.move(1, 1);
            
            // Проверка типа и приведение
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle) shape;
                System.out.println("Rechteck: " + rect.getWidth() + "x" + rect.getHeight());
            } else if (shape instanceof Circle) {
                Circle circle = (Circle) shape;
                System.out.println("Kreis mit Radius: " + circle.getRadius());
            }
        }
    }
}

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

Наследование и интерфейсы в C#

// Интерфейс для определения контракта
public interface IShape
{
    string Color { get; }
    double Area { get; }
    double Perimeter { get; }
    void Move(double dx, double dy);
}

// Абстрактный базовый класс
public abstract class Shape : IShape
{
    protected string Color { get; set; }
    protected double X { get; set; }
    protected double Y { get; set; }
    
    protected Shape(string color, double x, double y)
    {
        Color = color ?? throw new ArgumentNullException(nameof(color));
        X = x;
        Y = y;
    }
    
    // Реализация интерфейса
    public string GetColor() => Color;
    
    // Абстрактные свойства
    public abstract double Area { get; }
    public abstract double Perimeter { get; }
    
    // Виртуальный метод — может быть переопределён
    public virtual void Move(double dx, double dy)
    {
        X += dx;
        Y += dy;
        Console.WriteLine($"Form verschoben nach ({X}, {Y})");
    }
    
    public override string ToString()
    {
        return $"Shape{{Color='{Color}', X={X}, Y={Y}}}";
    }
}

// Конкретный класс
public class Rectangle : Shape
{
    public double Width { get; private set; }
    public double Height { get; private set; }
    
    public Rectangle(string color, double x, double y, double width, double height) 
        : base(color, x, y)
    {
        SetDimensions(width, height);
    }
    
    public override double Area => Width * Height;
    public override double Perimeter => 2 * (Width + Height);
    
    public override void Move(double dx, double dy)
    {
        base.Move(dx, dy);
        Console.WriteLine("Rechteck bewegt");
    }
    
    public void SetDimensions(double width, double height)
    {
        if (width <= 0 || height <= 0)
            throw new ArgumentException("Maße müssen positiv sein");
        
        Width = width;
        Height = height;
    }
    
    public override string ToString()
    {
        return $"Rectangle{{{base.ToString()}, Width={Width}, Height={Height}}}";
    }
}

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

from abc import ABC, abstractmethod
from typing import List

# Абстрактный базовый класс
class Shape(ABC):
    def __init__(self, color: str, x: float, y: float):
        if not color:
            raise ValueError("Color дarf nicht leer sein")
        self.color = color
        self.x = x
        self.y = y
    
    @abstractmethod
    def area(self) -> float:
        """Berechnet die Fläche der Form"""
        pass
    
    @abstractmethod
    def perimeter(self) -> float:
        """Berechnet den Umfang der Form"""
        pass
    
    def move(self, dx: float, dy: float):
        """Verschiebt die Form"""
        self.x += dx
        self.y += dy
        print(f"Form verschoben nach ({self.x}, {self.y})")
    
    def __str__(self):
        return f"Shape{{color='{self.color}', x={self.x}, y={self.y}}}"

# Конкретный подкласс
class Rectangle(Shape):
    def __init__(self, color: str, x: float, y: float, width: float, height: float):
        super().__init__(color, x, y)  # Aufruf des Basisklassen-Konstruktors
        self.set_dimensions(width, height)
    
    def area(self) -> float:
        return self.width * self.height
    
    def perimeter(self) -> float:
        return 2 * (self.width + self.height)
    
    def move(self, dx: float, dy: float):
        super().move(dx, dy)  # Basismethode aufrufen
        print("Rechteck bewegt")
    
    def set_dimensions(self, width: float, height: float):
        if width <= 0 or height <= 0:
            raise ValueError("Maße müssen positiv sein")
        self.width = width
        self.height = height
    
    def __str__(self):
        return f"Rectangle{{{super().__str__()}, width={self.width}, height={self.height}}}"

# Еще один подкласс
class Circle(Shape):
    def __init__(self, color: str, x: float, y: float, radius: float):
        super().__init__(color, x, y)
        self.set_radius(radius)
    
    def area(self) -> float:
        return math.pi * self.radius ** 2
    
    def perimeter(self) -> float:
        return 2 * math.pi * self.radius
    
    def set_radius(self, radius: float):
        if radius <= 0:
            raise ValueError("Radius muss positiv sein")
        self.radius = radius
    
    def __str__(self):
        return f"Circle{{{super().__str__()}, radius={self.radius}}}"

# Полиморфное использование
def process_shapes(shapes: List[Shape]):
    total_area = 0
    for shape in shapes:
        print(shape)
        print(f"Fläche: {shape.area():.2f}")
        print(f"Umfang: {shape.perimeter():.2f}")
        total_area += shape.area()
        
        # Type Checking mit isinstance
        if isinstance(shape, Rectangle):
            print(f"Rechteck: {shape.width}x{shape.height}")
        elif isinstance(shape, Circle):
            print(f"Kreis mit Radius: {shape.radius}")
        
        print("---")
    
    print(f"Gesamtfläche: {total_area:.2f}")

# Verwendung
shapes = [
    Rectangle("rot", 0, 0, 5, 3),
    Circle("blau", 10, 10, 2),
    Rectangle("grün", 5, 5, 2, 2)
]

process_shapes(shapes)

Liskov Substitution Principle (LSP)

Понимание принципа

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

Пример нарушения LSP

// Плохой дизайн - нарушает LSP
public class Rectangle {
    protected double width, height;
    
    public void setWidth(double width) {
        this.width = width;
    }
    
    public void setHeight(double height) {
        this.height = height;
    }
    
    public double getWidth() { return width; }
    public double getHeight() { return height; }
    
    public double area() {
        return width * height;
    }
}

// Проблемный подкласс
public class Square extends Rectangle {
    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setHeight(width); // Квадрат должен иметь равные стороны
    }
    
    @Override
    public void setHeight(double height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

// Нарушение LSP на практике
public void testRectangle(Rectangle rect) {
    rect.setWidth(5);
    rect.setHeight(4);
    // Ожидание: area() == 20
    // При Square: area() == 16 (неожиданно!)
    assert rect.area() == 20 : "LSP verletzt!";
}

Конформный LSP дизайн

// Лучше: абстрактный базовый класс
public abstract class Shape {
    public abstract double area();
    public abstract double perimeter();
}

// Rectangle как самостоятельный класс
public class Rectangle extends Shape {
    private double width, height;
    
    public Rectangle(double width, double height) {
        setDimensions(width, height);
    }
    
    public void setDimensions(double width, double height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("Maße müssen positiv sein");
        }
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double area() {
        return width * height;
    }
    
    @Override
    public double perimeter() {
        return 2 * (width + height);
    }
    
    public double getWidth() { return width; }
    public double getHeight() { return height; }
}

// Square как самостоятельный класс
public class Square extends Shape {
    private double side;
    
    public Square(double side) {
        setSide(side);
    }
    
    public void setSide(double side) {
        if (side <= 0) {
            throw new IllegalArgumentException("Seite muss positiv sein");
        }
        this.side = side;
    }
    
    @Override
    public double area() {
        return side * side;
    }
    
    @Override
    public double perimeter() {
        return 4 * side;
    }
    
    public double getSide() { return side; }
}

Композиция вместо наследования

Проблема жестких иерархий

// Плохо: глубокая иерархия наследования
public class Animal {
    public void eat() { System.out.println("Eating"); }
}

public class Mammal extends Animal {
    public void walk() { System.out.println("Walking"); }
}

public class Dog extends Mammal {
    public void bark() { System.out.println("Barking"); }
}

public class RobotDog extends Dog {
    // Проблема: RobotDog не является настоящим животным!
    @Override
    public void eat() { 
        throw new UnsupportedOperationException("Robots don't eat"); 
    }
}

Лучше: композиция

// Интерфейсы для поведения
public interface Eater {
    void eat();
}

public interface Walker {
    void walk();
}

public interface Barker {
    void bark();
}

// Базовый класс с основным поведением
public class Animal implements Eater {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    @Override
    public void eat() {
        System.out.println(name + " is eating");
    }
}

// Композиция для поведений
public class Dog implements Walker, Barker {
    private Animal animal;
    
    public Dog(String name) {
        this.animal = new Animal(name);
    }
    
    @Override
    public void walk() {
        System.out.println(animal.name + " is walking");
    }
    
    @Override
    public void bark() {
        System.out.println(animal.name + " says: Woof!");
    }
    
    // Делегирование к Animal
    public void eat() {
        animal.eat();
    }
}

// Более гибко для RobotDog
public class RobotDog implements Walker, Barker {
    private String name;
    private int batteryLevel;
    
    public RobotDog(String name) {
        this.name = name;
        this.batteryLevel = 100;
    }
    
    @Override
    public void walk() {
        if (batteryLevel > 10) {
            System.out.println(name + " is walking on wheels");
            batteryLevel -= 5;
        } else {
            System.out.println(name + " needs charging");
        }
    }
    
    @Override
    public void bark() {
        System.out.println(name + " says: Electronic Woof!");
    }
    
    public void charge() {
        batteryLevel = 100;
        System.out.println(name + " is fully charged");
    }
}

Абстрактные классы против интерфейсов

Абстрактный класс

// Абстрактный класс с общей реализацией
public abstract class Vehicle {
    protected String brand;
    protected int year;
    
    public Vehicle(String brand, int year) {
        this.brand = brand;
        this.year = year;
    }
    
    // Конкретный метод
    public void startEngine() {
        System.out.println("Engine starting...");
    }
    
    // Абстрактные методы
    public abstract void accelerate();
    public abstract void brake();
    
    // Template Method Pattern
    public final void drive() {
        startEngine();
        accelerate();
        System.out.println("Driving...");
        brake();
    }
    
    // Геттеры
    public String getBrand() { return brand; }
    public int getYear() { return year; }
}

public class Car extends Vehicle {
    public Car(String brand, int year) {
        super(brand, year);
    }
    
    @Override
    public void accelerate() {
        System.out.println("Car accelerating");
    }
    
    @Override
    public void brake() {
        System.out.println("Car braking");
    }
}

Интерфейс

// Интерфейс для поведения
public interface Electric {
    void charge();
    int getBatteryLevel();
}

public interface Autonomous {
    void enableAutopilot();
    boolean isAutopilotActive();
}

// Класс реализует несколько интерфейсов
public class Tesla extends Vehicle implements Electric, Autonomous {
    private int batteryLevel = 80;
    private boolean autopilotActive = false;
    
    public Tesla(String brand, int year) {
        super(brand, year);
    }
    
    @Override
    public void accelerate() {
        System.out.println("Tesla accelerating silently");
    }
    
    @Override
    public void brake() {
        System.out.println("Tesla regenerative braking");
    }
    
    // Реализация интерфейсов
    @Override
    public void charge() {
        System.out.println("Tesla charging...");
        batteryLevel = 100;
    }
    
    @Override
    public int getBatteryLevel() {
        return batteryLevel;
    }
    
    @Override
    public void enableAutopilot() {
        autopilotActive = true;
        System.out.println("Autopilot enabled");
    }
    
    @Override
    public boolean isAutopilotActive() {
        return autopilotActive;
    }
}

Проблема ромба

Множественное наследование в C++

#include <iostream>

class Animal {
public:
    void eat() { std::cout << "Animal eating" << std::endl; }
};

class Mammal : virtual public Animal {
public:
    void walk() { std::cout << "Mammal walking" << std::endl; }
};

class Bird : virtual public Animal {
public:
    void fly() { std::cout << "Bird flying" << std::endl; }
};

// Проблема ромба решена виртуальным наследованием
class Bat : public Mammal, public Bird {
public:
    void echolocate() { std::cout << "Bat echolocating" << std::endl; }
};

int main() {
    Bat bat;
    bat.eat();        // Однозначно благодаря виртуальному наследованию
    bat.walk();       // Из Mammal
    bat.fly();        // Из Bird
    bat.echolocate(); // Собственный метод
    return 0;
}

Решение на основе интерфейсов в Java/C#

// Интерфейс для способностей
public interface Flyable {
    void fly();
}

public interface Walkable {
    void walk();
}

// Базовый класс
public abstract class Animal {
    public abstract void eat();
}

// Класс реализует несколько интерфейсов
public class Bat extends Animal implements Flyable, Walkable {
    @Override
    public void eat() {
        System.out.println("Bat eating insects");
    }
    
    @Override
    public void fly() {
        System.out.println("Bat flying");
    }
    
    @Override
    public void walk() {
        System.out.println("Bat walking");
    }
    
    public void echolocate() {
        System.out.println("Bat echolocating");
    }
}

Лучшие практики для наследования

1. Избегайте глубоких иерархий

// Плохо: слишком глубоко
class Animal -> Mammal -> Dog -> Labrador -> GoldenRetriever

// Лучше: более плоская структура
class Animal -> Dog
class Dog -> Labrador
class Dog -> GoldenRetriever

2. Используйте final для стабильных классов

public final class ImmutablePoint {
    private final double x, y;
    
    public ImmutablePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }
    
    // Не может быть унаследован - гарантирует стабильность
}

3. Template Method Pattern

public abstract class DataProcessor {
    
    // Template Method, определяющий последовательность
    public final void processData() {
        loadData();
        validateData();
        transformData();
        saveData();
        cleanup();
    }
    
    // Конкретные методы
    private void loadData() {
        System.out.println("Loading data...");
    }
    
    private void cleanup() {
        System.out.println("Cleaning up...");
    }
    
    // Абстрактные методы, реализуемые подклассами
    protected abstract void validateData();
    protected abstract void transformData();
    protected abstract void saveData();
}

public class CSVProcessor extends DataProcessor {
    @Override
    protected void validateData() {
        System.out.println("Validating CSV data");
    }
    
    @Override
    protected void transformData() {
        System.out.println("Transforming CSV data");
    }
    
    @Override
    protected void saveData() {
        System.out.println("Saving CSV data");
    }
}

Концепции для экзамена

Важные различия

  1. Переопределение против перегрузки

    • Переопределение: одна и та же сигнатура в подклассе
    • Перегрузка: одно имя, разные параметры
  2. Абстрактный класс против интерфейса

    • Абстрактный класс: общая реализация
    • Интерфейс: только контракт
  3. Отношение “является” против “имеет”

    • Является: наследование
    • Имеет: композиция
  4. Ковариантность против контравариантности

    • Ковариантность: типы возврата могут быть более специфичными
    • Контравариантность: типы параметров могут быть более общими

Типичные экзаменационные задачи

// Пример полиморфизма
public class PolymorphismDemo {
    public static void main(String[] args) {
        Shape[] shapes = {
            new Rectangle("красный", 0, 0, 5, 3),
            new Circle("синий", 10, 10, 2)
        };
        
        for (Shape shape : shapes) {
            // Динамическая диспетчеризация
            System.out.println(shape.area());
            
            // Type Casting
            if (shape instanceof Rectangle) {
                Rectangle rect = (Rectangle) shape;
                System.out.println("Width: " + rect.getWidth());
            }
        }
    }
}

Заключение

Наследование - это мощный инструмент, но требует осторожного применения:

  • Используйте наследование для настоящих отношений “является”
  • Соблюдайте принцип подстановки Лисков
  • Предпочитайте композицию при простом переиспользовании кода
  • Держите иерархии плоскими и стабильными
  • Используйте интерфейсы для гибких контрактов
  • Применяйте абстрактные классы для общей реализации

Хорошее наследование способствует переиспользованию и полиморфизму, а плохое наследование приводит к жесткой связанности и хрупким архитектурам.

Продолжение пути обучения OOP

Все статьи по OOP теперь полностью готовы. Вернитесь к первой статье: Основы объектно-ориентированного программирования OOP.

Назад к блогу
Share:

Похожие статьи