在程式設計中,正確規劃應用程式架構非常重要。為此,一個不可或缺的工具就是設計模式。今天我們要講的是Proxy,或者說代理。
為什麼需要副手?
此模式有助於解決與物件的受控存取相關的問題。您可能有一個問題:“為什麼我們需要這種受控訪問?” 讓我們來看看幾種情況,它們將幫助您弄清楚是什麼。實施例1
假設我們有一個包含一堆舊程式碼的大型項目,其中有一個類別負責從資料庫下載報告。該類別同步工作,即在資料庫處理請求時整個系統處於空閒狀態。平均而言,報告會在 30 分鐘內產生。由於此功能,其上傳從 00:30 開始,管理層在早上收到此報告。經過分析發現,報告產生後必須立即收到,即一天之內。無法重新安排開始時間,因為系統將等待資料庫的回應。解決方案是透過在單獨的執行緒中啟動上傳和報告生成來改變操作原理。該解決方案將允許系統照常運行,並且管理層將收到新的報告。然而,有一個問題:目前的程式碼無法重寫,因為它的功能被系統的其他部分使用。在這種情況下,您可以使用代理模式引入中間代理類,它將接收上傳報告的請求,記錄開始時間並啟動單獨的執行緒。當報告生成後,線程將完成其工作,每個人都會高興。實施例2
開發團隊創建了一個海報網站。為了獲取有關新事件的數據,他們求助於第三方服務,並透過特殊的封閉程式庫實現與第三方服務的互動。開發過程中出現了一個問題:第三方系統每天更新一次數據,用戶每次刷新頁面都會產生一次請求。這會建立大量請求並且服務停止回應。解決方案是快取服務回應並在每次重新啟動時向訪客提供已儲存的結果,並根據需要更新此快取。在這種情況下,使用代理模式是一個很好的解決方案,無需更改已完成的功能。該模式如何運作
要實現此模式,您需要建立一個代理類別。它實作一個服務類接口,為客戶端程式碼模擬其行為。因此,客戶端與其代理進行交互,而不是真實的物件。通常,所有請求都會傳遞到服務類,但在呼叫之前或之後會執行其他操作。簡單地說,這個代理物件是客戶端程式碼和目標物件之間的一層。讓我們看一個快取來自非常慢的舊磁碟的請求的範例。讓它成為某個古老應用中的電動火車時刻表,其運作原理無法改變。每天在固定時間插入具有更新時間表的磁碟。所以我們有:- 介面
TimetableTrains
. TimetableElectricTrains
實作該介面的類別。- 客戶端程式碼正是透過這個類別與磁碟檔案系統互動。
- 客戶類
DisplayTimetable
。它的方法printTimetable()
使用類別方法TimetableElectricTrains
。
printTimetable()
該類別TimetableElectricTrains
都會存取磁碟、卸載資料並將其提供給客戶端。該系統運作良好,但速度非常慢。因此,決定透過添加快取機制來提高系統效能。這可以使用代理模式來完成: 這樣,類別DisplayTimetable
甚至不會注意到它正在與該類別交互TimetableElectricTrainsProxy
,而不是與前一個類別交互。新的實作每天載入一次計劃,並根據重複的請求,從記憶體中傳回已載入的物件。
對於哪些任務最好使用 Proxy?
在以下幾種情況下,這種模式肯定會派上用場:- 快取.
- 惰性實現也稱為惰性實現。當您可以根據需要載入一個物件時,為什麼要一次載入它呢?
- 記錄請求。
- 臨時資料和存取檢查。
- 啟動並行處理線程。
- 記錄或統計通話紀錄。
的優點和缺點
- + 您可以根據需要控制對服務對象的存取;
- + 用於管理服務對像生命週期的附加功能;
- + 無需服務對象即可工作;
- + 提高程式碼效能和安全性。
- - 由於額外的治療而存在表現惡化的風險;
- - 使程式類別的結構複雜化。
實踐中的替代模式
讓我們與您一起實作一個從磁碟讀取火車時刻表的系統:public interface TimetableTrains {
String[] getTimetable();
String getTrainDepartureTime();
}
實作主介面的類別:
public class TimetableElectricTrains implements TimetableTrains {
@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 DisplayTimetable {
private TimetableTrains timetableTrains = new TimetableElectricTrains();
public void printTimetable() {
String[] timetable = timetableTrains.getTimetable();
String[] tmpArr;
System.out.println("Поезд\tОткуда\tКуда\t\tВремя отправления\tВремя прибытия\tВремя в пути");
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;Лондон;Прага;13:43;21:15;07:32
BA-1404;Париж;Грац;14:25;21:25;07:00
9B-8710;Прага;Вена;04:48;08:49;04:01;
9B-8122;Прага;Грац;04:48;08:49;04:01
我們來測試一下:
public static void main(String[] args) {
DisplayTimetable displayTimetable = new DisplayTimetable();
displayTimetable.printTimetable();
}
結論:
Поезд Откуда Куда Время отправления Время прибытия Время в пути
9B-6854 Лондон Прага 13:43 21:15 07:32
BA-1404 Париж Грац 14:25 21:25 07:00
9B-8710 Прага Вена 04:48 08:49 04:01
9B-8122 Прага Грац 04:48 08:49 04:01
現在讓我們來看看實現我們的模式的步驟:
-
定義一個接口,允許您使用新的代理而不是原始物件。在我們的示例中是
TimetableTrains
。 -
建立一個代理類別。它必須包含對服務物件的參考(在類別中建立或傳入建構函數);
這是我們的代理類別:
public class TimetableElectricTrainsProxy implements TimetableTrains { // Ссылка на оригинальный an object private TimetableTrains timetableTrains = new TimetableElectricTrains(); private String[] timetableCache = null @Override public String[] getTimetable() { return timetableTrains.getTimetable(); } @Override public String getTrainDepartureTime(String trainId) { return timetableTrains.getTrainDepartureTime(trainId); } public void clearCache() { timetableTrains = null; } }
在此階段,我們只需建立一個帶有對原始物件的引用的類,並將所有呼叫傳遞給它。
-
我們實作代理類別的邏輯。基本上,呼叫總是重定向到原始物件。
public class TimetableElectricTrainsProxy implements TimetableTrains { // Ссылка на оригинальный an object private TimetableTrains timetableTrains = new TimetableElectricTrains(); private String[] timetableCache = null @Override public String[] getTimetable() { if(timetableCache == null) { timetableCache = timetableTrains.getTimetable(); } return timetableCache; } @Override public String getTrainDepartureTime(String trainId) { if(timetableCache == null) { timetableCache = timetableTrains.getTimetable(); } for(int i = 0; i < timetableCache.length; i++) { if(timetableCache[i].startsWith(trainId+";")) return timetableCache[i]; } return ""; } public void clearCache() { timetableTrains = null; } }
此方法
getTimetable()
檢查調度數組是否緩存在記憶體中。如果沒有,它會發出從磁碟載入資料的請求,並儲存結果。如果請求已經在運行,它將快速從記憶體中傳回一個物件。由於其簡單的功能, getTrainDepartireTime() 方法不必重定向到原始物件。我們只是將其功能複製到一個新方法中。
你不能那樣做。如果您必須重複程式碼或執行類似的操作,則表示出現了問題,您需要從不同的角度看待問題。在我們的簡單範例中沒有其他方法,但在實際專案中,很可能程式碼會寫得更正確。
-
將客戶端程式碼中原始物件的建立替換為替換物件:
public class DisplayTimetable { // Измененная link private TimetableTrains timetableTrains = new TimetableElectricTrainsProxy(); public void printTimetable() { String[] timetable = timetableTrains.getTimetable(); String[] tmpArr; System.out.println("Поезд\tОткуда\tКуда\t\tВремя отправления\tВремя прибытия\tВремя в пути"); 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 Лондон Прага 13:43 21:15 07:32 BA-1404 Париж Грац 14:25 21:25 07:00 9B-8710 Прага Вена 04:48 08:49 04:01 9B-8122 Прага Грац 04:48 08:49 04:01
太好了,它工作正常。
您也可以考慮一個工廠,它將根據某些條件建立原始物件和替換物件。
GO TO FULL VERSION