百日筑基第二十四天-23种设计模式-结构型总汇
前言
设计模式可以说是对于七大设计原则的实现。
总体来说设计模式分为三大类:
- 创建型模式,共五种:单例模式、简单工厂模式、抽象工厂模式、建造者模式、原型模式。
- 结构型模式,共七种:代理模式、外观模式、享元模式、组合模式、桥接模式、装饰器模式、适配器模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
代理模式
简介
在现实生活中,一个对象不能直接访问另一个对象,这时需要找中介来访问目标对象,此时的中介就是代理对象。例如:租房子时,我们无法与房东取得联系,只能通过某网站与中介进行交易,获取自己心仪的房间等等。在软件设计中,使用代理模式的例子也很多,例如:访问阿里的 maven 仓库,其就是海外 maven 仓库的代理。还有因为安全原因需要屏蔽客户端直接访问真是对象,如某单位的内部数据等。
【1】代理模式: 为一个对象提供一个替身,以控制对目标对象的访问。即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,及扩展目标对象的功能。
【2】被代理的对象可以是远程对象,创建开销大的对象或需要安全控制的对象。
【3】代理模式有不同的形式,主要有三种:静态代理、动态代理(又称JDK代理、接口代理)和 Cglib 代理(可以在内存动态的创建对象,目标对象也不需要实现接口,它也属于动态代理的范畴,但比较特殊)
【4】代理模式的主要优点: ①、代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用。②、代理对象可以扩展目标对象的功能。③、代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度。
【5】代理模式的主要缺点: ①、在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢。②、增加了系统的复杂度。
静态代理
静态代理在使用时,需要定义接口或父类,被代理对象与代理对象一起实现相同的接口或者继承相同的父类。
静态代理 类图 如下:
【1】抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
【2】真实主题(Real Subject)类:实现了出现主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
【3】代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
静态代理 代码 实例:
【1】抽象主题类: 代理类与被代理类都需要继承的接口
//购票接口
public interface Ticketing {
//购票
public String buy();
}
【2】真实主题类: 目标类
//火车售票官方系统
public class RailwaySite implements Ticketing{
@Override
public String buy() {
String ticket = " 调用官方系统购票,票价=120 ";
System.out.println();
return ticket;
}
}
【3】代理类: 需要实现被代理类的接口,使用代理方法调用目标对象的方法,同时实现对目标方法的扩展。
//实现与目标系统一致的接口
public class ProxyTicketSystem implements Ticketing{
//组合 被代理对象
private Ticketing ticket;
//构造器
public ProxyTicketSystem(Ticketing ticket) {
this.ticket = ticket;
}
@Override
public String buy() {
System.out.println("代理(智行火车票 系统启动");
String ticketInfo = ticket.buy();
ticketInfo+="第三方系统服务费 20 总计:140元";
System.out.println("代理(智行火车票 系统结束");
return ticketInfo;
}
}
【4】客户端: 需要创建被代理对象和代理对象,并进行组合调用。
public class Client {
public static void main(String[] args) {
//被代理类
RailwaySite railwaySite = new RailwaySite();
//获取代理类
ProxyTicketSystem proxy = new ProxyTicketSystem(railwaySite);
//调用代理方法
String buy = proxy.buy();
/**
* 代理(智行火车票 系统启动
* 代理(智行火车票 系统结束
* 调用官方系统购票,票价=120 第三方系统服务费 20 总计:140元
*/
System.out.println(buy);
}
}
静态代理的 优缺点:
【1】优点:在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展。
【2】缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类。
【3】一旦接口增加方法,代理对象与目标对象都要维护。
动态代理
动态代理基本介绍:
1)代理对象,不需要实现接口,但是目标对象要实现接口,否则不能使用动态代理。
2)代理对象的生成,是利用 JDK 的 API,动态的在内存中构建代理对象。
3)动态代理又叫:JDK 代理、接口代理。
JDK 中生成代理对象的 API: 代理类所在包:java.lang.reflect.Proxy JDK 实现代理只需要使用 newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h )
动态代理类图如下: 与动态代理 代码实例相互参考实现。与静态最大的不同在于代理类的不同。代理类无需实现目标类的接口,同时组合的是 Object 通用对象,无需组合目标接口对象,适合所有类(实现了接口)的代理方式,非常通用。
动态代理代码实例:
【1】抽象主题类:被代理类(目标类)都需要继承的接口
public interface ITicket {
//购票
public String buy();
}
【2】真实主题类: 目标类
public class RailTicketImpl implements ITicket{
@Override
public String buy() {
String ticket = " 调用官方系统购票,票价=120 ";
System.out.println();
return ticket;
}
}
**【3】代理类:**也是动态代理与静态的区别之处,动态代理主要通过 JDK的 Proxy.newProxyInstance方法返回代理对象,且调用 method的 invoke内置方法,并将其结果返回。代理类实现了与目标类的解耦,适合为实现任意接口的所有类做代理。
//代理类 能够实现所有类(必须实现接口)的代理
public class ProxyTicket {
//注入目标类的接口
private Object target;
//构造器
public ProxyTicket(Object target) {
super();
this.target = target;
}
public Object getInstance() {
// ClassLoader loader =指定当前目标对象使用的类加载器,获取加载器的方法固定
// Class<?>[] interfaces = 目标类实现的接口 使用泛型方法确认类型
//InvocationHandler h = 事件处理,执行目标对象的方法,会触发事件处理器方法,会把当前执行的目标对象方法作为参数传入
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), new InvocationHandler() {
//Object proxy 传入代理对象
//method 代理的方法
// args 代理参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理对象 方法入口");
//调用代理方法时 传入目标对象和参数
Object invoke = method.invoke(target, args);
return invoke;
}
});
}
}
【4】客户端类: 目标对象和代理对象的返回值都必须使用接口接收,否则会出现转换异常。这也是为什么目标类必须实现接口的原因。
public class Client {
public static void main(String[] args) {
//定义目标类(被代理类)
ITicket railTicketImpl = new RailTicketImpl();
//创建代理类
ProxyTicket proxyTicket = new ProxyTicket(railTicketImpl);
//获取代理对象 需要强转对象类型
ITicket ticket = (ITicket)proxyTicket.getInstance();
//调用目标方法,使用 debugger 调试时会发现其调用了 invoke 方法
ticket.buy();
}
}
Cglib 代理
1)静态代理和动态代理都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理——这就是Cglib 代理。
2)Cglib 代理也叫子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展,有些书也将 Cglib 代理归属于动态代理。
3)Cglib 是一个非常强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现java 接口。它广泛的被许多 AOP 框架使用,例如:Spring AOP,实现方法的拦截。
4)在 AOP编程中如何选择代理:目标对象需要实现接口,用 JDK 代理。目标对象不需要实现接口,用 Cglib 代理。
5)Cglib 包在底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类。
【1】Cglib 依赖的 jar 包:
【2】在内存中动态构建子类,需要注意代理的类不能为 final,否则会出现:java.lang.IllegalArgumentException 错误。 【3】目标方法不能使用 final/static 修饰,否则不会被拦截,即不会执行。
【动态代理 类图 如下】:
【动态代理代码实例如下】:
【1】被代理类: 无需实现接口,很平常的一个类
public class RailTicketImpl{
public String buy() {
String ticket = " 调用官方系统购票,票价=120 ";
System.out.println(ticket);
return ticket;
}
}
【2】代理类: 需要实现 jar 包中的 MethodInterceptor 接口,重写 intercept 方法,此方法用于拦截代理对象的方法调用。同时需要创建一个代理对象返回方法:getProxyInstall() 可自行定义,其内部通过工具类 enhancer.create() 创建并返回代理对象,代理对象中需要传入父类即目标类等等参数。
public class ProxyFactory implements MethodInterceptor{
//组合目标对象
private Object target;
//构造器
public ProxyFactory(Object target) {
this.target = target;
}
//写一个返回代理对象的方法:target 的代理类
public Object getProxyInstall() {
//1.创建一个工具类
Enhancer enhancer = new Enhancer();
//2.将目标类设置为 父类 , 因为我们创建的是子类
enhancer.setSuperclass(target.getClass());
//3. 设置回调函数
enhancer.setCallback(this);
//4. 创建子类对象,即代理对象
return enhancer.create();
}
//重写 intercept 方法,会调用目标对象的方法
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy arg3) throws Throwable {
System.out.println("Cglib代理模式 ~~ 开始");
Object returnVal = method.invoke(target, args);
System.out.println("Cglib代理模式 ~~ 提交");
return returnVal;
}
}
【3】客户端类: 创建目标类和代理类,并调用目标方法,跟踪代码会发现调用了intercept方法,也就是方法会被 intercept 拦截。此时代理对象的返回值,就可以为目标类。
public class Client {
public static void main(String[] args) {
//目标类
RailTicketImpl ticket = new RailTicketImpl();
//代理工厂类
ProxyFactory proxyFactory = new ProxyFactory(ticket);
//获取代理类 需要强转类型
RailTicketImpl proxyInstall = (RailTicketImpl)proxyFactory.getProxyInstall();
//调用目标方法
proxyInstall.buy();
/**
* 输入如下:
* Cglib代理模式 ~~ 开始
* 调用官方系统购票,票价=120
* Cglib代理模式 ~~ 提交
*/
}
}
代理模式的变体(了解)
【1】防火墙(Firewall)代理: 内网通过代理穿透防火墙,实现对公网的访问。
【2】缓存(Cache)代理: 例如,当请求图片或文件等资源时,先到缓存代理取,如果取到资源则返回,如果取不到资源,再到公网或者数据库中取,然后缓存。
【3】远程(Remote)代理: 可以把远程对象在本地 cope 一份来调用。远程代理通过网络和真正的远程对象同步。
【4】同步(Synchronization)代理: 主要使用在多线程编程中,完成多线程间同步工作。
【5】智能引用(Smart Reference)代理 等等
外观模式
简介
外观模式(Facade Pattern): 隐藏系统的复杂性,并向客户端提供了一个可以访问系统的接口。这种类型的设计属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统方法的委托调用。现实生活中,常常存在很多复杂的例子,例如:去医院看病,可能要去挂号、门诊、化验、开药、取药,让患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。或者 Java 的三层开发模式等等。软件设计也是如此,当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这是如果系统内部发生改变,客户端也要改变,违背了 “开闭原则” 和 “迪米特法则” 所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度,这就是外观模式。
外观模式的优点与缺点
外观模式是 “迪米特法则” 的典型应用,优点如下:
1)降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类,便于子系统内部维护和扩展。
2)对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易,降低了复杂性。
3)降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因此编译一个子系统不会影响其他子系统,也不会影响外观对象。
4)子系统也不会影响外观系统。
5)通过合理的使用外观系统,可以更好的帮我们划分访问层次,当系统需要层次设计时,可以考虑外观模式。
6)在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facad类,来提供遗留系统的比较清晰简单的接口,让新系统与Facade类交互,提高复用性。
7)不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。要以让系统有层次,利于维护为目的。
外观模式的主要缺点如下:
1)不能很好地限制客户端使用子类系统。
2)增加了新的子类系统可能需要修改外观类。
外观模式结构类图
外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过访问外观类来访问各个子系统的功能。
外观模式主要包含一下三种角色:
【1】外观(Facade)角色:为多个子系统对外提供一个共同的接口。
【2】子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
【3】客户(Client)角色:通过一个外观系统访问各个子系统。
代码案例分析
【1】定义 CPU 、Hard、Memory 三大子系统之一: 通过懒汉式定义了实例化方法(genInstance())
public class Memory {
private static Memory memory = new Memory();
public static Memory getInstance() {
return memory;
}
public void open() {
System.out.println("打开内存");
}
public void down() {
System.out.println("关闭内存");
}
}
【2】定义外观类: 在构造器中通过调用子类的方法,实例化子类对象。
public class Facade {
private CPU cpu;
private Memory memory;
private Hard hard;
//构造器 初始化子类实例
public Facade() {
super();
this.cpu = CPU.getInstance();
this.memory = Memory.getInstance();
this.hard = Hard.getInstance();
}
//封装所有打开设备
public void open() {
cpu.open();
hard.open();
memory.open();
}
//封装所有关闭设备
public void down() {
cpu.down();
hard.down();
memory.down();
}
}
【3】客户端类: 通过调用外观类,实现对子类的调用
public class Client {
public static void main(String[] args) {
//外观类
Facade facade = new Facade();
//打开电脑
facade.open();
//关闭电脑
facade.down();
}
}
外观模式应用分析
外观模式在 MyBatis 框架中应用的源码分析:
【1】MyBatis 中的 Configuration 去创建 MetaObject 对象使用到外观模式
public class Configuration {
......
//创建子类实例
protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
protected ObjectFactory objectFactory = new DefaultObjectFactory();
protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
//封装复杂的业务逻辑,返回客户端需要的 MetaObject
public MetaObject newMetaObject(Object object) {
this.originalObject = object;
this.objectFactory = objectFactory;
this.objectWrapperFactory = objectWrapperFactory;
this.reflectorFactory = reflectorFactory;
if (object instanceof ObjectWrapper) {
this.objectWrapper = (ObjectWrapper) object;
} else if (objectWrapperFactory.hasWrapperFor(object)) {
this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
} else if (object instanceof Map) {
this.objectWrapper = new MapWrapper(this, (Map) object);
} else if (object instanceof Collection) {
this.objectWrapper = new CollectionWrapper(this, (Collection) object);
} else {
this.objectWrapper = new BeanWrapper(this, object);
}
}
【2】上述外观模式应用的类图
享元模式
简介
享元模式(Flyweight Pattern): 主要用于减少创建对象的数量,以减少内存占用和提高性能。在面向对象程序的设计过程中,有时需要创建大量相似的对象实例。如果都创建将会消耗很多系统资源,它是系统性能提高的一个瓶颈。但如果将这些对象的相似部分抽取出来共享,则能节约大量的系统资源,这就是享元模式的产生背景。在 Java 中 String 值的存储就使用了享元模式,相同的值只存一个。
1、享元模式(Flyweight Pattern)也叫 “蝇量模式”:运用共享技术有效地支持大量细粒度对象。
2、常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿过来用,避免重新创建,如果没有我们需要的,则创建一个。
3、享元模式能够解决重复对象的内存消耗问题,当系统中有大量相似对象,需要缓冲池时。不需要总创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率。
4、享元模式经典的应用的场景就是池技术,String 常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。
享元模式的特点
享元模式的主要优点: 相同对象只要保存一份,降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
享元模式的主要缺点: 为了使对象共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。需要分离出外部状态和内部状态,而且外部状态具有固有化性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
享元模式的主要意图: 运行享元模式有效地支持大量细粒度对象。
享元模式主要解决的问题: 在有大量相似对象时,有可能会造成内存溢出,我们把其中共同的部分抽取出来,如果有相同的业务请求,直接返回内存中已有的对象,避免重新创建。
享元模式如何解决问题: 用唯一标识码判断,如果内存中有,则返回唯一标识所标识的对象。
享元模式关键代码: 用 HashMap 存储对象,key 表示唯一标识,value 为共享对象。
享元模式使用场景: 1)系统有大量相似对象。2)需要缓冲池的场景。
享元模式注意事项: 1)注意划分外部状态和内部状态,否则可能会引起线程安全问题。2)这些类必须有一个工厂对象加以控制。
内部状态和外部状态
1)享元模式提出了两个要求:细粒度和共享对象。这里就涉及到内部状态和外部状态,既将对象的信息分为两部分:内部状态和外部状态。
2)内部状态:指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变。
3)外部状态:指对象得以依赖的一个标记,是随环境改变而改变的,不可共享的状态。
享元模式结构类图
享元模式的主要角色如下:
1)抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
2)具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
3)非享元(Unsharalbe Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具有享元的相关方法中。
4)享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在,则创建一个新的享元对象。
享元模式案例分析
享元模式在五子棋中的应用:包含多个内部状态 “黑” 和 “白” 颜色的棋子和外部状态 棋子的坐标 ,所以适合享元模式。
【1】抽象享元角色: 棋子(ChessPieces)类包含了一个落子的方法:downPieces(Point pt)
public interface ChessPieces {
//落子方法 color:内部状态 pt:外部状态
public void downPieces(Point pt);
}
【2】具体享元角色: 抽象享元角色的实现类(黑子/白子 的实现类)
☛ 黑子 实现类如下:
public class BlackPieces implements ChessPieces{
@Override
public void downPieces(Point pt) {
System.out.println("当前获取到的为===黑===颜色的棋子");
System.out.println("坐标X="+pt.getX()+";Y="+pt.getY());
}
}
☞ 白子 实现类如下:
public class WhitePieces implements ChessPieces{
@Override
public void downPieces(Point pt) {
System.out.println("当前获取到的为===白===颜色的棋子");
System.out.println("坐标X="+pt.getX()+";Y="+pt.getY());
}
}
【3】享元工厂角色: 通过内部状态,将对象进行分类存储,相同的内部状态只存一个对象即可。
public class PiecesFactory {
//存储已创建的棋子 享元模式的精华
HashMap<String, ChessPieces> pieces = new HashMap<>();
private final String WRITE = "Write";
private final String BLACK = "Black";
//创建一个静态方法 获取棋子对象
public ChessPieces getPieceInstance(String color) {
if(pieces.get(color) == null) {
if(color == WRITE) {
WhitePieces whitePieces = new WhitePieces();
pieces.put(color, whitePieces);
}else if(color == BLACK){
BlackPieces blackPieces = new BlackPieces();
pieces.put(color, blackPieces);
}else {
System.out.println("不存在的颜色");
return null;
}
}
return pieces.get(color);
}
//查看 hashmap 中总计的实例数量
public int getInstallCount() {
return pieces.size();
}
}
【4】客户端应用: 将内部状态传递给工厂类,外部状态传递给具体实现类。
public class Clinet {
private final static String WRITE = "Write";
private final static String BLACK = "Black";
public static void main(String[] args) {
//创建工程
PiecesFactory factory = new PiecesFactory();
//获取白色棋子
//下琪1 = 白
ChessPieces piece1 = factory.getPieceInstance(WRITE);
piece1.downPieces(new Point(1,2));
//下琪1 = 黑
ChessPieces pieceB1 = factory.getPieceInstance(BLACK);
pieceB1.downPieces(new Point(2,2));
//下琪2 = 白
ChessPieces piece2 = factory.getPieceInstance(WRITE);
piece2.downPieces(new Point(2, 3));
//下琪2 = 黑
ChessPieces pieceB2 = factory.getPieceInstance(BLACK);
pieceB2.downPieces(new Point(3,2));
//下琪3 = 白
ChessPieces piece3 = factory.getPieceInstance(WRITE);
piece3.downPieces(new Point(5, 7));
//下琪3 = 黑
ChessPieces pieceB3 = factory.getPieceInstance(BLACK);
pieceB3.downPieces(new Point(6,6));
/**
* 结果:
* 当前获取到的为===白===颜色的棋子
* 坐标X=1;Y=2
* 当前获取到的为===黑===颜色的棋子
* 坐标X=2;Y=2
* 当前获取到的为===白===颜色的棋子
* 坐标X=2;Y=3
* 当前获取到的为===黑===颜色的棋子
* 坐标X=3;Y=2
* 当前获取到的为===白===颜色的棋子
* 坐标X=5;Y=7
* 当前获取到的为===黑===颜色的棋子
* 坐标X=6;Y=6
*/
//重点是,这6颗棋子 总共创建了多少个对象
System.out.println(factory.getInstallCount());
/**
* 输入结果:2 复合享元模式的应用
*/
}
}
享元模式 JDK-Interger 应用源码分析
【1】我们在创建 Interger 对象时,有两种方式:分别是 valueOf() 和 new 的形式,如下:我们会发现 valueOf() 创建的实例是相等的,说明使用了享元模式,下面我们就查看下其源码:
public static void main(String[] args) {
Integer x = Integer.valueOf(127); // 得到 x实例,类型 Integer
Integer y = new Integer(127); // 得到 y 实例,类型 Integer
Integer z = Integer.valueOf(127);//..
Integer w = new Integer(127);
//我们会发现valueOf创建的实例是相等的,说明使用了享元模式。new 每次给创建一个新的对象
System.out.println(x == z ); // true
System.out.println(w == y ); // false
}
【2】进入 valueOf 方法:根据源码分析:只有当 -128 <= i >= 127 时,就使用享元模式,从缓存中获取值
public static Integer valueOf(int i) {
/**
* IntegerCache.low = -128
* IntegerCache.highhigh = 127
* 根据源码分析:只有当 -128 <= i >= 127 时,就使用享元模式,从缓存中获取值
* IntegerCache 相当于工厂类
*/
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
【3】我们进入工厂角色:Interger 则为我们的具体享元角色。
private static class IntegerCache {
static final int low = -128;
static final int high;
//工厂类中存储对象实例的数组
static final Integer cache[];
static {
······
high = 127;
//定义数组长度 = 127+128+1
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
//循环创建对象,并放入数组缓存
cache[k] = new Integer(j++);
// 断言 如果为true 继续执行,false 则抛错
assert IntegerCache.high >= 127;
}
}
享元模式的注意事项和细节
1)对享元模式的理解: “享” 表示共享 “元” 表示对象。
2)系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式。
3)用唯一标识码判断,如果内存中有则直接返回,一般使用 HashMap、HashTable 或者数组之内进行存储。
4)享元模式提高了系统的复杂度。需要分离内部状态和外部状态。而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是使用享元模式需要注意的地方。
5)使用享元模式时,注意划分内部状态和外部状态,并且需要一个工厂类对享元角色进行管理。
组合模式
简介
生活中存在很多 “部分-整体” 的关系,例如:大学中的学校与学院、学院与专业的关系。高楼与楼层和房间之间的关系等等。在软件开发中也有类似的情况。这些简单对象与复合对象之间的关系,如果用组合模式(把学校、院、系都看作是组织结构,他们之间没有继承的关系,而是一种树形结构,可以更好的实现管理操作)来实现会很方便。
1)组合模式(Composite Pattern):又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示“整体-部分”的层次关系。
2)组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
3)这种设计模式属于结构型模式。
4)组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客户以一致的方式处理个别对象以及组合对象。
5)优点:组合模式使得客户端代码可以一致地处理单个对象和组合对象,无需关注自己处理的是单个对象,还是组合对象,这简化了客户端代码;更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足 “开闭原则 OCP”。
6)缺点:设计复杂,客户端需要花更多时间理清类之间的层次关系。不容易限制容器中构建,不容易用继承的方式增加构建的新功能。
组合模式结构类图
组合模式分为透明式的组合模式和安全式组合模式:
1)透明方式:在该方法中,由于抽象构建声明了所有子类中的全部方法,所以客户端无需区别树叶对象和树枝对象。对客户端来说是透明的。其缺点是:树叶构件本来没有 Add()、Remove() 及 GetChild() 方法,却要实现它们(空实现或抛异常),这样会带来一些安全性问题。
2)安全方式:在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性。
组合模式代码案例分析
【1】抽象构件(Component)角色: 主要作用是为树叶和树枝构件生命公共接口,并实现他们的默认行为。在透明式的组合模式中,抽象构件还声明访问和管理子类的接口(add/remove)。在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构建完成。
public abstract class AbstractComponent {
//学校的名称
private String name;
//备注
private String remark;
//定义一个输入的抽象方法
public abstract void output();
//非叶子节点都具有的增加和删除方法
public void add(AbstractComponent abstractComponent) {
//当重写此方法直接调用时,就会抛出此异常。与Arrays.asList()内部类中的add与remove写法一致
throw new UnsupportedOperationException();
}
public void remove(AbstractComponent abstractComponent) {
throw new UnsupportedOperationException();
}
//构造器
public AbstractComponent(String name, String remark) {
super();
this.name = name;
this.remark = remark;
}
//get/set方法 tostring方法 略
}
}
【2】树枝构件(Composite)角色: 是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口。
public class University extends AbstractComponent{
//构造器
public University(String name, String remark) {
super(name, remark);
}
//大学中包含多个学院
List<AbstractComponent> college = new ArrayList<AbstractComponent>();
//重写输出方法:输出叶子节点
@Override
public void output() {
System.out.println("===="+super.getName()+"====");
for (AbstractComponent abstractComponent : college) {
abstractComponent.output();
}
}
//组合类也就是树枝节点,需要重写add与remove方法
@Override
public void add(AbstractComponent abstractComponent) {
college.add(abstractComponent);
}
@Override
public void remove(AbstractComponent abstractComponent) {
college.remove(abstractComponent);
}
}
【3】学院类也是树枝构件(Composite)角色,与上述基本一致。
public class College extends AbstractComponent{
//构造器
public College(String name, String remark) {
super(name, remark);
}
List<AbstractComponent> list = new ArrayList<>();
//输入
@Override
public void output() {
for (AbstractComponent abstractComponent : list) {
abstractComponent.output();
}
}
//重写添加和删除方法
@Override
public void add(AbstractComponent abstractComponent) {
list.add(abstractComponent);
}
@Override
public void remove(AbstractComponent abstractComponent) {
list.remove(abstractComponent);
}
}
【4】树叶构件(Leaf)角色: 是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。
public class Major extends AbstractComponent{
//构造器
public Major(String name, String remark) {
super(name, remark);
}
//add , remove 就不用写了,因为他是叶子节点
//输入
@Override
public void output() {
System.out.println(getName());
}
}
【5】测试类
public class Client {
public static void main(String[] args) {
//定义一个大学
AbstractComponent university = new University("浙江大学", "浙江人的骄傲");
//定义一个学院
AbstractComponent college = new College("计算机学院", "简称妓院");
//将妓院添加至学校
university.add(college);
//定义一个专业
AbstractComponent major = new Major("计算机科学与技术", "考研大专业");
//添加至学院
college.add(major);
//输出:计算机科学与技术 输入的都是叶子节点的output方法
major.output();
college.output();
university.output();
}
}
组合模式源码分析
【1】HashMap 组合模式:首先定义了抽象构建角色 Map<K,V>
public interface Map<K,V> {....}
【2】使用接口适配器模式,定义了抽象实现:AbstractMap
public abstract class AbstractMap<K,V> implements Map<K,V> {
public V put(K key, V value) {
//由子类实现
throw new UnsupportedOperationException();
}
......
}
【3】HashMap
等实现类属于树枝构建角色:组合了Node
叶子节点类
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
......
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
......
}
}
【4】Node 类是上述类的内部类,也就是组合模式中的树叶构建:数组存储时,通过 put 方法存入 Node 对象中,叶子节点 Node 中不包含添加方法等,只包含一些属性和其 get/set 方法
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
......
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
......
}
组合模式的注意事项和细节
☛ 简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题。
☛ 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做任何改动。
☛ 方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容器添加节点或者叶子。从而创建出复杂的树形结构。
☛ 需要遍历组织机构,或者处理的对象具有树形结构时,非常适合使用组合模式。
☛ 当要求较高的抽象性时,如果节点和叶子有很多差异的话,例如很多方法和属性都不一样,不适合使用组合模式。
桥接模式
简介
在生活中,一些对象具有两个或多个维度的变化,如用户购买汽车时,既要考虑品牌(奔驰、哈佛)又要考虑类型(轿车、SUV)等等。如果使用继承的方式实现,m 种品牌和 n 种类型就有 m*n 种,不但对应的子类多,而且扩展困难。如果使用桥接模式就能很好地解决这些问题。
传统方式实现
【1】传统方式:继承实现类图:
【2】传统解决方案问题分析:
● 扩展性问题(类爆炸),如果继续增加品牌,就需要增加各个类型品牌的类,同样如果我们增加一个汽车类型,也要在增加汽车品牌类。
● 违反了单一职责原则,当我们增加汽车类型时,要同时增加所有品牌的汽车,这样增加了代码的维护成本。
基本介绍
【1】桥接模式(Bridge模式): 将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。
【2】是一种结构型设计模式。
【3】Bridge 模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责。它的主要特点是把抽象(Abstract)与行为实现(implementation)分离开来,从而可以保持各部分的独立性以及应对他们的功能扩展。
【4】优点: 由于抽象与实现分离,所以扩展能力强,其实现细节对客户透明。
【5】缺点: 由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程。增加了系统的理解和设计难度。
桥接模式原理类图
桥接(Bridge)模式包含一下主要角色:
【1】实现化(Implementor)角色【品牌接口类】:定义实现化角色的接口,供扩展抽象化角色调用【将所有品牌的方法进行抽取】
【2】具体实现化(Concrete Implementor)角色【品牌接口实现类】:给出实现化角色接口的具体实现
【3】抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用
【4】具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现
桥接模式代码案例分析
【1】接口类
public interface IBrand {
//定义一个抽象方法 drive
public void drive();
}
【2】接口的实现类(奔驰品牌,其他品牌略)
public class BenChiCar implements IBrand{
@Override
public void drive() {
System.out.println("奔驰品牌");
}
}
【3】桥接类(抽象类): 将接口组合进来,并调用业务方法。使用桥接后,增加品牌或类型时只需要增加一个实现类。
public abstract class AbstarctCar {
//组合接口类
private IBrand iBrand;
//构造器
public AbstarctCar(IBrand iBrand) {
this.iBrand=iBrand;
}
//定义方法
public void drive() {
//调用品牌类的实现
this.iBrand.drive();
}
}
【4】抽象类的具体实现类(汽车类型的实现类)
public class SUVCar extends AbstarctCar{
//重写构造器
public SUVCar(IBrand iBrand) {
super(iBrand);
}
//重写diver 方法,调用品牌类的业务方法 。如下 super.drive 并添加SUV的业务
@Override
public void drive() {
// 品牌的实现
super.drive();
//SUV 的业务处理
System.out.println("SUV 型号汽车");
}
}
【5】Client 客户端调用类
public class Client {
public static void main(String[] args) {
//调用需要的类型的实例类和品牌实例类 的业务方法
new SUVCar(new BenChiCar()).drive();
/*输出如下:
* 奔驰品牌
* SUV 型号汽车
*/
}
}
桥接模式的注意事项和细节
1)实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统。
2)对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以,其他部分由具体业务来完成。
3)桥接模式代替多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
4)桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和开发。
5)桥接模式要正确识别出系统中两个独立的维度,因此其使用范围有一定的局限性,即需要有这样的应用场景。
装饰器/装饰者模式
简介
现实生活中常常需要给某类产品动态增加新的功能,如:给面条各种调味品。在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成一些核心功能。但在不改变其架构的情况下,可以动态地扩展其功能。所以这些都可以采用装饰模式来实现。
【1】装饰者模式: 动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则。
【2】设计模式属于结构型模式。
【3】这种模式创建一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
【4】优点: 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
【5】缺点: 多层装饰比较复杂。
装饰者类图
案例代码分析
【1】定义一个装饰者和被装饰者都需要集成的抽象类(Food 食物)
public abstract class Food {
private String des;
private Double price;
//其他类需要实现的抽象方法
public abstract double cost();
//get/set方法省略
}
【2】被装饰者需要继承的类(Noodles 面条)
public class Noodles extends Food{
//这里获取到的价格是子类实现时设置的价格
@Override
public double cost() {
return super.getPrice();
}
}
【3】被装饰者具体的实现类之一(ChineseNoodles 中式面条)
public class ChineseNoodles extends Noodles{
//构造器中只需要定义该产品的价格和描述,因为它是被装饰者,与平常类一样
public ChineseNoodles() {
setDes("中式面条");
setPrice(25.00);
}
}
【4】装饰者类都需要集成的公共类
public class Decorator extends Food{
//将被装饰者组合进来
private Food desFood;
//构造器将其引入
public Decorator(Food desFood) {
this.desFood = desFood;
}
//将自己的价格与被装饰者价格进行成绩
@Override
public double cost() {
System.out.println(desFood.getDes() +"价格:"+desFood.getPrice()+ "配料如下:"
+super.getDes()+"价格:"+this.getPrice()+"总价"+(super.getPrice()+desFood.cost()) );
//价格总计
return super.getPrice()+desFood.cost();
}
}
【5】装饰者实现类之一
//孜然类
public class Cumin extends Decorator{
//构造器
public Cumin(Food desFood) {
super(desFood);
setDes("孜然");
setPrice(2.00);
}
}
【6】客户端调用: 优点,一个产品可以被多个装饰者装饰,同时装饰者和被装饰者扩展时都非常灵活,只需要扩展自己的类即可,无需修改其他类。
public class Client {
public static void main(String[] args) {
//先定义一个被装饰者,返回对象要为最顶层的对象,这样被装饰者才能接受
Food noodles = new ChineseNoodles();
//定义一个装饰者对象
Food cumin = new Cumin(noodles);
//输出为:中式面条价格:25.0配料如下:孜然价格:2.0
cumin.cost();
//再定义一个装饰者 Pepper辣椒类
Food pepper = new Pepper(cumin);
//输出为:中式面条价格:25.0配料如下:孜然价格:2.0配料如下:辣椒价格:1.0总价28.0
pepper.cost();
}
}
装饰者模式在JDK应用的源码分析
【1】JDK 中流的使用用到了装饰者模式。从下面的客户端使用能够得出 FileInputStream 是被装饰者,DataInputStream 是装饰者类的一个实现类,下面就进入 FileInputStream 中查看源代码:
public class Decorator {
public static void main(String[] args) throws Exception{
DataInputStream dis = new DataInputStream(new FileInputStream("d:\\a.txt"));
}
}
【2】FileInputStream 继承了 InputStream ,也就是上面提到的 Food 类及 ChineseNoodles 之间的关系。
public class FileInputStream extends InputStream{
......
}
【3】进入 DataInputStream 装饰者类的实现类
public class DataInputStream extends FilterInputStream implements DataInput {
/**
* 构造器,将顶层接口 进行组合,父类装饰类的重写
*/
public DataInputStream(InputStream in) {
super(in);
}
}
【4】最重要的部分:装饰者类 FilterInputStream 继承和组合了 InputStream 接口,被装饰者也实现了此接口。
//集成最顶层 InputStream 接口,被装饰者也集成的接口,因为此类的返回值要与被装饰类类型相同
public class FilterInputStream extends InputStream {
/**
* 组合 被装饰者类
*/
protected volatile InputStream in;
/**
* 构造器
*/
protected FilterInputStream(InputStream in) {
this.in = in;
}
}
【5】源码类图如下:与装饰者类的类图一致,易理解。
适配器模式
简介
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。它结合了两个独立接口的功能。在现实生活中,经常会遇到类似接口不兼容而不能在一起工作的实例,这是就需要第三者j进行适配,例如:手机与电源,当手机需要充电时,就需要充电器来适配电源。
● 适配器模式是将某个类的接口转化成客户端需要的另一个接口表示,主要的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为包装器(Wrapper)
● 适配器模式属于结构型模式
● 主要分为三类:类适配器模式、对象适配器模式、接口适配器模式
适配器的工作原理
1)适配器模式:将一个类的接口转化成另一种接口,让原本不兼容的类可以兼容。
2)从用户的角度看不到被适配者,是解耦的。
3)用户调用适配器转化出来的目标接口方法,适配器再调用被适配者的相关接口方法。
4)用户收到反馈结果,感觉知识和目标接口交互。
类适配器模式
类适配器模式可采用多重继承方式实现,如 C++ 可定义一个适配器类来同时继承当前系统的业务接口和现有组件库中已经存在的组件接口;Java 不支持多继承,但可以定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
类适配器应用实例: 以手机、充电器、电源为例来说明适配器模式。适配器模式(Adapter)包含以下主要角色:
①:目标(Target)接口:充电器输入的 5v 电压及与手机对接的头(目标)所抽象出来的接口或抽象类(当前系统业务所期待的接口,它可以是抽象类或接口)
②:适配者(Adaptee)类:220V的电源(它是被访问和适配的现存组件库中的组件)
③:适配器(Adapter)类:充电器(它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者)
类适配器的类图
代码示例
【1】被适配类,即电源对象
public class Voltage220V {
//被适配类即电源,输入的值为220伏
public int output() {
int volateage = 220;
return volateage;
}
}
【2】目标接口,既将220V转化为5V的接口
public interface IVoltage5V {
//将被适配对象转化成目标对象方法
public int output5v();
}
【3】适配器,将220V转化为5V的电压的具体实现,并继承被适配器类(类适配器的原因)
public class VoltageAdpter extends Voltage220V implements IVoltage5V{
@Override
public int output5v() {
//获取目标输入的结果
int src = output();
//处理输入的电压
int dst = src/44;
return dst;
}
}
【4】客户端,手机充电方法:此方法只能传入5v的电压。
public class Phone {
//充电方法
public void charging(IVoltage5V voltage5v) {
if(5 == voltage5v.output5v()) {
System.out.println("手机充电中");
}else {
System.out.println("电压不符合标准");
}
}
}
对象适配器
基本思路和类适配器相同,只是将 Adapter 类进行了修改,将原有的继承被适配类,修改为聚合的形式。使其持有 src 类的实例,以解决兼容性问题。这样也复核了“合成复用原则 OCP”(在系统中尽量使用关联代替继承关系),因此对象适配器模式是适配器模式常用的一种。
public class VoltageAdpter implements IVoltage5V{
//定义一个被适配类对象 聚合的方式
private Voltage220V voltage22ov;
//定义一个构造器,传入被适配的类
public VoltageAdpter(Voltage220V voltage22ov) {
this.voltage22ov = voltage22ov;
}
@Override
public int output5v() {
int dst = 0;
//获取目标输入的结果
if(voltage22ov != null) {
int src = voltage22ov.output();
dst = src/44;
}
//处理输入的电压
return dst;
}
}
接口适配器模式
当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现方法(可以是空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求。适用于一个类不想使用其接口中所有方法的情况。
【1】接口(被适配类)
public interface IAdpter {
public void method1();
public void method2();
}
【2】接口的抽象实现类(适配器)
public abstract class AdpterAbsClass implements IAdpter{
@Override
public void method1() {
}
@Override
public void method2() {
}
}
【3】目标类:重写自己需要的方法
public class TargetClass extends AdpterAbsClass{
//需要哪个方法重写哪个方法
@Override
public void method1() {
// TODO Auto-generated method stub
super.method1();
}
}
适配器模式的应用
SpringMVC 中的 HandlerAdapter,就是用了适配器模式
【1】进入 SpringMVC 的 DispatcherServlet 类的 doDispatch:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
//通过 HandlerMapper 来映射 Controller
mappedHandler = getHandler(processedRequest);
// 获取适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
//通过适配器调用controller的方法并返回 ModelAndView
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
}
【2】进入获取适配器的方法: getHandlerAdapter(…) 返回的 HandlerAdapter 是适配器接口,底层根据请求类型有多种实现(simpleControllerhandlerAdapter/HttpRequestHandlerAdapter 等等),显然如果要直接调用对应的 Controller 就需要使用判断语句。那么后面扩展 Controller 就需要修改控制层的代码,违反了 OCP 原则。因此下面 handlerAdapters 方法将获取所有的适配器实现类,循环调用适配器类的 supports 方法筛选出对应的适配器(return (handler instanceof Controller);)
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
for (HandlerAdapter ha : this.handlerAdapters) {
if (logger.isTraceEnabled()) {
logger.trace("Testing handler adapter [" + ha + "]");
}
if (ha.supports(handler)) {
return ha;
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
【3】最终调用统一的业务处理方法:handle 进行业务逻辑处理,并最总返回视图对象。HandlerAdapter 接口的方法如下:
public interface HandlerAdapter {
//根据请求的类型,来判断是否为当前适配器的类型。如果是就返回当前适配器。
boolean supports(Object handler);
//业务处理逻辑方法入口
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
//与缓存相关,判断最后一次修改时间与浏览器的时间是否一致,一致则启用缓存。。。
long getLastModified(HttpServletRequest request, Object handler);
}
总结
优点: ①、可以让任何两个没有关联的类一起运行。②、提高了类的复用。③、增加了类的透明度。④、灵活性好。
缺点: ①、过多地使用适配器,会让系统非常凌乱,不易整体进行把握。比如,我们调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不用适配器,而是直接对系统进行重构。②、如果使用类适配器只能适配一个适配者,而且目标类必须是抽象类。