设计模式
一、结构型模式
结构型模式 描述如何将类后对象按某种布局组成更大的结构,它分为 类结构型模式
和 对象结构型模式
。前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足"合成复用原则"
,所以 对象结构型模式 比 类结构型模式 具有更大的灵活性。
我们在上面已经了解都有 7 种 结构性模式 ,我们接下来就先来认识第一种设计模式 代理模式
1)代理模式
由于某些原因需要给某对象提供一个代理以控制对该对象的访问,这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介
代理模式 作为开发的我们肯定也不会感到陌生,JDK 代理、CGLib 代理 随口就来。在 Java 中的代理按照代理类生成实际不同又分为 静态代理
和 动态代理
。
- 静态代理是在编译时期就生成的
- 动态代理是在 Java 运行时动态生成的
其中动态代理又分为 JDK 动态代理
和 CGLib 代理
两种
1. 静态代理
我们结合 静态代理模式 的 UML图 来了解下:
可以看出 静态代理模式 有三种角色,分别是:
- 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类: 实现了抽象主题,提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
我们举个例子理解一下:比如之前我们如果需要乘坐火车的话,我们需要去火车站买票,但是火车站距离远我们还得坐车去火车站,万一人多还得排队,这显然是十分不方便的。这个时候就出现了 火车站代售点,通过 代售点 我们就可以直接买票。
码示如下:
从上面代码中可以看出测试类直接访问的是 ProxyStation ,它作为访问对象和目标对象的中介,同时也可以对抽象方法进行增加(收取服务费)
2. 动态代理
(1)JDK 动态代理
在 JDK动态代理 中,抽象主题 Subject 和 真实主题 RealSubject 是不变的,我们需要改的地方是 代理 Proxy,将静态转换成动态,然后客户端的调用方式有所改变,具体代码如下:
我们在代码中看到 ProxyStation 好像不需要实现 SellTicket 这个接口了,但是真的是这样吗。有兴趣的同学可以编译查看下代理类的结构,我这里接直接说结论了:
ProxyStation 在这里面其实不算是 代理模式中所说的代理类,通过 Proxy 创建出来的才是实际上的代理类,它实现了 SellTicket 这个接口,为我们提供了匿名内部类对象传递给了父类,然后在测试类通过代理对象调用的 sell()
方法,实际上是根据多态
的特性,执行的是 Proxy 类 中的 sell()
方法,因此会出现 代售点收取代理费
的输出。
那么如果我们没有定义 SellTicket 这个接口,只定义了 RailwayStation 这个类,那么 JDK动态代理 是不适用的,因为它必须要求定义接口,对接口进行代理。那我们这个时候就需要使用 CGLib 动态代理 了。
(2) CGLib 动态代理
由于 CGLib 是第三方工具包,我们需要先引入依赖:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
然后代码做出以下修改:
CGLib动态代理实际上是利用 ASM
开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来实现代理。接下来我们比较以下各个代理:
静态代理 和 动态代理
:
如果接口方法的数量比较多的话,静态代理需要对每一个方法进行中转,而动态代理是将声明的所有方法都转移到调用处理器一个集中的方法中进行处理。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也都需要实现这个方法,而动态代理则不用。
结论: 动态代理相对静态代理来说更加灵活
JDK动态代理 和 CGLib动态代理
:
在 JDK 1.6 之前,使用 CGLib 动态代理 效率会比 JDK 动态代理 要高,但是在后面的版本对 JDK 动态代理 进行优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。
结论:CGLib动态代理不能对声明为final
的类或者方法进行代理,JDK动态代理 不能对没有接口的类进行代理。
2)适配器模式
将一个类的接口转换为客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作
适配器模式 讲究的便是 适配
两个字,我们生活中的 手机充电器(电压转换),读卡器 等就是使用到了 适配器模式。
适配器模式 又分为 类适配器模式
和 对象适配器
模式,前者的耦合度会比后者高,因此应用相对较少。
我们总结一下 适配器模式 中的几种角色:
- 目标接口(Target): 当前系统业务所期待的接口,可以是抽象类或者接口
- 适配源类(Adaptee): 它是被访问和适配的现存组件库中的组件接口
- 适配者类(Adapter): 它是一个转换器,通过集成或引用适配者的对象,把适配源接口转换成目标接口,让客户按目标接口的格式访问适配者
1. 类适配器模式
看图我们就可以知道了个大概,首先就是定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件
我们举个例子来说明一下:
现有一台电脑只能读取 SD 卡,但是我们现在手上只有 TF 卡,如果需要读取 TF 卡就需要使用到适配器模式,创建一个读卡器(适配器)来将 TF 卡中的内容读取出来。
我们按照上面的 UML图 仿写一下:
大致没有什么差别,只不过多了两个实现类,然后我们接下来就可以写代码了:
照代码的功能来看,类适配器 确实实现了我们想要的适配功能,但是从原则上看,类适配器 却违背了 合成复用原则 ,这样会导致的问题就是只有当客户端有一个接口规范的情况下可用,否则不可用。
啥?你还不知道 合成复用原则 是啥,赶紧来复习下吧! 软件设计原则。
2. 对象适配器模式
对象适配器模式可采用将现有组件库中已经实现的组件引入适配器中,该类同时实现当前系统的业务接口
怎么个意思呢,其实就是将继承关系 改成了聚合关系,UML示图如下:
然后修改部分代码如下:
3)装饰者模式
在不改变现有对象结构的情况下,动态地给该对象增加一些职责(增加额外功能)的模式
我们老样子看图理一下 装饰者模式 中有几种角色:
- 抽象构件(Component): 定义一个抽象接口以规范准备接收附加责任的对象
- 具体构件(ConcreteComponent): 实现抽象构件,通过装饰角色为其添加一些职责
- 抽象装饰(Decorator): 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能
- 具体装饰(ConcreteDecorator): 实现抽象装饰的相关方法,并给具体构件对象添加附加的责任
我们以 炒面 为例,炒面我们可以加 蛋 或者加 肉,当然,加这两种材料的价钱肯定是不一样的,我们先画 UML 来理解一下:
然后我们用代码实现一下:
功能已经完美的实现,那我们从中能发现什么好处呢?
- 装饰者模式 可以比继承更加灵活地扩展功能,使用起来更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。完美的遵循了开闭原则,
继承是静态的附加责任,装饰者则是动态的附加责任
- 装饰类 和 被装饰类可以独立发展,不会相互耦合,装饰者模式是继承的一个替代模式,装饰者模式可以动态扩展一个实现类的功能
有些小伙伴也可以已经发现了 代理模式 和 装饰者模式 有点相似,都是可以通过 聚合 的方式动态的增加额外的责任。下面是 静态代理模式和 装饰者模式 的比较:
相同点
- 都要实现与目标类相同的业务接口
- 在两个类中都要声明目标对象
- 都可以在不修改目标类的前提下增强目标方法
不同点
- 目的不同: 装饰者是为了增强目标对象,静态代理是为了保护和隐藏目标对象
- 获取目标对象构建的地方不同: 装饰者是由外界传递进来,可以通过构造方法传递,静态代理是在代理类内部创建,以此来隐藏目标对象
4)桥接模式
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度
如果有个需求我们需要创建不同的图形(矩形,圆形,正方形),每个图形需要有不同的颜色(红色,白色,黑色)。当我们不做过多思考,写出第一种解决方案应该是这样的:
这种应该是最简单的实现方式了,但是如果我们需要在增加一种形状或颜色,那么就需要创建爱你更多的类,这跟 工厂方法 模式有点类似,造成的结果就是 类爆炸
,那么有没有优化的方案,我们就想到了第二种实现方式如下:
这个方案是根据实际需要对形状和颜色进行组合,对于有两个变化维度(即两个变化的原因)的系统,采用第二个方案进行设计系统,类的数量会更少,系统扩展也会更加方便。而这种模式便是 桥接模式,好处便是降低了类与类之间的耦合度,减少了代码编写量。
接下来我们看下 桥接模式的 UML图:
然后我们再来分析一下其中有哪几种角色:
- 抽象化(Abstraction)角色: 定义抽象类,并包含一个对实现化对象的引用
- 扩展抽象化(RefinedAbstraction)角色: 是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实例化角色中的业务方法
- 实例化(Implementor)角色: 定义实现化角色的接口,供扩展抽象化角色调用
- 具体实例化(ConcreteImplementor)角色: 给出实例化角色接口的具体实现
接下来我们举个例子来加深一下理解吧!
现在有两种规格的画笔,分别是 大画笔 和 小画笔 ,而且我们需要能够绘制三种颜色(红黄蓝)的颜色。如果使用传统蜡笔来实现的话,那么需要 2*3=6
支画笔,如果我们将画笔改成颜料笔,那我们只需要 3盒颜料 加 两支画笔
UML 图 如下:
代码如下:
理解完上述的例子,我们也来分析一下 桥接模式 的优缺点
优点:
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统
- 实现细节对客户透明,可以对用户隐藏实现细节
缺点:
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性
5)外观模式
外观模式又称为 门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式,该模式对外有一个统一的接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性
其实生活中最简单的例子便是 股票和基金 的关。有些人不会炒股,这个时候基金就是一个好的帮手,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。
外观模式是迪米特法则的典型应用
没用 外观模式 之前:
用了 外观模式 之后:
如果理解了上面一层的关系,那么得出 UML图 也是十分容易:
外观模式 中存在以下几种角色:
- 外观(Facade)角色: 为多个子系统对外提供一个共同的接口
- 子系统(SubSystem)角色: 实现系统的部分功能,客户可以通过外观角色访问它
那我们就举个例子简单说明一下吧!
小米的生态现在都挺好用的,只需要我们对 小爱同学音箱(Facade) 进行语音控制便可以控制智能家居(灯,空调,窗帘 --- SubSystem) ,这里面其实就是用到 外观模式。
代码实现如下:
这样子就是用了 外观模式,客户不同跟具体某个家居交互,直接跟音箱交互即可,然后让我们扒一扒 外观模式 的优缺点吧!
优点:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易
缺点:
- 不符合开闭原则,修改起来较为麻烦
6)组合模式
组合模式又称为整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
文字看的麻烦,用张图你就知道了
这种就是一个文件系统,而这种结构我们也称为 树形结构。这里面用到的便是 组合模式。
组合模式(Composite),将对象组合成树形结构以表示 "部分-整体"
的层次结构,用户对单个对象和组合对象的使用具有一致性。所以当我们的案例是 树形结构 或 部分-整体 的关系时,就可以考虑使用组合模式。
其中 组合模式 又有两种不同的实现,分别是 透明模式
和 安全模式
,其中角色都是一致的,如下:
- 抽象根节点(Component)角色: 定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性
- 树枝节点(Composite)角色: 定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构
- 叶子节点(Leaf)角色: 叶子节点对象,其下再无分支,是系统层次遍历的最小单位
1. 透明模式
我们看下透明模式的 UML图:
透明模式是把使用的方法放到抽象类中,不管是 树枝对象 还是 叶子对象 都有相同的结构,代码如下:
这样做的好处便是叶子节点和树枝节点对于外界来说没有区别,它们具备完全一致的行为接口,但是 Leaf 类 本身不具备 add()
和remove()
方法的功能,所以实现它是没有意义的,这样子就又出现了另外一种模式,便是 组合模式 - 安全模式。
2. 安全模式
安全模式 的 UML 图如下:
安全模式是把树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方法比较安全。代码实现如下:
这种方式由于不够透明,树叶节点和树枝节点将不具有相同的接口,客户端的调用需要做相应的判断,不能完全整堆抽象编程,必须有区别地对待叶子构件和容器构件。
7)享元模式
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。
**享元模式(Flyweight)**中存在两种状态:
- 内部状态,不会随着环境的改变而改变的可共享的部分
- 外部状态,会随环境改变而改变的不可共享的部分
享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式在开发中运行到的场景挺多的,首先能想到的便是各种 池技术了,线程池
、数据库连接池
、String常量池
等等,所以说 享元模式 是池技术的重要实现方式。
所以 享元模式的用处很简单,便是减少对象的创建
在 享元模式 中存在着这几种角色:
- 抽象享元(Flyweight)角色: 通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
- 具体享元(ConcreteFlyweight)角色: 它实现了抽象享元类,称为 享元对象,在具体享元类中为内部状态提供了存储空间,通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象
- 非享元(UnsharedFlyweight)角色: 并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可以设计为非共享具体享元类。当需要一个非共享具体享元类的对象时可以直接通过实例化创建
- 享元工厂(FlyweightFactory)角色: 负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户,如果不存在的话,则创建一个新的享元对象
接下来我们用代码来实现一下:
这样子就简单实现了 享元模式,我们老样子来扒一扒 享元模式 的优缺点
优点:
- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
- 享元模式中的外部状态相对独立,且不影响内部状态
缺点:
为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