Composition
Part of the Object Oriented Programming section of Coddy's Rust journey — lesson 55 of 61.
Many object-oriented languages rely heavily on inheritance to share functionality between types. Rust takes a different approach: composition. Instead of inheriting behavior from a parent, you build complex types by including other types as fields.
Consider modeling a car. Rather than creating a base Vehicle class and inheriting from it, you compose a Car from smaller, focused components:
struct Engine {
horsepower: u32,
}
struct Wheels {
count: u8,
diameter: f32,
}
struct Car {
engine: Engine,
wheels: Wheels,
brand: String,
}
The Car doesn't inherit from Engine or Wheels—it has them. This "has-a" relationship is composition. You access the inner components through their fields and can call their methods directly:
impl Engine {
fn start(&self) {
println!("Engine with {} HP started!", self.horsepower);
}
}
impl Car {
fn start(&self) {
self.engine.start(); // Delegate to the inner component
}
}
This pattern keeps each struct focused on a single responsibility. Need to change how engines work? Modify only the Engine struct. Want to reuse Wheels for a motorcycle? Just include it in a new struct. Composition gives you flexibility without the tight coupling that inheritance often creates.
Challenge
EasyLet's build a computer system using composition! Instead of creating one massive struct, you'll compose a Computer from smaller, focused components—a CPU and Memory. Each component will know how to describe itself, and the Computer will delegate to its parts.
You'll organize your code across three files:
cpu.rs: Define a publicCPUstruct with two public fields:cores(u32) for the number of cores, andspeed_ghz(f32) for the clock speed. Add aspecsmethod that returns a String describing the CPU in the format{cores}-core @ {speed_ghz}GHz.memory.rs: Define a publicMemorystruct with a public fieldsize_gb(u32) for the memory size. Add aspecsmethod that returns a String in the format{size_gb}GB RAM.main.rs: Bring in both modules and create a publicComputerstruct that has acpufield of typeCPUand amemoryfield of typeMemory. Implement asystem_infomethod on Computer that delegates to its components and prints the full system specification. Use the provided inputs to build a Computer and display its info.
The system_info method should print:
System: {cpu_specs} | {memory_specs}For example, with inputs 8, 3.5, and 16:
System: 8-core @ 3.5GHz | 16GB RAMAnd with inputs 4, 2.8, and 32:
System: 4-core @ 2.8GHz | 32GB RAMYou will receive three inputs: CPU cores (parse as u32), CPU speed (parse as f32), and memory size (parse as u32).
Cheat sheet
Rust uses composition instead of inheritance to share functionality between types. You build complex types by including other types as fields, creating a "has-a" relationship.
Basic Composition
Define smaller, focused structs and compose them into larger ones:
struct Engine {
horsepower: u32,
}
struct Wheels {
count: u8,
diameter: f32,
}
struct Car {
engine: Engine,
wheels: Wheels,
brand: String,
}
Delegating to Components
Access inner components through their fields and call their methods:
impl Engine {
fn start(&self) {
println!("Engine with {} HP started!", self.horsepower);
}
}
impl Car {
fn start(&self) {
self.engine.start(); // Delegate to the inner component
}
}
Composition keeps each struct focused on a single responsibility, providing flexibility without tight coupling.
Try it yourself
mod cpu;
mod memory;
use cpu::CPU;
use memory::Memory;
// TODO: Define a public Computer struct with two public fields:
// - cpu: CPU
// - memory: Memory
// TODO: Implement a system_info method on Computer that:
// - Delegates to cpu.specs() and memory.specs()
// - Prints: 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: Create CPU and Memory instances using the parsed inputs
// TODO: Create a Computer instance with the CPU and Memory
// TODO: Call system_info() on the computer to print the result
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Methods and Behavior
Intro Implementation BlocksThe Self ParameterMutable MethodsAssociated FunctionsMultiple Implementation BlocksMethod ChainingRecap - Rectangle Actions4Project: Virtual Pet
Defining the PetFeeding the Pet7Standard Traits
The Debug TraitThe Display TraitClone and CopyEquality TraitsRecap - Printable Point10Project: Document System
The Draw TraitText Component2Encapsulation and Modules
Modules BasicsThe Public KeywordPrivate FieldsGettersSettersRecap - Secure Locker5Generics
Generic StructsGeneric MethodsMultiple Generic TypesGeneric FunctionsRecap - Coordinate Point8Traits as Bounds
Trait Bounds SyntaxMultiple BoundsThe Where ClauseReturning Types with TraitsRecap - Generic Printer11Design Patterns in Rust
Newtype PatternCompositionThe Drop TraitFrom and IntoRecap - Smart Pointer Mock