Lập trình · Mẫu thiết kế
Mẫu Factory Method giải thích kèm ví dụ
Trả lời nhanh
Factory Method là mẫu thiết kế khởi tạo định nghĩa một phương thức để tạo đối tượng nhưng để các lớp con quyết định lớp cụ thể nào sẽ được khởi tạo. Lớp cơ sở viết logic của mình dựa trên một interface sản phẩm và gọi phương thức factory, như createTransport(), thay vì new. Nhờ đó bạn thêm được kiểu sản phẩm mới chỉ bằng cách thêm một lớp con, mà không sửa mã đang dùng các sản phẩm.
Mục đích
Định nghĩa một interface để tạo đối tượng, nhưng để các lớp con quyết định lớp nào sẽ được khởi tạo. Factory Method cho phép một lớp hoãn việc khởi tạo cho các lớp con. Đây là một trong năm mẫu khởi tạo trong cuốn sách Gang of Four (GoF), và là mẫu khởi tạo duy nhất có phạm vi lớp, vì nó dựa trên kế thừa.
Vấn đề nó giải quyết
Hãy hình dung một module thông báo. Nó định dạng thông điệp, gửi đi và ghi nhật ký kết quả. Ban đầu nó chỉ gửi email, nên mã gọi new EmailChannel() trực tiếp. Sau đó sản phẩm cần SMS, rồi thông báo đẩy. Mỗi kênh mới đồng nghĩa với việc sửa cùng một lớp và thêm một nhánh if, dù các bước định dạng và ghi nhật ký không bao giờ thay đổi.
Quy trình gửi là ổn định; chỉ loại kênh thay đổi. Factory Method chuyển quyết định thay đổi duy nhất, tức là tạo lớp nào, vào một phương thức có thể ghi đè.
Cấu trúc và các thành phần tham gia
| Thành phần | Trách nhiệm | Trong ví dụ |
|---|---|---|
| Product | Interface của các đối tượng mà phương thức factory tạo ra. | Channel |
| ConcreteProduct | Triển khai interface Product. | EmailChannel, SmsChannel |
| Creator | Khai báo phương thức factory và chứa logic dùng sản phẩm. | Notifier |
| ConcreteCreator | Ghi đè phương thức factory để trả về một ConcreteProduct cụ thể. | EmailNotifier, SmsNotifier |
Triển khai bằng TypeScript
interface Channel {
send(to: string, text: string): string;
}
class EmailChannel implements Channel {
send(to: string, text: string): string { return `email to ${to}: ${text}`; }
}
class SmsChannel implements Channel {
send(to: string, text: string): string { return `sms to ${to}: ${text.slice(0, 20)}`; }
}
abstract class Notifier {
// The factory method: subclasses choose the concrete product.
protected abstract createChannel(): Channel;
// Shared logic that works with any Channel.
notify(to: string, text: string): void {
const channel = this.createChannel();
const result = channel.send(to, text.trim());
console.log("[log]", result);
}
}
class EmailNotifier extends Notifier {
protected createChannel(): Channel { return new EmailChannel(); }
}
class SmsNotifier extends Notifier {
protected createChannel(): Channel { return new SmsChannel(); }
}
const notifiers: Notifier[] = [new EmailNotifier(), new SmsNotifier()];
for (const n of notifiers) n.notify("ada", " Your order has shipped today ");
// [log] email to ada: Your order has shipped today
// [log] sms to ada: Your order has shippGiờ đây thêm thông báo đẩy chỉ cần viết PushChannel và PushNotifier. Phương thức notify, và mọi nơi gọi nó, giữ nguyên.
Trước khi áp dụng mẫu, quyết định tạo đối tượng bị chôn trong logic dùng chung:
class Notifier {
constructor(private kind: "email" | "sms") {}
notify(to: string, text: string): void {
// every new channel edits this method
const result = this.kind === "email"
? `email to ${to}: ${text}`
: `sms to ${to}: ${text.slice(0, 20)}`;
console.log("[log]", result);
}
}Một lựa chọn nhẹ hơn: truyền hàm factory
Factory Method dùng kế thừa. Trong TypeScript bạn thường đạt được cùng độ linh hoạt bằng composition, bằng cách truyền một hàm tạo vào hàm khởi tạo. Cách này tránh một lớp con cho mỗi sản phẩm:
interface Channel { send(to: string, text: string): string; }
class Notifier {
constructor(private createChannel: () => Channel) {}
notify(to: string, text: string): void {
console.log("[log]", this.createChannel().send(to, text));
}
}
const push = new Notifier(() => ({ send: (to, text) => `push to ${to}: ${text}` }));
push.notify("ada", "Hello"); // [log] push to ada: HelloDùng phiên bản lớp con khi mỗi creator vốn đã có lớp riêng vì lý do khác, hoặc khi framework mong bạn mở rộng một lớp cơ sở. Dùng phiên bản hàm khi khác biệt duy nhất giữa các biến thể là đối tượng chúng tạo ra.
Khi nào nên dùng, và khi nào không
Dùng mẫu Factory Method khi:
- Một lớp chứa thuật toán ổn định nhưng không thể biết trước nó phải tạo những đối tượng cụ thể nào.
- Bạn viết framework hoặc thư viện và muốn người dùng cắm các lớp sản phẩm của riêng họ bằng cách kế thừa.
- Bạn muốn giữ logic tạo của một kiểu ở một chỗ thay vì rải các lời gọi
newkhắp nơi.
Tránh nó khi:
- Chỉ có một kiểu sản phẩm và không có dấu hiệu thực sự về kiểu thứ hai. Dùng
newtrực tiếp rõ ràng hơn. - Lý do duy nhất là để có thể kiểm thử. Truyền phụ thuộc qua hàm khởi tạo thì đơn giản hơn.
- Bạn sẽ tạo một cây kế thừa creator song song phản chiếu từng sản phẩm một mà không có logic nào khác.
Ứng dụng thực tế
- Collection của Java:
Iterable.iterator()được khai báo một lần, và mỗi lớp collection (ArrayList,HashSetvà các lớp khác) ghi đè để trả về lớp iterator riêng. Cuốn sách GoF mô tả cách dùng Factory Method này cho duyệt đa hình. - Iterable của JavaScript: mọi iterable triển khai
[Symbol.iterator](), một phương thức trả về iterator mới phù hợp với collection đó. - Các framework giao diện thường cho lớp con ghi đè một phương thức tạo component, ô hay view, trong khi lớp cơ sở kiểm soát bố cục và vòng đời.
Các mẫu liên quan
| Mẫu | Khác nhau thế nào |
|---|---|
| Abstract Factory | Tạo cả một họ sản phẩm liên quan thông qua một đối tượng có nhiều phương thức tạo. Các phương thức của nó thường được triển khai dưới dạng factory method. |
| Template Method | Factory Method thường được gọi từ một template method, như notify gọi createChannel ở trên. |
| Prototype | Tạo đối tượng bằng cách sao chép một thể hiện thay vì kế thừa creator, nên không cần cây creator song song. |
| Builder | Tập trung xây dựng từng bước một đối tượng phức tạp, không phải chọn lớp nào để tạo. |
Câu hỏi thường gặp
Khác nhau giữa Factory Method và Abstract Factory là gì?
Factory Method là một phương thức đơn mà các lớp con ghi đè để tạo một sản phẩm. Abstract Factory là một đối tượng có nhiều phương thức tạo, sinh ra một họ sản phẩm liên quan phải dùng cùng nhau.
Hàm factory đơn giản có giống Factory Method không?
Không hẳn. Hàm như createShape(kind) với switch bên trong thường được gọi là simple factory. Factory Method của GoF dựa vào việc lớp con ghi đè phương thức tạo, nhưng cả hai đều giấu new khỏi client.
Factory Method có tuân theo nguyên lý đóng/mở không?
Có. Bạn thêm sản phẩm mới bằng cách thêm một lớp sản phẩm mới và một lớp con creator mới, mà không đổi logic hiện có của creator.
Phương thức factory có bắt buộc là abstract không?
Không. Lớp cơ sở có thể cung cấp cách triển khai mặc định trả về một sản phẩm chuẩn, và các lớp con chỉ ghi đè khi cần thứ khác.