Design Pattern - Prototype (TypeScript & Rust)

ayou - Mar 19 - - Dev Community

The Prototype Pattern is a creational design pattern that involves creating new objects by copying existing objects, rather than using the regular instantiation process. When the cost of object creation is high or the creation process is complex, it's a great approach to copy an existing object and modify it as needed. Our student, Xiao Ming, who is not good at studying, is an expert in using the Prototype Pattern. When the final exams come, even if Xiao Ming doesn't know anything, there's no need to panic. He can simply use the Prototype Pattern.

TypeScript

There is a class called ExaminationPaper that contains three properties: name (string), choice questions (Question[]), and simple answer questions (Question[]).

interface Prototype {
  clone(): Prototype
}

class Question implements Prototype {
  private answer: string

  constructor(answer: string) {
    this.answer = answer
  }

  setAnswer(answer: string) {
    this.answer = answer
  }

  getAnswer(): string {
    return this.answer
  }

  clone(): Prototype {
    return new Question(this.answer)
  }
}

class ExaminationPaper implements Prototype {
  choiceQuestions: Question[]
  shortAnswerQuestions: Question[]
  name: string

  constructor(
    name: string,
    choiceQuestions: Question[],
    shortAnswerQuestions: Question[]
  ) {
    this.name = name
    this.choiceQuestions = choiceQuestions
    this.shortAnswerQuestions = shortAnswerQuestions
  }

  clone(): Prototype {
    return new ExaminationPaper(
      this.name,
      this.choiceQuestions.map((q) => q.clone() as Question),
      this.shortAnswerQuestions.map((q) => q.clone() as Question)
    )
  }

  print() {
    console.log(this.name, 'paper:')
    console.log(this.choiceQuestions.map((q) => q.getAnswer()))
    console.log(this.shortAnswerQuestions.map((q) => q.getAnswer()))
  }
}

const xiaohongPaper = new ExaminationPaper(
  'xiaohong',
  [new Question('A'), new Question('B')],
  [new Question('answer1.'), new Question('answer2.')]
)
xiaohongPaper.print()

// Copy xiaohong's paper
const xiaomingPager = xiaohongPaper.clone() as ExaminationPaper
// Modify name
xiaomingPager.name = 'xiaoming'
// For short answer questions, add a closing word to the end
xiaomingPager.shortAnswerQuestions.forEach((q) =>
  q.setAnswer(q.getAnswer() + `That's all, thanks!`)
)
xiaomingPager.print()
Enter fullscreen mode Exit fullscreen mode

First, Xiao Hong instantiates an ExaminationPaper object to complete the answering. Then, Xiao Ming calls the clone method of Xiao Hong's paper to create a new instance, modifies the name to xiaoming, and cleverly adds a unique ending phrase after each answer to avoid similarities. This way, Xiao Ming effortlessly obtains a set of answers for himself.

From this example, the Prototype Pattern seems simple. The key is to implement the clone method in the Prototype and ensure deep copying of complex-type properties.

In the case of Rust, leveraging its powerful macro features makes implementing the Prototype Pattern even more convenient.

Rust

#[derive(Clone)]
struct Question {
    answer: String,
}

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

    fn get_answer(&self) -> &str {
        self.answer.as_str()
    }

    fn set_answer(&mut self, answer: String) {
        self.answer = answer
    }
}

#[derive(Clone)]
struct ExaminationPaper {
    choice_questions: Vec<Question>,
    short_answer_questions: Vec<Question>,
    name: String,
}

impl ExaminationPaper {
    fn new(
        name: &str,
        choice_questions: Vec<Question>,
        short_answer_questions: Vec<Question>,
    ) -> Self {
        Self {
            name: name.to_string(),
            choice_questions,
            short_answer_questions,
        }
    }

    fn print(&self) {
        println!("{} paper:", self.name);
        println!(
            "{}",
            self.choice_questions
                .iter()
                .map(|q| q.get_answer())
                .collect::<Vec<_>>()
                .join(" ")
        );
        println!(
            "{}",
            self.short_answer_questions
                .iter()
                .map(|q| q.get_answer())
                .collect::<Vec<_>>()
                .join(" ")
        );
    }
}

fn main() {
    let xiaohong_paper = &ExaminationPaper::new(
        "xiaohong",
        vec![Question::new("A"), Question::new("B")],
        vec![Question::new("answer1."), Question::new("answer2.")],
    );
    xiaohong_paper.print();

    let xiaoming_paper = &mut xiaohong_paper.clone();
    xiaoming_paper.name = "xiaoming".to_string();
    for q in xiaoming_paper.short_answer_questions.iter_mut() {
        q.set_answer(format!("{} {}", q.get_answer(), "That's all. Thanks!"));
    }
    xiaoming_paper.print();
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we didn't implement the clone method for ExaminationPaper and Question. Instead, we simply added #[derive(Clone)] before the type declarations.

When you add the #[derive(Clone)] derive macro to a struct or enum type in Rust, the compiler automatically generates an implementation of the Clone trait for that type.

For struct types, the automatically generated Clone implementation clones each field one by one and returns a new struct object. This means that each field must implement the Clone trait or be a primitive type (such as integers, floating-point numbers, etc.), otherwise the compiler will throw an error.

For enum types, the automatically generated Clone implementation clones each variant one by one and returns a new enum object. Similarly, each field within each variant must implement the Clone trait or be a primitive type.

By using #[derive(Clone)], we can easily generate the cloning functionality for custom types without manually implementing the Clone trait. This helps reduce a lot of boilerplate code.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .