“无所不能的中介”——代理模式
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的实现。本文中的示例是针对“智能引用代理”场景的应用,也就是在目标对象原有的业务方法之上,为对象提供一些额外的通用处理。
本文属于代理模式的基础教程,所以在此不能详细阐述所有的应用场景,下面根据较常用的场景进行简单概要:
- 远程代理:当你的主机想要访问远程主机中的对象时,可以使用远程代理帮你建立一个网络桥梁,它会帮你访问网络转发请求来完成远程对象的调用。
- 虚拟代理:当加载的对象资源大、耗时长,可以使用虚拟代理为这种对象建立一个轻量级的替身对象先预载,从而降低系统开销、缩短运行时间时。
- 保护代理:当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时,可以使用保护代理。
- 智能引用代理:当访问某个对象的行为需要做一些额外的扩展操作时,可以使用智能引用代理。