iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
自我挑戰組

一條龍的軟體開發到維護,從校園工讀到職場工程師系列 第 13

Day13-Code design我遇到比較常見的patten/代理人模式 (Proxy Pattern)

  • 分享至 

  • xImage
  •  

呈上一篇Adapter

這一篇來講解 代理人模式 (Proxy Pattern)

前面提到,代理模式的目的是 「為另一個物件提供代理或佔位符,以控制對這個物件的存取」。它的核心在於 「控制」,代理人物件與真實物件會實現相同的介面,讓使用者感覺不出差異,但代理人卻在中間加上了一層控制邏輯。


情境說明

我們用一個最經典的「虛擬代理 (Virtual Proxy)」情境來舉例:載入高解析度大圖

想像你在開發一個相簿應用程式。載入一張高解析度的圖片需要耗費大量的記憶體和時間。如果使用者一打開相簿,程式就把所有圖片的完整資料都載入到記憶體中,那應用程式會變得非常緩慢且耗資源。

我們希望達成的效果是:只有當使用者真的要看某張圖片時,程式才去硬碟中載入那張圖片。 在此之前,只顯示一個佔位符。這就是代理模式的完美應用場景。


程式碼實作 (以 Java 為例)

1. 主體介面 (Subject Interface)

首先,我們定義一個共同的介面 Image,真實圖片和代理圖片都會實現它。這樣客戶端才能無差別地對待它們。

// Subject Interface: 圖片的共同介面
public interface Image {
    void display();
}

2. 真實主體 (Real Subject)

這是代表高解析度大圖的類別 RealImage。它的建構子 RealImage() 模擬了從硬碟載入圖片的耗時操作。

// Real Subject: 真實的、昂貴的圖片物件
public class RealImage implements Image {

    private String fileName;

    // 建構子模擬了耗時的磁碟讀取操作
    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }

    private void loadFromDisk(String fileName) {
        System.out.println("(!) 正在從硬碟載入圖片: " + fileName);
        // 模擬延遲
        try {
            Thread.sleep(2000); // 假設載入需要 2 秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void display() {
        System.out.println("顯示圖片: " + fileName);
    }
}

3. 代理人 (Proxy)

現在,我們來建立代理類別 ProxyImage。它也實現了 Image 介面。

  • 它內部持有一個 RealImage 的參考,但初始為 null
  • 當客戶端第一次呼叫 display() 方法時,ProxyImage 才會去 new RealImage(),建立真實的物件。
  • 之後再呼叫 display(),它就會直接使用已經建立好的真實物件,不再重複載入。
// Proxy: 圖片的代理人
public class ProxyImage implements Image {

    private RealImage realImage; // 持有真實物件的參考,但一開始是 null
    private String fileName;

    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void display() {
        System.out.println(">> 代理人準備顯示圖片: " + fileName);
        // 這就是「延遲載入 (Lazy Loading)」的關鍵
        // 只有在真的需要 display 時,才建立 RealImage 物件
        if (realImage == null) {
            System.out.println(">> 真實圖片尚未載入,現在開始建立...");
            realImage = new RealImage(fileName);
        }
        
        // 將請求轉發給真實物件
        realImage.display();
    }
}

4. 客戶端 (Client)

客戶端程式碼現在可以使用 ProxyImage。從客戶端的角度來看,它只是在操作一個 Image 物件,完全不知道背後的代理機制和延遲載入邏輯。

public class Main {
    public static void main(String[] args) {
        // 客戶端建立的是代理物件,這個操作非常快,因為沒有真的去讀取硬碟
        Image image1 = new ProxyImage("風景照.jpg");
        Image image2 = new ProxyImage("家庭聚會.png");

        System.out.println("--- 第一次點擊風景照 ---");
        // 第一次呼叫 display(),代理會觸發真實圖片的載入
        image1.display();

        System.out.println("\n--- 第二次點擊風景照 ---");
        // 第二次呼叫 display(),代理直接使用已載入的圖片,速度很快
        image1.display();

        System.out.println("\n--- 第一次點擊家庭聚會照 ---");
        // 點擊另一張照片,會觸發另一張真實圖片的載入
        image2.display();
    }
}

執行結果:

--- 第一次點擊風景照 ---
>> 代理人準備顯示圖片: 風景照.jpg
>> 真實圖片尚未載入,現在開始建立...
(!) 正在從硬碟載入圖片: 風景照.jpg  <-- (這裡會停頓 2 秒)
顯示圖片: 風景照.jpg

--- 第二次點擊風景照 ---
>> 代理人準備顯示圖片: 風景照.jpg
顯示圖片: 風景照.jpg

--- 第一次點擊家庭聚會照 ---
>> 代理人準備顯示圖片: 家庭聚會.png
>> 真實圖片尚未載入,現在開始建立...
(!) 正在從硬碟載入圖片: 家庭聚會.png <-- (這裡會停頓 2 秒)
顯示圖片: 家庭聚會.png

總結

  • 控制存取: ProxyImage 控制了對 RealImage 的建立時機。
  • 相同介面: ProxyImageRealImage 都實現了 Image 介面,使得 Client 可以一致地對待它們,降低了耦合。
  • 增加職責: 代理人在不修改 RealImage 程式碼的前提下,為其增加了「延遲載入」的功能。同理,我們也可以在這裡輕鬆加入權限檢查、日誌記錄等功能。

它們的結構相似,但 意圖 (Intent) 完全不同。

這是一張能讓你一目了然的比較表:

比較面向 適配器模式 (Adapter) 代理人模式 (Proxy)
核心意圖 轉換介面 (Convert) 控制存取 (Control)
解決的問題 解決兩個既有介面不相容的問題。 為物件的存取過程增加額外職責管理
介面關係 適配器實現的是目標介面 (Target),但它包裝的是介面不同被適配者 (Adaptee) 代理人與真實物件 (Real Subject) 實現完全相同的介面。
對客戶端的透明度 客戶端通常知道它在使用一個適配器來跟舊物件溝通。 客戶端通常不知道自己在跟代理人溝通,它以為直接在用真實物件。
生活比喻 萬國插座轉接頭:讓你的兩腳插頭能用在三腳插座上。 信用卡:你帳戶的代理,增加了支付安全、消費記錄等控制。

一句話總結差別:

所以,當你遇到兩個介面對不上的時候,就想到 Adapter
當你想要在不驚動原始物件的情況下,為它增加像是權限檢查、延遲載入、快取等功能時,就想到 Proxy


上一篇
Day12-Code design我遇到比較常見的patten/適配器模式 (Adapter Pattern)
系列文
一條龍的軟體開發到維護,從校園工讀到職場工程師13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言