“無所不能的中介”——代理模式

1.簡介

定義:將某個對象中圍繞某個主題的一些列行爲委託給一個代理對象去執行,代理對象將控制和管理對原有對象的訪問,調用者想要訪問目標對象,必須通過代理對象去間接訪問,代理對象在調用方和目標對象之間可以起到”中介“的作用。代理一詞本身,其實就可以很好發現的關鍵點,如果暫時無法理解晦澀的概念,那麼在閱讀本文之前先通俗的理解:”就是找其他人代表你、協助你,去更好的幫你做事情“。

在現實生活中代理模式的場景其實無處不在,例如民用汽車消費場景就存在代理模式的影子,我們個人一般沒有權限直接從汽車生產商購買汽車的,並且需要專業人員爲我們介紹汽車的參數,所以我們選擇從4S店作爲購買汽車的途徑。那麼在這個場景中,汽車4S店就屬於一個代理者,它代理了消費者從汽車生產商購買汽車的行爲,不但提供給消費者便捷的購車渠道,還可以享受到售前的專業講解和售後維修保養服務,這些都是無法直接從汽車生產商獲得的。


2.應用場景介紹

在實際的開發場景中,我經常會遇到一種場景,簡單來說就是對現有的函數增加一些通用處理。例如,在訪問函數之前進行身份驗證、在數據操作之後進行日誌記錄等。簡單粗暴的方式,就是將身份驗證和日誌記錄的代碼直接添加在相應函數代碼最前面和代碼最後面。雖然這種方式解決了功能問題,但是在設計上存在一些弊端。

如果實現身份驗證或日誌記錄的代碼邏輯存在隱患或產生變動,並且涉及使用的函數很多,那麼這個改動量是比較大的,改動勢必會對系統產生風險,則需要爲每個改動的方法及業務進行測試工作(因爲任何改動都會存在風險)。這樣的場景反應了一個問題:函數中通用功能代碼和業務耦合在一起,通用功能的變化會引起這個函數的變化,以及不排除調用層對這個函數使用的變化。

爲了減少函數中通用功能代碼和業務代碼之間的耦合性,這個時候我們就可以運用代理模式。簡單來說,我們在客戶端對象訪問原有業務函數之間增加一個代理對象,促使客戶端不能直接訪問業務函數,而只能通過代理對象間接訪問。

這樣一來,業務函數本身只專注於業務,與業務無關的擴展功能則轉交給代理對象。如果擴展功能發生變化,我們無需修改業務函數,而是修改代理對象。基於這種現象可以看出,代理模式很好的遵循了”開閉原則“,即類對擴展開放,對修改關閉。另外,代理對象除了提供給客戶端調用業務函數之外,還額外在業務函數執行之前和之後,提供身份驗證和日誌記錄。


3.代理模式結構

3.1.Subject(抽象主題)

它是基於代理的“主題行爲”抽象出的接口層。之所以稱爲主題,是因爲代理的行爲會圍繞某個主題存在多個,比如數據庫操作這個主題,就存在“增刪改查”多個行爲。

抽象主題是代理類和真實主題類都必須實現的接口,二者通過實現同一接口,代理類就可以在“真實主題類”使用的地方取代它。在調用層,客戶端對象就可以使用多態的形式,面向抽象主題接口編程,而抽象主題接口類型中實際的引用則是代理對象,代理對象中又包含了對“真實主題”對象的引用,從而促使調用層對“真實主題”對象的間接使用。

 3.2.Proxy(代理類)

代理類很好理解,相當於“中介”,主要作用是控制對象的訪問。代理類中處理實現抽象主題以外,還需要包含對被代理對象的引用。之所以要引用被代理對象,那是因爲代理行爲具體的實現任然是被代理者提供的,代理類只是類似於擴展的性質,在代理行爲的執行之前或之後,結合應用場景做額外的附加操作,如權限控制、日誌等。所以代理的行爲還是建立在“被代理者”提供的行爲基礎之上。

 3.3.RealSubject(真實主題)

真實主題是真正做事的對象,它的訪問將由代理類進行控制,俗稱“被代理者”。抽象主題的定義往往就是根據“真實主題”的行爲作爲切入點抽象出來的。“真實主題”會承擔代理行爲的具體實現邏輯,代理類中會引用“真實主題”對象對其進行調用。而在調用層不允許之間訪問該對象,而是通過代理對象間接的訪問它。


