cv.cm

Lập trình · Mẫu thiết kế

Mẫu Prototype giải thích kèm ví dụ

6 phút đọc

Trả lời nhanh

Prototype là mẫu thiết kế khởi tạo tạo đối tượng mới bằng cách sao chép một thể hiện có sẵn, tức là nguyên mẫu (prototype), thay vì xây từ đầu bằng new. Mỗi nguyên mẫu triển khai một phương thức clone(), nên mã client có thể nhân bản một đối tượng mà không cần biết lớp cụ thể của nó. Nó hữu ích khi việc cấu hình đối tượng tốn kém hoặc có nhiều biến thể cài sẵn. Cạm bẫy chính là sao chép nông: các đối tượng lồng bên trong cũng phải được sao chép, nếu không bản sao và bản gốc sẽ dùng chung chúng.

Mục đích

Xác định loại đối tượng cần tạo bằng một thể hiện nguyên mẫu, và tạo đối tượng mới bằng cách sao chép nguyên mẫu này. Prototype là một trong năm mẫu khởi tạo trong cuốn sách Gang of Four (GoF).

Vấn đề nó giải quyết

Hãy hình dung một trình soạn thảo sơ đồ với bảng màu các hình cài sẵn: hộp cảnh báo đỏ, ghi chú xanh dương, huy hiệu "xong" xanh lá. Mỗi mẫu cài sẵn có kích thước, màu sắc, phông chữ và danh sách quy tắc kiểu. Khi người dùng kéo một mẫu vào canvas, trình soạn thảo cần một hình mới, độc lập, bắt đầu với đúng những thiết lập đó.

Không có mẫu này, trình soạn thảo sẽ cần một hàm tạo riêng cho mỗi mẫu cài sẵn, và phải biết mọi lớp hình cụ thể. Tệ hơn, các mẫu cài sẵn do người dùng định nghĩa lúc chạy hoàn toàn không thể tạo được, vì không có mã nào biết trước thiết lập của chúng. Với Prototype, trình soạn thảo chỉ cần gọi preset.clone().

Cấu trúc và các thành phần tham gia

Thành phầnTrách nhiệmTrong ví dụ
PrototypeKhai báo interface để tự nhân bản.Shape với clone()
ConcretePrototypeTriển khai clone() bằng cách sao chép trạng thái của chính nó.Box
ClientTạo đối tượng mới bằng cách yêu cầu một nguyên mẫu tự nhân bản.mã của trình soạn thảo
Prototype registry (tùy chọn)Lưu các nguyên mẫu có tên để client tra cứu.map palette

Triển khai bằng TypeScript

clone() sao chép mọi trường, kể cả đối tượng và mảng lồng bên trong, nên bản sao độc lập với bản gốc:

interface Shape {
  clone(): Shape;
  describe(): string;
}

class Box implements Shape {
  constructor(
    public width: number,
    public height: number,
    public style: { fill: string; border: string },
    public tags: string[],
  ) {}

  clone(): Box {
    // copy nested objects too, so the clone shares nothing mutable
    return new Box(this.width, this.height, { ...this.style }, [...this.tags]);
  }

  describe(): string {
    return `${this.width}x${this.height} ${this.style.fill} [${this.tags.join(", ")}]`;
  }
}

// A registry of named prototypes, which could also be filled at run time.
const palette = new Map<string, Shape>([
  ["warning", new Box(120, 40, { fill: "red", border: "black" }, ["alert"])],
  ["note", new Box(160, 80, { fill: "blue", border: "none" }, [])],
]);

const a = palette.get("warning")!.clone() as Box;
a.width = 200;
a.tags.push("urgent");
console.log(a.describe());                        // 200x40 red [alert, urgent]
console.log(palette.get("warning")!.describe());  // 120x40 red [alert]

Trình soạn thảo không bao giờ nêu tên Box. Một mẫu cài sẵn mới là một mục mới trong registry, không phải một lớp mới hay một hàm tạo mới.

Sao chép nông so với sao chép sâu

Lỗi phổ biến nhất với Prototype là bản nhân bản dùng chung các đối tượng lồng bên trong với bản gốc. Sao chép bằng spread là nông: nó chỉ sao chép cấp trên cùng.

const original = { name: "note", style: { fill: "blue" } };

const shallow = { ...original };
shallow.style.fill = "green";
console.log(original.style.fill); // green (shared object was changed)

