Composite Design Pattern in Rust

1. Definition

The Composite design pattern allows you to compose objects into tree structures to represent part-whole hierarchies. The pattern enables clients to treat individual objects and compositions of objects uniformly.

2. Problem Statement

Imagine you're developing a graphical system where shapes can be both individual entities and groups of shapes. A group of shapes can also contain other groups, creating a hierarchy. How can we treat both individual shapes and groups in a consistent manner, without checking if an object is a single shape or a group every time?

3. Solution

The Composite pattern addresses this problem by treating both individual objects (leaf nodes) and their containers (composite nodes) as instances of the same interface.

4. Real-World Use Cases

1. Graphics systems where shapes can be grouped together.

2. Organizational structures (e.g., departments containing employees and sub-departments).

3. File and directory structures in file systems.

5. Implementation Steps

1. Define a component interface with operations suitable for both leaf and composite objects.

2. Create leaf objects that implement the component interface.

3. Create composite objects that also implement the component interface and hold child components (either leaf or other composites).

6. Implementation in Rust Programming

// Step 1: Define the component interface
pub trait Graphic {
    fn draw(&self);
}
// Step 2: Leaf object
pub struct Circle;
impl Graphic for Circle {
    fn draw(&self) {
        println!("Drawing a circle");
    }
}
pub struct Square;
impl Graphic for Square {
    fn draw(&self) {
        println!("Drawing a square");
    }
}
// Step 3: Composite object
pub struct GraphicGroup {
    graphics: Vec<Box<dyn Graphic>>,
}
impl GraphicGroup {
    pub fn new() -> Self {
        GraphicGroup { graphics: Vec::new() }
    }
    pub fn add(&mut self, graphic: Box<dyn Graphic>) {
        self.graphics.push(graphic);
    }
}
impl Graphic for GraphicGroup {
    fn draw(&self) {
        for graphic in &self.graphics {
            graphic.draw();
        }
    }
}
// Client code
fn main() {
    let circle1 = Circle;
    let square1 = Square;
    let mut group1 = GraphicGroup::new();
    group1.add(Box::new(circle1));
    group1.add(Box::new(square1));
    let circle2 = Circle;
    let mut group2 = GraphicGroup::new();
    group2.add(Box::new(circle2));
    group2.add(Box::new(group1));
    group2.draw();
}

Output:

"Drawing a circle"
"Drawing a circle"
"Drawing a square"

Explanation:

1. The Graphic trait defines the interface for both leaf and composite objects.

2. Circle and Square are leaf objects implementing the Graphic trait.

3. GraphicGroup is a composite object that can hold both leaf objects and other composites.

4. In the client code, we created two groups and nested one inside the other. When we call the draw method on the outer group (group2), it recursively calls the draw method for all its children.

7. When to use?

The Composite pattern is useful when:

1. You want to represent part-whole hierarchies of objects.

2. You want clients to be able to ignore the difference between compositions of objects and individual objects. Clients will treat all objects uniformly.


Comments