Композиция против наследования
Часть раздела Объектно-ориентированное программирование путешествия по Dart на Coddy — урок 82 из 110.
Вы узнали, что наследование создает отношение «is-a» — Dog является Animal. Но существует другой мощный подход: композиция, которая создает отношение «has-a». Вместо наследования поведения класс содержит экземпляры других классов и делегирует им работу.
Рассмотрим класс Car. Используя наследование, вы могли бы попытаться расширить класс Engine — но машина не является двигателем, у неё есть двигатель:
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Композиция: Car ИМЕЕТ Engine
void start() {
_engine.start(); // Делегирование двигателю
print('Car is ready to drive');
}
}
void main() {
var car = Car();
car.start();
}Ключевым преимуществом композиции является гибкость. При наследовании вы привязаны к одному родительскому классу. С помощью композиции вы можете комбинировать несколько компонентов и даже заменять их во время выполнения:
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}Общепринятая рекомендация гласит: «отдавайте предпочтение композиции перед наследованием». Используйте наследование, когда существует истинная связь типа «является» (is-a) и вам необходим полиморфизм. Используйте композицию, когда вы хотите повторно использовать поведение без жесткой связи или когда объекту требуются возможности из нескольких источников.
Задание
ЛегкоДавайте создадим компьютерную систему, используя композицию! Вместо того чтобы пытаться наследовать компьютер от его частей, вы создадите отдельные классы компонентов и скомпонуете их вместе — потому что компьютер имеет (has) процессор и имеет память, он не является процессором или памятью.
Вы организуете свой код в три файла:
components.dart: Создайте два класса компонентов, представляющих части компьютера:- Класс
CPUс полямиString brandиdouble speedGHz. Добавьте методprocess(), который возвращает строку:'$brand processing at ${speedGHz}GHz' - Класс
Memoryс полемint sizeGB. Добавьте методload(), который возвращает строку:'Loading ${sizeGB}GB of data'
- Класс
computer.dart: Создайте классComputer, который использует композицию для объединения компонентов. Ваш компьютер должен:- Иметь поле
String name - Содержать экземпляры
CPUиMemory(композиция — компьютер ИМЕЕТ эти части) - Принимать все необходимые значения через конструктор для внутреннего создания обоих компонентов
- Иметь метод
boot(), который делегирует работу обоим компонентам и выводит три строки: имя компьютера, за которым следует' starting...', затем результат метода process процессора, затем результат метода load памяти - Иметь метод
specs(), который возвращает строку с описанием системы:'$name: ${cpu.brand} ${cpu.speedGHz}GHz, ${memory.sizeGB}GB RAM'
- Иметь поле
main.dart: Импортируйте ваши файлы и продемонстрируйте композицию в действии:- Создайте
Computerс именем'Workstation', процессором'Intel', работающим на частоте3.5ГГц, и16ГБ оперативной памяти - Вызовите метод
boot() - Выведите результат метода
specs()
- Создайте
Обратите внимание, что класс Computer не расширяет CPU или Memory — он содержит их и делегирует им работу. Это и есть отношение «has-a» (имеет), которое создает композиция!
Ожидаемый вывод:
Workstation starting...
Intel processing at 3.5GHz
Loading 16GB of data
Workstation: Intel 3.5GHz, 16GB RAMШпаргалка
Композиция создает отношение «has-a» (имеет), при котором класс содержит экземпляры других классов и делегирует им работу, а не наследует от них.
Базовый пример композиции:
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Car ИМЕЕТ Engine
void start() {
_engine.start(); // Делегирование двигателю
print('Car is ready to drive');
}
}Композиция с гибкостью — замена компонентов:
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}Когда использовать композицию, а когда наследование:
- Используйте наследование для истинных отношений «is-a» (является), когда вам нужен полиморфизм
- Используйте композицию для повторного использования поведения без жесткой связи или когда объекту требуются возможности из нескольких источников
- Общая рекомендация: «отдавайте предпочтение композиции перед наследованием»
Попробуйте сами
import 'computer.dart';
void main() {
// TODO: Создайте Computer с именем "Workstation" со следующими параметрами:
// - Intel CPU работающий на частоте 3.5 GHz
// - 16 GB памяти
// TODO: Вызовите метод boot()
// TODO: Выведите результат работы specs()
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыБиблиотеки и импортВведение в ООПКлассы и объектыКлючевое слово thisМетодыПеременные экземпляраОсновы конструкторовИтоги — Простой калькулятор4Null-безопасность
Введение в Null-безопасностьNullable и Non-Nullable типыОператоры ? и !Ключевое слово Late и Null-безопасностьNull-aware операторыNull-безопасность в классахИтоги — Система профилей пользователей7Абстрактные классы и интерфейсы
Абстрактные классыАбстрактные методыИнтерфейсы в DartНеявные интерфейсыРеализация против наследованияМножественные интерфейсыИтоги — Калькулятор фигур10Коллекции и обобщения
Обзор List, Set, MapТипобезопасные коллекцииОбобщенные классыОбобщенные методыОграничения обобщенийIterable и IteratorИтоги: Обобщенное хранилище13Продвинутые концепции ООП
Композиция против наследованияМетоды расширенияВызываемые классыЗапечатанные классы (Dart 3)Записи (Dart 3)Паттерны и сопоставление (3.0)Перечисления с методами16Проект: Управление библиотекой
Обзор проектаКлассы Book и User2Конструкторы в Dart
Конструктор по умолчаниюИменованные конструкторыСписки инициализацииКонстантные конструкторыФабричные конструкторыПеренаправляющие конструкторыИтоги — Shape Builder5Инкапсуляция
Публичные и приватные членыСоглашение о префиксе _Приватность на уровне библиотекГеттеры и сеттеры: подробный разборСкрытие данныхИтоги — Записи о студентах8Mixins
Введение в MixinsСоздание MixinsИспользование нескольких MixinsКлючевое слово on в MixinsMixin и наследованиеMixin и интерфейсИтоги: Система животных11Специальные методы
Переопределение toString()Переопределение hashCode и ==Интерфейс ComparableМетод call()Переопределение noSuchMethodПовторение — Пользовательская коллекция14Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн ObserverПаттерн Strategy3Свойства классов
Члены экземпляра и статические членыПоля Final и ConstПеременные LateСтатические методы и поляГеттеры и сеттерыИтоги — Менеджер банковского счета6Наследование
Основы наследованияКлючевое слово superПереопределение методовАннотация @overrideКлючевое слово final для классовКонструкторы и наследованиеИтоги: Иерархия сотрудников9Полиморфизм
Основы полиморфизмаПолиморфизм через интерфейсыПроверка типов во время выполненияОператоры is и asКлючевое слово covariantИтоги — Обработчик платежей12Асинхронное ООП
Futures и async/awaitОсновы StreamsКонтроллеры StreamАсинхронные конструкторыАсинхронность в методах классовИтоги — Data Fetcher15Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикПаттерн Репозиторий