cv.cm

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

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

6 phút đọc

Trả lời nhanh

Builder là mẫu thiết kế khởi tạo tách việc xây dựng một đối tượng phức tạp khỏi cách biểu diễn cuối cùng của nó, để cùng một quy trình từng bước có thể tạo ra các kết quả khác nhau. Trong mã hiện đại, nó thường xuất hiện dưới dạng fluent API: bạn gọi các phương thức như .url(), .header() và .timeout() trên một builder, rồi .build() trả về một đối tượng hoàn chỉnh đã được kiểm tra. Nó thay thế các hàm khởi tạo dài với nhiều tham số tùy chọn.

Mục đích

Tách việc xây dựng một đối tượng phức tạp khỏi cách biểu diễn của nó để cùng một quy trình xây dựng có thể tạo ra các biểu diễn khác nhau. Builder 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 đối tượng yêu cầu HTTP với URL, phương thức, số header tùy ý, body tùy chọn, thời gian chờ và số lần thử lại. Một hàm khởi tạo cho tất cả những thứ này rất khó gọi đúng:

class HttpRequest {
  constructor(
    readonly url: string,
    readonly method: string,
    readonly headers: Record<string, string>,
    readonly body?: string,
    readonly timeoutMs?: number,
    readonly retries?: number,
  ) {}
}

// Which argument is which? Easy to swap 3 and 5000 by mistake.
const req = new HttpRequest("https://api.example.com", "POST", {}, undefined, 3, 5000);

Đây gọi là hàm khởi tạo kính viễn vọng (telescoping constructor). Nó cũng không có chỗ cho các quy tắc như "yêu cầu GET không được có body", và không thể xây dựng dần dần, ví dụ khi header được thêm ở nhiều nơi trước khi gửi.

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

Thành phầnTrách nhiệmTrong ví dụ
BuilderKhai báo các bước tạo các phần của sản phẩm.Các phương thức của RequestBuilder
ConcreteBuilderTriển khai các bước, giữ trạng thái dở dang và trả về kết quả.RequestBuilder
DirectorTùy chọn. Chạy các bước theo thứ tự cố định để tạo các cấu hình thông dụng.jsonPost()
ProductĐối tượng phức tạp đang được xây dựng.HttpRequest

Triển khai bằng TypeScript

Mỗi bước lưu một giá trị và trả về this, nên các lời gọi có thể nối chuỗi. build() kiểm tra tổ hợp và trả về một sản phẩm bất biến:

interface HttpRequest {
  readonly url: string;
  readonly method: "GET" | "POST";
  readonly headers: Readonly<Record<string, string>>;
  readonly body?: string;
  readonly timeoutMs: number;
}

class RequestBuilder {
  private method: "GET" | "POST" = "GET";
  private headers: Record<string, string> = {};
  private body?: string;
  private timeoutMs = 10_000;

  constructor(private readonly url: string) {}

  post(body: string): this { this.method = "POST"; this.body = body; return this; }
  header(name: string, value: string): this { this.headers[name] = value; return this; }
  timeout(ms: number): this { this.timeoutMs = ms; return this; }

  build(): HttpRequest {
    if (this.timeoutMs <= 0) throw new Error("timeout must be positive");
    return Object.freeze({
      url: this.url, method: this.method, headers: { ...this.headers },
      body: this.body, timeoutMs: this.timeoutMs,
    });
  }
}

const request = new RequestBuilder("https://api.example.com/orders")
  .header("Accept", "application/json")
  .timeout(5_000)
  .build();
console.log(request.method, request.timeoutMs, request.headers);
// GET 5000 { Accept: 'application/json' }

Director chỉ là đoạn mã biết một công thức thông dụng. Trong TypeScript, nó thường là một hàm thuần điều khiển builder:

interface Steps {
  header(name: string, value: string): this;
  post(body: string): this;
}

// Director: a reusable recipe that drives any builder with these steps.
function jsonPost<B extends Steps>(builder: B, data: unknown): B {
  return builder
    .header("Content-Type", "application/json")
    .post(JSON.stringify(data));
}

class LogBuilder implements Steps {
  log: string[] = [];
  header(name: string, value: string): this { this.log.push(`${name}=${value}`); return this; }
  post(body: string): this { this.log.push(`POST ${body}`); return this; }
}

