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);
}
}
code link on my github:
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/