Codementor Events

Design Patterns in Rust: Prototype or creating your own Clone and Debug implementations

Published Apr 03, 2023

The prototype-pattern is a creational design pattern that allows us to create new objects by cloning existing ones, i.e. the existing objects function as a kind of template. This can save time in some cases when for example an object creation involves some heavy calculation or things like network traffic or database queries.

So what does it look like? Well, it is actually quite simple:

prototype.drawio.png

A short explanation

  1. The main part is the _Prototype _interface. This defines a _Clone _method which return a Prototype as well. We will see in the implementation why this is important.
  2. There are some concrete classes which implement the Clone method. In this example there are two, but this could off course be any number.
  3. Finally there is the Client, the class which needs the concrete classes.

The easiest way to implement is to use the Copyand Clonetraits. That can be done this way:

#[derive(Debug,Clone)]
struct Dog {
    name:String,
}

impl Dog {
    fn new(name:&str)->Self {
        Self {
            name: name.to_string()
        }
    }
}

fn main() {
    let fido=Dog::new("fido");
    let mut nero=fido.clone();
    nero.name="nero".to_string();

    println!("Fido is {:?}",fido);
    println!("Nero is {:?}",nero); 
}

This can be done, because a String can be cloned. Even if we add another object with all clonable fields, we have problem:

#[derive(Debug)]
struct DogOwner {
    name: String,
    age: i8,
}

impl DogOwner {
    fn new(name:&str,age:i8)->Self {
        Self {
            name: name.to_string(),
            age:age,
        }
    }
}

#[derive(Debug,Clone)]
struct Dog<'a> {
    name:String,
    owner: &'a DogOwner,
}

impl<'a> Dog<'a> {
    fn new(name:&str,owner:&'a DogOwner)->Self {
        Self {
            name: name.to_string(),
            owner: owner,
        }
    }
}

fn main() {
    let owner=DogOwner::new("Martin", 35);    
    let fido=Dog::new("fido",&owner);
    let mut nero=fido.clone();
    nero.name="nero".to_string();

    println!("Fido is {:?}",fido);
    println!("Nero is {:?}",nero); 
}

Now let’s build in the possibility for DogOwner to own more than one dog:

#[derive(Debug,Clone)]
struct DogOwner<'a> {
    name: String,
    age: i8,
    dogs:Vec<&'a Dog>
}

impl<'a> DogOwner<'a> {
    fn new(name:&str,age:i8)->Self {
        Self {
            name: name.to_string(),
            age:age,
            dogs:Vec::new()
        }
    }

    fn add_dog(&mut self,dog:&'a Dog) {
        self.dogs.push(dog);
    }
}

#[derive(Debug,Clone)]
struct Dog {
    name:String,
}

impl Dog {
    fn new(name:&str)->Self {
        Self {
            name: name.to_string(),
        }
    }
}

fn main() {
    let mut owner=DogOwner::new("Martin", 35);    
    let fido=Dog::new("fido");
    let mut nero=fido.clone();
    nero.name="nero".to_string();

    owner.add_dog(&fido);
    owner.add_dog(&nero);

    println!("Fido is {:?}",fido);
    println!("Nero is {:?}",nero); 

    println!("Owner is {:?}",owner);

    let new_owner=owner.clone();
    println!("New owner is {:?}",new_owner);
    
}

What if we wanted to extend this program to generalize animal ownership? Let’s start:

use std::fmt::{Debug};

We need this statement because we will be implementing a Debug trait later on. Now define the Animal trait:

trait Animal {
    fn name(&self) -> String;
}

This just defines one method which returns the animal name.

Now define the AnimalOwner:

#[derive(Clone)]
struct AnimalOwner<'a> {
    name: String,
    age: i8,
    animals: Vec<&'a dyn Animal>,
}

impl<'a> AnimalOwner<'a> {
    fn new(name: &str, age: i8) -> Self {
        Self {
            name: name.to_string(),
            age: age,
            animals: Vec::new(),
        }
    }

    fn add_animal(&mut self, animal: &'a dyn Animal) {
        self.animals.push(animal);
    }
}

An AnimalOwner has three fields:

  1. The name
  2. The age
  3. A list of owned animals

Now for the sake of example we will define a Dog:

#[derive(Debug)]
struct Dog {
    name: String,
}

