Prototype design pattern allows you to create objects by cloning an existing object instead of creating a new object from scratch. This pattern is used when the process of object creation is costly. When cloning, the newly copied object contains the same characteristics as its source object. After cloning, we can change the values of the new object’s properties as required. This pattern comes under a creational pattern.

Summary

Sử dụng prototype pattern khi:

  • Chi phí để tạo mới một object quá lớn.
  • Chúng ta nhận object từ một bên thứ ba thông qua interface nào đó, đồng thời chúng ta không biết thông tin của các concrete class. Và chúng ta muốn code của mình không phụ thuộc vào bên thứ ba.
  • Có quá nhiều class kế thừa phức tạp mà chỉ khác nhau bởi cách chúng khởi tạo và cấu hình.

Problem

Giả sử chúng ta có một đối tượng và chúng ta muốn có một bản sao y hệt của đối tượng đó. Để sao chép, trước tiên ta cần tạo ra một đối tượng mới có cùng class với đối tượng đó. Sau đó, chúng ta cần duyệt qua tất cả các thuộc tính của đối tượng nguồn để sao chép sang đối tượng mới.

Cách làm này không sai. Tuy nhiên, không phải lúc nào các thuộc tính cũng có thể truy cập nên việc sao chép không phải lúc nào cũng khả thi.

Ngoài ra, cách làm trên có một nhược điểm: do chúng ta cần phải tạo ra một đối tượng mới có cùng class với đối tượng nguồn, code của chúng ta trở nên phụ thuộc vào class đó.

Solution

Giải pháp cho vấn đề này là ta sẽ xây dựng phương thức clone dùng để tạo ra đối tượng mới dựa trên đối tượng nguồn. Đối tượng mà hỗ trợ clone thì được gọi là prototype.

Trong trường hợp object có hàng tá thuộc tính và hàng trăm cách cấu hình thì việc cloning sẽ đơn giản hơn là subclassing (chia thành các lớp con).

Ý tưởng của prototype pattern là: tạo ra một danh sách các đối tượng được cấu hình sẵn. Khi cần sử dụng một đối tượng tương tự như một trong số những đối tượng đã được cấu hình, ta chỉ cần clone một prototype thay vì xây dựng một đối tượng mới từ ban đầu.

Structure

Có hai cấu trúc của prototype pattern:

Basic

Cấu trúc cơ bản sẽ bao gồm những thành phần sau:

  1. Prototype: là một interface định nghĩa phương thức clone.
  2. Concrete prototype: cài đặt phương thức clone.
  3. Client: tạo ra bản sao của một đối tượng mà tuân theo prototype interface.

Registry

Cấu trúc registry có thêm một prototype registry: là một class có chứa một danh sách các prototype thường dùng. Danh sách này thường là một hash map.

Implementation

Giả sử ta có một nhà máy sản xuất máy tính với hai prototype là gaming và working computer. Chúng ta cần áp dụng prototype pattern để có thể sản xuất hàng loạt nhiều máy tính từ hai prototype đó.

Computer Types

Trước tiên ta tạo ra enum cho các kiểu máy tính như sau:

enum ComputerTypes {
	GAMING,
	WORKING
};

Prototype

Xây dựng prototype:

class BaseComputer {
protected:
	int _price;
 
public:
	BaseComputer() { _price = 0; }
	virtual int getPrice() { return _price; }
	virtual void increasePrice(int value) { _price += value; }
 
public:
	virtual BaseComputer* clone() = 0;
};

Ngoài phương thức clone, BaseComputer còn có thêm những phương thức nền cho các concrete prototype chẳng hạn như constructor, getter và setter.

Concrete Prototype

Xây dựng prototype cho gaming và working computer:

class GamingComputer : public BaseComputer {
public:
	GamingComputer() : BaseComputer() {}
	GamingComputer(const GamingComputer& other) {
		this->_price = other._price;
	}
 
	BaseComputer* clone() {
		return new GamingComputer(*this);
	}
};
 
class WorkingComputer : public BaseComputer {
public:
	WorkingComputer() : BaseComputer() {}
	WorkingComputer(const WorkingComputer& other) {
		this->_price = other._price;
	}
 
	BaseComputer* clone() {
		return new WorkingComputer(*this);
	}
};

Có thể thấy, chúng ta sử dụng copy constructor (xem thêm Constructor) để tạo ra một đối tượng mới dựa trên đối tượng đã có. Phương thức clone chỉ đơn giản là gọi copy constructor và truyền vào đối tượng hiện tại.

Prototype Registry

Xây dựng một class có factory method giúp quản lý các prototype thường dùng:

class ComputerPrototypeRegistry {
private:
	std::vector<BaseComputer*> _prototypes;
 
public:
	ComputerPrototypeRegistry() {
		_prototypes.push_back(new GamingComputer());
		_prototypes.push_back(new WorkingComputer());
	}
 
	BaseComputer* createPC(ComputerTypes type) {
		return _prototypes[type]->clone();
	}
};

Usage

Gọi sử dụng như sau:

void TestPrototype() {
	ComputerPrototypeRegistry* computerRegistry = new ComputerPrototypeRegistry();
	BaseComputer* computer = computerRegistry->createPC(ComputerTypes::GAMING);
 
	std::cout << "Prototype computer's price: " << computer->getPrice() << "\n";
	computer->increasePrice(10);
	std::cout << "New prototype computer's price: " << computer->getPrice() << "\n";
 
	BaseComputer* clonedComputer = computer->clone();
	std::cout << "Cloned computer's price: " << clonedComputer->getPrice() << "\n";
}

Output:

Prototype computer's price: 0
New prototype computer's price: 10
Cloned computer's price: 10

Applicability

Sử dụng prototype pattern khi:

  • Ta không muốn phụ thuộc vào các concrete class của đối tượng mà ta muốn sao chép.
  • Giảm số lượng các class con mà chỉ khác nhau cách khởi tạo.

Pros and Cons

ProsCons
Có thể sao chép đối tượng mà không phụ thuộc vào concrete class của đối tượng đóCloning các đối tượng có tham chiếu vòng có thể khó khăn
Giảm bớt các đoạn code khởi tạo khi sao chép các prototype có sẵn
Tạo ra các đối tượng phức tạp dễ dàng hơn

Resources