CodeGym /Java Blog /Toto sisi /代理設計模式
John Squirrels
等級 41
San Francisco

代理設計模式

在 Toto sisi 群組發布
在編程中,正確規劃應用程序的體系結構非常重要。設計模式是實現這一目標不可或缺的方法。今天我們來談談代理。

為什麼需要代理?

此模式有助於解決與對象的受控訪問相關的問題。您可能會問,“為什麼我們需要受控訪問?” 讓我們看一下可以幫助您弄清楚什麼是什麼的幾種情況。

示例 1

想像一下,我們有一個包含一堆舊代碼的大型項目,其中有一個類負責從數據庫中導出報告。該類同步工作。也就是說,當數據庫處理請求時,整個系統處於空閒狀態。生成報告平均需要 30 分鐘。因此,導出過程從凌晨 12:30 開始,管理層在早上收到報告。審計顯示,如果能夠在正常工作時間內立即收到報告會更好。開始時間不能推遲,系統在等待數據庫響應時不能阻塞。解決方案是改變系統的工作方式,在單獨的線程上生成和導出報告。該解決方案將使系統照常工作,管理層將收到新的報告。然而,存在一個問題:無法重寫當前代碼,因為系統的其他部分使用了它的功能。在這種情況下,我們可以使用代理模式引入一個中間代理類,該類將接收導出報告的請求,記錄開始時間,並啟動一個單獨的線程。生成報告後,線程終止,大家都很高興。

示例 2

一個開發團隊正在創建一個活動網站。為了獲取新事件的數據,該團隊查詢第三方服務。一個特殊的私人圖書館促進與服務的互動。在開發過程中,發現一個問題:第三方系統一天更新一次數據,但用戶每次刷新頁面都會向其發送請求。這會創建大量請求,並且服務會停止響應。解決方案是緩存服務的響應,並在重新加載頁面時將緩存的結果返回給訪問者,並根據需要更新緩存。在這種情況下,代理設計模式是一種不改變現有功能的優秀解決方案。

設計模式背後的原理

要實現此模式,您需要創建一個代理類。它實現服務類的接口,模仿其對客戶端代碼的行為。以這種方式,客戶端與代理而不是真實對象進行交互。通常,所有請求都傳遞給服務類,但之前或之後有其他操作。簡而言之,代理是客戶端代碼和目標對象之間的一層。考慮從一個舊的和非常慢的硬盤緩存查詢結果的例子。假設我們正在談論某個邏輯無法更改的古老應用程序中的電動火車時刻表。每天在固定時間插入帶有更新時間表的磁盤。所以,我們有:
  1. TrainTimetable界面。
  2. ElectricTrainTimetable,它實現了這個接口。
  3. 客戶端代碼通過這個類與文件系統進行交互。
  4. TimetableDisplay客戶端類。它的printTimetable()方法使用類的方法ElectricTrainTimetable
該圖很簡單: 代理設計模式:- 2目前,每次調用printTimetable()方法時,ElectricTrainTimetable類都會訪問磁盤、加載數據並將其呈現給客戶端。系統運行正常,但速度很慢。因此,決定通過添加緩存機制來提高系統性能。這可以使用代理模式來完成: 代理設計模式:- 3因此,TimetableDisplay類甚至不會注意到它正在與ElectricTrainTimetableProxy類而不是舊類進行交互。新的實現每天加載一次時間表。對於重複請求,它從內存中返回先前加載的對象。

什麼任務最適合代理?

以下是這種模式肯定會派上用場的幾種情況:
  1. 緩存
  2. 延遲或惰性初始化 如果可以根據需要加載對象,為什麼要立即加載它?
  3. 記錄請求
  4. 數據和訪問的中間驗證
  5. 啟動工作線程
  6. 記錄對對象的訪問
還有其他用例。了解此模式背後的原理,您可以確定可以成功應用它的情況。乍一看,代理與門面做同樣的事情,但事實並非如此。代理具有與服務對象相同的接口。另外,不要將此模式與裝飾器適配器模式混淆。裝飾提供擴展接口,適配器提供替代接口。

的優點和缺點

  • + 您可以根據需要控制對服務對象的訪問
  • + 與管理服務對像生命週期相關的附加能力
  • + 它在沒有服務對象的情況下工作
  • + 它提高了性能和代碼安全性。
  • - 由於額外的請求,性能可能會變差
  • - 它使類層次結構更加複雜

實踐中的代理模式

讓我們實現一個從硬盤讀取火車時刻表的系統:

public interface TrainTimetable {
   String[] getTimetable();
   String getTrainDepartureTime();
}
這是實現主接口的類:

public class ElectricTrainTimetable implements TrainTimetable {

   @Override
   public String[] getTimetable() {
       ArrayList<String> list = new ArrayList<>();
       try {
           Scanner scanner = new Scanner(new FileReader(new File("/tmp/electric_trains.csv")));
           while (scanner.hasNextLine()) {
               String line = scanner.nextLine();
               list.add(line);
           }
       } catch (IOException e) {
           System.err.println("Error:  " + e);
       }
       return list.toArray(new String[list.size()]);
   }

