「無所不能的中介」——代理模式

2023-02-28 15:00:40

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. 智慧參照代理:當存取某個物件的行為需要做一些額外的擴充套件操作時,可以使用智慧參照代理。