cv.cm

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

Mẫu Template Method (Template Method pattern) giải thích kèm ví dụ

6 phút đọc

Trả lời nhanh

Template Method là một mẫu thiết kế hành vi (behavioral design pattern): nó định nghĩa bộ khung của một thuật toán trong một phương thức của lớp cơ sở và cho phép lớp con ghi đè từng bước cụ thể mà không đổi thứ tự chung. Lớp cơ sở quyết định điều gì xảy ra và khi nào; lớp con quyết định một số bước được làm như thế nào. Những bước tùy chọn có cách triển khai mặc định được gọi là hook.

Mục đích và vấn đề cần giải quyết

Mục đích: định nghĩa bộ khung của một thuật toán trong một thao tác, hoãn một số bước cho các lớp con. Mẫu Template Method cho phép lớp con định nghĩa lại một số bước của thuật toán mà không thay đổi cấu trúc của nó. Đây là một trong các mẫu hành vi trong cuốn sách của nhóm Gang of Four (GoF).

Hãy hình dung một công cụ báo cáo xuất dữ liệu ra CSV và JSON. Cả hai bộ xuất làm cùng những việc theo cùng thứ tự: tải các dòng dữ liệu, kiểm tra xem có dòng nào không, định dạng chúng và thêm phần chân trang với số dòng. Chỉ phần định dạng là khác nhau. Nếu viết riêng từng bộ xuất, các bước chung bị sao chép, và một lần sửa ở bộ này (ví dụ xử lý tốt hơn kết quả rỗng) rất dễ bị quên ở bộ kia.

Với mẫu Template Method, lớp cơ sở Exporter chứa phương thức export với chuỗi bước cố định. Các bước thay đổi là phương thức trừu tượng. CsvExporter và JsonExporter chỉ cần triển khai những bước đó.

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

Vai tròTrách nhiệm
Abstract class (lớp trừu tượng)Triển khai template method, gọi các bước theo thứ tự cố định; khai báo các bước trừu tượng và có thể cung cấp hook
Template method (phương thức khuôn mẫu)Phương thức công khai định nghĩa thuật toán; thường không dành để ghi đè
Primitive operation (thao tác nguyên thủy)Một bước trừu tượng mà mọi lớp con đều phải triển khai
HookMột bước có cách triển khai mặc định (thường để trống) mà lớp con có thể ghi đè
Concrete class (lớp cụ thể)Triển khai các thao tác nguyên thủy và ghi đè hook khi cần

Thiết kế này đảo ngược quyền điều khiển: lớp cơ sở gọi các phương thức của lớp con, chứ không phải ngược lại. Đôi khi người ta gọi đây là nguyên lý Hollywood: "đừng gọi chúng tôi, chúng tôi sẽ gọi bạn".

Triển khai bằng TypeScript

export là template method. format là một thao tác nguyên thủy mà lớp con phải triển khai, còn header là một hook có mặc định để trống.

type Row = { name: string; score: number };

abstract class Exporter {
  // The template method: fixed order of steps
  export(rows: Row[]): string {
    if (rows.length === 0) return "no data";
    const lines = [this.header(), this.format(rows), "rows: " + rows.length];
    return lines.filter((line) => line !== "").join("\n");
  }

  protected abstract format(rows: Row[]): string; // required step
  protected header(): string { return ""; }       // hook: optional step
}

class CsvExporter extends Exporter {
  protected header() { return "name,score"; }
  protected format(rows: Row[]) {
    return rows.map((r) => r.name + "," + r.score).join("\n");
  }
}

class JsonExporter extends Exporter {
  protected format(rows: Row[]) {
    return JSON.stringify(rows);
  }
}

const rows = [{ name: "Ana", score: 9 }, { name: "Ben", score: 7 }];
console.log(new CsvExporter().export(rows));
// name,score
// Ana,9
// Ben,7
// rows: 2
console.log(new JsonExporter().export(rows));
// [{"name":"Ana","score":9},{"name":"Ben","score":7}]
// rows: 2
console.log(new JsonExporter().export([])); // no data

Việc kiểm tra dữ liệu rỗng và phần chân trang nằm ở một chỗ duy nhất. Một bộ xuất XML mới chỉ cần triển khai format là có sẵn phần còn lại.

Cùng ý tưởng bằng kết hợp (composition)

