1. Prototype Pattern là gì?

Prototype Pattern là một mẫu thiết kế tạo đối tượng cho phép sao chép các đối tượng hiện có mà không làm cho code phụ thuộc vào các lớp cụ thể của chúng. Mẫu này đặc biệt hữu ích khi việc tạo một đối tượng mới tốn kém tài nguyên hơn việc sao chép một đối tượng hiện có.

Các thành phần chính trong Prototype Pattern:

  • Prototype: Interface khai báo phương thức clone()
  • Concrete Prototype: Các lớp cụ thể triển khai phương thức clone()
  • Client: Tạo đối tượng mới bằng cách yêu cầu prototype thực hiện clone

JavaScript là một ngôn ngữ prototype-based, vì vậy Prototype Pattern có thể được triển khai một cách tự nhiên và hiệu quả.

Structure

Prototype Pattern có cấu trúc như sau:


  classDiagram
    class Prototype {
        <<interface>>
        +clone() Prototype
    }

    class ConcretePrototypeA {
        -fieldA
        +clone() ConcretePrototypeA
    }

    class ConcretePrototypeB {
        -fieldB
        +clone() ConcretePrototypeB
    }

    class Client {
        +operation()
    }

    Prototype <|.. ConcretePrototypeA
    Prototype <|.. ConcretePrototypeB
    Client --> Prototype : uses

    note for Prototype "Interface hoặc abstract class<br>với phương thức clone()"
    note for ConcretePrototypeA "Triển khai phương thức clone()<br>để sao chép đối tượng hiện tại"

Các thành phần chính:

  • Prototype: Interface hoặc abstract class định nghĩa phương thức clone() để sao chép đối tượng
  • Concrete Prototype: Các lớp cụ thể triển khai phương thức clone(), tạo bản sao của chính nó
  • Client: Sử dụng phương thức clone() để tạo các đối tượng mới

Trong JavaScript, Prototype Pattern được hỗ trợ tự nhiên thông qua prototype chain và các phương thức như Object.create() hoặc structuredClone().

2. Triển khai trong JavaScript

2.1 Triển khai cơ bản

class Shape {
  constructor() {
    this.type = "";
    this.color = "";
  }

  clone() {
    const clone = Object.create(Object.getPrototypeOf(this));
    clone.type = this.type;
    clone.color = this.color;
    return clone;
  }
}

class Rectangle extends Shape {
  constructor(width, height, color) {
    super();
    this.type = "Rectangle";
    this.width = width;
    this.height = height;
    this.color = color;
  }

  clone() {
    const clone = super.clone();
    clone.width = this.width;
    clone.height = this.height;
    return clone;
  }

  calculateArea() {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor(radius, color) {
    super();
    this.type = "Circle";
    this.radius = radius;
    this.color = color;
  }

  clone() {
    const clone = super.clone();
    clone.radius = this.radius;
    return clone;
  }

  calculateArea() {
    return Math.PI * this.radius * this.radius;
  }
}

// Usage
const redRectangle = new Rectangle(10, 5, "red");
const blueRectangle = redRectangle.clone();
blueRectangle.color = "blue";

console.log(redRectangle.calculateArea()); // 50
console.log(blueRectangle.calculateArea()); // 50
console.log(redRectangle.color); // 'red'
console.log(blueRectangle.color); // 'blue'

const greenCircle = new Circle(5, "green");
const yellowCircle = greenCircle.clone();
yellowCircle.color = "yellow";
yellowCircle.radius = 7;

console.log(greenCircle.calculateArea()); // ~78.54
console.log(yellowCircle.calculateArea()); // ~153.94

2.2 Triển khai với Registry

class ShapeRegistry {
  constructor() {
    this.shapes = new Map();
  }

  register(key, shape) {
    this.shapes.set(key, shape);
  }

  unregister(key) {
    this.shapes.delete(key);
  }

