0%

java-设计模式(三)

结构型模式

核心作用:是从程序的结构上实现松耦合,从而可以扩大整体的类结构,用来解决更大的问题。

分类:

  • 适配器模式、代理模式、桥接模式、 装饰模式、组合模式、外观模式、享元模式
代理模式 为真实对象提供一个代理,从而控制对真实对象的访问
适配模式 使原本由于接口不兼容不能一起工作的类可以一起工作
桥接模式 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立的扩展在抽象层建立关联。
组合模式 将对象组合成树状结构以表示”部分和整体”层次结构,使得客户可以统一的调用叶子对象和容器对象
装饰模式 动态地给一个对象添加额外的功能,比继承灵活
外观模式 为子系统提供统一的调用接口,使得子系统更加容易使用
享元模式 运用共享技术有效的实现管理大量细粒度对象,节省内存,提高效率

五、代理模式(proxy)

控制对其它对象的访问。

核心作用:

控制对其他对象的访问。

核心角色:

  • 抽象角色:定义代理角色和真实角色的公共给对外方法

  • 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用

    关注真正的业务逻辑!

  • 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。

    将统一的流程控制放到代理角色中处理!

开发框架中应用场景:

  • struts2中拦截器的实现
  • 数据库连接池关闭处理
  • Hibernate中延时加载的实现
  • mybatis中实现拦截器插件
  • AspectJ的实现
  • spring中AOP的实现
    • 日志拦截
    • 声明式事务处理
  • web service
  • RMI远程方法调用

包含四类:

  • 远程代理(remote proxy):控制对源程对象不同地址空间)的访问。它负责将请求及其参数进行编码,并向不同地址空间中的对象发送已经编码的请求。

  • 虚拟代理(Virtual Proxy):根据需要创建开销很大的对象,它可以缓存实体的附加信息,以便延迟对它的访问,例如在网站加载一个很大图片时,不能马上完成,可以用虚拟代理缓存图片的大小信息,然后生成一张临时图片代替原始图片。

  • 保护代理(Protection Proxy):按权限控制对象的访问,它负责检查调用者是否具有实现一个请求所必须的访问权限。

  • 智能代理(Smart Reference):取代了简单的指针,它在访问对象时执行一些附加操作:记录对象的引用次数;当第一次引用一个对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。


例子:

以下是一个虚拟代理的实现,模拟了图片延迟加载的情况下使用与图片大小相等的临时内容去替换原始图片,直到图片加载完成才将图片显示出来。

1
2
3
public interface Image {
void showImage();
}
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
public class HighResolutionImage implements Image {

private URL imageURL;
private long startTime;
private int height;
private int width;

public int getHeight() {
return height;
}

public int getWidth() {
return width;
}

public HighResolutionImage(URL imageURL) {
this.imageURL = imageURL;
this.startTime = System.currentTimeMillis();
this.width = 600;
this.height = 600;
}

public boolean isLoad() {
// 模拟图片加载,延迟 3s 加载完成
long endTime = System.currentTimeMillis();
return endTime - startTime > 3000;
}

@Override
public void showImage() {
System.out.println("Real Image: " + imageURL);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ImageProxy implements Image {

private HighResolutionImage highResolutionImage;

public ImageProxy(HighResolutionImage highResolutionImage) {
this.highResolutionImage = highResolutionImage;
}

@Override
public void showImage() {
while (!highResolutionImage.isLoad()) {
try {
System.out.println("Temp Image: " + highResolutionImage.getWidth() + " " + highResolutionImage.getHeight());
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
highResolutionImage.showImage();
}
}
1
2
3
4
5
6
7
8
9
10
public class ImageViewer {

public static void main(String[] args) throws Exception {
String image = "http://image.jpg";
URL url = new URL(image);
HighResolutionImage highResolutionImage = new HighResolutionImage(url);
ImageProxy imageProxy = new ImageProxy(highResolutionImage);
imageProxy.showImage();
}
}

面向切面编程AOP介绍

AOP(Aspect-Oriented Programming,面向切面的编程)

  • 它是可以通过预编译方式和运行期动态代理实现在不修改源代码的情 况下给程序动态统一添加功能的一种技术。它是一种新的方法论,它是对传统OOP编程的一种补充。

常用术语:

  • 切面(Aspect):其实就是共有功能的实现。

  • 通知(Advice):是切面的具体实现。

  • 连接点(Joinpoint):就是程序在运行过程中能够插入切面的地点。

  • 切入点(Pointcut):用于定义通知应该切入到哪些连接点上。

  • 目标对象(Target):就是那些即将切入切面的对象,也就是那些被通知的对象

  • 代理对象(Proxy):将通知应用到目标对象之后被动态创建的对象。

  • 织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。

开源的AOP框架:

  • Aspect

六、适配器模式(Adapter)

把一个类接口转换成另一个用户需要的接口。

使用场景

  • 系统需要使用现有的类,而这些类的接口不符合系统的需要。
  • 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
  • 需要一个统一的输出接口,而输入端的类型不可预知。

工作中的场景

  • 经常用来做旧系统改造和升级

  • 如果我们的系统开发之后再也不需要维护,那么很多模式都是没必要的,但是不幸的是,事实却是维护一个系统的代价往往是开发一个系统的数倍。

我们学习中见过的场景

  • java.io.InputStreamReader(InputStream)
  • java.io.OutputStreamWriter(OutputStream)

类适配器模式

一句话描述:Adapter类,通过继承需要被适配的类,实现我们想要的类接口,完成src->dst的适配。

我们现有的src类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 介绍:src类: 我们有的220V电压
* 作者:zhangxutong
* 邮箱:zhangxutong@imcoming.com
* 时间: 2016/10/18.
*/

public class Voltage220 {
public int output220V() {
int src = 220;
System.out.println("我是" + src + "V");
return src;
}
}

我们想要的dst接口:

1
2
3
4
5
6
7
8
9
10
/**
* 介绍:dst接口:客户需要的5V电压
* 作者:zhangxutong
* 邮箱:zhangxutong@imcoming.com
* 时间: 2016/10/18.
*/

public interface Voltage5 {
int output5V();
}

适配器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 介绍:Adapter类:完成220V-5V的转变
* 通过继承src类,实现 dst 类接口,完成src->dst的适配。
* 作者:zhangxutong
* 邮箱:zhangxutong@imcoming.com
* 时间: 2016/10/18.
*/

public class VoltageAdapter extends Voltage220 implements Voltage5 {
@Override
public int output5V() {
int src = output220V();
System.out.println("适配器工作开始适配电压");
int dst = src / 44;
System.out.println("适配完成后输出电压:" + dst);
return dst;
}
}

对象的适配器模式(常用)

基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承src类,而是持有src类的实例,以解决兼容性的问题。
即:持有 src类,实现 dst 类接口,完成src->dst的适配。
(根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是对象结构型模式。)

Adapter类如下:

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
/**
* 介绍:对象适配器模式:
* 持有 src类,实现 dst 类接口,完成src->dst的适配。 。以达到解决**兼容性**的问题。
* 作者:zhangxutong
* 邮箱:zhangxutong@imcoming.com
* 时间: 2016/10/18.
*/

public class VoltageAdapter2 implements Voltage5 {
private Voltage220 mVoltage220;

public VoltageAdapter2(Voltage220 voltage220) {
mVoltage220 = voltage220;
}

@Override
public int output5V() {
int dst = 0;
if (null != mVoltage220) {
int src = mVoltage220.output220V();
System.out.println("对象适配器工作,开始适配电压");
dst = src / 44;
System.out.println("适配完成后输出电压:" + dst);
}
return dst;
}
}

对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。
根据合成复用原则,组合大于继承,
所以它解决了类适配器必须继承src的局限性问题,也不再强求dst必须是接口。
同样的它使用成本更低,更灵活。

(和装饰者模式初学时可能会弄混,这里要搞清,装饰者是对src的装饰,使用者毫无察觉到src已经被装饰了(使用者用法不变)。 这里对象适配以后,使用者的用法还是变的。
即,装饰者用法: setSrc->setSrc,对象适配器用法:setSrc->setAdapter.)

接口的适配器模式

定义:

当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。

七、桥接模式(Bridge)

将抽象与实现分离开来,使得它们可以独立变化。

核心要点

  • 处理多层继承结构,处理多维度变化的场景,将各个维度设计成独立的继承结构,使各个维度可以独立的扩展在抽象层建立关联。

效果及实现要点

  1. 桥接模式使用对象见的组合关系解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。
  2. 所谓抽象和实现沿着各自维度的变化,即“子类化”它们,得到各个子类之后,便可以任意它们,从而获得不同路上的不同其次。
  3. 桥接模式有时候类似于多继承方案,但是多继承方案往往违背了SRP原则,复用性较差。桥接模式是比继承方案更好的解决方法。
  4. 桥接模式的应用一般在“两个非常强的变化维度”,有时候即使有两个变化的维度,但是某个方向的变化维度并不剧烈——换而言之两个变化不会导致纵横交错的结果,并不一定要使用桥接模式。

使用场景

  1. 如果你不希望在抽象和实现部分采用固定的绑定关系,可以采用桥接模式,来把抽象和实现部分分开,然后在程序运行期间来动态的设置抽象部分需要用到的具体的实现,还可以动态切换具体的实现。
  2. 如果出现抽象部分和实现部分都应该可以扩展的情况,可以采用桥接模式,让抽象部分和实现部分可以独立的变化,从而可以灵活的进行单独扩展,而不是搅在一起,扩展一边会影响到另一边。
  3. 如果希望实现部分的修改,不会对客户产生影响,可以采用桥接模式,客户是面向抽象的接口在运行,实现部分的修改,可以独立于抽象部分,也就不会对客户产生影响了,也可以说对客户是透明的。
  4. 如果采用继承的实现方案,会导致产生很多子类,对于这种情况,可以考虑采用桥接模式,分析功能变化的原因,看看是否能分离成不同的纬度,然后通过桥接模式来分离它们,从而减少子类的数目。

实际开发中应用场景

  • JDBC驱动程序
    • AWT中的Peer架构
  • 银行日志管理:
    • 格式分类:操作日志、交易日志、异常日志
    • 距离分类:本地记录日志、异地记录日志
  • 人力资源系统中的奖金计算模块:
    • 奖金分类:个人奖金、团体奖金、激励奖金。
    • 部门分类:人事部门、销售部门、研发部门。
  • OA系统中的消息处理:
    • 业务类型:普通消息、加急消息、特急消息
    • 发送消息方式:系统内消息、手机短信、邮件

八、组合模式(composite)

将对象组合成树形结构来表示“整体/部分”层次关系,允许用户以相同的方式处理单独对象和组合对象。

组合模式核心:

  • 组合部件(Component):它是一个抽象角色,为要组合的对象提供统一的接口。
  • 叶子(Leaf):在组合中表示子节点对象,叶子节点不能有子节点。
  • 合成部件(Composite):定义有枝节点的行为,用来存储部件,实现在Component接口中的有关操作,如增加(Add)和删除(Remove)

组件(Component)类是组合类(Composite)和叶子类(Leaf)的父类,可以把组合类看成是树的中间节点。

组合对象拥有一个或者多个组件对象,因此组合对象的操作可以委托给组件对象去处理,而组件对象可以是另一个组合对象或者叶子对象。

透明模式

在Component中声明所有来管理子对象的方法,其中包括Add,Remove等。这样实现Component接口的所有子类都具备了Add和Remove方法。这样做的好处是叶节点和枝节点对于外界没有区别,它们具备完全一致的接口。

component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Component {

protected String name;

public Component(String name) {
this.name = name;
}

//增加一个叶子构件或树枝构件
public abstract void add(Component component);

//删除一个叶子构件或树枝构件
public abstract void remove(Component component);

//获取分支下的所有叶子构件和树枝构件
public abstract void display(int depth);

}

composite:

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
public class Composite extends Component {

public Composite(String name) {
super(name);
}

//构建容器
private ArrayList<Component> componentArrayList = new ArrayList<Component>();

@Override
public void add(Component component) {
this.componentArrayList.add(component);
}

@Override
public void remove(Component component) {
this.componentArrayList.remove(component);
}

@Override
public void display(int depth) {
//输出树形结构
for(int i=0; i<depth; i++) {
System.out.print('-');
}
System.out.println(name);

//下级遍历
for (Component component : componentArrayList) {
component.display(depth + 1);
}
}

}

leaf:

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
public class Leaf extends Component {

public Leaf(String name) {
super(name);
}

@Override
public void add(Component component) {
//空实现,抛出“不支持请求”异常
throw new UnsupportedOperationException();
}

@Override
public void remove(Component component) {
//空实现,抛出“不支持请求”异常
throw new UnsupportedOperationException();
}

@Override
public void display(int depth) {
//输出树形结构的叶子节点
for(int i=0; i<depth; i++) {
System.out.print('-');
}
System.out.println(name);
}

}

弊端:客户端对叶节点和枝节点是一致的,但叶节点并不具备Add和Remove的功能,因而对它们的实现是没有意义的

安全模式

安全模式是把树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方法比较安全。

component:

这里相比透明模式就少了add()和romove()抽象方法的声明。

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Component {

protected String name;

public Component(String name) {
this.name = name;
}

//获取分支下的所有叶子构件和树枝构件
public abstract void display(int depth);

}

composite:

这里add()和remove()方法的实现就从继承变为了自己实现。

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
public class Composite extends Component {

public Composite(String name) {
super(name);
}

//构建容器
private ArrayList<Component> componentArrayList = new ArrayList<Component>();

//增加一个叶子构件或树枝构件
public void add(Component component) {
this.componentArrayList.add(component);
}

//删除一个叶子构件或树枝构件
public void remove(Component component) {
this.componentArrayList.remove(component);
}

@Override
public void display(int depth) {
//输出树形结构
for(int i=0; i<depth; i++) {
System.out.print('-');
}
System.out.println(name);

//下级遍历
for (Component component : componentArrayList) {
component.display(depth + 1);
}
}

}

leaf:

叶子节点中没有了空实现,比较安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Leaf extends Component {

public Leaf(String name) {
super(name);
}

@Override
public void display(int depth) {
//输出树形结构的叶子节点
for(int i=0; i<depth; i++) {
System.out.print('-');
}
System.out.println(name);
}

}

组合模式的应用

  1. 何时使用

  • 想表达“部分-整体”层次结构(树形结构)时
  • 希望用户忽略组合对象与单个对象的不同,用户将统一的使用组合结构中的所有对象

  2. 方法

  • 树枝和叶子实现统一接口,树枝内部组合该接口

  3. 优点

  • 高层模块调用简单。一棵树形机构中的所有节点都是Component,局部和整体对调用者来说没有任何区别,高层模块不必关心自己处理的是单个对象还是整个组合结构。
  • 节点自由增加

  4. 缺点

  • 使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒转原则

  5. 使用场景

  • 维护和展示部分-整体关系的场景(如树形菜单、文件和文件夹管理)
  • 从一个整体中能够独立出部分模块或功能的场景

  6. 应用实例

  • Swing中,Button、Checkbox等组件都是树叶,而Container容器是树枝
  • 文本编辑时,可以单个字编辑,也可以整段编辑,还可以全文编辑
  • 文件复制时,可以一个一个文件复制,也可以整个文件夹复制

参考文章

简说设计模式——组合模式 - JAdam - 博客园 (cnblogs.com)