Композиция против наследования
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 76 из 104.
При проектировании отношений между классами у вас есть два основных варианта: наследование ("is-a") и композиция ("has-a"). Выбор правильного подхода существенно влияет на гибкость и удобство сопровождения вашего кода.
Наследование создает сильную связанность между классами. Класс Car, который наследуется от Vehicle, навсегда привязан к этой связи:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};Композиция встраивает объекты как члены, создавая более гибкую связь. Car содержит Engine, а не является двигателем:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // Car СОДЕРЖИТ Engine
public:
void start() { engine.start(); }
};Ключевым преимуществом композиции является гибкость. Вы можете легко заменять компоненты, изменять поведение во время выполнения или комбинировать несколько возможностей без ограничений жесткой иерархии классов. Современное проектирование на C++ в большинстве случаев отдает предпочтение композиции перед наследованием.
Используйте наследование, когда существует подлинная связь «является» (is-a) и вам необходим полиморфизм. Используйте композицию, когда вы хотите повторно использовать функциональность или когда отношения лучше описываются как «имеет» (has-a) или «использует» (uses-a).
Задание
ЛегкоДавайте создадим симулятор компьютерной системы, который демонстрирует, когда использовать композицию вместо наследования. Вы смоделируете компьютер, который имеет компоненты (композиция), а не является компонентом, показывая, как композиция обеспечивает гибкость для замены деталей во время выполнения.
Вы организуете свой код в трех файлах:
Components.h: Определите отдельные компоненты, которые может содержать компьютер.Создайте класс
CPUс приватными полямиstd::string modelиint cores. Реализуйте конструктор, принимающий оба значения, методprocess(), который выводитCPU [model] processing with [cores] cores, и методgetModel().Создайте класс
Memoryс приватным полемint sizeGB. Реализуйте конструктор, методload(), который выводитLoading data into [sizeGB]GB RAM, и методgetSize().Создайте класс
Storageс приватными полямиstd::string type(например, "SSD" или "HDD") иint capacityGB. Реализуйте конструктор, методread(), который выводитReading from [capacityGB]GB [type], и геттеры для обоих полей.Computer.h: Определите классComputer, который использует композицию для объединения компонентов.Ваш
Computerдолжен иметь приватные объекты-члены:CPU,MemoryиStorage. Используйте список инициализации конструктора для инициализации этих компонентов из параметров.Реализуйте следующие методы:
boot()— выводитBooting computer..., затем вызываетload()у памяти,read()у хранилища иprocess()у процессора, каждый вызов на новой строке.specs()— выводит технические характеристики компьютера в следующем формате:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: Считайте шесть входных значений (каждое на отдельной строке):- Название модели процессора (string)
- Количество ядер процессора (integer)
- Объем оперативной памяти в ГБ (integer)
- Тип хранилища (string, например, "SSD")
- Емкость хранилища в ГБ (integer)
- Команда: либо "boot", либо "specs"
Создайте объекты компонентов, затем создайте объект
Computer, используя композицию. В зависимости от команды вызовите либоboot(), либоspecs().
Например, при входных данных Intel i7, 8, 16, SSD, 512 и boot:
Booting computer...
Loading data into 16GB RAM
Reading from 512GB SSD
CPU Intel i7 processing with 8 coresПри входных данных AMD Ryzen, 6, 32, HDD, 1000 и specs:
Computer Specs:
- CPU: AMD Ryzen (6 cores)
- RAM: 32GB
- Storage: 1000GB HDDОбратите внимание, что класс Computer не наследуется ни от одного компонента — он содержит их. Это отношение «имеет» (has-a) означает, что вы можете легко создавать компьютеры с различными комбинациями компонентов или даже заменять компоненты позже, если добавите методы-сеттеры. Эта гибкость является ключевым преимуществом композиции перед наследованием.
Шпаргалка
При проектировании отношений между классами выбирайте между наследованием ("is-a") и композицией ("has-a").
Наследование создает сильную связанность между классами:
class Vehicle {
public:
virtual void start() { std::cout << "Starting vehicle\n"; }
};
class Car : public Vehicle {
public:
void start() override { std::cout << "Starting car\n"; }
};Композиция встраивает объекты как члены, создавая гибкие отношения:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // У Car есть Engine
public:
void start() { engine.start(); }
};Когда использовать каждый из подходов:
- Используйте наследование для подлинных отношений "is-a", когда вам нужен полиморфизм
- Используйте композицию для повторного использования функциональности или для отношений "has-a"/"uses-a"
- Современный дизайн на C++ отдает предпочтение композиции из-за ее гибкости — вы можете заменять компоненты, изменять поведение во время выполнения и комбинировать возможности без жестких иерархий
Попробуйте сами
#include <iostream>
#include <string>
#include "Computer.h"
using namespace std;
int main() {
// Чтение входных данных
string cpuModel;
getline(cin, cpuModel); // Название модели CPU (может содержать пробелы)
int cpuCores;
cin >> cpuCores; // Количество ядер CPU
int memorySize;
cin >> memorySize; // Объем памяти в ГБ
string storageType;
cin >> storageType; // Тип накопителя (например, "SSD")
int storageCapacity;
cin >> storageCapacity; // Емкость накопителя в ГБ
string command;
cin >> command; // Команда: "boot" или "specs"
// TODO: Создать объекты компонентов (CPU, Memory, Storage)
// TODO: Создать объект Computer, используя композицию
// TODO: В зависимости от команды вызвать либо boot(), либо specs()
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер