结构型模式是用来设计程序的结构的 。结构型模式就像搭积木,将不同的类结合在一起形成契合的结构。包括以下几种:
适配器模式
桥接模式
组合模式(不常用)
装饰模式
外观模式(不常用)
享元模式(不常用)
代理模式
1、适配器模式 说到适配器,我们最熟悉的莫过于电源适配器了,也就是手机的充电头。它就是适配器模式的一个应用。
试想一下,你有一条连接电脑和手机的 USB 数据线,连接电脑的一端从电脑接口处接收 5V 的电压,连接手机的一端向手机输出 5V 的电压,并且他们工作良好。
中国的家用电压都是 220V,所以 USB 数据线不能直接拿来给手机充电,这时候我们有两种方案:
单独制作手机充电器,接收 220V 家用电压,输出 5V 电压。
添加一个适配器,将 220V 家庭电压转化为类似电脑接口的 5V 电压,再连接数据线给手机充电。
如果你使用过早期的手机,就会知道以前的手机厂商采用的就是第一种方案:早期的手机充电器都是单独制作的,充电头和充电线是连在一起的。现在的手机都采用了电源适配器加数据线的方案。这是生活中应用适配器模式的一个进步。
适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配的意思是适应、匹配。通俗地讲,适配器模式适用于 有相关性但不兼容的结构 ,源接口通过一个中间件转换后才可以适用于目标接口,这个转换过程就是适配,这个中间件就称之为适配器。
家用电源和 USB 数据线有相关性:家用电源输出电压,USB 数据线输入电压。但两个接口无法兼容,因为一个输出 220V,一个输入 5V,通过适配器将输出 220V 转换成输出 5V 之后才可以一起工作。
让我们用程序来模拟一下这个过程。
首先,家庭电源提供 220V 的电压:
1 2 3 4 5 6 class HomeBattery { int supply () { return 220 ; } }
USB 数据线只接收 5V 的充电电压:
1 2 3 4 5 6 7 8 class USBLine { void charge (int volt) { if (volt != 5 ) throw new Il legalArgumentException ("只能接收 5V 电压" ) ; System.out.println("正常充电" ); } }
先来看看适配之前,用户如果直接用家庭电源给手机充电:
1 2 3 4 5 6 7 8 9 10 11 public class User { @Test public void chargeForPhone () { HomeBattery homeBattery = new HomeBattery(); int homeVolt = homeBattery.supply(); System.out.println("家庭电源提供的电压是 " + homeVolt + "V" ); USBLine usbLine = new USBLine(); usbLine.charge(homeVolt); } }
运行程序,输出如下:
1 2 家庭电源提供的电压是 220V java.lang.IllegalArgumentException: 只能接收 5V 电压
这时,我们加入电源适配器:
1 2 3 4 5 6 7 class Adapter { int convert (int homeVolt) { int chargeVolt = homeVolt - 215 ; return chargeVolt; } }
然后,用户再使用适配器将家庭电源提供的电压转换为充电电压:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class User { @Test public void chargeForPhone () { HomeBattery homeBattery = new HomeBattery(); int homeVolt = homeBattery.supply(); System.out.println("家庭电源提供的电压是 " + homeVolt + "V" ); Adapter adapter = new Adapter(); int chargeVolt = adapter.convert(homeVolt); System.out.println("使用适配器将家庭电压转换成了 " + chargeVolt + "V" ); USBLine usbLine = new USBLine(); usbLine.charge(chargeVolt); } }
运行程序,输出如下:
1 2 3 家庭电源提供的电压是 220V 使用适配器将家庭电压转换成了 5V 正常充电
这就是适配器模式。在我们日常的开发中经常会使用到各种各样的 Adapter,都属于适配器模式的应用。
但适配器模式并不推荐多用。因为未雨绸缪好过亡羊补牢,如果事先能预防接口不同的问题,不匹配问题就不会发生,只有遇到源接口无法改变时,才应该考虑使用适配器。比如现代的电源插口中很多已经增加了专门地充电接口,让我们不需要再使用适配器转换接口,这又是社会的一个进步。
2、桥接模式 考虑这样一个需求:绘制矩形、圆形、三角形这三种图案。按照面向对象的理念,我们至少需要三个具体类,对应三种不同的图形。
抽象接口 IShape:
1 2 3 public interface IShape { void draw () ; }
三个具体形状类:
1 2 3 4 5 6 class Rectangle implements IShape { @Override public void draw () { System.out.println("绘制矩形" ); } }
1 2 3 4 5 6 class Round implements IShape { @Override public void draw () { System.out.println("绘制圆形" ); } }
1 2 3 4 5 6 class Triangle implements IShape { @Override public void draw () { System.out.println("绘制三角形" ); } }
接下来我们有了新的需求,每种形状都需要有四种不同的颜色:红、蓝、黄、绿。
这时我们很容易想到两种设计方案:
为了复用形状类,将每种形状定义为父类,每种不同颜色的图形继承自其形状父类。此时一共有 12 个类。
为了复用颜色类,将每种颜色定义为父类,每种不同颜色的图形继承自其颜色父类。此时一共有 12 个类。
乍一看没什么问题,我们使用了面向对象的继承特性,复用了父类的代码并扩展了新的功能。
但仔细想一想,如果以后要增加一种颜色,比如黑色,那么我们就需要增加三个类;如果再要增加一种形状,我们又需要增加五个类,对应 5 种颜色。
更不用说遇到增加 20 个形状,20 种颜色的需求,不同的排列组合将会使工作量变得无比的庞大。看来我们不得不重新思考设计方案。
形状和颜色,都是图形的两个属性。他们两者的关系是平等的,所以不属于继承关系。更好的的实现方式是:将形状和颜色分离,根据需要对形状和颜色进行组合 ,这就是桥接模式的思想。
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体模式或接口模式。
官方定义非常精准、简练,但却有点不易理解。通俗地说,如果一个对象有两种或者多种分类方式,并且两种分类方式都容易变化,比如本例中的形状和颜色。这时使用继承很容易造成子类越来越多,所以更好的做法是把这种分类方式分离出来,让他们独立变化,使用时将不同的分类进行组合即可。
说到这里,不得不提一个设计原则:合成/聚合复用原则。虽然它没有被划分到六大设计原则中,但它在面向对象的设计中也非常重要。
合成/聚合复用原则:优先使用合成/聚合,而不是类继承。
继承虽然是面向对象的三大特性之一,但继承会导致子类与父类有非常紧密的依赖关系,它会限制子类的灵活性和子类的复用性。而使用合成/聚合,也就是使用接口实现的方式,就不存在依赖问题,一个类可以实现多个接口,可以很方便地拓展功能。
让我们一起来看一下本例使用桥接模式的程序实现:
新建接口类 IColor,仅包含一个获取颜色的方法:
1 2 3 public interface IColor { String getColor () ; }
每种颜色都实现此接口:
1 2 3 4 5 6 public class Red implements IColor { @Override public String getColor () { return "红" ; } }
1 2 3 4 5 6 public class Blue implements IColor { @Override public String getColor () { return "蓝" ; } }
1 2 3 4 5 6 public class Yellow implements IColor { @Override public String getColor () { return "黄" ; } }
1 2 3 4 5 6 public class Green implements IColor { @Override public String getColor () { return "绿" ; } }
在每个形状类中,桥接 IColor 接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Rectangle implements IShape { private IColor color; void setColor (IColor color) { this .color = color; } @Override public void draw () { System.out.println("绘制" + color.getColor() + "矩形" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 class Round implements IShape { private IColor color; void setColor (IColor color) { this .color = color; } @Override public void draw () { System.out.println("绘制" + color.getColor() + "圆形" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 class Triangle implements IShape { private IColor color; void setColor (IColor color) { this .color = color; } @Override public void draw () { System.out.println("绘制" + color.getColor() + "三角形" ); } }
测试函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void drawTest () { Rectangle rectangle = new Rectangle(); rectangle.setColor(new Red()); rectangle.draw(); Round round = new Round(); round.setColor(new Blue()); round.draw(); Triangle triangle = new Triangle(); triangle.setColor(new Yellow()); triangle.draw(); }
运行程序,输出如下:
这时我们再来回顾一下官方定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。抽象部分指的是父类,对应本例中的形状类,实现部分指的是不同子类的区别之处。将子类的区别方式 —— 也就是本例中的颜色 —— 分离成接口,通过组合的方式桥接颜色和形状,这就是桥接模式,它主要用于 两个或多个同等级的接口 。
3、组合模式(不常用) 上文说到,桥接模式用于将同等级的接口互相组合,那么组合模式和桥接模式有什么共同点吗?
事实上组合模式和桥接模式的组合完全不一样。组合模式用于 整体与部分的结构 ,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。例如:
文件夹和子文件夹的关系:文件夹中可以存放文件,也可以新建文件夹,子文件夹也一样。
总公司子公司的关系:总公司可以设立部门,也可以设立分公司,子公司也一样。
树枝和分树枝的关系:树枝可以长出叶子,也可以长出树枝,分树枝也一样。
在这些关系中,虽然整体包含了部分,但无论整体或部分,都具有一致的行为。
组合模式:又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
考虑这样一个实际应用:设计一个公司的人员分布结构,结构如下图所示。
我们注意到人员结构中有两种结构,一是管理者,如老板,PM,CFO,CTO,二是职员。其中有的管理者不仅仅要管理职员,还会管理其他的管理者。这就是一个典型的整体与部分的结构。
3.1、不使用组合模式的设计方案 要描述这样的结构,我们很容易想到以下设计方案:
新建管理者类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class Manager { private String position; private String job; private List<Manager> managers = new ArrayList<>(); private List<Employee> employees = new ArrayList<>(); public Manager (String position, String job) { this .position = position; this .job = job; } public void addManager (Manager manager) { managers.add(manager); } public void removeManager (Manager manager) { managers.remove(manager); } public void addEmployee (Employee employee) { employees.add(employee); } public void removeEmployee (Employee employee) { employees.remove(employee); } public void work () { System.out.println("我是" + position + ",我正在" + job); } public void check () { work(); for (Employee employee : employees) { employee.work(); } for (Manager manager : managers) { manager.check(); } } }
新建职员类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Employee { private String position; private String job; public Employee (String position, String job) { this .position = position; this .job = job; } public void work () { System.out.println("我是" + position + ",我正在" + job); } }
客户端建立人员结构关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Client { @Test public void test () { Manager boss = new Manager("老板" , "唱怒放的生命" ); Employee HR = new Employee("人力资源" , "聊微信" ); Manager PM = new Manager("产品经理" , "不知道干啥" ); Manager CFO = new Manager("财务主管" , "看剧" ); Manager CTO = new Manager("技术主管" , "划水" ); Employee UI = new Employee("设计师" , "画画" ); Employee operator = new Employee("运营人员" , "兼职客服" ); Employee webProgrammer = new Employee("程序员" , "学习设计模式" ); Employee backgroundProgrammer = new Employee("后台程序员" , "CRUD" ); Employee accountant = new Employee("会计" , "背九九乘法表" ); Employee clerk = new Employee("文员" , "给老板递麦克风" ); boss.addEmployee(HR); boss.addManager(PM); boss.addManager(CFO); PM.addEmployee(UI); PM.addManager(CTO); PM.addEmployee(operator); CTO.addEmployee(webProgrammer); CTO.addEmployee(backgroundProgrammer); CFO.addEmployee(accountant); CFO.addEmployee(clerk); boss.check(); } }
运行测试方法,输出如下(为方便查看,添加了缩进):
1 2 3 4 5 6 7 8 9 10 11 我是老板,我正在唱怒放的生命 我是人力资源,我正在聊微信 我是产品经理,我正在不知道干啥 我是设计师,我正在画画 我是运营人员,我正在兼职客服 我是技术主管,我正在划水 我是程序员,我正在学习设计模式 我是后台程序员,我正在CRUD 我是财务主管,我正在看剧 我是会计,我正在背九九乘法表 我是文员,我正在给老板递麦克风
这样我们就设计出了公司的结构,但是这样的设计有两个弊端:
name 字段,job 字段,work 方法重复了。
管理者对其管理的管理者和职员需要区别对待。
关于第一个弊端,虽然这里为了讲解,只有两个字段和一个方法重复,实际工作中这样的整体部分结构会有相当多的重复。比如此例中还可能有工号、年龄等字段,领取工资、上下班打卡、开各种无聊地会等方法。
大量的重复显然是很丑陋的代码,分析一下可以发现, Manager 类只比 Employee 类多一个管理人员的列表字段,多几个增加 / 移除人员的方法,其他的字段和方法全都是一样的。
有读者应该会想到:我们可以将重复的字段和方法提取到一个工具类中,让 Employee 和 Manager 都去调用此工具类,就可以消除重复了。
这样固然可行,但属于 Employee 和 Manager 类自己的东西却要通过其他类调用,并不利于程序的高内聚。
关于第二个弊端,此方案无法解决,此方案中 Employee 和 Manager 类完全是两个不同的对象,两者的相似性被忽略了。
所以我们有更好的设计方案,那就是组合模式!
3.2、使用组合模式的设计方案 组合模式最主要的功能就是让用户可以一致对待整体和部分结构,将两者都作为一个相同的组件,所以我们先新建一个抽象的组件类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public abstract class Component { private String position; private String job; public Component (String position, String job) { this .position = position; this .job = job; } public void work () { System.out.println("我是" + position + ",我正在" + job); } abstract void addComponent (Component component) ; abstract void removeComponent (Component component) ; abstract void check () ; }
管理者继承自此抽象类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Manager extends Component { private List<Component> components = new ArrayList<>(); public Manager (String position, String job) { super (position, job); } @Override public void addComponent (Component component) { components.add(component); } @Override void removeComponent (Component component) { components.remove(component); } @Override public void check () { work(); for (Component component : components) { component.check(); } } }
职员同样继承自此抽象类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Employee extends Component { public Employee (String position, String job) { super (position, job); } @Override void addComponent (Component component) { System.out.println("职员没有管理权限" ); } @Override void removeComponent (Component component) { System.out.println("职员没有管理权限" ); } @Override void check () { work(); } }
修改客户端如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Client { @Test public void test () { Component boss = new Manager("老板" , "唱怒放的生命" ); Component HR = new Employee("人力资源" , "聊微信" ); Component PM = new Manager("产品经理" , "不知道干啥" ); Component CFO = new Manager("财务主管" , "看剧" ); Component CTO = new Manager("技术主管" , "划水" ); Component UI = new Employee("设计师" , "画画" ); Component operator = new Employee("运营人员" , "兼职客服" ); Component webProgrammer = new Employee("程序员" , "学习设计模式" ); Component backgroundProgrammer = new Employee("后台程序员" , "CRUD" ); Component accountant = new Employee("会计" , "背九九乘法表" ); Component clerk = new Employee("文员" , "给老板递麦克风" ); boss.addComponent(HR); boss.addComponent(PM); boss.addComponent(CFO); PM.addComponent(UI); PM.addComponent(CTO); PM.addComponent(operator); CTO.addComponent(webProgrammer); CTO.addComponent(backgroundProgrammer); CFO.addComponent(accountant); CFO.addComponent(clerk); boss.check(); } }
运行测试方法,输出结果与之前的结果一模一样。
可以看到,使用组合模式后,我们解决了之前的两个弊端。一是将共有的字段与方法移到了父类中,消除了重复,并且在客户端中,可以一致对待 Manager 和 Employee 类:
Manager 类和 Employee 类统一声明为 Component 对象
统一调用 Component 对象的 addComponent 方法添加子对象即可。
3.3、组合模式中的安全方式与透明方式 读者可能已经注意到了,Employee 类虽然继承了父类的 addComponent 和 removeComponent 方法,但是仅仅提供了一个空实现,因为 Employee 类是不支持添加和移除组件的。这样是否违背了接口隔离原则呢?
接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
答案是肯定的,这样确实违背了接口隔离原则。这种方式在组合模式中被称作透明方式.
透明方式:在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口。
这种方式有它的优点:让 Manager 类和 Employee 类具备完全一致的行为接口,调用者可以一致对待它们。
但它的缺点也显而易见:Employee 类并不支持管理子对象,不仅违背了接口隔离原则,而且客户端可以用 Employee 类调用 addComponent 和 removeComponent 方法,导致程序出错,所以这种方式是不安全的。
那么我们可不可以将 addComponent 和 removeComponent 方法移到 Manager 子类中去单独实现,让 Employee 不再实现这两个方法呢?我们来尝试一下。
将抽象类修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public abstract class Component { private String position; private String job; public Component (String position, String job) { this .position = position; this .job = job; } public void work () { System.out.println("我是" + position + ",我正在" + job); } abstract void check () ; }
可以看到,我们在父类中去掉了 addComponent 和 removeComponent 这两个抽象方法。
Manager 类修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class Manager extends Component { private List<Component> components = new ArrayList<>(); public Manager (String position, String job) { super (position, job); } public void addComponent (Component component) { components.add(component); } void removeComponent (Component component) { components.remove(component); } @Override public void check () { work(); for (Component component : components) { component.check(); } } }
Manager 类单独实现了 addComponent 和 removeComponent 这两个方法,去掉了 @Override 注解。
Employee 类修改为:
1 2 3 4 5 6 7 8 9 10 11 public class Employee extends Component { public Employee (String position, String job) { super (position, job); } @Override void check () { work(); } }
客户端建立人员结构关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Client { @Test public void test () { Manager boss = new Manager("老板" , "唱怒放的生命" ); Employee HR = new Employee("人力资源" , "聊微信" ); Manager PM = new Manager("产品经理" , "不知道干啥" ); Manager CFO = new Manager("财务主管" , "看剧" ); Manager CTO = new Manager("技术主管" , "划水" ); Employee UI = new Employee("设计师" , "画画" ); Employee operator = new Employee("运营人员" , "兼职客服" ); Employee webProgrammer = new Employee("程序员" , "学习设计模式" ); Employee backgroundProgrammer = new Employee("后台程序员" , "CRUD" ); Employee accountant = new Employee("会计" , "背九九乘法表" ); Employee clerk = new Employee("文员" , "给老板递麦克风" ); boss.addComponent(HR); boss.addComponent(PM); boss.addComponent(CFO); PM.addComponent(UI); PM.addComponent(CTO); PM.addComponent(operator); CTO.addComponent(webProgrammer); CTO.addComponent(backgroundProgrammer); CFO.addComponent(accountant); CFO.addComponent(clerk); boss.check(); } }
运行程序,输出结果与之前一模一样。
这种方式在组合模式中称之为安全方式。
安全方式:在 Component 中不声明 add 和 remove 等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可。
安全方式遵循了接口隔离原则,但由于不够透明,Manager 和 Employee 类不具有相同的接口,在客户端中,我们无法将 Manager 和 Employee 统一声明为 Component 类了,必须要区别对待,带来了使用上的不方便。
安全方式和透明方式各有好处,在使用组合模式时,需要根据实际情况决定。但大多数使用组合模式的场景都是采用的透明方式,虽然它有点不安全,但是客户端无需做任何判断来区分是叶子结点还是枝节点,用起来是真香。
使用场景
注意:只要是树形结构,就考虑使用组合模式。
4、装饰模式 提到装饰,我们先来想一下生活中有哪些装饰:
女生的首饰 :戒指、耳环、项链等装饰品
家居装饰品 :粘钩、镜子、壁画、盆栽等
我们为什么需要这些装饰品呢?很容易想到是为了美,戒指、耳环、项链、壁画、盆栽等都是为了提高颜值或增加美观度。但粘钩、镜子不一样,它们是为了方便我们挂东西、洗漱。所以我们可以总结出装饰品共有两种功能:
增强原有的特性 :我们本身就是有一定颜值的,添加装饰品提高了我们的颜值。同样,房屋本身就有一定的美观度,家居装饰提高了房屋的美观度。
添加新的特性 :在墙上挂上粘钩,让墙壁有了挂东西的功能。在洗漱台装上镜子,让洗漱台有了照镜子的功能。
并且,我们发现装饰品并不会改变物品本身,只是起到锦上添花的作用。装饰模式也一样,它的主要作用就是:
并且 装饰模式也不会改变原有的类 。
装饰模式 :动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器,与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”。
1、用于增强功能的装饰模式 我们用程序来模拟一下戴上装饰品提高我们颜值的过程:
新建颜值接口:
1 2 3 public interface IBeauty { int getBeautyValue () ; }
新建 Me 类,实现颜值接口:
1 2 3 4 5 6 7 public class Me implements IBeauty { @Override public int getBeautyValue () { return 100 ; } }
戒指装饰类,将 Me 包装起来:
1 2 3 4 5 6 7 8 9 10 11 12 public class RingDecorator implements IBeauty { private final IBeauty me; public RingDecorator (IBeauty me) { this .me = me; } @Override public int getBeautyValue () { return me.getBeautyValue() + 20 ; } }
客户端测试:
1 2 3 4 5 6 7 8 9 10 public class Client { @Test public void show () { IBeauty me = new Me(); System.out.println("我原本的颜值:" + me.getBeautyValue()); IBeauty meWithRing = new RingDecorator(me); System.out.println("戴上了戒指后,我的颜值:" + meWithRing.getBeautyValue()); } }
运行程序,输出如下:
1 2 我原本的颜值:100 戴上了戒指后,我的颜值:120
这就是最简单的增强功能的装饰模式。以后我们可以添加更多的装饰类,比如:
耳环装饰类:
1 2 3 4 5 6 7 8 9 10 11 12 public class EarringDecorator implements IBeauty { private final IBeauty me; public EarringDecorator (IBeauty me) { this .me = me; } @Override public int getBeautyValue () { return me.getBeautyValue() + 50 ; } }
项链装饰类:
1 2 3 4 5 6 7 8 9 10 11 12 public class NecklaceDecorator implements IBeauty { private final IBeauty me; public NecklaceDecorator (IBeauty me) { this .me = me; } @Override public int getBeautyValue () { return me.getBeautyValue() + 80 ; } }
客户端测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Client { @Test public void show () { IBeauty me = new Me(); System.out.println("我原本的颜值:" + me.getBeautyValue()); IBeauty meWithNecklace = new NecklaceDecorator(me); System.out.println("戴上了项链后,我的颜值:" + meWithNecklace.getBeautyValue()); IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me))); System.out.println("戴上耳环、戒指、项链后,我的颜值:" + meWithManyDecorators.getBeautyValue()); IBeauty meWithNecklaceAndRing = new NecklaceDecorator(new RingDecorator(me)); System.out.println("戴上戒指、项链后,我的颜值:" + meWithNecklaceAndRing.getBeautyValue()); } }
运行程序,输出如下:
1 2 3 4 我原本的颜值:100 戴上了项链后,我的颜值:180 戴上耳环、戒指、项链后,我的颜值:250 戴上戒指、项链后,我的颜值:200
可以看到,装饰器也实现了 IBeauty 接口,并且没有添加新的方法,也就是说这里的装饰器仅用于增强功能,并不会改变 Me 原有的功能 ,这种装饰模式称之为 透明装饰模式 ,由于没有改变接口,也没有新增方法,所以透明装饰模式可以无限装饰 。
装饰模式是 继承 的一种替代方案。本例如果不使用装饰模式,而是改用继承实现的话,戴着戒指的 Me 需要派生一个子类、戴着项链的 Me 需要派生一个子类、戴着耳环的 Me 需要派生一个子类、戴着戒指 + 项链的需要派生一个子类……各种各样的排列组合会造成类爆炸。而采用了装饰模式就只需要为每个装饰品生成一个装饰类即可,所以说就 增加对象功能 来说,装饰模式比生成子类实现更为灵活 。
2、用于添加功能的装饰模式 我们用程序来模拟一下房屋装饰粘钩后,新增了挂东西功能的过程:
新建房屋接口:
1 2 3 public interface IHouse { void live () ; }
房屋类:
1 2 3 4 5 6 7 public class House implements IHouse { @Override public void live () { System.out.println("房屋原有的功能:居住功能" ); } }
新建粘钩装饰器接口,继承自房屋接口:
1 2 3 public interface IStickyHookHouse extends IHouse { void hangThings () ; }
粘钩装饰类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class StickyHookDecorator implements IStickyHookHouse { private final IHouse house; public StickyHookDecorator (IHouse house) { this .house = house; } @Override public void live () { house.live(); } @Override public void hangThings () { System.out.println("有了粘钩后,新增了挂东西功能" ); } }
客户端测试:
1 2 3 4 5 6 7 8 9 10 11 public class Client { @Test public void show () { IHouse house = new House(); house.live(); IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house); stickyHookHouse.live(); stickyHookHouse.hangThings(); } }
运行程序,显示如下:
1 2 3 房屋原有的功能:居住功能 房屋原有的功能:居住功能 有了粘钩后,新增了挂东西功能
这就是用于 新增功能 的装饰模式。我们在接口中新增了方法:hangThings,然后在装饰器中将 House 类包装起来,之前 House 中的方法仍然调用 house 去执行,也就是说我们并没有修改原有的功能,只是扩展了新的功能,这种模式在装饰模式中称之为 半透明装饰模式 。
为什么叫半透明呢?由于新的接口 IStickyHookHouse 拥有之前 IHouse 不具有的方法,所以我们如果要使用装饰器中添加的功能,就不得不区别对待 装饰前的对象和装饰后的对象 。也就是说客户端要使用新方法,必须知道具体的装饰类 StickyHookDecorator,所以这个装饰类对客户端来说是可见的、不透明的。而被装饰者不一定要是 House,它可以是实现了 IHouse 接口的任意对象,所以被装饰者对客户端是不可见的、透明的。由于一半透明,一半不透明,所以称之为半透明装饰模式。
我们可以添加更多的装饰器:
新建镜子装饰器的接口,继承自房屋接口:
1 2 3 public interface IMirrorHouse extends IHouse { void lookMirror () ; }
镜子装饰类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MirrorDecorator implements IMirrorHouse { private final IHouse house; public MirrorDecorator (IHouse house) { this .house = house; } @Override public void live () { house.live(); } @Override public void lookMirror () { System.out.println("有了镜子后,新增了照镜子功能" ); } }
客户端测试:
1 2 3 4 5 6 7 8 9 10 11 public class Client { @Test public void show () { IHouse house = new House(); house.live(); IMirrorHouse mirrorHouse = new MirrorDecorator(house); mirrorHouse.live(); mirrorHouse.lookMirror(); } }
运行程序,输出如下:
1 2 3 房屋原有的功能:居住功能 房屋原有的功能:居住功能 有了镜子后,新增了照镜子功能
现在我们仿照 透明装饰模式 的写法,同时添加粘钩和镜子装饰试一试:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Client { @Test public void show () { IHouse house = new House(); house.live(); IStickyHookHouse stickyHookHouse = new StickyHookDecorator(house); IMirrorHouse houseWithStickyHookMirror = new MirrorDecorator(stickyHookHouse); houseWithStickyHookMirror.live(); houseWithStickyHookMirror.hangThings(); houseWithStickyHookMirror.lookMirror(); } }
我们会发现,第二次装饰时,无法获得上一次装饰添加的方法。原因很明显,当我们用 IMirrorHouse 装饰器后,接口变为了 IMirrorHouse,这个接口中并没有 hangThings 方法。
那么我们能否让 IMirrorHouse 继承自 IStickyHookHouse,以实现新增两个功能呢?
可以,但那样做的话两个装饰类之间有了依赖关系,那就不是装饰模式了。装饰类不应该存在依赖关系,而应该在原本的类上进行装饰。这就意味着,半透明装饰模式中 ,我们无法多次装饰 。
有的同学会问了,既增强了功能,又添加了新功能的装饰模式叫什么呢?
举一反三,肯定是叫全不透明装饰模式!
并不是!只要添加了新功能的装饰模式都称之为 半透明装饰模式 ,他们都具有不可以多次装饰的特点。仔细理解上文半透明名称的由来就知道了,“透明”指的是我们无需知道被装饰者具体的类,既增强了功能,又添加了新功能的装饰模式仍然具有半透明特性。
看了这两个简单的例子,是不是发现装饰模式很简单呢?恭喜你学会了 1 + 1 = 2,现在你已经掌握了算数的基本思想,接下来我们来做一道微积分题练习一下。
I/O 中的装饰模式 I/O 指的是 Input/Output,即输入、输出。我们以 Input 为例。先在 src
文件夹下新建一个文件 readme.text,随便写点文字:
然后用 Java 的 InputStream 读取,代码一般长这样:
1 2 3 4 5 6 7 8 public void io () throws IOException { InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt" )); byte [] buffer = new byte [1024 ]; while (in.read(buffer) != -1 ) { System.out.println(new String(buffer)); } in.close(); }
这样写有一个问题,如果读取过程中出现了 IO 异常,InputStream 就不能正确关闭,所以我们要用 try...finally
来保证 InputStream 正确关闭:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void io () throws IOException { InputStream in = null ; try { in = new BufferedInputStream(new FileInputStream("src/readme.txt" )); byte [] buffer = new byte [1024 ]; while (in.read(buffer) != -1 ) { System.out.println(new String(buffer)); } } finally { if (in != null ) { in.close(); } } }
这种写法实在是太丑了,而 IO 操作又必须这么写,显然 Java 也意识到了这个问题,所以 Java 7 中引入了 try(resource)
语法糖,IO 的代码就可以简化如下:
1 2 3 4 5 6 7 8 public void io () throws IOException { try (InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt" ))) { byte [] buffer = new byte [1024 ]; while (in.read(buffer) != -1 ) { System.out.println(new String(buffer)); } } }
这种写法和上一种逻辑是一样的,运行程序,显示如下:
观察获取 InputStream 这句代码:
1 InputStream in = new BufferedInputStream(new FileInputStream("src/readme.txt" ));
是不是和我们之前多次装饰的代码非常相似:
1 2 IBeauty meWithManyDecorators = new NecklaceDecorator(new RingDecorator(new EarringDecorator(me)));
事实上,查看 I/O 的源码可知,Java I/O 的设计框架便是使用的 装饰者模式 ,InputStream 的继承关系如下:
其中,InputStream 是一个抽象类,对应上文例子中的 IHouse,其中最重要的方法是 read 方法,这是一个抽象方法:
1 2 3 4 5 6 public abstract class InputStream implements Closeable { public abstract int read () throws IOException ; }
这个方法会读取输入流的下一个字节,并返回字节表示的 int 值(0~255),返回 -1 表示已读到末尾。由于它是抽象方法,所以具体的逻辑交由子类实现。
上图中,左边的三个类 FileInputStream、ByteArrayInputStream、ServletInputStream 是 InputStream 的三个子类,对应上文例子中实现了 IHouse 接口的 House。
右下角的三个类 BufferedInputStream、DataInputStream、CheckedInputStream 是三个具体的装饰者类,他们都为 InputStream 增强了原有功能或添加了新功能。
FilterInputStream 是所有装饰类的父类,它没有实现具体的功能,仅用来包装了一下 InputStream:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class FilterInputStream extends InputStream { protected volatile InputStream in; protected FilterInputStream (InputStream in) { this .in = in; } public int read () throws IOException { return in.read(); } }
我们以 BufferedInputStream 为例。原有的 InputStream 读取文件时,是一个字节一个字节读取的,这种方式的执行效率并不高,所以我们可以设立一个缓冲区,先将内容读取到缓冲区中,缓冲区读满后,将内容从缓冲区中取出来,这样就变成了一段一段读取,用内存换取效率。BufferedInputStream 就是用来做这个的。它继承自 FilterInputStream:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class BufferedInputStream extends FilterInputStream { private static final int DEFAULT_BUFFER_SIZE = 8192 ; protected volatile byte buf[]; public BufferedInputStream (InputStream in) { this (in, DEFAULT_BUFFER_SIZE); } public BufferedInputStream (InputStream in, int size) { super (in); if (size <= 0 ) { throw new IllegalArgumentException("Buffer size <= 0" ); } buf = new byte [size]; } }
我们先来看它的构造方法,在构造方法中,新建了一个 byte[]
作为缓冲区,从源码中我们看到,Java 默认设置的缓冲区大小为 8192 byte,也就是 8 KB。
然后我们来查看 read 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class BufferedInputStream extends FilterInputStream { public synchronized int read () throws IOException { if (pos >= count) { fill(); if (pos >= count) return -1 ; } return getBufIfOpen()[pos++] & 0xff ; } private void fill () throws IOException { } }
在 read 方法中,调用了 fill 方法,fill 方法的作用就是往缓冲区中填充读取的内容。这样就实现了增强原有的功能。
在源码中我们发现,BufferedInputStream 没有添加 InputStream 中没有的方法,所以 BufferedInputStream 使用的是 透明的装饰模式 。
DataInputStream 用于更加方便地读取 int、double 等内容,观察 DataInputStream 的源码可以发现,DataInputStream 中新增了 readInt、readLong 等方法,所以 DataInputStream 使用的是 半透明装饰模式 。
理解了 InputStream 后,再看一下 OutputStream 的继承关系,相信大家一眼就能看出各个类的作用了:
这就是装饰模式,注意不要和适配器模式混淆了。两者在使用时都是包装一个类,但两者的区别其实也很明显:
纯粹的适配器模式 仅用于改变接口 ,不改变其功能,部分情况下我们需要改变一点功能以适配新接口。但使用适配器模式时,接口一定会有一个 回炉重造 的过程。
装饰模式 不改变原有的接口 ,仅用于增强原有功能或添加新功能,强调的是 锦上添花 。
掌握了装饰者模式之后,理解 Java I/O 的框架设计就非常容易了。但对于不理解装饰模式的人来说,各种各样相似的 InputStream 非常容易让开发者感到困惑。这一点正是装饰模式的缺点:容易造成程序中有大量相似的类 。虽然这更像是开发者的缺点,我们应该做的是提高自己的技术,掌握了这个设计模式之后它就是我们的一把利器。现在我们再看到 I/O 不同的 InputStream 装饰类,只需要关注它增强了什么功能或添加了什么功能即可。
使用场景
需要扩展一个类的功能,或给一个类增加附加功能。
需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式。
5、外观模式(不常用) 外观模式非常简单,体现的就是 Java 中封装的思想。将多个子系统封装起来,提供一个更简洁地接口供外部调用。
外观模式 :外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式。
举个例子,比如我们每天打开电脑时,都需要做三件事:
每天下班时,关机前需要做三件事:
用程序模拟如下:
新建浏览器类:
1 2 3 4 5 6 7 8 9 public class Browser { public static void open () { System.out.println("打开浏览器" ); } public static void close () { System.out.println("关闭浏览器" ); } }
新建 IDE 类:
1 2 3 4 5 6 7 8 9 public class IDE { public static void open () { System.out.println("打开 IDE" ); } public static void close () { System.out.println("关闭 IDE" ); } }
新建微信类:
1 2 3 4 5 6 7 8 9 public class Wechat { public static void open () { System.out.println("打开微信" ); } public static void close () { System.out.println("关闭微信" ); } }
客户端调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Client { @Test public void test () { System.out.println("上班:" ); Browser.open(); IDE.open(); Wechat.open(); System.out.println("下班:" ); Browser.close(); IDE.close(); Wechat.close(); } }
运行程序,输出如下:
1 2 3 4 5 6 7 8 上班: 打开浏览器 打开 IDE 打开微信 下班: 关闭浏览器 关闭 IDE 关闭微信
由于我们每天都要做这几件事,所以我们可以使用外观模式 ,将这几个子系统封装起来,提供更简洁的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Facade { public void open () { Browser.open(); IDE.open(); Wechat.open(); } public void close () { Browser.close(); IDE.close(); Wechat.close(); } }
客户端就可以简化代码,只和这个外观类打交道:
1 2 3 4 5 6 7 8 9 10 11 public class Client { @Test public void test () { Facade facade = new Facade(); System.out.println("上班:" ); facade.open(); System.out.println("下班:" ); facade.close(); } }
运行程序,输出与之前一样。
外观模式就是这么简单,它使得两种不同的类不用直接交互,而是通过一个中间件——也就是外观类——间接交互。外观类中只需要暴露简洁的接口,隐藏内部的细节,所以说白了就是封装的思想。
外观模式非常常用,(当然了!写代码哪有不封装的!)尤其是在第三方库的设计中,我们应该尽量提供简洁的接口供别人调用。另外,在 MVC 架构中,C 层(Controller)就可以看作是外观类,Model 和 View 层通过 Controller 交互,减少了耦合。
使用场景
为一个复杂的模块或子系统提供一个供外界访问的接口
子系统相对独立——外界对子系统的访问只要黑箱操作即可
预防低水平人员带来的风险扩散
6、享元模式(不常用) 享元模式体现的是 程序可复用 的特点,为了节约宝贵的内存,程序应该尽可能地复用,就像《极限编程》作者 Kent 在书里说到的那样:Don’t repeat yourself. 简单来说 享元模式就是共享对象 ,提高复用性,官方的定义倒是显得文绉绉的:
享元模式 :运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。
有个细节值得注意:有些对象本身不一样,但通过一点点变化后就可以复用,我们编程时可能稍不注意就会忘记复用这些对象。比如说伟大的《超级玛丽》,谁能想到草和云更改一下颜色就可以实现复用呢?
还有里面的三种乌龟,换一个颜色、加一个装饰就变成了不同的怪:
在《超级玛丽》中,这样的细节还有很多,正是这些精湛的复用使得这一款红遍全球的游戏仅有 40KB 大小。正是印证了那句名言:神在细节之中。
7、代理模式 现在我们有一个 人
类,他整天就只负责吃饭、睡觉:
人
类的接口
1 2 3 4 public interface IPerson { void eat () ; void sleep () ; }
人
类:
1 2 3 4 5 6 7 8 9 10 11 12 public class Person implements IPerson { @Override public void eat () { System.out.println("我在吃饭" ); } @Override public void sleep () { System.out.println("我在睡觉" ); } }
客户端测试:
1 2 3 4 5 6 7 8 public class Client { @Test public void test () { Person person = new Person(); person.eat(); person.sleep(); } }
运行程序,输出如下:
我们可以把这个类包装到另一个类中,实现完全一样的行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class PersonProxy implements IPerson { private final Person person; public PersonProxy (Person person) { this .person = person; } @Override public void eat () { person.eat(); } @Override public void sleep () { person.sleep(); } }
将客户端修改为调用这个新的类:
1 2 3 4 5 6 7 8 9 public class Client { @Test public void test () { Person person = new Person(); PersonProxy proxy = new PersonProxy(person); proxy.eat(); proxy.sleep(); } }
运行程序,输出如下:
这就是代理模式。
笔者尽量用最简洁的代码讲解此模式,只要理解了上述这个简单的例子,你就知道代理模式是怎么一回事了。我们在客户端和 Person 类之间新增了一个中间件 PersonProxy,这个类就叫做代理类,他实现了和 Person 类一模一样的行为。
代理模式 :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
现在这个代理类还看不出任何意义,我们来模拟一下工作中的需求。在实际工作中,我们可能会遇到这样的需求:在网络请求前后,分别打印将要发送的数据和接收到数据作为日志信息。此时我们就可以新建一个网络请求的代理类,让它代为处理网络请求,并在代理类中打印这些日志信息。
新建网络请求接口:
1 2 3 4 5 public interface IHttp { void request (String sendData) ; void onSuccess (String receivedData) ; }
新建 Http 请求工具类:
1 2 3 4 5 6 7 8 9 10 11 public class HttpUtil implements IHttp { @Override public void request (String sendData) { System.out.println("网络请求中..." ); } @Override public void onSuccess (String receivedData) { System.out.println("网络请求完成。" ); } }
新建 Http 代理类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HttpProxy implements IHttp { private final HttpUtil httpUtil; public HttpProxy (HttpUtil httpUtil) { this .httpUtil = httpUtil; } @Override public void request (String sendData) { httpUtil.request(sendData); } @Override public void onSuccess (String receivedData) { httpUtil.onSuccess(receivedData); } }
到这里,和我们上述吃饭睡觉的代码是一模一样的,现在我们在 HttpProxy 中新增打印日志信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class HttpProxy implements IHttp { private final HttpUtil httpUtil; public HttpProxy (HttpUtil httpUtil) { this .httpUtil = httpUtil; } @Override public void request (String sendData) { System.out.println("发送数据:" + sendData); httpUtil.request(sendData); } @Override public void onSuccess (String receivedData) { System.out.println("收到数据:" + receivedData); httpUtil.onSuccess(receivedData); } }
客户端验证:
1 2 3 4 5 6 7 8 9 public class Client { @Test public void test () { HttpUtil httpUtil = new HttpUtil(); HttpProxy proxy = new HttpProxy(httpUtil); proxy.request("request data" ); proxy.onSuccess("received result" ); } }
运行程序,输出如下:
1 2 3 4 发送数据:request data 网络请求中... 收到数据:received result 网络请求完成。
这就是代理模式的一个应用,除了 打印日志 ,它还可以用来做权限管理 。读者看到这里可能已经发现了,这个代理类看起来和装饰模式的 FilterInputStream 一模一样,但两者的目的不同,装饰模式是为了 增强功能或添加功能 ,代理模式主要是为了加以控制 。
动态代理 上例中的代理被称之为静态代理,动态代理与静态代理的原理一模一样,只是换了一种写法。使用动态代理,需要把一个类传入,然后根据它正在调用的方法名判断是否需要加以控制。用伪代码表示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class HttpProxy { private final HttpUtil httpUtil; public HttpProxy (HttpUtil httpUtil) { this .httpUtil = httpUtil; } public visit (String methodName, Object[] args) { if (methodName.equals("request" )) { System.out.println("发送数据:" + args[0 ]); httpUtil.request(args[0 ].toString()); } else if (methodName.equals("onSuccess" )) { System.out.println("收到数据:" + args[0 ]); httpUtil.onSuccess(args[0 ].toString()); } } }
伪代码看起来还是很简单的,实现起来唯一的难点就是怎么让 httpUtil 调用任意方法时 ,都通过一个方法间接调用 。这里需要用到反射技术,不了解反射技术也没有关系,不妨把它记做固定的写法。实际的动态代理类代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class HttpProxy implements InvocationHandler { private HttpUtil httpUtil; public IHttp getInstance (HttpUtil httpUtil) { this .httpUtil = httpUtil; return (IHttp) Proxy.newProxyInstance(httpUtil.getClass().getClassLoader(), httpUtil.getClass().getInterfaces(), this ); } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Object result = null ; if (method.getName().equals("request" )) { System.out.println("发送数据:" + args[0 ]); result = method.invoke(httpUtil, args); } else if (method.getName().equals("onSuccess" )) { System.out.println("收到数据:" + args[0 ]); result = method.invoke(httpUtil, args); } return result; } }
先看 getInstance 方法,Proxy.newProxyInstance 方法是 Java 系统提供的方法,专门用于动态代理。其中传入的第一个参数是被代理的类的 ClassLoader,第二个参数是被代理类的 Interfaces,这两个参数都是 Object 中的,每个类都有,这里就是固定写法。我们只要知道系统需要这两个参数才能让我们实现我们的目的:调用被代理类的任意方法时 ,都通过一个方法间接调用 。现在我们给系统提供了这两个参数,系统就会在第三个参数中帮我们实现这个目的。
第三个参数是 InvocationHandler 接口,这个接口中只有一个方法:
1 public Object invoke (Object proxy, Method method, Object[] args) throws Throwable ;
那么不用猜就知道,现在我们调用被代理类 httpUtil 的任意方法时,都会通过这个 invoke 方法调用了。invoke 方法中,第一个参数我们暂时用不上,第二个参数 method 就是调用的方法,使用 method.getName() 可以获取到方法名,第三个参数是调用 method 方法需要传入的参数。本例中无论 request 还是 onSuccess 都只有一个 String 类型的参数,对应到这里就是 args[0]。返回的 Object 是 method 方法的返回值,本例中都是无返回值的。
我们在 invoke 方法中判断了当前调用方法的方法名,如果现在调用的方法是 request,那么打印请求参数,并使用这一行代码继续执行当前方法:
1 result = method.invoke(httpUtil, args);
这就是 反射调用函数 的写法,如果不了解可以记做固定写法,想要了解的同学可以看之前的这篇文章:**详解面试中常考的 Java 反射机制 **。虽然这个函数没有返回值,但我们还是将 result 返回,这是标准做法。
如果现在调用的方法是 onSuccess,那么打印接收到的数据,并反射继续执行当前方法。
修改客户端验证一下:
1 2 3 4 5 6 7 8 9 public class Client { @Test public void test () { HttpUtil httpUtil = new HttpUtil(); IHttp proxy = new HttpProxy().getInstance(httpUtil); proxy.request("request data" ); proxy.onSuccess("received result" ); } }
运行程序,输出与之前一样:
1 2 3 4 发送数据:request data 网络请求中... 收到数据:received result 网络请求完成。
动态代理本质上与静态代理没有区别,它的好处是节省代码量 。比如被代理类有 20 个方法,而我们只需要控制其中的两个方法,就可以用动态代理通过方法名对被代理类进行动态的控制,而如果用静态方法,我们就需要将另外的 18 个方法也写出来,非常繁琐。这就是动态代理的优势所在。
总结 代理模式、适配器模式、装饰器模式很像,只是目的不同。
代理模式:在不改变原始接口的条件下,为原始类控制访问、新增一些业务无关的功能。控制和管理对对象的访问。
适配器模式:将不兼容的接口转换为可兼容的接口。(一种以修正为目的的设计模式)。
装饰器模式:是对原始类的功能进行增强,支持多个装饰类的嵌套增强。不改变接口,但加入责任。
外观模式:将一群对象包装起来以简化其接口。让接口更简单,提供子系统的一个简化接口
组合模式,其实就是一个类中调用另一个类。