cv.cm

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

Mẫu Proxy (Design Pattern) giải thích kèm ví dụ

6 phút đọc

Trả lời nhanh

Proxy là một mẫu thiết kế cấu trúc (structural), cung cấp một đối tượng thay thế có cùng interface với đối tượng thật, để proxy có thể kiểm soát truy cập vào nó. Client nói chuyện với proxy như với đối tượng thật, và proxy quyết định khi nào và cách chuyển tiếp từng lời gọi. Các loại phổ biến là virtual proxy (khởi tạo lười), protection proxy (kiểm tra quyền truy cập), caching proxy và remote proxy.

Mục đích và vấn đề mà mẫu này giải quyết

Mục đích: cung cấp một đại diện hoặc chỗ giữ chỗ cho đối tượng khác để kiểm soát truy cập vào nó. Proxy là một trong các mẫu cấu trúc trong sách Gang of Four (GoF).

Giả sử một ứng dụng gọi dịch vụ thời tiết qua mạng. Mỗi lần gọi mất hàng trăm mili giây và tốn tiền, trong khi cùng một thành phố thường được yêu cầu nhiều lần mỗi phút. Bạn không thể sửa lớp của dịch vụ, và cũng không muốn thêm mã cache vào mọi nơi gọi nó. Proxy giải quyết việc này: nó cài đặt cùng interface, giữ một cache và chỉ gọi dịch vụ thật khi cache không có. Nơi gọi hoàn toàn không phải đổi.

Cùng ý tưởng đó đáp ứng các nhu cầu khác: chỉ tạo đối tượng tốn kém khi dùng lần đầu, kiểm tra quyền trước khi gọi, ghi log mọi lời gọi, hoặc che giấu việc đối tượng thật nằm trên máy khác.

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

Vai tròTrách nhiệm
SubjectInterface dùng chung cho đối tượng thật và proxy, để client dùng được cả hai
Real subjectĐối tượng thực hiện công việc thật sự
ProxyCài đặt interface của subject, giữ hoặc tạo tham chiếu đến real subject và thêm logic kiểm soát trước hoặc sau khi chuyển tiếp
ClientChỉ làm việc với interface của subject và không biết mình đang dùng proxy hay không
Loại proxyĐiều nó kiểm soát
Virtual proxyTrì hoãn việc tạo đối tượng tốn kém cho đến khi cần
Protection proxyKiểm tra xem bên gọi có được phép thực hiện thao tác hay không
Caching proxyLưu kết quả và trả lại cho các yêu cầu lặp lại
Remote proxyĐại diện cho đối tượng ở tiến trình hoặc máy khác và xử lý việc giao tiếp
Logging proxyGhi lại lời gọi và đối số mà không thay đổi đối tượng thật

Cài đặt bằng TypeScript: caching proxy

WeatherService là subject. CachedWeatherService là proxy: nó có cùng phương thức và chỉ chuyển tiếp đến dịch vụ thật khi giá trị trong cache bị thiếu hoặc quá cũ.

interface WeatherService {
  getTemperature(city: string): Promise<number>;
}
class RemoteWeatherService implements WeatherService {
  calls = 0;
  async getTemperature(city: string): Promise<number> {
    this.calls++; // imagine a slow HTTP request here
    return city.length * 3;
  }
}
class CachedWeatherService implements WeatherService {
  private cache = new Map<string, { value: number; time: number }>();
  constructor(private real: WeatherService, private maxAgeMs = 60_000) {}
  async getTemperature(city: string): Promise<number> {
    const hit = this.cache.get(city);
    if (hit && Date.now() - hit.time < this.maxAgeMs) return hit.value;
    const value = await this.real.getTemperature(city);
    this.cache.set(city, { value, time: Date.now() });
    return value;
  }
}
async function main() {
  const real = new RemoteWeatherService();
  const weather: WeatherService = new CachedWeatherService(real);
  await weather.getTemperature("Paris");
  await weather.getTemperature("Paris");
  console.log(await weather.getTemperature("Hanoi"), "real calls:", real.calls);
  // 15 real calls: 2
}
main();

Biến client được khai báo kiểu WeatherService, nên bạn có thể chuyển giữa dịch vụ thật và proxy ở một nơi, ví dụ trong cấu hình dependency injection.

Virtual proxy và protection proxy với JavaScript Proxy