  clone(key) {
    const shape = this.shapes.get(key);
    if (!shape) {
      throw new Error(`Shape with key ${key} not found in registry`);
    }
    return shape.clone();
  }
}

// Usage
const registry = new ShapeRegistry();

const prototypeRectangle = new Rectangle(0, 0, "white");
const prototypeCircle = new Circle(0, "white");

registry.register("rectangle", prototypeRectangle);
registry.register("circle", prototypeCircle);

const customRectangle = registry.clone("rectangle");
customRectangle.width = 15;
customRectangle.height = 8;
customRectangle.color = "purple";

const customCircle = registry.clone("circle");
customCircle.radius = 10;
customCircle.color = "orange";

console.log(customRectangle.calculateArea()); // 120
console.log(customCircle.calculateArea()); // ~314.16

3. Triển khai trong TypeScript

TypeScript cho phép chúng ta định nghĩa interface và kiểu dữ liệu rõ ràng hơn:

interface Prototype<T> {
  clone(): T;
}

interface Document extends Prototype<Document> {
  name: string;
  content: string[];
  metadata: {
    author: string;
    createdAt: Date;
    tags: string[];
  };
}

class TextDocument implements Document {
  name: string;
  content: string[];
  metadata: {
    author: string;
    createdAt: Date;
    tags: string[];
  };

  constructor(
    name: string,
    content: string[],
    author: string,
    tags: string[] = []
  ) {
    this.name = name;
    this.content = content;
    this.metadata = {
      author,
      createdAt: new Date(),
      tags,
    };
  }

  clone(): TextDocument {
    const clone = new TextDocument(
      this.name,
      [...this.content],
      this.metadata.author,
      [...this.metadata.tags]
    );
    clone.metadata.createdAt = new Date(this.metadata.createdAt);
    return clone;
  }

  addContent(text: string): void {
    this.content.push(text);
  }

  addTag(tag: string): void {
    this.metadata.tags.push(tag);
  }
}

// Usage
const originalDoc = new TextDocument(
  "Design Patterns",
  ["Introduction to design patterns..."],
  "John Doe",
  ["programming", "design"]
);

const clonedDoc = originalDoc.clone();
clonedDoc.name = "Design Patterns - Copy";
clonedDoc.addContent("More content in the clone...");
clonedDoc.addTag("copy");

console.log(originalDoc.content.length); // 1
console.log(clonedDoc.content.length); // 2
console.log(originalDoc.metadata.tags); // ['programming', 'design']
console.log(clonedDoc.metadata.tags); // ['programming', 'design', 'copy']

4. Ví dụ thực tế: Database Query Builder

Hãy xem xét một ví dụ thực tế về việc sử dụng Prototype Pattern trong một hệ thống backend với Database Query Builder:

interface QueryPrototype<T> {
  clone(): T;
  execute(): Promise<any>;
  getQueryString(): string;
}

interface QueryOptions {
  table: string;
  fields?: string[];
  conditions?: Record<string, any>;
  limit?: number;
  offset?: number;
  orderBy?: string;
  orderDirection?: "ASC" | "DESC";
  joins?: Array<{
    table: string;
    on: string;
    type?: "INNER" | "LEFT" | "RIGHT";
  }>;
}

class DatabaseQuery implements QueryPrototype<DatabaseQuery> {
  private options: QueryOptions;
  private connection: any; // Giả định đây là kết nối database

  constructor(options: QueryOptions, connection?: any) {
    this.options = {
      table: options.table,
      fields: options.fields || ["*"],
      conditions: options.conditions || {},
      limit: options.limit,
      offset: options.offset,
      orderBy: options.orderBy,
      orderDirection: options.orderDirection || "ASC",
      joins: options.joins || [],
    };

    // Trong thực tế, connection sẽ được inject hoặc lấy từ pool
    this.connection = connection || {
      query: async (sql: string, params: any[]) => {
        console.log(`Executing query: ${sql}`);
        console.log(`With params: ${JSON.stringify(params)}`);
        return { rows: [], affectedRows: 0 };
      },
    };
  }

