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()
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();
}
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.