1、装饰者模式与代理模式 (静态代理)
在日常开发里面,我们经常需要给某个类的方法增加加某些特定的功能。
例如:有婴儿,婴儿会吃饭和走动,如以下类
1 package com.scl.designpattern.proxy; 2 3 //婴儿类 4 public class Child implements Human 5 { 6 public void eat() 7 { 8 System.out.println("eat something...."); 9 } 10 11 @Override 12 public void run() 13 { 14 System.out.println("Child run very slow"); 15 } 16 }
婴儿类
突然有一天,家长发现不行,孩子不能随便吃东西,而且吃饭前一定要洗手。但是孩子太小(被委托方),不会自己洗手。家长(Client 端)又没法照顾孩子。那简单,找个保姆照顾孩子! 让保姆类和婴儿类共同实现同一个接口,让保姆类全程管理小孩,同时在家长眼里,只要看到保姆在帮孩子洗手就可以了。于是,有以下内容。
1 package com.scl.designpattern.proxy; 2 3 //保姆类 4 public class BabySitter implements Human 5 { 6 7 @Override 8 public void eat() 9 { 10 11 } 12 13 @Override 14 public void run() 15 { 16 17 } 18 19 }
保姆类
现在保姆已经有了,孩子也有了,怎么把孩子跟保姆关联起来。让保姆给相应的孩纸洗手。于是保姆类更改如下
1 package com.scl.designpattern.proxy; 2 3 //保姆类 4 public class BabySitter implements Human 5 { 6 private Human human; 7 8 public BabySitter(Human human) 9 { 10 this.human = human; 11 } 12 13 @Override 14 public void eat() 15 { 16 // 添加washHand的方法 17 this.washHandForChild(); 18 human.eat(); 19 } 20 21 @Override 22 public void run() 23 { 24 25 } 26 27 public void washHandForChild() 28 { 29 System.out.println("help the child to wash his hands"); 30 } 31 }
保姆与婴儿类关联
好,那么家长就是给孩纸找了个保姆代理,让他附加了一些婴儿做不了事。同时家长也没有强迫孩纸自己学会洗手(不更改Child类)
1 package com.scl.designpattern.proxy; 2 3 //客户端 4 public class Client 5 { 6 public static void main(String[] args) 7 { 8 Human human = new BabySitter(new Child()); 9 human.eat(); 10 } 11 }
家长客户端代码
以上就是一个简单的装饰模式,来看一下这一块完整的类图。
装饰模式的一个很重要特点就是,在客户端可以看到抽象对象的实例,如Human human = new BabySitter(new Child()); 因为装饰模式通过聚合方式,把内容整合到装饰类里面了。
装饰者模式能够使用装饰类对抽象对象进行装饰。假如来了个OldMan类手脚不利索。保姆类BabySitter同样能够胜任这个OldMan的饭前洗手操作。
例子想了很久,也看了不少别人的博客。发现一个很令人迷惑的问题:为什么我用想的代理模式,确是别人口中的装饰模式?装饰者模式和代理模式有什么区别?。大致的代理模式类图如下:
由该类图可知,以上BabySitter代码应该如下:
1 package com.scl.designpattern.proxy; 2 3 //保姆类 4 public class BabySitter implements Human 5 { 6 private Child child; 7 8 public BabySitter() 9 { 10 child = new Child(); 11 } 12 13 @Override 14 public void eat() 15 { 16 // 添加washHand的方法 17 this.washHandForChild(); 18 human.eat(); 19 } 20 21 @Override 22 public void run() 23 { 24 25 } 26 27 public void washHandForChild() 28 { 29 System.out.println("help the child to wash his hands"); 30 } 31 }
代理模式下的代理类
1 package com.scl.designpattern.proxy; 2 3 //客户端 4 public class Client 5 { 6 public static void main(String[] args) 7 { 8 Human human = new BabySitter(); 9 human.eat(); 10 } 11 }
代理模式下的客户端
装饰者模式和代理模式的最后运行的结果都是一样的,显示如下
由代理模式代码可知,客户端不关心代理类了哪个类。但代码控制了客户端对委托类的访问。客户端代码表现为 Human human = new BabySitter( );
所以资料上都说了,装饰模式主要是强调对类中代码的拓展,而代理模式则偏向于委托类的访问限制。两者都一样拥有抽象角色(接口)、真实角色(委托类)、代理类 。
由于代理类实现了抽象角色的接口,导致代理类无法通用。如有天,一个有钱人养了只小猩猩,他要一个保姆在猩猩吃东西前,帮猩猩洗手....保姆根本不懂猩猩的特性(跟猩猩类不是同一类型的,保姆属于Human类,而猩猩可能属于Animal类型。),但洗手这个方法是不变的,用水洗。能不能找一个代理说既可以照看人吃饭前洗手也可以照看猩猩吃饭前洗手?
用机器人吧... (编不下去了)。要实现这种功能,必须让代理类与特定的接口分离。在代理的时候能够了解每个委托的特性,这就有可能了。这时候动态代理就出现了。
2、动态代理
如静态代理的内容所描述的,静态代理受限于接口的实现。动态代理就是通过使用反射,动态地获取抽象接口的类型,从而获取相关特性进行代理。因动态代理能够为所有的委托方进行代理,因此给代理类起个通用点的名字HealthHandle。先看保姆类可以变成什么样。
1 package com.scl.designpattern.proxy.dynamic; 2 3 import java.lang.reflect.InvocationHandler; 4 import java.lang.reflect.Method; 5 import java.lang.reflect.Proxy; 6 7 public class HealthHandle implements InvocationHandler 8 { 9 10 private Object proxyTarget; 11 12 public Object getProxyInstance(Object target) 13 { 14 this.proxyTarget = target; 15 return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this); 16 } 17 18 @Override 19 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 20 { 21 Object methodObject = null; 22 System.out.println("proxy washing hand start"); 23 methodObject = method.invoke(proxyTarget, args); 24 System.out.println("proxy washing hand end"); 25 return methodObject; 26 } 27 }
由保姆类转化而来的动态代理类
1 package com.scl.designpattern.proxy.dynamic; 2 3 import java.lang.reflect.Proxy; 4 5 //客户端 6 public class Client 7 { 8 public static void main(String[] args) 9 { 10 HealthHandle h = new HealthHandle(); 11 Human human = (Human) h.getProxyInstance(new Child()); 12 human.eat(); 13 human.run(); 14 } 15 }
动态代理模式下的客户端代码
首先,使用动态代理
1. 必须实现InvocationHandler接口,订立一个契约标识该类为一个动态代理执行类。
2. InvocationHandler接口内有一实现方法签名如下: public Object invoke(Object proxy, Method method, Object[] args) 。使用时需要重写这个方法
3. 获取代理类,需要使用 Proxy.newProxyInstance(Clas loader, Class<?>[] interfaces, InvocationHandler h) 这个方法去获取代理对象。
先看获取代理对象签名解析如下:
//Clas loader : 类的加载器 //Class<?>[] interfaces : 委托类实现的接口,保证代理类返回的是同一个实现接口下的类型,保持代理类与抽象角色行为的一致 //invocationHandler, 该类最重要的一环。一个设计模式:策略模式.即告诉代理类,代理类遇到某个委托类的方法时该调用哪个类下的invoke方法。 Proxy.newProxyInstance(Class loader, Class<?>[] interfaces, InvocationHandler h)
再看实现InvocationHandler方法下的签名 public Object invoke(Object proxy, Method method, Object[] args)
//第一个参数为代理类 //第二个参数为委托类的方法对象//第三个参数为委托类的方法参数 //返回类型为委托类某个方法的返回对象 public Object invoke(Object proxy, Method method, Object[] args) 总的来说这个方法就是动态获取委托类里面的方法,在调用委托类的方法时在这个方法内进行拓展。 通过上面的Proxy.newProxyInstance方法,告诉底层代码,该去哪个类里面执行invoke方法。
由上面的描述,在客户端代码的调用后,结果如下:
由此可见,在代理类对系统下每个方法的调用时,都会去调用invocationHandler里面的invoke方法. 包括里面的eat和run方法,可见受作用的范围是多广:每个方法都受代理类作用了。由例子可以看出,如果要实现我们想要的方法上面添加特定的代理,可以通过invoke方法里面的方法反射获取method对象方法名称即可实现(但这样会产生硬编码)。修改可如下:
1 @Override 2 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 3 { 4 Object methodObject = null; 5 if (method.getName().equals("eat")) 6 { 7 System.out.println("proxy washing hand start"); 8 methodObject = method.invoke(proxyTarget, args); 9 System.out.println("proxy washing hand end"); 10 } 11 else 12 { 13 methodObject = method.invoke(proxyTarget, args); 14 } 15 return methodObject; 16 }
由此,我们可以猜想出来动态代理在开发中的用法:
1. 监控程序下执行某个方法的耗时、性能及日志的编写
2. 监控程序方法的权限,不再单一地实行Controler→ServiceImpl→DaoImpl这种纵向编程。增加了在同一层面间横向编程的可能(切面编程:AOP)
另外有一点必须指出的内容:在使用Invoke方法的时候,需要对method的可访问性进行加工吗?不需要,动态代理模式最后返回的是具有抽象角色(顶层接口)的对象。在委托类内被private或者protected关键修饰的方法将不会予以调用,即使允许调用。也无法在客户端使用代理类转换成子类接口,对方法进行调用。