4.應用示例

接下來我們基於上文中的應用場景,以系統中常用的一個“用戶服務類”中的查詢方法作爲我們實現代理模式的示例。我們將使用代理模式達到對“用戶服務類”的訪問控制,然後在代理類中在調用查詢方法的基礎上,在額外的增加身份驗證和日誌記錄功能。該示例的代理模式結構如下:

 

在上面的類圖中,我們基於“用戶服務”中的行爲作爲主題抽象成了一個接口,接口中包含了我們需要代理的某個行爲,即獲取用戶信息。爲了在編碼上代理類可以代替“用戶服務類”,故將它們都實現了“用戶服務接口”,這樣一來客戶端可以面向抽象編碼,將“代理類”和“用戶服務類”一致性看待。代理類中新增了Validate方法和Log方法,它們分別用於在“獲取用戶信息”方法的基礎上,額外進行身份驗證和日誌記錄。代理類中還引用了“用戶服務類”對象,它會重寫“GetUserList”方法,並在重寫方法中調用“用戶服務類”提供的“GetUserList”方法,然後再進行額外的功能附加。代碼示例如下:

 1      /// <summary>
 2     /// 用戶服務接口,代理模式中的“抽象主題類”
 3     /// </summary>
 4     public interface IUserService
 5     {
 6         List<string> GetUserList();
 7     }
 8 
 9 
10      /// <summary>
11     /// 用戶服務類,代理模式中的“真實主題類”
12     /// </summary>
13     public class UserService : IUserService
14     {
15         public List<string> GetUserList()
16         {
17             Console.WriteLine("正在連接數據庫,查詢所有用戶信息。。。");
18 
19             List<string> userList = new List<string> 
20             { 
21                 "蘇軾","李白","辛棄疾","岳飛","白居易"
22             };
23 
24             return userList;
25         }  // END GetUserList()
26 
27     }
28 
29   /// <summary>
30     /// 用戶服務代理類,代理模式中的“代理類”
31     /// </summary>
32     public class ProxyUserService : IUserService
33     {
34         private IUserService _userService = new UserService();
35 
36         public List<string> GetUserList()
37         {
38             if (Validate()) //身份驗證
39             {
40                 List<string> userList = _userService.GetUserList(); //調用真實主題對象的查詢方法
41                 Log();//日誌記錄
42                 return userList;
43             }
44 
45             return null;
46         } // END GetUserList()
47 
48         public bool Validate()
49         {
50             //僞代碼,模擬獲取用戶信息
51             string currentUserId = "張三";
52 
53             if (currentUserId== "張三")
54             {
55                 Console.WriteLine($"“{currentUserId}”用戶的權限認證成功!");
56                 return true;
57             }
58             else
59             {
60                 Console.WriteLine($"“{currentUserId}”用戶的權限認證失敗!");
61                 return false;
62             }
63 
64         } // END Validate()
65 
66         public void Log()
67         {
68             //僞代碼,模擬獲取用戶信息
69             string currentUserId = "張三";
70 
71             Console.WriteLine($"用戶:“{currentUserId}與{DateTime.Now}查詢了用戶信息。”");
72 
73         }// END Log()
74 
75     }

客戶端調用代碼:

 1 //創建代理對象
 2 IUserService proxyUserService = new ProxyUserService();
 3 
 4 //使用代理對象獲取用戶信息
 5 List<string> userList= proxyUserService.GetUserList();
 6 
 7 //輸出
 8 Console.WriteLine("\r\n輸出用戶信息:");
 9 foreach (var user in userList)
10 {
11     Console.WriteLine(user);
12 }

輸出結果:

 


5.動態代理

5.1.靜態代理的不足

代理模式中通過“代理對象”實現了對“目標對象”的控制,從而可以在“目標對象”原有的方法基礎上進行額外的擴展,並且這種擴展方式是可以在不修改原有目標對象代碼的基礎上實現,促使原有目標對象實現了開閉原則。

儘管如此,目前的代理模式仍有美中不足。由於我們代理類以及代理的行爲都是預先定義好的,如果抽象主題中需要新增方法,也就是某個代理類要新增代理行爲,那麼代理類則必須要做出相應的實現,並且在實現的方法中,對於通用處理的功能,會在不同的方法中出現冗餘。