   @Override
   public String getTrainDepartureTime(String trainId) {
       String[] timetable = getTimetable();
       for (int i = 0; i < timetable.length; i++) {
           if (timetable[i].startsWith(trainId+";")) return timetable[i];
       }
       return "";
   }
}
每次獲得火車時刻表時,程序都會從磁盤讀取一個文件。但這只是我們麻煩的開始。每次您獲得哪怕是一列火車的時刻表時,都會讀取整個文件!很好的是,這樣的代碼只存在於不做什麼的例子中:)客戶端類:

public class TimetableDisplay {
   private TrainTimetable trainTimetable = new ElectricTrainTimetable();

   public void printTimetable() {
       String[] timetable = trainTimetable.getTimetable();
       String[] tmpArr;
       System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time");
       for (int i = 0; i < timetable.length; i++) {
           tmpArr = timetable[i].split(";");
           System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
       }
   }
}
示例文件:

9B-6854;London;Prague;13:43;21:15;07:32
BA-1404;Paris;Graz;14:25;21:25;07:00
9B-8710;Prague;Vienna;04:48;08:49;04:01;
9B-8122;Prague;Graz;04:48;08:49;04:01
讓我們測試一下:

public static void main(String[] args) {
   TimetableDisplay timetableDisplay = new timetableDisplay();
   timetableDisplay.printTimetable();
}
輸出:

Train  From  To  Departure time  Arrival time  Travel time
9B-6854  London  Prague  13:43  21:15  07:32
BA-1404  Paris  Graz  14:25  21:25  07:00
9B-8710  Prague  Vienna  04:48  08:49  04:01
9B-8122  Prague  Graz  04:48  08:49  04:01
現在讓我們來看看介紹我們的模式所需的步驟:
  1. 定義一個接口,允許使用代理而不是原始對象。在我們的示例中,這是TrainTimetable.

  2. 創建代理類。它應該有對服務對象的引用(在類中創建它或傳遞給構造函數)。

    這是我們的代理類:

    
    public class ElectricTrainTimetableProxy implements TrainTimetable {
       // Reference to the original object
       private TrainTimetable trainTimetable = new ElectricTrainTimetable();
      
       private String[] timetableCache = null
    
       @Override
       public String[] getTimetable() {
           return trainTimetable.getTimetable();
       }
    
       @Override
       public String getTrainDepartureTime(String trainId) {
           return trainTimetable.getTrainDepartureTime(trainId);
       }
      
       public void clearCache() {
           trainTimetable = null;
       }
    }
    

    在此階段,我們只是創建一個引用原始對象的類,並將所有調用轉發給它。

  3. 讓我們來實現代理類的邏輯。基本上,調用總是重定向到原始對象。

    
    public class ElectricTrainTimetableProxy implements TrainTimetable {
       // Reference to the original object
       private TrainTimetable trainTimetable = new ElectricTrainTimetable();
    
       private String[] timetableCache = null
    
       @Override
       public String[] getTimetable() {
           if (timetableCache == null) {
               timetableCache = trainTimetable.getTimetable();
           }
           return timetableCache;
       }
    
       @Override
       public String getTrainDepartureTime(String trainId) {
           if (timetableCache == null) {
               timetableCache = trainTimetable.getTimetable();
           }
           for (int i = 0; i < timetableCache.length; i++) {
               if (timetableCache[i].startsWith(trainId+";")) return timetableCache[i];
           }
           return "";
       }
    
       public void clearCache() {
           trainTimetable = null;
       }
    }
    

    檢查getTimetable()時間表數組是否已緩存在內存中。如果沒有,它會發送請求從磁盤加載數據並保存結果。如果已經請求了時間表,它會迅速從內存中返回對象。

    由於其簡單的功能,getTrainDepartureTime() 方法不必重定向到原始對象。我們只是用一種新方法複製了它的功能。

    不要這樣做。如果你必須複製代碼或做類似的事情,那就是出了問題,你需要從不同的角度重新審視問題。在我們的簡單示例中,我們別無選擇。但在實際項目中,代碼很可能會寫得更正確。

  4. 在客戶端代碼中,創建一個代理對象而不是原始對象:

    
    public class TimetableDisplay {
       // Changed reference
       private TrainTimetable trainTimetable = new ElectricTrainTimetableProxy();
    
       public void printTimetable() {
           String[] timetable = trainTimetable.getTimetable();
           String[] tmpArr;
           System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time");
           for (int i = 0; i < timetable.length; i++) {
               tmpArr = timetable[i].split(";");
               System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
           }
       }
    }
    

    查看

    
    Train  From  To  Departure time  Arrival time  Travel time
    9B-6854  London  Prague  13:43  21:15  07:32
    BA-1404  Paris  Graz  14:25  21:25  07:00
    9B-8710  Prague  Vienna  04:48  08:49  04:01
    9B-8122  Prague  Graz  04:48  08:49  04:01
    

    太好了,它工作正常。

    您還可以考慮根據特定條件創建原始對象和代理對象的工廠選項。

在我們說再見之前,這裡有一個有用的鏈接

今天就到這裡!返回課程並在實踐中嘗試您的新知識並不是一個壞主意:)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION