cv.cm

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

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

5 phút đọc

Trả lời nhanh

Adapter là mẫu thiết kế cấu trúc chuyển interface của một lớp có sẵn thành interface mà mã client mong đợi. Adapter bọc đối tượng không tương thích và dịch từng lời gọi, ví dụ biến charge(amountCents) thành makePayment("12.50") cũ. Nó cho phép bạn tái sử dụng một thành phần bên thứ ba hoặc kế thừa mà không cần thay đổi nó, và không để API vụng về của nó lan khắp mã của bạn. Nó còn được gọi là Wrapper.

Mục đích

Chuyển interface của một lớp thành interface khác mà client mong đợi. Adapter cho phép các lớp làm việc cùng nhau dù không thể vì interface không tương thích. Đây là một trong bảy mẫu cấu trúc trong cuốn sách Gang of Four (GoF).

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

Mã cửa hàng của bạn làm việc với interface PaymentGateway nhỏ: charge(amountCents, currency) trả về kết quả có cờ ok và mã giao dịch. Nay doanh nghiệp ký hợp đồng với một nhà cung cấp thanh toán có SDK trông hoàn toàn khác: số tiền là chuỗi thập phân, đơn vị tiền tệ nằm trong một đối tượng yêu cầu, và lỗi trả về dưới dạng mã trạng thái số.

Bạn không thể đổi SDK, và không muốn viết lại mọi nơi gọi để dùng hình dạng mới. Nếu phong cách của SDK lan vào thanh toán, hoàn tiền và báo cáo, việc đổi nhà cung cấp sau này sẽ tốn kém. Một adapter giữ việc dịch trong một lớp.

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

Thành phầnTrách nhiệmTrong ví dụ
TargetInterface mà client mong đợi.PaymentGateway
AdapteeLớp có sẵn với interface không tương thích.LegacyPayClient
AdapterTriển khai Target và dịch các lời gọi sang Adaptee.LegacyPayAdapter
ClientChỉ làm việc với interface Target.checkout

Triển khai bằng TypeScript

// Target: what our code expects.
interface PaymentGateway {
  charge(amountCents: number, currency: string): { ok: boolean; id: string };
}

// Adaptee: a third-party SDK we cannot change.
class LegacyPayClient {
  makePayment(req: { amount: string; curr: string }): { status: number; ref: string } {
    return { status: req.curr === "USD" ? 200 : 422, ref: `TX-${req.amount}` };
  }
}

// Adapter: implements the Target by translating to the Adaptee.
class LegacyPayAdapter implements PaymentGateway {
  constructor(private client: LegacyPayClient) {}

  charge(amountCents: number, currency: string) {
    const res = this.client.makePayment({
      amount: (amountCents / 100).toFixed(2),
      curr: currency.toUpperCase(),
    });
    return { ok: res.status === 200, id: res.ref };
  }
}

// Client code knows nothing about LegacyPayClient.
function checkout(gateway: PaymentGateway, totalCents: number): string {
  const result = gateway.charge(totalCents, "usd");
  return result.ok ? `paid ${result.id}` : "payment failed";
}

console.log(checkout(new LegacyPayAdapter(new LegacyPayClient()), 1250)); // paid TX-12.50

Đổi sang nhà cung cấp khác sau này nghĩa là viết thêm một adapter. checkout và mọi nơi gọi khác giữ nguyên, và các bài kiểm thử có thể truyền một PaymentGateway giả.

Object adapter so với class adapter

Cuốn sách GoF mô tả hai dạng:

DạngCách hoạt độngGhi chú
Object adapterGiữ một tham chiếu tới adaptee và ủy thác cho nó (composition).Làm việc với adaptee và mọi lớp con của nó. Lựa chọn thông thường trong TypeScript, Java và C#.
Class adapterKế thừa từ adaptee và triển khai interface target.Cần kế thừa từ adaptee, nên chỉ điều hợp đúng lớp đó. Phổ biến trong C++ với đa kế thừa.

Trong TypeScript và JavaScript, điều hợp một hàm thường là đủ. Node.js có sẵn một adapter nhỏ kiểu này: util.promisify() biến một hàm kiểu callback thành hàm trả về promise.

import { promisify } from "node:util";

// Adaptee: old callback style (error first, then result).
function readConfig(name: string, cb: (err: Error | null, value?: string) => void): void {
  setTimeout(() => cb(null, `config for ${name}`), 10);
}

// Adapted: the same function, now with a promise-based interface.
const readConfigAsync = promisify(readConfig);
console.log(await readConfigAsync("app")); // config for app

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

Dùng mẫu Adapter khi:

  • Bạn muốn dùng một lớp có sẵn, nhưng interface của nó không khớp với interface mã của bạn cần.
  • Bạn tích hợp thư viện bên thứ ba hoặc hệ thống kế thừa và muốn giữ API của chúng ở rìa chương trình.
  • Bạn muốn nhiều cách triển khai khác nhau, như hai nhà cung cấp thanh toán, sau một interface.

Tránh nó khi:

  • Bạn sở hữu cả hai phía. Đổi một trong hai interface thường đơn giản hơn thêm một lớp dịch.
  • Adapter sẽ phải giả mạo những phần lớn của interface target mà adaptee thực sự không hỗ trợ được. Điều đó che giấu sự không khớp thật thay vì giải quyết nó.

Ứng dụng thực tế

  • Node.js: util.promisify() điều hợp các hàm dựa trên callback thành promise.
  • Java: Arrays.asList() cho phép dùng một mảng qua interface List, và InputStreamReader cho phép đọc một InputStream dựa trên byte như một Reader dựa trên ký tự.
  • Các thư viện cơ sở dữ liệu và ORM dùng một adapter hoặc driver cho mỗi cơ sở dữ liệu, nên cùng một API truy vấn chạy với PostgreSQL, MySQL hoặc SQLite.
  • Mã frontend thường bọc các API trình duyệt hoặc SDK phân tích sau một interface nhỏ riêng của dự án vì cùng lý do.

Các mẫu liên quan

MẫuKhác Adapter như thế nào
DecoratorGiữ nguyên interface và thêm hành vi. Adapter thay đổi interface và không thêm hành vi mới.
ProxyGiữ nguyên interface và kiểm soát truy cập tới đối tượng (tải lười, lưu đệm, phân quyền).
FacadeĐịnh nghĩa một interface mới, đơn giản hơn cho cả một hệ thống con gồm nhiều lớp. Adapter làm một interface có sẵn khớp với một interface có sẵn khác.
BridgeĐược thiết kế từ đầu để abstraction và phần triển khai của nó biến đổi riêng rẽ. Adapter thường được thêm sau để các lớp có sẵn vừa với nhau.

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

Khác nhau giữa Adapter và Facade là gì?

Adapter làm một lớp có sẵn khớp với interface mà client đã mong đợi. Facade tạo một interface mới, đơn giản hơn phía trước một hệ thống con phức tạp gồm nhiều lớp.

Khác nhau giữa Adapter và Decorator là gì?

Cả hai đều bọc một đối tượng. Decorator giữ nguyên interface và thêm hành vi; adapter thay đổi interface để đối tượng được bọc dùng được ở nơi nó vốn không dùng được.

Adapter có giống wrapper không?

Wrapper là tên gọi khác của Adapter trong cuốn sách GoF. Trong lời nói hằng ngày, "wrapper" cũng được dùng cho decorator và proxy, nên mục đích cho bạn biết đang nói tới mẫu nào.

Một adapter có thể điều hợp nhiều lớp không?

Một object adapter làm việc được với một adaptee và mọi lớp con của nó. Điều hợp các lớp không liên quan thường nghĩa là một adapter cho mỗi lớp, tất cả cùng triển khai một interface target.

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