Hướng dẫn Java Design Pattern – Proxy

Khi cần điều khiển truy nhập tới một đối tượng được thực hiện từ quá trình khởi tạo nó cho tới khi thực sự cần sử dụng nó. Hoặc cần bảo vệ quyền truy xuất vào các phương thức của object thực. Trong trường hợp như vậy, ta nên dùng mẫu thiết kế Proxy.

1. Proxy Pattern là gì?

Provide a surrogate or placeholder for another object to control access to it.

Proxy Pattern là một trong những Pattern thuộc nhóm cấu trúc (Structural Pattern).

Proxy có nghĩa là “ủy quyền” hay “đại diện”. Mục đích xây dựng Proxy pattern cũng chính vì muốn tạo ra một đối tượng sẽ ủy quyền, thay thế cho một đối tượng khác.

Proxy Pattern là mẫu thiết kế mà ở đó tất cả các truy cập trực tiếp đến một đối tượng nào đó sẽ được chuyển hướng vào một đối tượng trung gian (Proxy Class). Mẫu Proxy (người đại diện) đại diện cho một đối tượng khác thực thi các phương thức, phương thức đó có thể được định nghĩa lại cho phù hợp với múc đích sử dụng.

Để đơn giản hơn bạn có thể nghĩ đến khái niệm HTTP proxy trong mạng máy tính, nó là một gateway giữa trình duyệt (client) và máy chủ (server). HTTP proxy giúp nâng cao trải nghiệm người dùng, tăng tốc với lưu đệm các dữ liệu, loại bỏ các trang quảng cáo, giới hạn các vùng thông tin được xem… Proxy Pattern cũng có chung một mục đích như với HTTP proxy.

Proxy Pattern còn được gọi là Surrogate (thay thế) hoặc Placeholder (trình giữ chỗ).

2. Phân loại Proxy

  • Virtual Proxy : Virtual Proxy tạo ra một đối tượng trung gian mỗi khi có yêu cầu tại thời điểm thực thi ứng dụng, nhờ đó làm tăng hiệu suất của ứng dụng.
  • Protection Proxy : Phạm vi truy cập của các client khác nhau sẽ khác nhau. Protection proxy sẽ kiểm tra các quyền truy cập của client khi có một dịch vụ được yêu cầu.
  • Remote Proxy : Client truy cập qua Remote Proxy để chiếu tới một đối tượng được bảo về nằm bên ngoài ứng dụng (trên cùng máy hoặc máy khác).
  • Monitor Proxy : Monitor Proxy sẽ thiết lập các bảo mật trên đối tượng cần bảo vệ, ngăn không cho client truy cập một số trường quan trọng của đối tượng. Có thể theo dõi, giám sát, ghi log việc truy cập, sử dụng đối tượng.
  • Firewall Proxy : bảo vệ đối tượng từ chối các yêu cầu xuất xứ từ các client không tín nhiệm.
  • Cache Proxy : Cung cấp không gian lưu trữ tạm thời cho các kết quả trả về từ đối tượng nào đó, kết quả này sẽ được tái sử dụng cho các client chia sẻ chung một yêu cầu gửi đến. Loại Proxy này hoạt động tương tự như Flyweight Pattern.
  • Smart Reference Proxy : Là nơi kiểm soát các hoạt động bổ sung mỗi khi đối tượng được tham chiếu.
  • Synchronization Proxy : Đảm bảo nhiều client có thể truy cập vào cùng một đối tượng mà không gây ra xung đột. Khi một client nào đó chiếm dụng khóa khá lâu khiến cho số lượng các client trong danh sách hàng đợi cứ tăng lên, và do đó hoạt động của hệ thống bị ngừng trệ, có thể dẫn đến hiện tượng “tắc nghẽn”.
  • Copy-On-Write Proxy : Loại này đảm bảo rằng sẽ không có client nào phải chờ vô thời hạn. Copy-On-Write Proxy là một thiết kế rất phức tạp.

3. Cài đặt Proxy Pattern như thế nào?

Proxy Pattern có những đặc điểm chung sau đây:

  • Cung cấp mức truy cập gián tiếp vào một đối tượng.
  • Tham chiếu vào đối tượng đích và chuyển tiếp các yêu cầu đến đối tượng đó.
  • Cả Proxy và đối tượng đích đều kế thừa hoặc thực thi chung một lớp giao diện. Mã máy dịch cho lớp giao diện thường “nhẹ” hơn các lớp cụ thể và do đó có thể giảm được thời gian tải dữ liệu giữa server và client.

