结构型
适配器模式
意图
适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。
问题提出
- 假如你正在开发一款股票市场监测程序, 它会从不同来源下载 XML 格式的股票数据, 然后向用户呈现出美观的图表。
- 在开发过程中, 你决定在程序中整合一个第三方智能分析函数库。 但是遇到了一个问题, 那就是分析函数库只兼容 JSON 格式的数据。
- 你可以修改程序库来支持 XML。 但是, 这可能需要修改部分依赖该程序库的现有代码。 甚至还有更糟糕的情况, 你可能根本没有程序库的源代码, 从而无法对其进行修改
解决办法
创建将 XML 转换为 JSON 格式的适配器
适配器模式结构
- 对象适配器
实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。 - 类适配器
这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现, 例如 C++。
方钉和圆孔问题分析适配器模式
方钉和圆孔问题: 如何将方形钉子放入圆孔内. 适配器假扮成一个圆钉 (RoundPeg), 其半径等于方钉 (SquarePeg) 横截面对角线的一半 (即能够容纳方钉的最小外接圆的半径),下面给出模式结构和伪代码。
伪代码如下:
//假设你有两个接口相互兼容的类:圆孔(RoundHole)和圆钉(RoundPeg)。
class RoundHole is
constructor RoundHole(radius) { ... }
method getRadius() is
// 返回孔的半径。
method fits(peg: RoundPeg) is
return this.getRadius() >= peg.radius()
class RoundPeg is
constructor RoundPeg(radius) { ... }
method getRadius() is
// 返回钉子的半径。
// 但还有一个不兼容的类:方钉(SquarePeg)。
class SquarePeg is
constructor SquarePeg(width) { ... }
method getWidth() is
// 返回方钉的宽度。
// 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适
// 配器对象作为圆钉。
class SquarePegAdapter extends RoundPeg is
// 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。
private field peg: SquarePeg
constructor SquarePegAdapter(peg: SquarePeg) is
this.peg = peg
method getRadius() is
// 适配器会假扮为一个圆钉,
// 其半径刚好能与适配器实际封装的方钉搭配起来。
return peg.getWidth() * Math.sqrt(2) / 2
// 客户端代码中的某个位置。
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true
small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // 此处无法编译(类型不一致)。
small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false
适用场景
- 当你希望使用某个类,但是其接口与其他代码不兼容时,可以使用适配器类。
- 适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、第三方类或提供怪异接口的类之间的转换器。
- 当你希望复用多个由于缺少相同功能而无法被添加到超类的已有子类时,可以使用该模式。
- 你可以扩展每个子类,将缺少的功能添加到新的子类中。但是,你必须在所有新子类中重复添加这些代码。
- 将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。然后你可以将缺少功能的对象封装在适配器中,从而动态地获取所需功能。如要这一点正常运作,目标类必须要有通用接口,适配器的成员变量应当遵循该通用接口。这种方式同装饰模式非常相似。
适配器模式优缺点
- 单一职责原则。你可以将接口或数据转换代码从程序主要业务逻辑中分离。
- 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
- 代码整体复杂度增加。因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。
用到的多态机制
SquarePegAdapter(适配器)继承了的RoundPeg(圆钉),重写了父类的getRadius()方法,当父类指针指向子类时,调用getRadius()方法,此时会调用适配器的getRadius()方法,利用了多态机制。
明模块抽象封装的方法
适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。还以扩展每个子类, 将缺少的功能添加到新的子类中。
分析各个模块的内聚度和模块之间的耦合度
圆钉、方钉、圆孔有各自属性和方法,内聚度高,耦合度低,适配器继承圆孔,与圆孔耦合度高,与其他耦合度低。
桥接模式
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式。
使用场景
桥我们大家都熟悉,顾名思义就是用来将河的两岸联系起来的。而此处的桥是用来将两个独立的结构联系起来,而这两个被联系起来的结构可以独立的变化,所有其他的理解只要建立在这个层面上就会比较容易。
下面是一些官方的说明,比较晦涩,必须等你有一定的经验后才能理解: 1. 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
- “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
- 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
- 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
其实就是某个抽象类再去持有某些抽象类/接口(这样就会有多维度的变化啦)。具体可以用在支付上哦,因为支付的话我们一般有支付宝支付、微信支付、银联支付等,然后呢我们支付的时候还要选择指纹, 图案锁等验证,那么这个验证器我们就可以抽象出一个接口,让我们的支付抽象类去持有它。这样具体使用的时候就像是,Pay pay = new WeiXinPay(new ImageLockValidator());
pay.pay(); 这个时候就是可以有两个维度的变化哦。(WeiXinPay可以变化成AliPay,PayValidator选取你自己的想要的Validator即可)。如果要有更多维度的变化,就让抽象类持有更多的抽象接口即可。例如支付限额, 抽象类Pay增加持有新接口PayLimitation.
组合模式
概念
组合模式实际上就是将多个组件进行组合,让用户可以对它们进行一致性处理。比如我们的文件夹,一个文件夹中可以有很多个子文件夹或是文件:
代码实例
/**
* 首先创建一个组件抽象,组件可以包含组件,组件有自己的业务方法
*/
public abstract class Component {
public abstract void addComponent(Component component); //添加子组件
public abstract void removeComponent(Component component); //删除子组件
public abstract Component getChild(int index); //获取子组件
public abstract void test(); //执行对应的业务方法,比如修改文件名称
}
文件夹和文件分别对应一个实现类:
public class Directory extends Component{ //目录可以包含多个文件或目录
List<Component> child = new ArrayList<>(); //这里我们使用List来存放目录中的子组件
@Override
public void addComponent(Component component) {
child.add(component);
}
@Override
public void removeComponent(Component component) {
child.remove(component);
}
@Override
public Component getChild(int index) {
return child.get(index);
}
@Override
public void test() {
child.forEach(Component::test); //将继续调用所有子组件的test方法执行业务
}
}
public class File extends Component{ //文件就相当于是树叶,无法再继续添加子组件了
@Override
public void addComponent(Component component) {
throw new UnsupportedOperationException(); //不支持这些操作了
}
@Override
public void removeComponent(Component component) {
throw new UnsupportedOperationException();
}
@Override
public Component getChild(int index) {
throw new UnsupportedOperationException();
}
@Override
public void test() {
System.out.println("文件名称修改成功!"+this); //具体的名称修改操作
}
}
测试:
public static void main(String[] args) {
Directory outer = new Directory(); //新建一个外层目录
Directory inner = new Directory(); //新建一个内层目录
outer.addComponent(inner);
outer.addComponent(new File()); //在内层目录和外层目录都添加点文件,注意别导错包了
inner.addComponent(new File());
inner.addComponent(new File());
outer.test(); //开始执行文件名称修改操作
}
装饰模式
装饰模式是在不必改变原类和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象
使用场景
- 需要在运行时动态的给一个对象增加额外的职责时候
- 需要给一个现有的类增加职责,但是又不想通过继承的方式来实现的时候(应该优先使用组合而非继承),或者通过继承的方式不现实的时候(可能由于排列组合产生类爆炸的问题)。
秒懂设计模式之装饰者模式(Decorator Pattern) - 知乎 (zhihu.com)
关于装饰模式和代理模式的区别: Java中“装饰模式”和“代理模式”有啥区别 • Worktile社区
代理模式
我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
Java 代理模式详解 | JavaGuide(Java面试 + 学习指南)
外观模式
提供一个高层次的接口,使得子系统更易于使用. 例如实现的快捷工具栏, 提供给前端的接口就一个, 但却集成了若干接口的功能.
享元模式
允许使用对象共享来有效地支持大量细粒度对象
从上图看,享元模式由3个部分组成
- Flyweight
享元接口,定义所有对象共享的操作
- ConcreteFlyweight
具体的要被共享的对象,其一般是一个不可变类,内部只保存需要共享的内部状态,它可能不止一个。
- FlyweightFactory
负责给客户端提供共享对象
其实经典的享元模式还有一个UnshareConcreteFlyweight
的角色,它也实现Flyweight
接口。它的作用是当你不需要共享对象时,但是又需要以统一接口处理此对象,就可以添加这个角色。
在举例前我们有必要牢记一个非常重要的概念:
共享对象的状态分为内部状态与外部状态,内部状态在各个对象间共享,而外部状态由客户端传入,这一点一定要牢记。