Sling Academy
Home/Rust/Emulating Aggregation and Composition in Rust with Struct Fields

Emulating Aggregation and Composition in Rust with Struct Fields

Last updated: January 06, 2025

When working with object-oriented programming in languages like Java or C++, the concepts of aggregation and composition are commonly used to model relationships between objects. While Rust is not traditionally object-oriented, you can still emulate these concepts using structs and ownership semantics. This article will guide you through understanding and implementing aggregation and composition in Rust.

Understanding Aggregation and Composition

Aggregation is a relationship where a child can exist independently of the parent. It signifies a 'has-a' relationship. For example, a university 'has a' student, but the student's existence is not dependent solely on the university.

Composition, on the other hand, indicates a strong relationship where the child’s lifecycle is tightly bound to the parent's. If the parent is destroyed, the child is as well. For instance, a human body 'has a' heart, and it typically does not exist independently out of the body context.

Using Structs in Rust

In Rust, structs are a way to create custom data types. They can encapsulate different values and define relationships reflective of aggregation and composition.

Aggregation with Structs

To implement aggregation in Rust, we create structs where fields hold types that can outlive the struct. The ownership is more flexible, often implemented using borrowed references or smart pointers like Rc (Reference Counted pointers) to manage shared ownership.

use std::rc::Rc;

struct Student {
    name: String,
}

struct University {
    students: Vec>,
}

fn main() {
    let student1 = Rc::new(Student { name: String::from("Alice") });
    let student2 = Rc::new(Student { name: String::from("Bob") });
    
    let university = University {
        students: vec![student1.clone(), student2.clone()],
    };
    
    println!("Students: {} and {}", 
        university.students[0].name, university.students[1].name);
}

In this code snippet, the University struct instance aggregates students. Each student is referenced using the Rc type allowing them to exist independently beyond the University instance.

Composition with Structs

Composition can be represented in Rust using structs where child components are strictly owned by the parent struct by value. They do not exist independently and get dropped when the owning struct is dropped.

struct Heart;

struct Human {
    heart: Heart,
}

fn main() {
    let human = Human { heart: Heart };
    // The heart is an integral, non-detachable part of Human
}

In this example, Heart cannot exist outside of the Human instance. When an instance of Human is created, it owns a Heart.

Utility of Smart Pointers

Smart pointers like Box, Rc, and Arc in Rust provide controlled ways to handle ownership and lifetimes in context of aggregation and composition:

  • Box is often used for defining composition due to its heap allocation, holding ownership over contained data.
  • Rc and Arc (Atomically Reference Counted pointers) are used in aggregation to share memory safely among multiple owners.

Through these constructs, Rust allows for fine-grained control over data ownership, lifetimes, and memory safety, prevent issues common in other languages.

Conclusion

By mimicking aggregation and composition in Rust using structs and smart pointers, you can maintain the benefits of these design philosophies while embracing Rust’s unique safety and concurrent programming models. In doing so, Rust offers robust tools for your system's object management, underscoring its strength as a systems programming language.

Next Article: Using Enums Instead of Derived Classes for Representing Variants in Rust

Previous Article: Understanding Object Safety Constraints for Rust Trait Objects

Series: Object-Oriented Programming 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