Kế thừa không phải là cách duy nhất. Bạn có thể truyền các bước thay đổi dưới dạng hàm, khiến mẫu này trở thành họ hàng gần của mẫu Strategy và tránh được cây phân cấp lớp:

type Row = { name: string; score: number };
type Steps = { format: (rows: Row[]) => string; header?: () => string };

function exportRows(rows: Row[], steps: Steps): string {
  if (rows.length === 0) return "no data";
  const lines = [steps.header?.() ?? "", steps.format(rows), "rows: " + rows.length];
  return lines.filter((line) => line !== "").join("\n");
}

console.log(exportRows([{ name: "Ana", score: 9 }], { format: (r) => r[0].name }));
// Ana
// rows: 1

Nên dùng dạng này khi các bước nhỏ và bạn không cần trạng thái dùng chung trong lớp cơ sở, hoặc khi một bộ xuất cần kết hợp các bước từ nhiều nguồn.

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

  • Nên dùng khi nhiều lớp chạy cùng một thuật toán và chỉ khác nhau ở vài bước, và bạn muốn loại bỏ phần mã trùng lặp.
  • Nên dùng trong các framework cần kiểm soát thứ tự thao tác nhưng vẫn cho người dùng tùy biến từng bước, như setup và teardown trong kiểm thử.
  • Nên dùng hook để cung cấp các điểm mở rộng tùy chọn mà không bắt mọi lớp con phải triển khai.
  • Không nên dùng khi các biến thể lớn hoặc cần thay đổi lúc chạy. Mẫu Strategy, dùng kết hợp, linh hoạt hơn.
  • Tránh cây phân cấp template method quá sâu. Khi lớp con ghi đè các bước của lớp con của lớp con, luồng chạy trở nên khó theo dõi.

Ứng dụng trong thực tế

  • AbstractList của Java: bạn triển khai get(int) và size(), còn lớp cơ sở cung cấp việc duyệt, indexOf, contains và nhiều phương thức khác xây dựng trên hai phương thức đó.
  • InputStream của Java: lớp con triển khai read() đọc từng byte, còn lớp cơ sở triển khai read(byte[], int, int) bằng cách gọi nó (lớp con có thể ghi đè để chạy nhanh hơn).
  • Các framework kiểm thử: JUnit, unittest của Python và những framework khác gọi setUp, phương thức kiểm thử và tearDown theo thứ tự cố định, và bạn ghi đè những bước mình cần.
  • Các framework giao diện có phương thức vòng đời, như componentDidMount trong lớp component của React, gọi các phương thức của bạn tại những thời điểm cố định trong thuật toán của chúng.

Các mẫu liên quan

MẫuKhác với mẫu Template Method như thế nào
StrategyStrategy thay cả thuật toán bằng kết hợp lúc chạy; Template Method giữ cố định thuật toán và thay đổi từng phần của nó qua kế thừa lúc biên dịch.
Factory MethodFactory method thường là một trong các bước của template method: bước tạo ra một đối tượng.
Hook function và callbackTruyền callback vào một thuật toán cố định là phiên bản dựa trên kết hợp của cùng ý tưởng này.

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

Hook trong mẫu Template Method là gì?

Hook là một bước có cách triển khai mặc định trong lớp cơ sở, thường không làm gì cả. Lớp con có thể ghi đè để thêm hành vi tại điểm đó, nhưng không bắt buộc.

Mẫu Template Method khác mẫu Strategy ở điểm nào?

Template Method dùng kế thừa: lớp cơ sở sở hữu thuật toán và lớp con ghi đè các bước. Strategy dùng kết hợp: context giao cả thuật toán cho một đối tượng hoặc hàm có thể hoán đổi lúc chạy.

Bản thân template method có nên cho ghi đè không?

Không. Mục đích của nó là cố định thứ tự các bước. Trong Java hoặc C#, bạn có thể đánh dấu nó là final hoặc non-virtual; trong TypeScript bạn phải ghi chú trong tài liệu vì không có từ khóa final.

Mẫu Template Method liên quan thế nào đến mẫu Factory Method?

Factory Method thường là một dạng chuyên biệt của Template Method: thuật toán của lớp cơ sở có một bước tạo đối tượng, và lớp con quyết định tạo lớp nào.

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