首頁 > 軟體

Head First 設計模式 —— 13. 代理 (Proxy) 模式

2021-01-16 13:30:12

思考題

如何設計一個支援遠端方法呼叫的系統?你要怎樣才能讓開發人員不用寫太多程式碼?讓遠端呼叫看起來像本地呼叫一樣,毫無瑕疵? P435

  • 已經接觸過 RPC 了,所以就很容易知道具體流程:使用者端呼叫目標類的代理物件(消費者)的方法,消費者內部將相關呼叫資訊通過網路傳到伺服器端對應的目標類的代理物件(生產者)中,生產者解析呼叫資訊,然後真正去呼叫目標類的實際物件,並將返回結果回傳給消費者,消費者再返回給使用者端。 RPC 框架使用代理模式使得內部一系列處理及資訊傳輸等對使用者端和伺服器端是透明的,使用者端會認為實際是本地呼叫一樣,不知道呼叫了遠端方法;伺服器端也不知道是在給遠端物件提供服務。

思考題

遠端呼叫程式應該完全透明嗎?這是個好主意嗎?這個方法可能會產生什麼問題? P435

  • 遠端呼叫程式不應該完全透明。由於引入了網路通訊和資料處理(序列化、反序列化和壓縮等),可能在相關過程會異常,使用者端應該知曉並處理這些異常,而不應該讓 RPC 框架消化掉這些異常而返回一些預設值。

代理模式

為另一個物件提供一個替身或預留位置以控制對這個物件的存取。 P460

特點

  • 代理控制存取
    • 遠端代理:控制存取遠端物件 P460
    • 虛擬代理:控制存取建立開銷大的資源 P460
    • 保護代理:基於許可權控制對資源的存取 P460
    • 動態代理:在執行時動態地建立一個代理類,並將方法的呼叫轉發到指定的類 P474
    • 防火牆代理:控制網路資源的存取,保護主題免於「壞客戶」的侵害。多用於防火牆系統 P488
    • 智慧參照代理:當主題被參照時,進行額外的動作,例如計一個物件被參照的次數。可用於對某些操作的紀錄檔記錄 P488
    • 快取代理:為開銷大的運算結果提供暫時儲存;也允許多個客戶共用結果,以減少計算或網路延遲。多用於 Web 伺服器代理,以及內容管理與出版系統 P488
    • 同步代理:在多執行緒的情況下為主題提供安全的存取。可用於 JavaSpaces ,為分散式環境內的潛在物件集合提供同步存取控制 P489
    • 複雜隱藏代理:用來隱藏一個類的複雜集合的複雜度,並進行存取控制;有時候也成為外觀代理,但與外觀模式不同,因為代理控制存取,而外觀模式只提供另一組介面 P489
    • 寫入時複製代理:用來控制物件的複製,方法是延遲物件的複製,直到客戶真的需要為止。這是虛擬代理的變體。CopyOnWriteArrayList 使用這種方式 P489

缺點

  • 代理會造成設計中類的數目增加 P491

代理模式和裝飾器模式 P471

  • 代理模式控制物件的存取
  • 裝飾器模式為物件增加行為

代理模式和介面卡模式的區別 P471

  • 代理模式實現相同的介面(保護代理可能只提供給客戶部分介面,與某些介面卡很像)
  • 介面卡模式改變物件適配的介面

思考題

class ImageProxy implements Icon {
    // 範例變數構造器在這裡
    public int getIconWidth() {
        if (imageIcon != null) {
            return imageIcon.getIconWidth();
        } else {
            return 800;
        }
    }
    
    public int getIconHeight() {
        if (imageIcon != null) {
            return imageIcon.getIconHeight();
        } else {
            return 600;
        }
    }
    
    public void paintIcon(final Component c, Graphics g, int y, int y) {
        if (imageIcon != null) {
            imageIcon.paintIcon(c, g, x, y);
        } else {
            g.drawString("Loading CD cover, please wait...", x + 300, y + 190);
            // 範例化 imageIcon 獲取圖片
        }
    }
}