Các thành phần tham gia vào mẫu Proxy Pattern:

  • Subject : là một interface định nghĩa các phương thực để giao tiếp với client. Đối tượng này xác định giao diện chung cho RealSubject và Proxy để Proxy có thể được sử dụng bất cứ nơi nào mà RealSubject mong đợi.
  • Proxy : là một class sẽ thực hiện các bước kiểm tra và gọi tới đối tượng của class service thật để thực hiện các thao tác sau khi kiểm tra. Nó duy trì một tham chiếu đến RealSubject để Proxy có thể truy cập nó. Nó cũng thực hiện các giao diện tương tự như RealSubject để Proxy có thể được sử dụng thay cho RealSubject. Proxy cũng điều khiển truy cập vào RealSubject và có thể tạo hoặc xóa đối tượng này.
  • RealSubject : là một class service sẽ thực hiện các thao tác thực sự. Đây là đối tượng chính mà proxy đại diện.
  • Client : Đối tượng cần sử dụng RealSubject nhưng thông qua Proxy.

3.1. Ví dụ Virtual Proxy

Trì hoãn việc tạo ra real subject bên trong proxy class. Chỉ đến khi cần, proxy class mới thật sự khởi tạo real class. Loại Proxy này làm việc theo cơ chế Lazy Loading.

Lazy Loading là một khái niệm mà ứng dụng trì hoãn việc tải các đối tượng cho đến thời điểm mà người dùng cần nó. Nói một cách đơn giản là tải theo yêu cầu của người dùng chứ không phải tải đối tượng không cần thiết. Lợi ích của việc này là giảm thiểu số lượng yêu cầu, giảm thiểu số lượng tài nguyên thừa cần tải cho tới khi người dùng cần đến chúng thực sự. Chúng ta đã thấy điều này qua cơ chế Lazy load của Hibernate.

Nó giải quyết vấn đề rất lớn về hiệu suất, lý do là vì proxy class có chi phí khởi tạo rất ít, việc duy trì nó không mất nhiều tài nguyên hệ thống. Trong khi đó Real class thường rất tốn chi phí , vì thế với virtual proxies, chỉ khi nào cần thiết, real class mới được khởi tạo.

Ví dụ một website hiển thị ảnh, có thể có rất nhều ảnh trên một trang hay một ảnh được hiển thị nhiều lần. Trường hợp này chúng ta chỉ cần load ảnh khi nó cần hiển thị (khi ta scroll tới nơi đặt image) hoặc là nó chưa được load (không như các website truyền thống là load hình ngay khi load website, nó rất tốn tài nguyên và đôi khi không cần thiết do người dùng không scroll tới nơi đặt image, có thể load 1 ảnh nhiều lần).

Image.java

package com.maixuanviet.patterns.structural.proxy.virtual;
 
public interface Image {
 
    void showImage();
 
}

RealImage.java

package com.maixuanviet.patterns.structural.proxy.virtual;
 
public class RealImage implements Image {
    private String url;
 
    public RealImage(String url) {
        this.url = url;
        System.out.println("Image loaded: " + this.url);
    }
 
    @Override
    public void showImage() {
        System.out.println("Image showed: " + this.url);
    }
}

ProxyImage.java

package com.maixuanviet.patterns.structural.proxy.virtual;
 
public class ProxyImage implements Image {
    private Image realImage;
    private String url;
 
    public ProxyImage(String url) {
        this.url = url;
        System.out.println("Image unloaded: " + this.url);
    }
 
    @Override
    public void showImage() {
        if (realImage == null) {
            realImage = new RealImage(this.url);
        } else {
            System.out.println("Image already existed: " + this.url);
        }
        realImage.showImage();
    }
}

Client.java

package com.maixuanviet.patterns.structural.proxy.virtual;
 
public class Client {
    public static void main(String[] args) {
        System.out.println("Init proxy image: ");
        ProxyImage proxyImage = new ProxyImage("https://www.maixuanviet.com/favicon.ico");
         
        System.out.println("---");
        System.out.println("Call real service 1st: ");
        proxyImage.showImage();
         
        System.out.println("---");
        System.out.println("Call real service 2nd: ");
        proxyImage.showImage();
    }
}

Output của chương trình:

Init proxy image: 
Image unloaded: https://www.maixuanviet.com/favicon.ico
---
Call real service 1st: 
Image loaded: https://www.maixuanviet.com/favicon.ico
Image showed: https://www.maixuanviet.com/favicon.ico
---
Call real service 2nd: 
Image already existed: https://www.maixuanviet.com/favicon.ico
Image showed: https://www.maixuanviet.com/favicon.ico

Như bạn thấy image chỉ thật sử được load lên khi proxy class gọi hàm.

3.2. Ví dụ Protection proxy

Yêu cầu người gọi proxy class phải chứng thực trước khi proxy class truy xuất vào real class. Cái này rất hữu dụng khi bạn bạn viết library cho một bên khác sử dụng và yêu cầu họ xác thực trước khi gọi hàm.

UserService.java

package com.maixuanviet.patterns.structural.proxy.protection;
 
public interface UserService {
 
    void load();
 
    void insert();
}

UserServiceImpl.java

package com.maixuanviet.patterns.structural.proxy.protection;
 
public class UserServiceImpl implements UserService {
 
    private String name;
 
    public UserServiceImpl(String name) {
        this.name = name;
    }
 
