Композиция против наследования
Часть раздела Объектно-ориентированное программирование путешествия по 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) и подкласс действительно представляет собой специализированную версию родительского класса.
Используйте композицию, когда вам нужно использовать функциональность другого класса, не являясь этим типом. Многие опытные разработчики следуют принципу: "предпочитайте композицию наследованию", потому что это приводит к более гибкому и поддерживаемому коду.
Задание
ЛегкоДавайте создадим компьютерную систему, используя композицию, чтобы продемонстрировать отношение "has-a" (содержит). Вместо того чтобы класс Computer наследовался от своих компонентов (что было бы бессмысленно), ваш компьютер будет содержать части, необходимые ему для работы.
Вы организуете свой код в трех файлах:
Processor.java: Создайте класс, представляющий процессор. У Processor есть два приватных поля:brand(String) иspeedGHz(double). Включите конструктор для инициализации обоих полей и геттеры для каждого из них. Добавьте методprocess(), который возвращает String:[brand] processing at [speedGHz] GHzMemory.java: Создайте класс, представляющий оперативную память. У Memory есть два приватных поля:type(String, например, "DDR4" или "DDR5") иsizeGB(int). Включите конструктор, геттеры и методload(), который возвращает:Loading [sizeGB]GB [type] memoryComputer.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()
}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыВведение в ООПКлассы и объектыКлючевое слово thisМетодыПоля (атрибуты)Метод-конструкторПерегрузка конструкторовИтоги — Простой калькулятор4Наследование
Основы наследования (extends)Ключевое слово superПереопределение методов (@Override)Цепочка конструкторовКласс ObjectОдиночное и многоуровневое наследованиеПочему нет множественного наследования классовИтоги — Иерархия сотрудников7Специальные методы и класс Object
Метод toString()equals() и hashCode()Метод clone()compareTo() и ComparableИнтерфейс ComparatorПовторение — Кастомная сортировка2Модификаторы доступа и инкапсуляция
Обзор уровней доступаМетоды геттеры и сеттерыСокрытие данныхКлючевое слово finalИтоги — Менеджер банковского счета5Полиморфизм
Основы перегрузки методовПереопределение методов (Run-Time)Upcasting и DowncastingОператор instanceofАбстрактные классы и методыИтоги: Калькулятор фигур8Продвинутые концепции ООП
Композиция против наследованияАгрегация против композицииВнутренние, вложенные и анонимные классыEnums и методы перечисленийRecords (Java 16+)Sealed-классы (Java 17+)11Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy3Свойства класса и статические члены
Переменные экземпляра и статические переменныеСтатические методыСтатические блокиКонстанты (static final)Итоги: Счетчик и утилиты6Интерфейсы и абстрактные классы
Введение в интерфейсыРеализация интерфейсовРеализация нескольких интерфейсовDefault и Static методы в интерфейсахАбстрактные классы vs ИнтерфейсыФункциональные интерфейсыИтоги — Платежная система9Обобщения
Введение в обобщенияОбобщенные классыОбобщенные методыОграниченные параметры типаСимволы подстановки (Wildcards: ?, extends, super)Итоги — Обобщенный контейнер12Паттерны проектирования. Часть 2
Паттерн «Команда»Паттерн «Адаптер»Паттерн «Декоратор»Паттерн «Шаблонный метод»Паттерн «Состояние»Паттерн «Компоновщик»Паттерн «Итератор»