JavaScript có sẵn đối tượng Proxy (ES2015) chặn các thao tác như đọc thuộc tính, ghi thuộc tính và gọi hàm thông qua các hàm xử lý gọi là trap. Đây là cách tiện lợi để tạo proxy mà không phải viết một lớp cho từng interface.

type Account = { owner: string; balance: number };
function readOnly<T extends object>(target: T): T {
  return new Proxy(target, {
    set(_obj, prop) {
      throw new Error("Cannot change " + String(prop) + ": read-only");
    },
  });
}
const account = readOnly<Account>({ owner: "Ana", balance: 100 });
console.log(account.balance); // 100
try {
  account.balance = 0;
} catch (e) {
  console.log((e as Error).message); // Cannot change balance: read-only
}

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

  • Dùng để thêm tải lười, kiểm soát truy cập, lưu đệm, ghi log hoặc truy cập từ xa mà không thay đổi lớp thật hay nơi gọi nó.
  • Dùng khi đối tượng thật nằm ngoài tầm kiểm soát của bạn, như lớp thư viện hoặc client mạng.
  • Không dùng khi một hàm bọc đơn giản là đủ. Với một hàm, hàm bậc cao như memoize(fn) đơn giản hơn một lớp.
  • Đừng che giấu các lời gọi từ xa chậm hoặc dễ lỗi sau một proxy trông như cục bộ. Nơi gọi vẫn cần xử lý độ trễ và lỗi, nên hãy thể hiện điều đó trong interface (ví dụ bằng cách trả về Promise).
  • Lưu ý về danh tính: proxy là một đối tượng khác với real subject, nên proxy === real là false.

Ứng dụng thực tế

  • Vue 3 xây dựng hệ thống reactivity trên Proxy của JavaScript: đọc một thuộc tính sẽ ghi nhận phụ thuộc và ghi nó sẽ kích hoạt cập nhật.
  • Java cung cấp java.lang.reflect.Proxy để tạo các lớp proxy động lúc chạy; các framework dùng nó để thêm giao dịch, kiểm tra bảo mật hoặc gọi từ xa quanh các interface.
  • Các ORM như Hibernate trả về lazy proxy cho các thực thể liên quan và tải dữ liệu thật từ cơ sở dữ liệu ở lần truy cập đầu tiên.
  • Các thư viện gọi thủ tục từ xa (RPC) sinh ra client stub trông như đối tượng cục bộ nhưng gửi mỗi lời gọi qua mạng.

Các mẫu liên quan

MẫuKhác với Proxy ở điểm nào
DecoratorCùng cấu trúc, khác mục tiêu: Decorator thêm hành vi mới và thường được xếp chồng; Proxy kiểm soát truy cập và thường tự quản lý vòng đời của đối tượng thật.
AdapterAdapter cho đối tượng một interface khác; Proxy giữ nguyên đúng interface.
FacadeFacade đưa ra interface mới, đơn giản hơn cho cả hệ thống con; Proxy đại diện cho một đối tượng.
FlyweightFlyweight chia sẻ đối tượng để tiết kiệm bộ nhớ; virtual proxy trì hoãn việc tạo đối tượng để tiết kiệm thời gian và bộ nhớ.

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

Mẫu Proxy khác mẫu Decorator ở điểm nào?

Cả hai đều bọc một đối tượng với cùng interface. Decorator thêm trách nhiệm và thường do client ghép lại, còn proxy kiểm soát truy cập vào đối tượng thật và thường tự tạo hoặc quản lý đối tượng đó.

Đối tượng Proxy của JavaScript có phải là mẫu Proxy không?

Đó là một tính năng ngôn ngữ giúp cài đặt mẫu này dễ dàng. Proxy của JavaScript chặn các thao tác trên đối tượng đích thông qua trap, đúng là điều mà một proxy theo nghĩa GoF làm.

Virtual proxy là gì?

Virtual proxy đại diện cho một đối tượng tốn kém để tạo, như một ảnh lớn. Nó chỉ tạo đối tượng thật khi một phương thức cần đến nó được gọi lần đầu.

Máy chủ reverse proxy có phải là ví dụ của mẫu Proxy không?

Nó theo cùng ý tưởng ở mức mạng: một máy chủ như nginx đứng trước các máy chủ khác với cùng interface HTTP và thêm lưu đệm, kiểm soát truy cập hoặc cân bằng tải. Mẫu GoF mô tả cùng ý tưởng cho các đối tượng trong một chương trình.

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