impl Animal for Dog {
    fn name(&self) -> String {
        self.name.to_string()
    }
}

impl Dog {
    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
        }
    }
}

In this simplified example a Dog just has a name. The rest of this code is self-explanatory.

Now we will implement the Debug traits. This is because if we don’t, we get this error:

animals: Vec<&'a dyn Animal>,
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Animal + 'a)` cannot be formatted using `{:?}` because it doesn't implement `Debug`

First of all, remove all the Debug attributes from AnimalOwner and Dog so that they look like this:

struct AnimalOwner<'a> {
    name: String,
    age: i8,
    animals: Vec<&'a dyn Animal>,
}

And:

struct Dog {
    name: String,
}

Now implement the Debug trait for Animal:

impl<'a> Debug for dyn Animal + 'a {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.name())
    }
}

This trait just returns the Animal’s name

And now for AnimalOwner:

impl<'a> Debug for AnimalOwner<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let dog_names = self
            .animals
            .iter()
            .map(|a| format!("{:?}", a))
            .collect::<Vec<_>>()
            .join(", ");
        write!(f, "{} {} animals:{}", self.name, self.age, dog_names)
    }
}

Here a little more explanation is needed:

  1. iter() loops over all the items in the animals Vector
  2. map maps the items to the format! (which returns a String)
  3. collect collects it back into a vector
  4. join joins the strings with the specified separator
  5. The last line writes the output to the formatter.

As you can see this almost looks like a functional language, quite nice I think.

Now it is time to put it to the test:

fn main() {
    let mut owner = AnimalOwner::new("Martin", 35);
    let fido = Dog::new("fido");

    let mut nero = fido.clone();
    nero.name = "nero".to_string();

    owner.add_animal(&fido);
    owner.add_animal(&nero);

    println!("Owner is {:?}", owner);

    let mut new_owner = owner.clone();
    new_owner.name = "Chloe".to_string();
    new_owner.age = 57;

    println!("New owner is {:?}", new_owner);
}

Again a line by line explanation:

  1. Instantiate an owner.
  2. Create a dog
  3. Clone the dog and set its name to ‘nero’
  4. Now add the two dogs to the owner
  5. Print out the owner.
  6. Clone the owner, set her name to ‘Chloe’ and set her age to 57.
  7. Print out the new owner.

Now do a ‘cargo build’ and hey presto, or not?

28 | struct Dog {
   | ---------- method `clone` not found for this struct
...
68 | let mut nero = fido.clone();
   | ^^^^^ method not found in `Dog`
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `clone`, perhaps you need to implement it:
           candidate #1: `Clone`

error[E0599]: no method named `clone` found for struct `AnimalOwner` in the current scope
  --> src\main.rs:76:31
   |
8 | struct AnimalOwner<'a> {
   | ---------------------- method `clone` not found for this struct
...
76 | let mut new_owner = owner.clone();
   | ^^^^^ method not found in `AnimalOwner<'_>`
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `clone`, perhaps you need to implement it:
           candidate #1: `Clone`

Of course we could add a #[derive(Clone)] to the structs, but for fun, why not implement the clone method ourselves? First let us clone the Dog:

impl Clone for Dog {
    fn clone(&self) -> Self {
        Self {
            name:self.name.to_string()
        }
    }
}

The clone method for AnimalOwner is a little bit more complicated but still elegant:

impl<'a> Clone for AnimalOwner<'a> {
    fn clone(&self) -> Self {
        let cloned_animals = self.animals.iter().map(|a| *a).collect();
        Self {
            name: self.name.to_string(),
            age: self.age,
            animals: cloned_animals,
        }
    }
}

Line by line:

  1. We clone the Vector by iterating over it, and the collecting it.
  2. Return a new AnimalOwner, with the correct values

Trying building it again, and yes, this works

This blog is about a bit more than I expected, however, implementing your own Clone and Debug traits is quite interesting, and gives you great flexibility.

For example, you can determine exactly what parts of an object must be cloned. The rest of the values could then be initialized by a proxy whenever they are needed, but that is a subject for another article.

Also this example gave me a first glance at the functional-like possibilities in Rust, which makes for quite elegant code.

Discover and read more posts from Iede Snoek
get started
post commentsBe the first to share your opinion
Show more replies