Menu
Coddy logo textTech

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 icon

Challenge

Easy

Let'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 public CPU struct with two public fields: cores (u32) for the number of cores, and speed_ghz (f32) for the clock speed. Add a specs method that returns a String describing the CPU in the format {cores}-core @ {speed_ghz}GHz.
  • memory.rs: Define a public Memory struct with a public field size_gb (u32) for the memory size. Add a specs method that returns a String in the format {size_gb}GB RAM.
  • main.rs: Bring in both modules and create a public Computer struct that has a cpu field of type CPU and a memory field of type Memory. Implement a system_info method 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 RAM

And with inputs 4, 2.8, and 32:

System: 4-core @ 2.8GHz | 32GB RAM

You 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
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming