结构型——适配器、装饰、外观

适配器、装饰、外观这三种设计模式本质都是在原有类的基础上进行“外观”上的改动,让原来的类看起来不一样。不过它们的意图是不同的:

  • 装饰器模式:不改变原有接口,添加新的功能;
  • 适配器模式:将一个接口转变成另一个接口,解决客户和功能组件之间接口不兼容的问题;
  • 外观模式:让接口更简单,将复杂系统的各种方法进行封装,让系统看起来更简单,方便客户使用。

下面,我们一起看看这三种模式是如何实现各自的意图的。

适配器模式

什么是适配器模式

适配器模式:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

适配器的类图定义如下:

如上图所示,适配器模式具有很好的OO设计原则,创建一个实现了目标接口的适配器类,通过组合方式,将目标接口的任务委托给被适配者执行。上面这种适配器模式称之为对象适配器模式,其实还有一种类适配器模式:
类适配器通过多重继承实现,由于Java不支持多重继承,因此只能使用对象适配器模式。

类结构型模式和对象结构型模式相比,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

适配器模式有什么用

在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,讲中文的人同讲英文的人对话时需要一个翻译,用直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。

在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题

适配器模式的优缺点如下:

  • 优点
    • 客户端通过适配器可以透明地调用目标接口。
    • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
    • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
    • 在很多业务场景中符合开闭原则。
  • 缺点
    • 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
    • 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。

适配器模式通常适用于:

  • 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
  • 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。

适配器模式的应用

适配器模式的实现

这里简单实现了对象适配器模式,至于类适配器实现代码类似,只不过通过继承被适配者来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//对象适配器类
class ObjectAdapter implements Target
{
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee)
{
this.adaptee=adaptee;
}
public void request()
{
adaptee.specificRequest();
}
}
//客户端代码
public class ObjectAdapterTest
{
public static void main(String[] args)
{
System.out.println("对象适配器模式测试:");
Adaptee adaptee = new Adaptee();
Target target = new ObjectAdapter(adaptee);
target.request();
}
}

实战演练

早期Java集合类型都实现了一个名为elements()的方法,该方法返回一个Enumeration对象。这个Enumeration接口可以逐个遍历集合中每个元素,而无需知道它们在集合内是如何被管理的。

在新版本的Java中,开始使用Iterator迭代器接口,这个接口和枚举接口很像,都可以用来遍历集合中每个元素,但不同的是,迭代器还提供了删除元素的功能。

面对历史遗留代码,这些代码可能暴露出枚举接口,但是希望在新的代码中只使用迭代器,如何利用适配器模式解决这个问题呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class EnumerationIterator implements Iterator{//适配器实现目标接口
Enumeration enum;//组合方式,和被适配者关联
public EnumerationIterator(Enumeration enum){
this.enum = enum;
}
public boolean hasNext(){
return enum.hasMoreElements();
}
public Object next(){
return enum.nextElement();
}
public void remove(){
//原有的枚举不支持remove()方法,因此需要抛出异常
throw new UnsupportedOperationException();
}
}
#### 适配器在JDK中的应用
  1. java.util.Arrays中的asList()方法
  2. java.io.InputStreamReader(InputStream) ,java.io.OutputStreamWriter(OutputStream)实现字符流和字节流的转换
  3. javax.xml.bind.annotation.adapters.XmlAdapter#marshal(),javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal()
  4. JUC中FutureTask类也用到了适配器模式,RunnableAdapter将一个Runnable封装成Callable。

适配器在Spring中的应用

这部分转自:http://c.biancheng.net/view/8447.html

  1. Spring AOP

    在 Spring 的 Aop 中,适配器模式应用的非常广泛。Spring 使用 Advice(通知)来增强被代理类的功能,Advice 的类型主要有 BeforeAdvice、AfterReturningAdvice、ThrowsAdvice。每种 Advice 都有对应的拦截器,即 MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor、ThrowsAdviceInterceptor。

    各种不同类型的 Interceptor,通过适配器统一对外提供接口,如下类图所示:client —> target —> adapter —> interceptor —> advice。最终调用不同的 advice来实现被代理类的增强。

  2. Spring MVC

    具体可以看原文。

外观模式

概述

外观模式,简单来说就是:把原来的若干个子系统重新封装一下,定义一个高层接口,客户端通过它来访问各个子系统的功能。

其目的在于:简化接口,对原来复杂的系统进行封装,暴露简单的接口供用户使用(因为用户往往不关注你的具体实现,实现一个功能越简单越好)。因此外观模式遵从最少知识原则,即减少对象之间的交互,只和密友谈话。下图展示了外观模式的简易类图:

外观模式的优点就是简化接口,方便系统使用,降低了客户端和子系统之间的耦合性,提升了子系统的可维护性。但是它也有缺点: 在外观模式中,当增加或移除子系统时需要修改外观类,这**违背了“开闭原则”**。 如果引入抽象外观类,则在一定程度上解决了该问题。

外观模式的应用

应用示例

观看电影需要操作很多电器,使用外观模式实现一键看电影功能。

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
public class SubSystem {
public void turnOnTV() {
System.out.println("turnOnTV()");
}

public void setCD(String cd) {
System.out.println("setCD( " + cd + " )");
}

public void starWatching(){
System.out.println("starWatching()");
}
}

public class Facade {
private SubSystem subSystem = new SubSystem();

public void watchMovie() {
subSystem.turnOnTV();
subSystem.setCD("a movie");
subSystem.starWatching();
}
}

public class Client {
public static void main(String[] args) {
Facade facade = new Facade();
facade.watchMovie();
}
}

JDK中的应用

在 JDK 中 java.lang.Classjava.util.logging.LogManager 都使用了外观模式。java.util.logging.LogManager 是外观角色,其它都是子系统角色 :

#### Spring中的应用

Spring的jdbc模块中的JdbcUtils类使用了外观模式,该工具类主要对原生的jdbc进行了封装。

装饰器模式

装饰器模式概述

什么是装饰器模式

装饰器模式指:在不改变原有对象的情况下,给对象动态添加功能。听起来有点抽象,后面可以通过实例,加深理解。

给对象添加功能,也就是要扩展一个类,为什么不用继承方式呢? 继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。 如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰器模式的目标。

装饰器模式主要包含以下角色。

  1. 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
  2. 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
  3. 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  4. 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

装饰器模式的结构图下图所示:

#### 装饰器模式优缺点

装饰器模式的优缺点可总结如下:

  • 优点
    • 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用;
    • 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果;
    • 装饰器模式完全遵守开闭原则。
  • 缺点
    • 装饰器模式会增加许多子类,过度使用会增加程序得复杂性。

装饰器模式的应用

应用场景

装饰器模式目的在于给类动态添加功能,主要用于以下场景:

  • 无法继承:当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时。例如,该类被隐藏或者该类是终极类或者采用继承方式会产生大量的子类。
  • 组合过多:当需要通过对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰器模式却很好实现。
  • 动态添加:当对象的功能要求可以动态地添加,也可以再动态地撤销时。

装饰器模式实现“饮料价格计算”

下面看一个“饮料”示例:

设计不同种类的饮料,饮料可以添加各种不同配料,并且支持动态添加新配料。每增加一种配料,该饮料的加个就要重新计算,要求计算每种饮料的价格。

下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。

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
48
49
50
51
52
53
54
55
56
//饮料接口
public interface Beverage {
double cost();
}
//具体饮料类,实现饮料接口
public class DarkRoast implements Beverage {
@Override
public double cost() {
return 1;
}
}
//具体饮料类,实现饮料接口
public class HouseBlend implements Beverage {
@Override
public double cost() {
return 1;
}
}
//抽象装饰器类,也实现了饮料接口
public abstract class CondimentDecorator implements Beverage {
//组合方式,拥有饮料对象
protected Beverage beverage;
}
//具体装饰类,继承了抽象装饰类
public class Milk extends CondimentDecorator {

public Milk(Beverage beverage) {
this.beverage = beverage;
}

@Override
public double cost() {
return 1 + beverage.cost();
}
}
//具体装饰类,继承了抽象装饰类
public class Mocha extends CondimentDecorator {

public Mocha(Beverage beverage) {
this.beverage = beverage;
}

@Override
public double cost() {
return 1 + beverage.cost();
}
}
//客户调用方式
public class Client {
public static void main(String[] args) {
Beverage beverage = new HouseBlend();//新建一个基本饮料
beverage = new Mocha(beverage);//添加摩卡
beverage = new Milk(beverage);//添加牛奶
System.out.println(beverage.cost());//计算价格
}
}

装饰器模式实现“游戏人物变身”

在《恶魔战士》中,游戏角色“莫莉卡·安斯兰”的原身是一个可爱少女,但当她变身时,会变成头顶及背部延伸出蝙蝠状飞翼的女妖,当然她还可以变为穿着漂亮外衣的少女。这些都可用装饰器模式来实现,在本实例中的“莫莉卡”原身有 setImage(String t) 方法决定其显示方式,而其 变身“蝙蝠状女妖”和“着装少女”可以用 setChanger() 方法来改变其外观,原身与变身后的效果用 display() 方法来显示(点此下载其原身和变身后的图片),下图是其结构图。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package decorator;

import java.awt.*;
import javax.swing.*;

public class MorriganAensland {
public static void main(String[] args) {
Morrigan m0 = new original();
m0.display();
Morrigan m1 = new Succubus(m0);
m1.display();
Morrigan m2 = new Girl(m0);
m2.display();
}
}

//抽象构件角色:莫莉卡
interface Morrigan {
public void display();
}

//具体构件角色:原身
class original extends JFrame implements Morrigan {
private static final long serialVersionUID = 1L;
private String t = "Morrigan0.jpg";

public original() {
super("《恶魔战士》中的莫莉卡·安斯兰");
}

public void setImage(String t) {
this.t = t;
}

public void display() {
this.setLayout(new FlowLayout());
JLabel l1 = new JLabel(new ImageIcon("src/decorator/" + t));
this.add(l1);
this.pack();
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
}

//抽象装饰角色:变形
class Changer implements Morrigan {
Morrigan m;

public Changer(Morrigan m) {
this.m = m;
}

public void display() {
m.display();
}
}

//具体装饰角色:女妖
class Succubus extends Changer {
public Succubus(Morrigan m) {
super(m);
}

public void display() {
setChanger();
super.display();
}

public void setChanger() {
((original) super.m).setImage("Morrigan1.jpg");
}
}

//具体装饰角色:少女
class Girl extends Changer {
public Girl(Morrigan m) {
super(m);
}

public void display() {
setChanger();
super.display();
}

public void setChanger() {
((original) super.m).setImage("Morrigan2.jpg");
}
}

装饰器模式JDK中的应用

  1. Java I/O标准库。 例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是装饰类。

    下面代码是为 FileReader 增加缓冲区而采用的装饰类 BufferedReader 的例子:

    1
    2
    BufferedReader in = new BufferedReader(new FileReader("filename.txt"));
    String s = in.readLine();
  2. java.util.Collections#synchronizedList(List)

装饰器模式Spring中的应用

  1. TransactionAwareCacheDecorator 类相当于装饰器模式中的抽象装饰角色,主要用来处理事务缓存。
  2. MVC 中的装饰器模式:HttpHeadResponseDecorator 类,相当于装饰器模式中的具体装饰角色。

参考资料

  1. https://pdai.tech/md/dev-spec/pattern/9_adapter.html
  2. http://c.biancheng.net/view/1361.html
  3. http://c.biancheng.net/view/8447.html
  4. http://c.biancheng.net/view/1369.html
  5. https://pdai.tech/md/dev-spec/pattern/8_facade.html
  6. https://pdai.tech/md/dev-spec/pattern/12_decorator.html
  7. http://c.biancheng.net/view/1366.html
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信