  clone(): DatabaseQuery {
    return new DatabaseQuery({ ...this.options }, this.connection);
  }

  async execute(): Promise<any> {
    const { sql, params } = this.buildQuery();
    return this.connection.query(sql, params);
  }

  getQueryString(): string {
    const { sql } = this.buildQuery();
    return sql;
  }

  private buildQuery(): { sql: string; params: any[] } {
    const {
      table,
      fields,
      conditions,
      limit,
      offset,
      orderBy,
      orderDirection,
      joins,
    } = this.options;
    const params: any[] = [];

    // Build SELECT clause
    let sql = `SELECT ${fields.join(", ")} FROM ${table}`;

    // Build JOIN clauses
    if (joins && joins.length > 0) {
      for (const join of joins) {
        const joinType = join.type || "INNER";
        sql += ` ${joinType} JOIN ${join.table} ON ${join.on}`;
      }
    }

    // Build WHERE clause
    if (Object.keys(conditions).length > 0) {
      sql += " WHERE ";
      const whereClauses = [];

      for (const [key, value] of Object.entries(conditions)) {
        whereClauses.push(`${key} = ?`);
        params.push(value);
      }

      sql += whereClauses.join(" AND ");
    }

    // Build ORDER BY clause
    if (orderBy) {
      sql += ` ORDER BY ${orderBy} ${orderDirection}`;
    }

    // Build LIMIT and OFFSET
    if (limit !== undefined) {
      sql += ` LIMIT ${limit}`;

      if (offset !== undefined) {
        sql += ` OFFSET ${offset}`;
      }
    }

    return { sql, params };
  }

  // Setter methods for modifying the query
  setFields(fields: string[]): DatabaseQuery {
    this.options.fields = fields;
    return this;
  }

  setCondition(key: string, value: any): DatabaseQuery {
    this.options.conditions = { ...this.options.conditions, [key]: value };
    return this;
  }

  setLimit(limit: number): DatabaseQuery {
    this.options.limit = limit;
    return this;
  }

  setOffset(offset: number): DatabaseQuery {
    this.options.offset = offset;
    return this;
  }

  setOrderBy(field: string, direction: "ASC" | "DESC" = "ASC"): DatabaseQuery {
    this.options.orderBy = field;
    this.options.orderDirection = direction;
    return this;
  }

  addJoin(
    table: string,
    on: string,
    type: "INNER" | "LEFT" | "RIGHT" = "INNER"
  ): DatabaseQuery {
    this.options.joins = [...(this.options.joins || []), { table, on, type }];
    return this;
  }
}

class QueryRegistry {
  private queries: Map<string, QueryPrototype<any>>;

  constructor() {
    this.queries = new Map();
  }

  register(key: string, query: QueryPrototype<any>): void {
    this.queries.set(key, query);
  }

  unregister(key: string): void {
    this.queries.delete(key);
  }