const deep = structuredClone(original);
deep.style.fill = "yellow";
console.log(original.style.fill); // green (unchanged)

structuredClone() có sẵn trong các trình duyệt hiện đại và Node.js, sao chép sâu dữ liệu thuần, gồm đối tượng lồng, mảng, Map, Set và Date. Nó có hai giới hạn đáng kể ở đây:

  • Thể hiện của lớp trở về thành đối tượng thuần: bản sao mất prototype của nó, nên các phương thức như describe() bị thiếu.
  • Hàm và nút DOM không thể nhân bản; structuredClone() ném DataCloneError.

Khi nào nên dùng, và khi nào không

Dùng mẫu Prototype khi:

  • Đối tượng tốn kém để thiết lập (nạp từ tệp, tính toán, cấu hình qua nhiều bước) và bạn cần nhiều bản sao tương tự.
  • Tập các biến thể được định nghĩa lúc chạy, ví dụ mẫu cài sẵn hoặc template do người dùng tạo.
  • Bạn muốn tạo đối tượng mà không phụ thuộc vào lớp cụ thể của chúng, và không cần cây factory song song.

Tránh nó khi:

  • Đối tượng rẻ và đơn giản để tạo. Gọi hàm khởi tạo hoặc hàm factory rõ ràng hơn.
  • Đối tượng giữ tài nguyên không nên bị nhân đôi, như tệp đang mở, socket hay ID duy nhất. Nhân bản chúng cần xử lý đặc biệt.
  • Đối tượng có tham chiếu vòng và quyền sở hữu phức tạp, khiến việc sao chép sâu đúng rất khó.

Ứng dụng thực tế

  • Bản thân JavaScript dựa trên prototype: Object.create(proto) tạo đối tượng mới kế thừa từ đối tượng có sẵn. Điều này liên quan nhưng không giống hệt mẫu thiết kế, vì đối tượng mới ủy thác cho prototype của nó thay vì sao chép nó.
  • DOM: node.cloneNode(true) sao chép sâu một phần tử và các phần tử con của nó, là cách thông dụng để tạo hàng loạt các mảnh giao diện lặp lại.
  • Java: các lớp có thể triển khai interface Cloneable và ghi đè Object.clone(), dù nhiều lập trình viên Java thích hàm khởi tạo sao chép (copy constructor) hơn.
  • Game engine và trình soạn thảo thường tạo đối tượng mới từ các template cấu hình sẵn (thường gọi là prefab).

Các mẫu liên quan

  • Mẫu Abstract Factory: một factory có thể lưu một tập nguyên mẫu và tạo sản phẩm bằng cách nhân bản chúng, thay vì cần một lớp con factory cho mỗi họ.
  • Mẫu Factory Method: tạo đối tượng thông qua các lớp con; Prototype tránh những lớp con đó bằng cách sao chép thể hiện.
  • Mẫu Composite và mẫu Decorator: các thiết kế dùng nhiều chúng thường hưởng lợi từ Prototype, vì sao chép một cấu trúc đã cấu hình dễ hơn dựng lại.
  • Mẫu Memento: cũng sao chép trạng thái của đối tượng, nhưng để khôi phục sau này, không phải để tạo đối tượng độc lập mới.

Câu hỏi thường gặp

Mẫu Prototype có giống prototype của JavaScript không?

Không. Prototype của JavaScript là cơ chế ngôn ngữ cho kế thừa: một đối tượng ủy thác các thuộc tính thiếu cho prototype của nó. Mẫu Prototype tạo một bản sao độc lập của đối tượng.

clone() nên sao chép sâu hay nông?

Thường là sâu đối với dữ liệu lồng có thể thay đổi, để bản nhân bản và bản gốc không ảnh hưởng lẫn nhau. Giá trị bất biến và tài nguyên chỉ đọc dùng chung có thể yên tâm giữ chung.

Tôi có thể dùng structuredClone() để triển khai clone() không?

Chỉ với đối tượng dữ liệu thuần. Với thể hiện của lớp, structuredClone() trả về đối tượng thuần không có phương thức, nên hãy tự viết logic sao chép hoặc dựng lại thể hiện bằng hàm khởi tạo của nó.

Khi nào Prototype tốt hơn factory?

Khi các biến thể nhiều, được cấu hình lúc chạy, hoặc tốn kém để thiết lập. Khi đó nhân bản một thể hiện dựng sẵn đơn giản hơn viết một hàm tạo cho từng biến thể.

Tất cả hướng dẫn lập trình