2.2 结构型模式
结构型模式一共有七种。其中,适配器模式和装饰模式统称为包装模式,装饰模式和代理模式的类图基本相同,但目的不同。这些有相似目的或者有相似结构的模式需要对其概念辨析清楚,才能较好地掌握。下面将对结构型模式分别进行介绍。
2.2.1 适配器模式
模式原理
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法一起工作的两个类能够一起工作。说白了就是功能提供者和功能使用者无法匹配,需要一个适配器角色进行功能转换。比如手机充电需要30V电压,家用插座提供220V电压,两者之间就需要充电器充当适配器角色,来将220V电压转换成30V才能正常充电。
在适配器模式中有三种角色,一种是目标接口target,一种是源类adaptee,一种是适配器adapter。目标接口对外提供功能,但实际上是通过适配器来调用源类的功能来实现的。以上面说的手机充电为例,手机具有充电功能,但该功能最终的实际提供者是插座,那手机和插座怎么连接呢?通过充电器进行连接。于是目标类即手机接口,该接口具有充电功能:
public interface IMobile {
// 目标接口的充电功能
void charge();
}
现在用户要求对手机进行充电:
public static void main(String[] args) {
IMobile iphone = new IPhone();
iphone.charge();
}
对用户(客户端)来讲,只关心目标(即手机),然后使用目标(手机)的充电功能。至于该功能具体怎么实现并不关心。而客户不关心的充电功能的实现正是适配器模式的适配体现:
public class IPhone implements IMobile {
private Charger adapter;
@Override
public void charge() {
System.out.println("我是手机,用户正在对我进行充电");
adapter = new Charger();
adapter.charge();
}
public Charger getAdapter() {
return adapter;
}
public void setAdapter(Charger adapter) {
this.adapter = adapter;
}
}
可以看到,手机(目标)的充电功能使用了充电器(适配器)进行充电。而充电器充电功能的实现使用了插座提供的充电功能,并对其进行适配处理(降低电压):
public class Charger {
private int voltage;
ElectricalSocket electricalSocket;
public void charge() {
System.out.println("我是充电器,我正被用来充电");
electricalSocket = new ElectricalSocket();
this.voltage = electricalSocket.produceVoltage() - 190;
System.out.println("我是充电器,我使用插座进行充电,我提供的电压为:" + this.voltage + "V");
}
public int getVoltage() {
return voltage;
}
public void setVoltage(int voltage) {
this.voltage = voltage;
}
}
最终充电结果:
我是手机,用户正在对我进行充电
我是充电器,我正被用来充电
我是充电器,我使用插座进行充电,我提供的电压为:30V
回过头来分析一下,目标接口(IMobile)提供了一个方法(charge),该方法无法被目标的实例(IPhone)实现(手机本身是无法充电的)。然后目标实例使用了适配器(Charger),调用了适配器的同样的方法(charge)。适配器的该方法中调用了源类(ElectricalSocket)的功能,并进行一定的适配处理(将电压降低),最终实现了目标接口的方法。一句话总结:目标接口的方法最终是靠适配器调用源类来实现的。
对象适配与类适配
适配器模式分为类适配和对象适配。这是从适配器与源类的关系来划分的。从上面的代码来看,适配器和源类之间是组合关系,即适配器持有源类的引用(对象),并通过这个引用来调用需要的方法与目标进行适配,这属于对象适配。还有一种实现方式就是让适配器Charger来继承源类ElectricalSocket,相当于适配器就拥有了源类的所有方法,如果将适配器的方法看成适配后的方法,就意味着适配了整个类的全部方法,这样的适配结构属于类适配。比较而言,对象适配比类适配更灵活。
应用实例
在Java中,IO相关的类的设计就使用了适配器模式。看下面的例子:
public void test() throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("D://a.txt")));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("D://b.txt")));
StringBuilder sb = new StringBuilder();
String str;
while ((str = br.readLine()) != null) {
sb.append(str);
bw.write(sb.toString());
sb.delete(0, sb.length());
}
bw.flush();
}
这个例子中,字符缓冲流对象br从D盘下的a文件读取内容。从用户的角度看,通过调用目标br的方法readLine就可以读取一行内容。但是,字符缓冲流是不能直接操作文件的,只有字节缓冲流能操作文件。因此需要一个适配器InputStreamReader,通过这个适配器调用源类FileInputStream来实现文件操作。
缺省适配器
适配器模式中有一种情况叫缺省适配。某接口定义了多个接口方法,实际可能只关心其中的一个或两个方法,其余的方法并不关注。但按照接口规则,要实现该接口的话,所有的方法都得实现,于是不关注的方法需要实现为空。这样虽然没有问题,但是写起来繁琐,可读性也差。这种情况下,可以用一个抽象类来实现该接口,在抽象类中,所有的接口方法都提供缺省的空实现。用户类继承该抽象类,选择关注的方法来实现,而不用管其他不关注的方法了。这个抽象类就是缺省适配器。
2.2.2 装饰模式
模式原理
前文说过,适配器模式和装饰模式都有一个别称叫包装模式。所谓包装,就是对源进行一定的装饰或者改变,以达到最终使用的目的。适配器模式通过适配器的适配将一个类的接口变成客户端所期待的另一种接口,而装饰模式则是源对象的功能进行某种扩展。适配器模式需要一个适配器角色来进行衔接,而装饰模式不需要。一句话总结:适配器模式的目标和源属于不同的类别;装饰模式的目标(被装饰者)和装饰者都实现了装饰接口,属于同一个类别。
装饰模式中有两种角色:装饰者和被装饰者。实际上这两种角色都是同一个类型的,实现了同一个接口。类图如下(该图引用自参考资料1,文中较好地阐述了装饰模式的作用):
举个栗子,假设被装饰者是一个礼物,然后分别用盒子和袋子进行装饰(包装),最终得到的是包裹了袋子和盒子的礼物。那么在这里栗子中,需要一个公共接口,该接口有一个描述方法,能够描述装饰的结果:
// 公共接口
public interface IComponent {
String desc();
}
第一个角色(被装饰者)实现了这个接口,描述了装饰为零的情况下的初始目标:
public class ConcreteComponent implements IComponent {
@Override
public String desc() {
return "礼物";
}
}
第二个角色(装饰者)也实现了这个接口,并持有被装饰者的引用,描述装饰结果时直接展示被装饰者的初始描述:
public class Decorator implements IComponent {
private IComponent component;
public Decorator(IComponent component) {
this.component = component;
}
@Override
public String desc() {
return component.desc();
}
}
具体的装饰者盒子:
public class FirstDecorator extends Decorator {
public FirstDecorator(IComponent component) {
super(component);
}
@Override
public String desc() {
System.out.println("盒子装饰者开始进行装饰");
return "盒(" + super.desc() + ")子";
}
}
具体的装饰者袋子:
public class SecondDecorator extends Decorator {
public SecondDecorator(IComponent component) {
super(component);
}
@Override
public String desc() {
System.out.println("袋子装饰者开始进行装饰");
return "最终得到:袋[" + super.desc() + "]子";
}
}
客户端调用:
public class Client {
public static void main(String[] args) {
Decorator decorator = new SecondDecorator(new FirstDecorator(new ConcreteComponent()));
System.out.println(decorator.desc());
}
}
输出结果:
袋子装饰者开始进行装饰
盒子装饰者开始进行装饰
最终得到:袋[盒(礼物)子]子
应用实例
这里仍然要用JAVA中IO类的设计来举例。上节的适配器模式中,举例时说过适配器模式在IO类设计中的应用。在适配器模式中,目标与被适配者(如BufferedReader和FileInputStream)并不一定是同一类,而装饰模式中,装饰者和被装饰者都必须是同一类的。如:
DataInputStream dataInputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
这里FileInputStream属于被装饰者,通过BufferedInputStream进行了一层装饰,拥有了缓冲输入流的功能,然后以被装饰后的目标new BufferedInputStream(new FileInputStream(file))为被装饰者,再次通过DataInputStream进行一层装饰,拥有了数据输入流的功能。不管是装饰者还是被装饰者,都属于InputStream的实例,属于同一类。
2.2.3 代理模式
代理模式的定义:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式通过代理对象访问目标对象,在目标对象实现的基础上,增强额外的功能操作,扩展目标对象的功能。
举个例子说明,假设某厂商要邀请一位明星做广告,但是明星不会亲自来洽谈商务合作,一般交由其经纪人进行处理,这时候厂商就通过明星的经纪人来达成目的。明星只需要到时候负责拍广告,其他琐碎的事则由经纪人来完成。在这个过程中,经纪人就是明星的代理。明星属于目标对象,经纪人属于代理对象。代理对象是目标对象的扩展,并调用目标对象。上文介绍装饰模式时说过,装饰模式和代理模式的类图基本一致。代理模式有一个抽象主题角色,是一个接口。然后有目标角色和代理角色实现了这个抽象主题角色。代理角色内部含有目标对象的引用,从而可以操作目标对象。代理对象拥有与目标对象相同的接口,以便在任何时刻都能代替真实对象。
代理模式分为两类,静态代理和动态代理,其中动态代理又分为JDK代理和CGLIB代理。Spring的AOP底层就是通过动态代理实现的。
静态代理
静态代理比较简单,与动态代理比,静态代理是由开发人员编写代理类,在程序运行之前就编译好。而动态代理是在程序运行的时候通过反射机制动态地创建代理对象的。
下面通过代码看静态代理的实现:
抽象主题:
public interface IUserDao {
void save();
}
目标类:
public class UserDao implements IUserDao {
@Override
public void save() {
System.out.println("保存数据");
}
}
代理类:
public class UserDaoProxy implements IUserDao {
private IUserDao target;
public UserDaoProxy(IUserDao target) {
this.target = target;
}
@Override
public void save() {
System.out.println("开始事务...");
target.save();
System.out.println("提交事务");
}
}
调用:
public class Client {
public static void main(String[] args) {
UserDaoProxy userDaoProxy = new UserDaoProxy(new UserDao());
userDaoProxy.save();
}
}
静态代理的缺点是代理对象与目标对象实现一样的接口,一旦接口增加方法,目标对象与代理对象都要维护。于是更好的代理方式是动态代理。
动态代理
动态代理对象不需要实现接口,在代理的过程中通过反射动态地生成。动态代理分为两种:JDK代理和CGLIB代理。
JDK代理
JDK代理中,抽象主题接口和目标类保持不变。只是代理类不再由开发者手动编写,而是通过系统自动生成。当然,即使是系统自动生成,也要做一些工作。直接看代码示例。
代理类生成工厂ProxyFactory:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始事务");
Object rsp = method.invoke(target, args);
System.out.println("提交事务");
return rsp;
}
});
}
}
该类通过getProxyInstance方法产生代理实例,实际上是利用了JDK的Proxy类的静态方法newProxyInstance来动态生成代理对象。该方法需要三个参数:
第一个参数为类加载器,描述为:the class loader to define the proxy class。该参数用来加载代理类的,与目标类加载器是同一个。
第二个参数为代理的接口,描述为:the list of interfaces for the proxy class to implement。动态生成的代理类要实现这些接口,实际上就是抽象主题。生成代理类的时候系统会让代理类实现接口的所有方法。同时代理类还会继承Proxy类,由于Java中不允许多继承,所以不能通过继承的方式代理其他类。这也是JDK代理的特点:只能代理接口。
第三个参数为调用处理器,描述为:the invocation handler to dispatch method invocations to。意思是代理方法最终被派发给调用处理器来完成,invoke方法即是实际的代理方法。
传入这三个参数之后,系统运行时Proxy.newProxyInstance方法会通过反射来动态生成代理类,至于具体如何生成的,感兴趣的可以直接看源码(参考资料2中也有比较详细的介绍),这里就不扩展了。
最后即是客户端调用:
public static void main(String[] args) {
IUserDao userDao = new UserDao();
IUserDao proxy = (IUserDao)new ProxyFactory(userDao).getProxyInstance();
System.out.println(proxy.getClass());
proxy.save();
}
输出:
class com.sun.proxy.$Proxy0
开始事务
保存数据
提交事务
Cglib代理
上面说到过,因为JDK代理时继承了Proxy类,因此只能代理接口。假如某个类没有接口,则不能通过JDK代理来进行动态代理,但可以用Cglib来实现代理。Spring AOP中就大量使用了cglib代理。
Cgcli代理也叫子类代理,它是在内存中构建一个子类对象从而实现目标对象功能的扩展。Cglib是一个强大的高性能代码生成包,可以在运行期扩展Java类与实现Java接口,底层是通过字节码处理框架ASM来转换字节码并生成新的类,在类的执行过程中比较高效。
Cglib不能代理final/static方法。
代码示例:
目标类没有实现接口:
public class UserDao {
public void save() {
System.out.println("保存数据");
}
}
代理工厂:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("开始事务");
Object returnValue = method.invoke(target, args);
System.out.println("提交事务");
return returnValue;
}
});
return enhancer.create();
}
}
这里通过代理工厂生产代理实例的过程与JDK代理是类似的。Enhancer类似Proxy,通过setSuperClass设置要代理的目标类,通过setCallBack设置回调对象。这里的回调对象类似生成JDK代理时的调用处理器InvokeHandler,回调对象的intercept方法类似调用处理器的invoke方法,最终目标类的子类代理的代理方法调用intercept方法来完成代理。
调用:
public class Client {
public static void main(String[] args) {
UserDao userDao = new UserDao();
ProxyFactory proxyFactory = new ProxyFactory(userDao);
UserDao proxy = (UserDao)proxyFactory.getProxyInstance();
proxy.save();
}
}
输出:
开始事务
保存数据
提交事务
代理模式与装饰模式的区别
使用装饰模式时,目标对象(被装饰者)需要以构造函数参数的形式传入到装饰器中,目标对象和装饰器之间的关系是聚合关系。而通常情况下,代理模式中目标对象不会从外部传入,也就是说,代理者完全屏蔽了目标对象。代理对象和目标对象之间是组合关系,目标对象的生命周期由代理对象来负责。但是,代理模式实际上也可以从外部传入目标对象的。如上文中的代码示例,在客户端调用过程中,目标对象都是通过构造函数参数的形式传入到ProxyFactory中去的,此时两者之间的关系也是聚合关系。
两者之间真正的不同其实是目的的不同。装饰模式是代理模式的一个特殊应用,代理模式着重对代理过程的控制,而装饰模式则是对类的功能的装饰(加强或减弱)。代理模式对目标对象有控制权,而装饰模式没有控制权。
2.2.4 桥接模式
桥接模式的定义是:将抽象和实现分离开来,从而可以保持各部分的独立性。
设计模式的定义一般都比较抽象,不太好理解,不妨从具体问题入手。
假设现在需要画几何图形,如矩形、圆形等,这个很好设计,先定义一个图形接口,然后实现不同的具体图形类:矩形类和圆形类。再进一步,要求图形是有颜色的,比如红色和黄色。显然一共有四种图形:红色矩形、红色圆形、黄色矩形、黄色圆形。最简单的方式是创建四个实现类,对应具体的每种颜色的图形。继续扩展,假如图形增加了三角形,颜色增加了蓝色,则一共会有九种图形,怎么办?老方法是继续增加九个子类,对应九种图案。但是,随着图形形状和图形颜色的越来越多,继续扩展子类是不现实的,因为会越来越臃肿。
在这个例子中,画图功能实现是通过类继承的方式来做的,因为继承关系是一种强关联关系,耦合非常紧密,所以扩展起来也是非常不方便的。那么怎么把继承这种强关联关系减弱呢?那就是构建组合关系。这时候桥接模式就能出场了,桥接模式能通过对象组合的方式将抽象与实现解耦。具体的设计就是将两个维度的可变因子(如例子中的形状和颜色)分离开来,保持各自独立的扩展,然后将两者进行组合,实现需要的功能。具体代码示例如下。
抽象类:
public abstract class AbstractShape {
IColor color;
public AbstractShape(IColor color) {
this.color = color;
}
public void setColor(IColor color) {
this.color = color;
}
public IColor getColor() {
return color;
}
public abstract void draw();
}
扩充抽象类:
public class CircleShape extends AbstractShape {
public CircleShape(IColor color) {
super(color);
}
@Override
public void draw() {
System.out.println(color.getColor() + "圆形");
}
}
public class RectangleShape extends AbstractShape {
public RectangleShape(IColor color) {
super(color);
}
@Override
public void draw() {
System.out.println(color.getColor() + "三角形");
}
}
实现类接口:
public interface IColor {
String getColor();
}
具体实现类:
public class GreenColor implements IColor {
private String color;
public GreenColor(String color) {
this.color = color;
}
@Override
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
调用:
public class Client {
public static void main(String[] args) {
IColor green = new GreenColor("绿色");
AbstractShape circleShape = new CircleShape(green);
circleShape.draw();
AbstractShape rectangle = new RectangleShape(green);
rectangle.draw();
}
}
输出:
绿色圆形
绿色矩形
桥接模式一共有两类,四种角色,抽象部分两个角色(抽象类和扩充类),实现部分两个角色(接口和接口的实现)。
设计建模时一般将较为基础的维度作为抽象部分,而其他具体的实现的维度作为实现部分(即使实现部分有两个以上的维度,它们的角色仍然属于实现部分的角色)。抽象类持有实现接口的引用,两者之间是组合的关系(严格来说应该是聚合)。这样就将原抽象与实现的继承关系变成了抽象与实现的组合关系,降低了耦合度,提高了扩展的灵活性。
桥接模式分离了抽象接口及其实现部分,通过聚合关系将抽象与实现部分关联起来,提供了比继承更好的解决方法。但凡事都具有两面性,桥接模式的引入会增加系统的理解和设计难度,由于聚合关系建立在抽象层,要求开发者针对抽象进行设计与编程。而且使用桥接模式需要识别出系统中两个或以上独立变化的维度,因此其使用范围具有一定的局限性。
待续…
参考资料
1.https://blog.csdn.net/nugongahou110/article/details/50413668.
2.https://blog.csdn.net/goskalrie/article/details/52458773.
3.https://stackoverflow.com/questions/18618779/differences-between-proxy-and-decorator-pattern
4.https://blog.csdn.net/zhangerqing/article/details/8239539
5.http://www.cnblogs.com/chenssy/p/3317866.html
6.http://www.cnblogs.com/chenssy/p/3317866.html