Menu
Coddy logo textTech

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

Часть раздела Объектно-ориентированное программирование путешествия по Java на Coddy — урок 47 из 87.

Наследование создает связь «is-a» (является): Dog является Animal. Но что, если классу нужна функциональность от чего-то, чем он не является? Здесь на помощь приходит композиция, устанавливающая вместо этого связь «has-a» (содержит).

При использовании композиции класс содержит экземпляры других классов в качестве полей, а не расширяет их:

// Подход с наследованием: Car ЯВЛЯЕТСЯ Engine (не имеет смысла)
class Car extends Engine { }

// Подход с композицией: Car СОДЕРЖИТ Engine (имеет смысл)
class Car {
    private Engine engine;
    
    public Car(Engine engine) {
        this.engine = engine;
    }
    
    public void start() {
        engine.ignite();
    }
}

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

Вы также можете комбинировать поведение нескольких классов, что невозможно при наследовании в Java. Составной класс предоставляет только те методы, которые он выбирает, обеспечивая лучшую инкапсуляцию.

Практическая рекомендация: используйте наследование, когда существует подлинная связь «является» (is-a) и подкласс действительно представляет собой специализированную версию родительского класса.

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

challenge icon

Задание

Легко

Давайте создадим компьютерную систему, используя композицию, чтобы продемонстрировать отношение "has-a" (содержит). Вместо того чтобы класс Computer наследовался от своих компонентов (что было бы бессмысленно), ваш компьютер будет содержать части, необходимые ему для работы.

Вы организуете свой код в трех файлах:

  • Processor.java: Создайте класс, представляющий процессор. У Processor есть два приватных поля: brand (String) и speedGHz (double). Включите конструктор для инициализации обоих полей и геттеры для каждого из них. Добавьте метод process(), который возвращает String: [brand] processing at [speedGHz] GHz
  • Memory.java: Создайте класс, представляющий оперативную память. У Memory есть два приватных поля: type (String, например, "DDR4" или "DDR5") и sizeGB (int). Включите конструктор, геттеры и метод load(), который возвращает: Loading [sizeGB]GB [type] memory
  • Computer.java: Вот где композиция проявляет себя во всей красе! Ваш класс Computer должен иметь Processor и Memory в качестве приватных полей — он не расширяет их, он их содержит. Включите конструктор, который принимает оба компонента. Добавьте метод boot(), который возвращает многострочную строку, показывающую запуск компьютера с использованием его компонентов:
    Booting computer...
    [processor.process() result]
    [memory.load() result]
    System ready!
    Также добавьте метод getSpecs(), который возвращает: Specs: [processor brand] CPU, [memory sizeGB]GB [memory type]
  • Main.java: Оживите вашу составную систему! Вы получите четыре входных значения: марка процессора (String), скорость процессора (double), тип памяти (String) и размер памяти (int).

    Создайте Processor и Memory с этими значениями, затем объедините их в Computer. Выведите результат вызова boot(), а затем выведите результат getSpecs().

Вы получите четыре входных значения по порядку: марка процессора, скорость процессора (GHz), тип памяти и размер памяти (GB).

Обратите внимание, как Computer делегирует работу своим компонентам, а не пытается быть ими — в этом и заключается сила композиции! Computer может использовать любой Processor или Memory, которые вы ему предоставите, что делает дизайн гибким и реалистичным.

Шпаргалка

Композиция создает отношение «имеет» (has-a), при котором класс содержит экземпляры других классов в качестве полей, а не расширяет их.

Наследование против композиции:

// Наследование: Car ЯВЛЯЕТСЯ Engine (не имеет смысла)
class Car extends Engine { }

// Композиция: Car ИМЕЕТ Engine (имеет смысл)
class Car {
    private Engine engine;
    
    public Car(Engine engine) {
        this.engine = engine;
    }
    
    public void start() {
        engine.ignite();
    }
}

Преимущества композиции:

  • Объекты можно изменять во время выполнения
  • Можно комбинировать поведение нескольких классов
  • Лучшая инкапсуляция — предоставляются только выбранные методы
  • Более гибкий и поддерживаемый код

Когда использовать:

  • Наследование: используйте, когда существует подлинная связь «является» (is-a) и подкласс является специализированной версией родительского класса
  • Композиция: используйте, когда вам нужна функциональность другого класса, не являясь при этом его типом

Лучшая практика: «Предпочитайте композицию наследованию»

Попробуйте сами

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        // Чтение входных данных
        String processorBrand = scanner.nextLine();
        double processorSpeed = Double.parseDouble(scanner.nextLine());
        String memoryType = scanner.nextLine();
        int memorySize = Integer.parseInt(scanner.nextLine());
        
        // TODO: Создать Processor с брендом и скоростью
        
        // TODO: Создать Memory с типом и размером
        
        // TODO: Создать Computer, скомпоновав Processor и Memory
        
        // TODO: Вывести результат boot()
        
        // TODO: Вывести результат getSpecs()
    }
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование