Composite

With this pattern, we can model object/structs hierarchies independently if the object is simple or composed it will be treated uniformly

Key points

  • The key point is to have a trait to represent both, simple and composed structs.

  • Clients should ignore the difference between compositions and individual objects.

  • Avoid case statements to specify individual treatment to every struct, making it easier to put new types of components

Participants

Component

An abstract representation of the component

Leaf

The simplest component, which has no children

Composite

The sort of Component which has children

Client

The software part that uses the composite api

Study

In the example I am going to model glasses because I already used them in my professional career and did not have any knowledge of the composite pattern at that time, the final solution was very close to the pattern but I would suffer much less if I knew it.

OO approach

Firstly I will show a possible approach to use as a more OO driven language, such as Java, Kotlin etc

Implemented approach

Secondly, I will show the approach that I choose using rust, because of its differences from the language cited earlier

Code solution

pub trait Item {
    fn price(&self) -> f64;
    fn add_item(&mut self, _: Box<dyn Item>) -> Result<(), &str>{
        return Err("not implemented on a simple item")
    }
    fn remove_item(&mut self, _: usize) -> Result<(), &str>{
        return Err("not implemented on a simple item")
    }
}

I decided to use a default implementation to deal with component children since it would simplify the use for the Composite client. It's important to evaluate the tradeoffs here.

struct ItemComposite {
    items: Vec<Box<dyn Item>>
}

impl ItemComposite {
    fn new() -> Self {
        return ItemComposite{
            items: Vec::new()
        }
    }
}

impl Item for ItemComposite {
    fn price(&self) -> f64 {
        let prices = self.items.iter().map(|x| x.price());
        return prices.sum()
    }

    fn add_item(&mut self, i: Box<dyn Item>) -> Result<(), &str>{
        self.items.push(i);
        return Ok(())
    }

    fn remove_item(&mut self, position: usize) -> Result<(), &str>{
        _ = self.items.remove(position);
        return Ok(())
    }
}

Since there's no inheritance in rust, I had to state the behavior in terms of traits, just exposing the behavior and not attributes like items for example. Therefore I decided to aggregate the composed structs with an ItemComposite in contrast to inheriting it as I would do in a OO language. When composed-related operations such as price are invoked by the client the composed class does part of the operation and delegates the children-related part to the Item composite.

pub struct Lens {
    composite: ItemComposite,
    pub(crate) price: f64,
}

impl Lens {
    pub fn new(price: f64) -> Self {
        return Lens{
            composite : ItemComposite::new(),
            price,
        }
    }
}


impl Item for Lens {
    fn price(&self) -> f64 {
        return &self.price + &self.composite.price()
    }

    fn add_item(&mut self, i: Box<dyn Item>) -> Result<(), &str>{
        return self.composite.add_item(i);
    }

    fn remove_item(&mut self, i: usize) -> Result<(), &str>{
        return self.composite.remove_item(i);
    }
}

pub struct Frame {
    composite: ItemComposite,
    price: f64,
}

impl Item for Frame {
    fn price(&self) -> f64 {
        return &self.price + &self.composite.price()
    }

    fn add_item(&mut self, i: Box<dyn Item>) -> Result<(), &str>{
        return self.composite.add_item(i);
    }

    fn remove_item(&mut self, i: usize) -> Result<(), &str>{
        return self.composite.remove_item(i)
    }
}

impl Frame {
    pub fn new(price: f64) -> Self {
        return Frame{
            composite : ItemComposite::new(),
            price,
        }
    }
}

pub struct Treatment {
    pub price: f64,
}

impl Item for Treatment {
    fn price(&self) -> f64 {
        return self.price
    }
}

impl Treatment {
    pub fn new(price: f64) -> Self {
        return Treatment{
            price,
        }
    }
}

pub struct Signature {
    pub price: f64,
}

impl Signature {
    pub fn new(price: f64) -> Self {
        return Signature{
            price,
        }
    }
}

impl Item for Signature {
    fn price(&self) -> f64 {
        return self.price
    }
}

Code tests

#[cfg(test)]
mod test {
    use crate::items::*;

    #[test]
    fn calling_add_item_from_an_not_composed_item() {
        let mut treatment = Treatment::new(200.0);
        let sig = Signature::new(100.0);

        let res = treatment.add_item(Box::new(sig));

        assert!(res.is_err())
    }

    #[test]
    fn assemblying_an_complte_glass_product() {
        let treatment = Treatment::new(200.0);
        let mut lens = Lens::new(300.0);
        let _ = lens.add_item(Box::new(treatment));

        let sig = Signature::new(100.0);
        let mut frame = Frame::new(500.0);
        let _ = frame.add_item(Box::new(lens));
        let _ = frame.add_item(Box::new(sig));

        assert_eq!(frame.price(), 1100.0);
    }
}

https://github.com/igorcavalcante/design_patterns/blob/main/composite

Alternative rust approach

A third approach would be to create a trait that extends the item trait and adds children-related behavior. It would work but could add complexity to the API client since probably they would need to differentiate between a Leaf and a Composed struct.

Difficulties that I found:

Basically, all my problems were hot to adapt an OO pattern to rust. Such as

  • No abstract classes with attributes

  • Some trouble with borrowing and other kinds of compile checks

  • Some optional aspect is to share behavior to manage the children's objects

Final thoughts

It would be easier to use a more Object Oriented language to do the same job, but It could be done using Rust with good effectiveness too. As a rust learner, I had some trouble trying to figure out how to borrow variables, use traits, and use modules as anyone should have. Certainly, I will use this pattern in a case of hierarchy-organized structures

Sources

Design patterns

Elements of Reusable Object-Oriented Software Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides

Composite in Rust

https://refactoring.guru/design-patterns/composite/rust/example

The "Composite Pattern" in Rust

AXEL FORTUN https://grapeprogrammer.com/composite-pattern-rust/