Композиция против наследования
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy. Урок 76 из 104.
При проектировании отношений между классами у вас есть два основных варианта: наследование («is-a») и composition («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, а не является engine:
class Engine {
public:
void start() { std::cout << "Engine running\n"; }
};
class Car {
private:
Engine engine; // Car ИМЕЕТ Engine
public:
void start() { engine.start(); }
};Ключевое преимущество composition — гибкость. Вы можете легко заменять компоненты, изменять поведение во время выполнения или объединять несколько возможностей без ограничений жёсткой иерархии class. В современном дизайне C++ в большинстве случаев composition предпочитают наследованию.
Используй наследование, когда существует подлинное отношение «является» и тебе нужен полиморфизм. Используй композицию, когда хочешь повторно использовать функциональность или когда отношение лучше описывается как «has-a» или «uses-a».
Задание
ЛегкоДавайте создадим симулятор компьютерной системы, демонстрирующий, когда следует использовать composition вместо наследования. Вы смоделируете компьютер, который has компоненты (composition), а не является компонентом, показывая, как composition обеспечивает гибкость для замены частей во время выполнения.
Вы организуете код в трёх файлах:
Components.h: Определите отдельные компоненты, которые может contain компьютер.Создайте класс
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], и getter-методы для обоих полей.Computer.h: Определите классComputer, использующий composition для объединения компонентов.Ваш
Computerдолжен иметь приватные объекты-члены:CPU,MemoryиStorage. Используйте список инициализации конструктора, чтобы инициализировать эти компоненты из параметров.Implement следующие методы:
boot(): выводитBooting computer..., затем вызываетload()для памяти,read()для хранилища иprocess()для CPU, каждый вызов — в отдельной строкеspecs(): выводит характеристики компьютера в следующем формате:Computer Specs: - CPU: [model] ([cores] cores) - RAM: [size]GB - Storage: [capacity]GB [type]
main.cpp: Считайте шесть входных значений (каждое в отдельной строке):- Имя модели CPU (строка)
- Количество ядер CPU (целое число)
- Объём памяти в GB (целое число)
- Тип хранилища (строка, например, "SSD")
- Ёмкость хранилища в GB (целое число)
- Команду: либо "boot", либо "specs"
Создайте объекты компонентов, затем создайте
Computerс использованием composition. В зависимости от команды вызовите либо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» означает, что вы можете легко создавать компьютеры с различными комбинациями компонентов или даже заменять компоненты позднее, если добавите setter-методы. Эта гибкость является главным преимуществом composition перед наследованием.
Попробуйте сами
#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Итоги — Обобщенный контейнерПотренируйтесь самостоятельно: Онлайн-компилятор C++