Start learning free

LESSON 409 OF 414 · OBJECT-ORIENTED PROGRAMMING

OOP Capstone in Rust: An Animal Hierarchy with Traits

Lesson Read the full explanation

TL ; DR:

Put it all together: a trait, three concrete types that implement it, and a polymorphic loop that dispatches by runtime type. This is the shape of nearly every real OOP design.


trait Animal {
    fn name(&self) -> &str;
    fn sound(&self) -> &str;   // each implementor picks
    fn speak(&self) {           // shared by all
        println!("{}: {}", self.name(), self.sound());
    }
}
// Dog, Cat, Bird each implement Animal
// let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog { name: "Rex".to_string() }), ...];
// for a in &zoo { a.speak(); }


Full lesson:

This problem stitches together everything you've learned in this chapter:

  1. Shared contract — Rust has no inheritance, so the Animal trait plays the role of the common parent.
  2. Structs — Dog, Cat and Bird each hold a name: String field.
  3. Required methods — the trait declares speak() with no body; each type must provide one.
  4. Per-type behavior — each impl Animal for block prints its own sound.
  5. Polymorphism — a Vec<Box<dyn Animal>> holds a Dog, a Cat, and a Bird; speak() dispatches to the right implementation at runtime.

The shape of this code is the shape of nearly every real OOP design you'll see — a small contract at the top (the trait), concrete variants below, and the rest of the program written against the trait. New variants get added without touching the consumer code.


Your task

The Animal trait is already defined, with a single method speak(&self). Add three structs, each with a name: String field and an impl Animal for block — Dog (prints "Woof"), Cat (prints "Meow"), and Bird (prints "Tweet") after the name. Then in main_function, build a polymorphic collection holding one of each (Rex, Whiskers, Tweety in that order) and loop through it calling speak().

Expected output:

Rex: Woof
Whiskers: Meow
Tweety: Tweet
STEP 1 OF 14
    Ctrl + Enter to check

    RUST · OBJECT-ORIENTED PROGRAMMING

    Oop Capstone in Rust, explained

    What the capstone combines

    The chapter's earlier lessons each covered one piece of object-oriented design. This exercise uses them together, in Rust's terms:

    • A shared contract. A trait Animal stands in for the abstract parent class. Rust has no inheritance, so each concrete type implements the trait itself.
    • Several concrete types. Dog, Cat and Bird are separate structs, each with a name: String field.
    • Per-type behaviour. Each type's speak prints its own sound.
    • Polymorphism. A Vec<Box<dyn Animal>> holds one of each, and a single loop calls speak() on all of them.

    The program must print:

    Rex: Woof
    Whiskers: Meow
    Tweety: Tweet
    

    Approach

    The starting file has only the trait, with the single method fn speak(&self);, and an empty main_function. The work splits into two parts: give each animal a type that implements the trait, then build and walk the collection.

    The three types are different, so they cannot go in a plain Vec, which holds elements of one type. Boxing each as Box<dyn Animal> turns them into one uniform element type, and each call to speak is dispatched to the right implementation at run time. The order of the vector decides the order of the output, so it must be Rex, Whiskers, Tweety.

    Step by step

    1. Define the three structs, each with one field name: String. The field is an owned String, so a struct does not depend on a borrowed string staying alive.
    2. Write an impl Animal for block for each. There is nothing to inherit, so every type implements speak on its own. The body is a println! with a {} placeholder for self.name, followed by the type's sound: Woof, Meow or Tweet. The method takes &self, so it reads the field without taking ownership.
    3. Declare the vector in main_function as Vec<Box<dyn Animal>>, filled with vec!. The annotation is what makes Rust treat each element as a trait object.
    4. Construct each animal inside Box::new(...). String literals are &str, so each name is converted with .to_string().
    5. Loop with for animal in animals and call animal.speak().

    The solution in Rust

    trait Animal {
        fn speak(&self);
    }
    
    struct Dog {
        name: String,
    }
    
    impl Animal for Dog {
        fn speak(&self) {
            println!("{}: Woof", self.name);
        }
    }
    
    struct Cat {
        name: String,
    }
    
    impl Animal for Cat {
        fn speak(&self) {
            println!("{}: Meow", self.name);
        }
    }
    
    struct Bird {
        name: String,
    }
    
    impl Animal for Bird {
        fn speak(&self) {
            println!("{}: Tweet", self.name);
        }
    }
    
    fn main_function() {
        let animals: Vec<Box<dyn Animal>> = vec![
            Box::new(Dog {
                name: "Rex".to_string(),
            }),
            Box::new(Cat {
                name: "Whiskers".to_string(),
            }),
            Box::new(Bird {
                name: "Tweety".to_string(),
            }),
        ];
        for animal in animals {
            animal.speak();
        }
    }
    

    Details worth noticing

    • The loop consumes the vector. for animal in animals moves the boxes out one by one, so animals can't be used afterwards. Write for animal in &animals to borrow it and keep it.
    • Adding a type needs no change to the loop. A new Fish with its own impl Animal for Fish can be boxed into the vector, and the loop prints its line.
    • Repetition. The three speak bodies differ only in the sound, which is a hint that a shared default would help. The next section shows it.

    Sharing behaviour with a default method

    A fuller version of the design puts the shared logic in the trait. Each type provides only the pieces that differ, name and sound, and speak is written once as a default method:

    trait Animal {
        fn name(&self) -> &str;
        fn sound(&self) -> &str;
        fn speak(&self) {
            println!("{}: {}", self.name(), self.sound());
        }
    }
    
    struct Dog { name: String }
    
    impl Animal for Dog {
        fn name(&self) -> &str { &self.name }
        fn sound(&self) -> &str { "Woof" }
    }
    

    Cat and Bird follow the same pattern. With Dog named "Rex" and a Cat named "Whiskers", the loop prints Rex: Woof and Whiskers: Meow. The lesson's tests are written for the first version, where the trait has only speak, so submit that one.

    Common mistakes

    • Passing a &str to a String field. Dog { name: "Rex" } fails with error[E0308]: mismatched types (expected `String`, found `&str`). Use "Rex".to_string().
    • Wrong order in the vector. The output follows the vector, so a different order prints the lines in a different order.
    • No Box, or no type annotation. Without them the vector would try to hold one concrete type, and the second element would be a type mismatch.
    • Changing the output text. The format is the name, a colon and a space, then the sound. A missing space fails the tests.
    • Forgetting the impl Animal for block. A struct with a method named speak in its own impl block is not an Animal and can't be boxed as one.

    Key takeaways

    • Rust builds the shape of an OOP design from traits, structs and trait objects instead of class inheritance.
    • A trait is the contract, structs carry the data, and impl Trait for Type supplies the behaviour.
    • Vec<Box<dyn Trait>> lets one loop work with different types, and new types can be added without changing it.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free