console.log(jsonPost(new LogBuilder(), { id: 7 }).log);
// [ 'Content-Type=application/json', 'POST {"id":7}' ]

Điều này cho thấy ý tưởng gốc của GoF: cùng các bước xây dựng, do cùng một director điều khiển, có thể cấp cho các builder khác nhau và tạo ra các biểu diễn khác nhau, ở đây là một bản ghi nhật ký thay vì một yêu cầu.

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

Dùng mẫu Builder khi:

  • Một đối tượng có nhiều phần tùy chọn, hoặc các phần phải được thêm lần lượt từng cái.
  • Việc xây dựng có các quy tắc liên quan đến nhiều trường và cần được kiểm tra một lần, trong build().
  • Bạn muốn đối tượng hoàn thiện là bất biến, nhưng vẫn lắp ráp từng bước.
  • Cùng một quy trình xây dựng phải tạo ra các biểu diễn khác nhau, như một tài liệu được xuất ra HTML, Markdown hoặc PDF.

Tránh nó khi:

  • Đối tượng chỉ có vài trường. Hàm khởi tạo ngắn hơn và rõ ràng không kém.
  • Ngôn ngữ của bạn đã có tham số có tên hoặc tham số tùy chọn. Trong TypeScript, một đối tượng options duy nhất thường loại bỏ hoàn toàn nhu cầu về builder.
interface RequestOptions {
  url: string;
  method?: "GET" | "POST";
  headers?: Record<string, string>;
  timeoutMs?: number;
}

function createRequest({ url, method = "GET", headers = {}, timeoutMs = 10_000 }: RequestOptions) {
  return { url, method, headers, timeoutMs };
}

console.log(createRequest({ url: "/orders", timeoutMs: 5_000 }).method); // GET

Ứng dụng thực tế

  • Java: java.net.http.HttpRequest.newBuilder() trả về một HttpRequest.Builder; bạn nối chuỗi uri(), header() và timeout(), rồi gọi build().
  • Java: Locale.Builder lắp ráp và kiểm tra một locale từ các phần ngôn ngữ, vùng và chữ viết.
  • Java và C#: StringBuilder lắp ráp một chuỗi từ nhiều phần được nối thêm trước khi tạo ra chuỗi bất biến cuối cùng.
  • Nhiều trình xây dựng truy vấn SQL và builder dữ liệu kiểm thử dùng cùng phong cách fluent.

Các mẫu liên quan

  • Mẫu Abstract Factory tạo cả họ sản phẩm và trả về từng cái ngay lập tức; Builder xây một sản phẩm phức tạp từng bước và trả về ở cuối.
  • Cây Composite thường được tạo bằng builder, vì xây một cây từng nút là một chuỗi bước tự nhiên.
  • Fluent interface là phong cách API nối chuỗi các lời gọi trả về this. Builder thường dùng nó, nhưng chúng không phải là một.
  • Mẫu Prototype có thể là lựa chọn thay thế khi hầu hết đối tượng mới là các biến thể nhỏ của một mẫu dựng sẵn.

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

Vì sao mỗi phương thức builder trả về this?

Trả về builder cho phép bạn nối chuỗi các lời gọi trong một biểu thức, như .header(...).timeout(...).build(). Trả về this trong TypeScript cũng giữ đúng kiểu trong các lớp con.

Tôi có cần lớp Director không?

Không. Director là tùy chọn trong mô tả của GoF. Trong thực tế, một hàm áp dụng một chuỗi bước thông dụng thường là đủ.

Builder còn hữu ích trong TypeScript không?

Đôi khi. Một đối tượng options với giá trị mặc định đáp ứng hầu hết trường hợp đơn giản. Builder vẫn hữu ích khi đối tượng được lắp ráp dần dần, khi cần kiểm tra chéo giữa các trường, hoặc khi bạn muốn kết quả bất biến.

Khác nhau giữa Builder và Factory Method là gì?

Factory Method quyết định lớp nào sẽ được khởi tạo và trả về đối tượng trong một lời gọi. Builder cho client kiểm soát cách một đối tượng được lắp ráp qua nhiều lời gọi.

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