Композиция
Часть раздела Object Oriented Programming путешествия по Rust на Coddy — урок 55 из 61.
Многие объектно-ориентированные языки в значительной степени полагаются на наследование для совместного использования функциональности между типами. Rust использует другой подход: композицию. Вместо того чтобы наследовать поведение от родителя, вы создаете сложные типы, включая другие типы в качестве полей.
Рассмотрим моделирование автомобиля. Вместо того чтобы создавать базовый класс Vehicle и наследоваться от него, вы компонуете Car из более мелких, специализированных компонентов:
struct Engine {
horsepower: u32,
}
struct Wheels {
count: u8,
diameter: f32,
}
struct Car {
engine: Engine,
wheels: Wheels,
brand: String,
}
Car не наследуется от Engine или Wheels — он содержит их. Это отношение «has-a» (имеет) является композицией. Вы получаете доступ к внутренним компонентам через их поля и можете напрямую вызывать их методы:
impl Engine {
fn start(&self) {
println!("Engine with {} HP started!", self.horsepower);
}
}
impl Car {
fn start(&self) {
self.engine.start(); // Делегирование внутреннему компоненту
}
}
Этот паттерн позволяет каждой структуре сохранять узкую специализацию. Нужно изменить принцип работы двигателей? Измените только структуру Engine. Хотите повторно использовать Wheels для мотоцикла? Просто включите её в новую структуру. Композиция дает вам гибкость без жесткой связи, которую часто создает наследование.
Задание
ЛегкоДавайте создадим компьютерную систему, используя композицию! Вместо того чтобы создавать одну массивную структуру, вы соберете Computer из более мелких, специализированных компонентов — CPU и Memory. Каждый компонент будет знать, как описать себя, а Computer будет делегировать задачи своим частям.
Вы организуете свой код в трех файлах:
cpu.rs: Определите публичную структуруCPUс двумя публичными полями:cores(u32) для количества ядер иspeed_ghz(f32) для тактовой частоты. Добавьте методspecs, который возвращает String, описывающую процессор в формате"{cores}-core @ {speed_ghz}GHz".memory.rs: Определите публичную структуруMemoryс публичным полемsize_gb(u32) для объема памяти. Добавьте методspecs, который возвращает String в формате"{size_gb}GB RAM".main.rs: Подключите оба модуля и создайте публичную структуруComputer, которая имеет полеcpuтипаCPUи полеmemoryтипаMemory. Реализуйте методsystem_infoдля Computer, который делегирует выполнение своим компонентам и выводит полную спецификацию системы. Используйте предоставленные входные данные для создания Computer и отображения информации о нем.
Метод system_info должен выводить:
System: {cpu_specs} | {memory_specs}Например, при входных данных 8, 3.5 и 16:
System: 8-core @ 3.5GHz | 16GB RAMИ при входных данных 4, 2.8 и 32:
System: 4-core @ 2.8GHz | 32GB RAMВы получите три входных значения: количество ядер процессора (преобразовать в u32), частоту процессора (преобразовать в f32) и объем памяти (преобразовать в u32).
Шпаргалка
Rust использует композицию вместо наследования для совместного использования функциональности между типами. Вы создаете сложные типы, включая другие типы в качестве полей, создавая отношение «имеет» (has-a).
Базовая композиция
Определяйте небольшие, специализированные структуры и объединяйте их в более крупные:
struct Engine {
horsepower: u32,
}
struct Wheels {
count: u8,
diameter: f32,
}
struct Car {
engine: Engine,
wheels: Wheels,
brand: String,
}
Делегирование компонентам
Обращайтесь к внутренним компонентам через их поля и вызывайте их методы:
impl Engine {
fn start(&self) {
println!("Engine with {} HP started!", self.horsepower);
}
}
impl Car {
fn start(&self) {
self.engine.start(); // Делегирование внутреннему компоненту
}
}
Композиция позволяет каждой структуре сосредоточиться на одной задаче, обеспечивая гибкость без жесткой связи.
Попробуйте сами
mod cpu;
mod memory;
use cpu::CPU;
use memory::Memory;
// TODO: Определите публичную структуру Computer с двумя публичными полями:
// - cpu: CPU
// - memory: Memory
// TODO: Реализуйте метод system_info для Computer, который:
// - Делегирует выполнение cpu.specs() и memory.specs()
// - Выводит: System: {cpu_specs} | {memory_specs}
fn main() {
let mut input = String::new();
std::io::stdin().read_line(&mut input).expect("Failed to read line");
let cores: u32 = input.trim().parse().expect("Invalid number");
input.clear();
std::io::stdin().read_line(&mut input).expect("Failed to read line");
let speed_ghz: f32 = input.trim().parse().expect("Invalid number");
input.clear();
std::io::stdin().read_line(&mut input).expect("Failed to read line");
let size_gb: u32 = input.trim().parse().expect("Invalid number");
// TODO: Создайте экземпляры CPU и Memory, используя полученные входные данные
// TODO: Создайте экземпляр Computer с CPU и Memory
// TODO: Вызовите system_info() у компьютера, чтобы вывести результат
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Object Oriented Programming
1Методы и поведение
Введение в блоки реализацииПараметр SelfИзменяемые методыАссоциированные функцииНесколько блоков реализацииЦепочки вызовов методовИтоги: действия с Rectangle4Проект: Виртуальный питомец
Определение питомцаКормление питомца2Инкапсуляция и модули
Основы модулейКлючевое слово PublicПриватные поляГеттерыСеттерыИтоги — Безопасный сейф5Обобщения
Обобщённые структурыОбобщённые методыНесколько обобщённых типовОбобщённые функцииПовторение — Координатная точка8Трейты как ограничения
Синтаксис ограничений трейтовМножественные ограниченияКонструкция whereВозврат типов с трейтамиИтоги — Обобщённый принтер11Паттерны проектирования в Rust
Паттерн NewtypeКомпозицияТрейт DropFrom и IntoИтоги — Мок-объект Smart Pointer3Продвинутые перечисления
Перечисления с даннымиМетоды в перечисленияхСопоставление вариантов с даннымиСнова об Option EnumИтоги — перечисление Shape6Определение трейтов
Что такое трейт?Реализация трейтовРеализации по умолчаниюПереопределение реализаций по умолчаниюТрейты с параметрамиИтоги — Media Player