例如本實例中的“用戶服務類”,在實際的項目中類似這種數據服務類,肯定不僅只有“查詢用戶”一種方法,必然會有“增刪改查”一系列的方法。如果要爲其增加“增刪改”方法,那麼代理類想要代理這些行爲,則必須在重寫“抽象主題接口”的方法,並且對於通用附加功能(權限、日誌等)的代碼會產生很多冗餘。

 

除此之外,實際項目中如果存在大量的代理需求,那麼我們可能會爲不同類型、不同業務領域的服務類編寫大量的代理類。在編寫大量代理類後,你會發現代理類的結構都幾乎相同,都只是在代理行爲的之前或之後做一些處理,那麼這樣也會產生許多重複。基於這種背景下,爲了尋找一種通用化的代理方案,就衍生出了一種動態代理模式,而以上我們示例中應用的模式反之爲靜態代理。

對於靜態代理而言,代理類都是預先編寫定義好的,這導致隨着代理需求的增加還需要新增相應的代理類,並且代理行爲增加,代理類也需要不斷去實現相應的方法。“唯一不變的是變化本身”,我們不可能預知系統的所有代理需求,不可能預估系統中,哪些類、哪些方法需要被代理。

爲了應對這種變化,我們可以使用動態代理,它相當於定義了一個通用化的代理模板,我們不需要預先定義代理類,它會根據你在客戶端使用的“抽象主題類型”動態創建代理對象,只要你使用的目標對象使用了代理模式,這個通用的代理模板都會爲目標對象動態的生成代理類。並且我們不需要在代理類中去實現代理行爲,它會有一種通用的調用方式,將代理擴展的行爲作用於每個方法。

 5.2.DispatchProxy

下面我們將使用System.Reflection命名空間下的DispatchProxy類型來實現動態代理,該類型只適用於.NET框架4.6以上版本和.NET Core,對於較低版本的.NET框架不支持。

我們將延用靜態代理中的“抽象主題”和“真實主題”,在此基礎之上編寫動態代理類。該代理類主要代理系統中服務類的“增刪改查”行爲,並在各個服務類的“增刪改查”方法之前和之後加上身份驗證和日誌記錄。具體代碼如下:

 1.創建動態代理類型

 1     /// <summary>
 2     /// 動態代理類
 3     /// </summary>
 4     /// <typeparam name="T">抽象主題類型</typeparam>
 5     public class ProxyCRUD<T> : DispatchProxy 
 6     {
 7         //目標對象,被代理對象
 8         public T Target { get; private set; }
 9 
10         /// <summary>
11         /// 創建“動態代理類”對象,並指定一個“被代理對象”
12         /// </summary>
13         /// <param name="target">被代理對象</param>
14         /// <returns>抽象主題類型(代理接口),但類型的引用指向的是“動態代理對象”</returns>
15         public static T Decorate(T target)
16         {
17             //創建一個實現“抽象主題接口”的“動態代理對象”
18             dynamic proxy = Create<T, ProxyCRUD<T>>();
19 
20             //指定“動態代理對象”代理的目標對象,即被代理的對象
21             proxy.Target = target;
22 
23             return proxy;
24         }
25         // END Decorate()
26 
27         /// <summary>
28         /// 動態代理對象執行代理行爲
29         /// “被代理對象”的方法被代理對象執行時,會通過該方法間接調用
30         /// </summary>
31         /// <param name="targetMethod">“被代理對象”的方法信息</param>
32         /// <param name="args">方法的參數</param>
33         /// <returns>方法執行的返回值</returns>
34         protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
35         {
36             if (Validate()) //擴展通用處理:身份驗證
37             {
38                 //通過反射的方式調用“被代理對象”的原始方法
39                 var result = targetMethod.Invoke(Target,args);
40 
41                 Log(targetMethod.Name);//擴展通用處理:日誌記錄
42 
43                 return result;
44             }
45             else
46             {
47                 return null;
48             }
49 
50         }// END Invoke ()
51 
52         /// <summary>
53         /// 身份驗證(僞代碼)
54         /// </summary>
55         public bool Validate()
56         {
57             //僞代碼,模擬獲取用戶信息
58             string currentUserId = "張三";
59 
60             if (currentUserId == "張三")
61             {
62                 Console.WriteLine($"“{currentUserId}”用戶的權限認證成功!");
63                 return true;
64             }
65             else
66             {
67                 Console.WriteLine($"“{currentUserId}”用戶的權限認證失敗!");
68                 return false;
69             }
70 
71         } // END Validate()
72 
73         /// <summary>
74         /// 日誌記錄(僞代碼)
75         /// </summary>
76         public void Log(string action)
77         {
78             //僞代碼,模擬獲取用戶信息
79             string currentUserId = "張三";
80 
81             Console.WriteLine($"用戶:{currentUserId}在{DateTime.Now}執行了{action}操作。");
82 
83         }// END Log()
84 
85     }