以上為 CD 封面虛擬代理, ImageProxy 類似乎有兩個,由條件語句控制的狀態。你能否用另一個設計模式清理這樣的程式碼?你要如何重新設計 ImageProxyP468

  • 可以用狀態模式清理掉條件語句。設定兩個狀態 ImageNotLoadedImageLoaded ,分別將各個方法內條件語句內的程式碼放入這個兩個狀態的對應方法中,初始狀態是 ImageNotLoaded ,當第一次呼叫 paintIcon 方法時,開始範例化 imageIcon 獲取圖片,成功後設定狀態為 ImageLoaded

思考題

NonOwnerInvocationHandler 工作的方式除了它允許呼叫 setHotOrNotRating() 和不允許呼叫其他 set 方法之外,與 NonOwnerInvocationHandler 是很相似的。請寫出 NonOwnerInvocationHandler 的程式碼: P482

import java.lang.reflect.*;

public class NonOwnerInvocationHandler implements InvocationHandler {
    PersonBean person;
    
    public NonOwnerInvocationHandler(PersonBean person) {
        this.person = person;
    }
    
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
        try {
            String methodName = method.getName();
            if (methodName.startsWith("get")) {
                return method.invoke(person, args);
            } else if(methodName.equals("setHotOrNotRating")) {
                return method.invoke(person, args);
            } else if(methodName.startsWith("set")) {
                throw new IllegalAccessException();
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

思考題

建立動態代理所需要的程式碼很短,請你寫下 getNonOwnerProxy() ,該方法會返回 NonOwnerInvocationHandler 的代理。更進一步,請寫下 getProxy() 方法,引數是 handlerperson ,返回值是使用此 handler 的代理。 P483

PersonBean getNonOwnerProxy(PersonBean person) {
    return (PersonBean) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), new NonOwnerInvocationHandler(person));
}

PersonBean getProxy(InvocationHandler handler, PersonBean person) {
    return (PersonBean) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), handler);
}

思考題

如何知道某個類是不是代理類? P486

  • JDK 動態代理的類是 Proxy 的子類,有一個靜態方法 isProxyClass() ,此方法的返回值如果為 true ,表示這是一個動態代理類。
  • 【存疑】 代理類還會實現特定的某些介面
    • 在 Java8 中呼叫 proxy.getClass().getInterfaces() 及其他與獲取介面有關的方法,並未發現實現新介面

思考題

能傳入 newProxyInstance() 的介面型別,有沒有什麼限制?

  • 傳入的介面陣列內只能有介面,不能有類 P486
  • 如果介面不是 public ,就必須屬於同一個 package P486
  • 【存疑】 不同的介面內,不可以有名稱和引數完全一樣的方法 P486
    • 經過 Java8 中實踐確認沒有此限制,不過永遠只會識別為其中一個介面(介面陣列內第一次出現該方法的介面)的方法
  • 介面陣列內的介面可以不是被代理類實現的介面,代理類實現了介面陣列內的所有介面,所有介面的呼叫都可以被攔截處理
  • 被代理類可以不實現任何介面,自己指定介面和相關呼叫處理邏輯也能使用

思考題

配對下列模式和描述: P487

代理模式:包裝另一個物件,並控制對它的存取

外觀模式:包裝許多物件以簡化它們的介面

裝飾器模式:包裝另一個物件,並提供額外的行為

介面卡模式:包裝另一個物件,並提供不同的介面

所思所想

  • 以前也看過不同動態代理的實現,但只是走馬觀花式地看一遍如何實現,沒有實際去動手,這次讀書時實際動手後感覺理解更深入了一點,大概更能瞭解內部是如何流轉的
  • 實踐是檢驗真理的唯一標準。書中存在部分說明可能錯誤或者不適用於當前版本,要利用好身邊的工具,多實踐操作,不能盡信書

本文首發於公眾號:滿賦諸機(點選檢視原文) 開源在 GitHub :reading-notes/head-first-design-patterns


IT145.com E-mail:sddin#qq.com