Factory Method Pattern says that just define an interface or abstract class for creating an object but let the subclasses decide which class to instantiate.

Problem

Giả sử có một anh lập trình viên may mắn sở hữu chiếc MacBook được công ty phát cho để code ra những dòng code thần thánh. Ta sẽ mô hình hóa như sau:

if (inCompany) {
	cppMacBook* laptop = new MacBook();
}

Thế nhưng, anh chàng này vốn là một người lăng nhăng. Vào một hôm trăng thanh gió mát nọ, anh lén chiếc MacBook ở nhà để đi ra ngoài mua con Legion 5 quốc dân.

// ...
else if (atNight) {
	LegionFive* laptop = new LegionFive();
}

Đúng là một người đào hoa, trong lúc đi công tác, anh đồng thời cũng làm quen được một em MSI laptop chip Inteo cực hút điện. Dẫu vậy, chàng ta vẫn thích, gu mặn thật đấy!

// ...
else if (onBusinessTrip) {
	MSI* laptop = new MSI();
}

Có thể thấy, client code bị phụ thuộc vào rất nhiều class và ta cần phải biết cách sử dụng constructor của các class đó. Điều này làm cho code trở nên khó bảo trì hơn.

Solution

Giải pháp cho vấn đề này là gom nhóm các direct constructor thành một factory method dùng chung. Khi đó, client code có thể tạo ra một object mà không cần biết object đó được tạo ra như thế nào.

Các bước triển khai:

  1. Tạo ra một interface như là một base class cho các derived class.
  2. Thiết lập các chuỗi định danh cho các loại derived class.
  3. Tạo ra một factory class có chứa factory method dùng để tạo ra các subclass dựa trên chuỗi định danh truyền vào.

Implementation

Interface

Xây dựng interface Laptop như sau:

class Laptop {
public:
	virtual string description() = 0;
};

Nhằm bắt buộc các phương thức của lớp con phải được implement ở trong C++, ta cần biến các hàm trong Laptop thành thuần ảo.

Implement đối với mỗi class:

class MacBookLaptop : public Laptop {
public:
	std::string description() {
		return "This is a MacBook laptop";
	}
};
 
class LegionLaptop : public Laptop {
public:
	std::string description() {
		return "This is a Legion laptop";
	}
};
 
class MsiLaptop : public Laptop {
public:
	std::string description() {
		return "This is a MSI laptop";
	}
};

Instance Type

Có nhiều cách để phân biệt các loại instance, chẳng hạn ta phân biệt bằng cách truyền vào hàm tạo một chuỗi đại diện cho loại instance ta cần tạo. Ví dụ, với MacBook sẽ là "MACBOOK", Legion là "LEGION" và MSI sẽ là "MSI".

Ngoài ra, ta có thể sử dụng kiểu enum (hiểu đơn giản là kiểu liệt kê).

enum LaptopTypes {
	MACBOOK,
	LEGION,
	MSI
};

Giá trị của các danh từ bên trong khai báo enum được gán mặc định và tăng dần, bắt đầu từ 0. Tuy nhiên, ta cũng có thể customize lại giá trị của nó bắng toán tử gán bằng

Có thể hiểu đoạn code trên tương tự việc chúng ta define nhiều macro liên tục:

#define MACBOOK 0;
#define LEGION 1;
#define MSI 2;

Bản chất của chúng là các macro, do đó chúng sẽ không tạo ra vùng nhớ để chứa các giá trị. Chỉ khi nào sử dụng đến thì các giá trị mới được thay vào nơi gọi macro. Có thể tham khảo thêm ở bài Compilation.

Factory Class

Sau cùng, ta tạo một class đại diện cho một cái nhà máy, hay còn gọi là creator class. Đồng thời tạo ra một static factory method để sản sinh ra những instance thuộc nhiều loại laptop khác nhau.

class LaptopFactory {
public:
	static Laptop* createLaptop(int type) {
		switch (type) {
		case LaptopTypes::MACBOOK:
			return new MacBookLaptop();
		case LaptopTypes::LEGION:
			return new LegionLaptop();
		case LaptopTypes::MSI:
			return new MsiLaptop();
		default:
			return new MacBookLaptop(); // default is MacBook
		}
	}
};

Test

void TestFactory() {
	Laptop* macbookLaptop = LaptopFactory::createLaptop(LaptopTypes::MACBOOK);
	std::cout << macbookLaptop->description() << std::endl;
 
	Laptop* legionLaptop = LaptopFactory::createLaptop(LaptopTypes::LEGION);
	std::cout << legionLaptop->description() << std::endl;
 
	Laptop* msiLaptop = LaptopFactory::createLaptop(LaptopTypes::MSI);
	std::cout << msiLaptop->description() << std::endl;
}

Output

This is a MacBook laptop
This is a Legion laptop
This is a MSI laptop

Tip

Lớp Factory của một giao diện cụ thể thường được dùng đi dùng lại. Vì thế, chúng ta nên cài đặt Singleton cho lớp này.

Repository Factory

Ba bước trên là để tạo ra một creator class quản lý việc khởi tạo của duy nhất một instance. Trong trường hợp vừa muốn quản lý việc khởi tạo và cả lưu trữ, ta sử dụng Repository Factory.

Repository Factory Class có thuộc tính là một danh sách các instance. Ngoài factory method thì nó còn có thêm một phương thức để lấy ra instance từ danh sách dựa vào key cho trước.

Applicability

Sử dụng factory method pattern khi:

  • Ta cần đưa trách nhiệm của việc khởi tạo một lớp từ phía người dùng (client) sang lớp factory.
  • Ta không biết trước được sau này sẽ có thêm subclass nào nữa. Nếu cần mở rộng, ta chỉ cần tạo ra subclass và thêm vào đoạn code khởi tạo cho subclass này ở trong factory method.

Pros and Cons

ProsCons
Giảm sự phụ thuộc giữa creator code và các concrete productCode phức tạp hơn do phải cài đặt nhiều subclass để triển khai pattern
Single responsibility: gom các creator code lại 1 chỗ trong chương trình
Open/closed: có thể thêm product mới mà không làm thay đổi code cũ

Resources