    @Override
    public void load() {
        System.out.println(name + " loaded");
    }
 
    @Override
    public void insert() {
        System.out.println(name + " inserted");
    }
 
}

UserServiceProxy.java

package com.maixuanviet.patterns.structural.proxy.protection;
 
public class UserServiceProxy implements UserService {
 
    private String role;
    private UserService userService;
 
    public UserServiceProxy(String name, String role) {
        this.role = role;
        userService = new UserServiceImpl(name);
    }
 
    @Override
    public void load() {
        userService.load();
    }
 
    @Override
    public void insert() {
        if (isAdmin()) {
            userService.insert();
        } else {
            throw new IllegalAccessError("Access denied");
        }
    }
 
    private boolean isAdmin() {
        return "admin".equalsIgnoreCase(this.role);
    }

Client.java

package com.maixuanviet.patterns.structural.proxy.protection;
 
public class Client {
    public static void main(String[] args) {
        UserService admin = new UserServiceProxy("maixuanviet", "admin");
        admin.load();
        admin.insert();
 
        UserService customer = new UserServiceProxy("customer", "guest");
        customer.load();
        customer.insert();
    }
}

Output của chương trình:

maixuanviet loaded
maixuanviet inserted
customer loaded
Exception in thread "main"
java.lang.IllegalAccessError: Access denied
    at com.maixuanviet.patterns.structural.proxy.rotection.UserServiceProxy.insert(UserServiceProxy.java:23)
    at com.maixuanviet.patterns.structural.proxy.rotection.Client.main(Client.java:11)

Các bạn thấy khi Client muốn gọi hàm insert(), trong Proxy luôn xác thực quyền của user trước khi thực hiện nó.

3.3. Ví dụ Remote Proxy

Với Remote Proxy, proxy class và real class nằm ở 2 địa chỉ khác nhau. Thông qua network, proxy class sẽ encode và gửi request tới real class để khởi tạo, truy xuất, …

Chúng ta có thể thấy remote Proxy khi implements Java RMI hoặc thông dụng nhất là ở WebService. Bên phía client sẽ có 1 proxy class, client sẽ khởi tạo proxy class và gọi tới real class nằm ở 1 địa chỉ khác.

3.4. Ví dụ Smart Proxy

Proxy Class sẽ thay đổi hoặc thêm 1 số thao tác trước khi gọi tới real class. Một số trường hợp thường thấy là:

  • Ở lần đâu tiên khởi tạo real class, Proxy class sẽ lưu lại thông tin của real class vào cache và hữu dụng cho lần tái sử dụng sau. Ta có thể thấy việc xuất hiện ở lazy-load ở các connection xuống database.
  • Trước khi gọi real class, ta có thể lock real class lại và không cho các thread khác phải chờ cho tới khi thread hiện tại release real class.
  • Đếm số lượng reference tới real class.

Ở Virtual Proxy, mình cũng có lồng 1 ví dụ của Smart Proxy vào. Khi ta khởi tạo RealImage, mình đã lưu cache RealImage lại và lần sau gọi hàm ShowImage(), ta không phải khởi tạo lại RealImage.

4. Lợi ích của Proxy Pattern là gì?

  • Cãi thiện Performance thông qua lazy loading, chỉ tải các tài nguyên khi chúng được yêu cầu.
  • Nó cung cấp sự bảo vệ cho đối tượng thực từ thế giới bên ngoài.
  • Giảm chi phí khi có nhiều truy cập vào đối tượng có chi phí khởi tạo ban đầu lớn.
  • Dễ nâng cấp, bảo trì.

5. Sử dụng Proxy Pattern khi nào?

  • Khi muốn bảo vệ quyền truy xuất vào các phương thức của object thực.
  • Khi cần một số thao tác bổ sung trước khi thực hiện phương thức của object thực.
  • Khi tạo đối tượng ban đầu là theo yêu cầu hoặc hệ thống yêu cầu sự chậm trễ khi tải một số tài nguyên nhất định (lazy loading).
  • Khi có nhiều truy cập vào đối tượng có chi phí khởi tạo ban đầu lớn.
  • Khi đối tượng gốc tồn tại trong môi trường từ xa (remote).
  • Khi đối tượng gốc nằm trong một hệ thống cũ hoặc thư viện của bên thứ ba.
  • Khi muốn theo dõi trạng thái và vòng đời đối tượng.

6. So sánh Proxy Pattern với Decorator Pattern

Cấu trúc của Proxy Pattern và Decorator Pattern là tương tự nhau (bạn có thể coi ở class diagram). Hai Pattern này đều Wrap một đối tượng thực bên trong nó. Tuy nhiên, khác nhau thật sự giữa Proxy Pattern và Decorator Pattern nằm ở mục đích sử dụng. Với Decorator Pattern, người sử dụng sẽ nhắm tới mục tiêu là có thể thêm tính năng động vào một đối tượng có trước, trong khi đó Proxy Pattern cho phép ta tạo ra một đại diện cho một đối tượng khác.