  clone(key: string): QueryPrototype<any> {
    const query = this.queries.get(key);
    if (!query) {
      throw new Error(`Query with key ${key} not found`);
    }
    return query.clone();
  }
}

// Usage
const registry = new QueryRegistry();

// Register prototype queries
const userBaseQuery = new DatabaseQuery({
  table: "users",
  fields: ["id", "username", "email", "created_at"],
});

const activeUsersQuery = new DatabaseQuery({
  table: "users",
  fields: ["id", "username", "email", "last_login"],
  conditions: { status: "active" },
  orderBy: "last_login",
  orderDirection: "DESC",
});

const userOrdersQuery = new DatabaseQuery({
  table: "users",
  fields: [
    "users.id",
    "users.username",
    "orders.id as order_id",
    "orders.total",
  ],
  joins: [{ table: "orders", on: "users.id = orders.user_id" }],
});

registry.register("user-base", userBaseQuery);
registry.register("active-users", activeUsersQuery);
registry.register("user-orders", userOrdersQuery);

// Create specific queries from prototypes
const adminUsersQuery = registry.clone("user-base") as DatabaseQuery;
adminUsersQuery.setCondition("role", "admin");
console.log(adminUsersQuery.getQueryString());
// SELECT id, username, email, created_at FROM users WHERE role = ?

const recentActiveUsersQuery = registry.clone("active-users") as DatabaseQuery;
recentActiveUsersQuery.setLimit(10);
console.log(recentActiveUsersQuery.getQueryString());
// SELECT id, username, email, last_login FROM users WHERE status = ? ORDER BY last_login DESC LIMIT 10

const userBigOrdersQuery = registry.clone("user-orders") as DatabaseQuery;
userBigOrdersQuery
  .setCondition("orders.total", 1000)
  .setOrderBy("orders.total", "DESC");
console.log(userBigOrdersQuery.getQueryString());
// SELECT users.id, users.username, orders.id as order_id, orders.total FROM users INNER JOIN orders ON users.id = orders.user_id WHERE orders.total = ? ORDER BY orders.total DESC

5. Khi nào nên sử dụng Prototype Pattern

Prototype Pattern phù hợp trong các tình huống sau:

  1. Khi cần tránh tạo các lớp factory phức tạp
  2. Khi các lớp cần được tạo chỉ khác nhau về cấu hình
  3. Khi việc tạo đối tượng mới tốn kém tài nguyên
  4. Khi cần giảm số lượng lớp con
  5. Khi muốn tránh code bị phụ thuộc vào các lớp cụ thể

Ví dụ thực tế:

  • Sao chép các đối tượng UI phức tạp
  • Tạo các bản sao của đối tượng cấu hình
  • Clone các đối tượng game
  • Tạo các template cho tài liệu
  • Sao chép các đối tượng từ database

6. So sánh với các Pattern khác

So sánh với Factory Pattern

Prototype PatternFactory Pattern
Sao chép đối tượng hiện cóTạo đối tượng mới từ đầu
Không cần biết chi tiết lớpCần biết lớp cụ thể
Hiệu quả với đối tượng phức tạpPhù hợp với đối tượng đơn giản
Giảm số lượng lớpCó thể tạo nhiều lớp con

So sánh với Builder Pattern

Prototype PatternBuilder Pattern
Sao chép đối tượng hoàn chỉnhXây dựng đối tượng từng bước
Nhanh hơn với đối tượng phức tạpKiểm soát chi tiết quá trình tạo
Duy trì trạng thái đối tượngTạo đối tượng mới mỗi lần

7. Ưu điểm và nhược điểm

Ưu điểm:

  • Giảm code trùng lặp khi tạo đối tượng
  • Tránh khởi tạo phức tạp cho các đối tượng
  • Tạo đối tượng động trong runtime
  • Thay thế kế thừa phức tạp bằng sao chép
  • Tạo đối tượng mà không cần biết chi tiết lớp

Nhược điểm:

  • Phức tạp trong việc clone đối tượng có tham chiếu vòng
  • Khó khăn khi clone đối tượng có phương thức private
  • Có thể gây nhầm lẫn giữa shallow và deep copy
  • Tăng độ phức tạp khi cần quản lý registry
  • Khó debug khi có nhiều bản sao

8. Kết luận

Prototype Pattern là một công cụ mạnh mẽ trong JavaScript và TypeScript, đặc biệt khi làm việc với các đối tượng phức tạp cần được sao chép thường xuyên. Pattern này tận dụng được bản chất prototype-based của JavaScript và cung cấp một cách tiếp cận linh hoạt để tạo các đối tượng mới.

Khi quyết định sử dụng Prototype Pattern, hãy cân nhắc độ phức tạp của đối tượng và tần suất cần sao chép. Đối với các đối tượng đơn giản, việc tạo mới có thể là lựa chọn tốt hơn. Tuy nhiên, đối với các đối tượng phức tạp hoặc khi cần tránh phụ thuộc vào các lớp cụ thể, Prototype Pattern là một giải pháp hiệu quả.