以上代碼中的“動態代理類”是一個泛型類,其中泛型的類型參數,需要指定代理模式中的“抽象主題類型”,也就是被代理類和代理類都需要實現的接口類型。在靜態模式中,“抽象主題類型”是指定的一個具體類型,而這裏使用了泛型的類型參數,這就意味該類可以適用於所有類型的代理,就像List<T>一樣,不光可以用於List<int>集合、還可以用於List<string>、List<object>等。

其中派生自“DispatchProxy”類,實現的Invoke方法是代理行爲的核心,在調用層通過代理對象調用任何方法時,都會將方法的執行帶入到Invoke方法中。換句話說,我們使用動態代理對象去執行方法時,就像通過“傳送門”就方法的執行轉發到Invoke方法中,然後在該方法中可以在原始方法的基礎上額外擴展其他功能。

 2.客戶端調用

 1 //創建真實主題對象,即被代理對象
 2 UserService userService = new UserService();
 3 
 4 /*【創建代理對象】
 5  * 根據“抽象主題接口”動態創建代理對象,並實現“抽象主題接口”
 6  * “被代理對象”作爲參數指定給了“代理對象”
 7  */
 8 var proxyUserService = ProxyCRUD<IUserService>.Decorate(userService);
 9 
10 /*
11  * 方法源於“抽象主題”,實現源於“被代理對象”,
12  * “代理對象”代理了方法的調用。
13  */
14 var userList = proxyUserService.GetUserList();
15 
16 Console.WriteLine("\r\n輸出用戶信息:");
17 foreach (var user in userList)
18 {
19     Console.WriteLine(user);
20 }

6.代理和裝飾

代理模式和裝飾模式在實現時有些類似,但是代理模式主要是給“真實主題類”增加一些全新的職責,例如在業務方法執行之前進行權限驗證、例如在業務方法執行之後附加日誌記錄等,這些職責往往是非業務的,與業務職責不屬於同一個問題域。

對於裝飾模式而言,它是通過裝飾類爲具體構建類增加一些與業務職責相關的職責,是對原有業務職責的擴展,擴展的職責和原有業務都屬於同一個問題域。代理模式和裝飾模式的目的也不相同,代理模式達到控制對象的訪問,而裝飾模式是爲對象動態地增加功能,可以看作是填補繼承不靈活性的另一種功能複用方案。


7.總結

代理模式的結構是比較簡單的,實際上就是將某個類型的“代理需求”(類的行爲/方法/業務)建立一個“抽象主題”(接口)並提供方法的實現。然後我們面向這個“抽象主題”創建一個代理類,並在代理類中引用“被代理對象”,然後在“被代理對象”的“行爲/方法/業務”執行的基礎上進行額外的加工、管控。

代理模式的應用場景非常廣泛,難點就在如何應用到不同場景,並且不同場景還涉及到其他領域的特有技術。其中常用的應用場景包括:遠程代理、虛擬代理、保護代理、智能引用代理,以及AOP的實現。本文中的示例是針對“智能引用代理”場景的應用,也就是在目標對象原有的業務方法之上,爲對象提供一些額外的通用處理。

本文屬於代理模式的基礎教程,所以在此不能詳細闡述所有的應用場景,下面根據較常用的場景進行簡單概要:

  1. 遠程代理:當你的主機想要訪問遠程主機中的對象時,可以使用遠程代理幫你建立一個網絡橋樑,它會幫你訪問網絡轉發請求來完成遠程對象的調用。
  2. 虛擬代理:當加載的對象資源大、耗時長,可以使用虛擬代理爲這種對象建立一個輕量級的替身對象先預載,從而降低系統開銷、縮短運行時間時。
  3. 保護代理:當需要控制對一個對象的訪問,爲不同用戶提供不同級別的訪問權限時,可以使用保護代理
  4. 智能引用代理:當訪問某個對象的行爲需要做一些額外的擴展操作時,可以使用智能引用代理。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章