Sling Academy
Home/Rust/Inheriting Behavior Through Composition of Rust Structs

Inheriting Behavior Through Composition of Rust Structs

Last updated: January 03, 2025

In Rust, a systems programming language known for safety and performance, inheritance as seen in object-oriented programming languages does not exist. Instead, Rust focuses on composition over inheritance. By composing structs and leveraging traits, developers can achieve flexible and reusable code similar to what inheritance offers in other languages. This article will discuss how to inherit behavior through the composition of Rust structs.

Understanding Composition

Composition is a design principle where you build complex types by combining objects. In Rust, you create structures that contain other structures as fields. This allows you to reuse the behavior of contained structures with efficacy.

Defining Structs

Let's begin by defining simple structs in Rust. We will use these to build more complex behaviors:


struct Engine {
    power: u32,
}

struct Wheels {
    count: u8,
}

struct Car {
    engine: Engine,
    wheels: Wheels,
}

Here, we define two basic structs, Engine and Wheels. The Car struct composes these two to represent a more complex entity.

Implementing Behavior

Each struct can implement its methods to behave in specific ways. Let's add some methods:


impl Engine {
    fn start(&self) -> String {
        format!("Engine with {} power is starting.", self.power)
    }
}

impl Wheels {
    fn roll(&self) -> String {
        format!("{} wheels are rolling.", self.count)
    }
}

Now, we add the behavior to the Car struct by creating methods that utilize its fields:


impl Car {
    fn start_car(&self) -> String {
        self.engine.start()
    }

    fn drive(&self) -> String {
        self.wheels.roll()
    }
}

Here, the Car struct effectively inherits behavior from both Engine and Wheels through composition, as it calls methods of its owned structs.

The Role of Traits

To create a common interface among multiple structs, Rust uses traits, akin to interfaces or abstract base classes in other languages:


trait Vehicle {
    fn vehicle_info(&self) -> String;
}

impl Vehicle for Car {
    fn vehicle_info(&self) -> String {
        format!("Car with engine power {} and {} wheels.", self.engine.power, self.wheels.count)
    }
}

Traits allow structs like Car to guarantee certain behaviors and provide polymorphic capabilities, enhancing modular interfaces without subclassing.

Benefits Over Inheritance

Composition in Rust helps avoid the problems associated with inheritance, such as the fragility of a base class possibly breaking extended classes when changed. By relying on composition, you focus on simpler, self-contained units that are easier to test and debug.

Practical Example

Consider the need to model an electric car in addition to our gas-fueled Car. Rather than worrying about subclassing, you directly compose a new struct:


struct Battery {
    capacity: u32,
}

struct ElectricCar {
    engine: Engine,
    wheels: Wheels,
    battery: Battery,    
}

impl ElectricCar {
    fn charge(&self) -> String {
        format!("Battery capacity: {} is charging.", self.battery.capacity)
    }
}

The ElectricCar struct is independent in its implementation, focusing on extending its behavior without worrying about limitations from superclass design.

Conclusion

By using composition and traits, Rust developers achieve effective code reuse and conceptual inheritance. Structs hold each other's components, and behavior is defined in terms of these integrations, leading to flexible and expressive code bases that embrace Rust’s strengths in safety and concurrency.

Next Article: Rust - Integrating Structs into Concurrency Models: Channels and Threads

Previous Article: Managing Mutability in Rust Struct Methods: &self vs &mut self

Series: Working with structs in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior