3. 结构型设计模式

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合组合对象。

由于组合关系或聚合关系比较继承关系耦合度低,满足“合成复合原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式分为以下 7 种:

1)代理(Proxy)模式:为某对象提供一种代理以控制对象的访问。即客户端通过代理简介地访问该对象,从而限制、增强或修改该对象的一些特征。

2)适配器(Adapter)模式:将一个类的接口转换成希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

3)桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。

4)装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。

5)外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。

6)享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。

3.1 代理(Proxy)模式


在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点购买。又如找女朋友、找保姆、找工作都可以通过中介完成。

在软件设计中,使用代理模式的例子很多,如,要访问原型对象比较大(如视频或者大图像等),其下载要花很多时间。还有因为安全需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。

3.1.1 代理模式的定义与特点

代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不合适或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

代理模式的主要优点:

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理模式可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度。

其主要缺点是:

  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
  • 增加了系统的复杂度。

3.1.2 代理模式的结构与实现

代理模式的结构比较简单,只要是通过定义一个集成抽象主题的代理来包含真实主题,从而实现对真实主题访问,下面来分析基本结构和实现方法。

(1)模式的结构

代理模式的主要角色如下:

1)抽象主题类:通过接口或抽象类声明真实主题或代理对象实现的业务方法。

2)真实主题类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要 引用的对象。

3)代理类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

其结构图如图3-1所以:

图3-1 代理模式的结构图

(2)模式的实现

代理模式的实现代码如下:

1 // 抽象主题
2 interface Subject {
3     void request();
4 }

[](javascript:void(0)😉

1 // 真实主题
2 class RealSubject implements Subject {
3     public void request() {
4         System.out.println("访问真实主题方法...");
5     }
6 }
 1 // 代理
 2 class Proxy implements Subject {
 3     private RealSubject realSubject;
 4     public void request() {
 5         if (realSubject == null) {
 6             realSubject = new RealSubject();
 7         }
 8         preRequest();
 9         realSubject.request();
10         postRequest();
11     }
12     public void preRequest() {
13         System.out.println("访问真实主题之前的预处理。");
14     }
15     public void postRequest() {
16         System.out.println("访问真实主题之后的后续处理。");
17     }
18 }
1 public class ProxyTest {
2     public static void main(String[] args) {
3         Proxy proxy = new Proxy();
4         proxy.Request();
5     }
6 }

程序运行的结果如下:

访问真实主题之前的预处理。
访问真实主题方法...
访问真实主题之后的后续处理。

3.1.3 代理模式的应用场景

前面分析了代理模式的结构与特点,现在分析以下的应用 场景 。

● 远程代理,这种方法通常是为了隐藏目标对象目标存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。

*●* 虚拟代理,这种方式通常用于要创建的目标对象开销很大。例如,下载一个很大的图片需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。

*●* 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。

*●* 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。

*●* 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。

3.1.4 代理模式的扩展

在前面介绍的代理模式中,代理类中包含了对真实主题引用,这种方式存在两个缺点。

1)真实主题与代理主题一一对应,增加真实主题也要增加代理。

2)设计代理以前真实主题必须事先存在,不太灵活。采用动态代理模式可以解决以上问题,如 SpringAOP,其结构图如图3-2所示。

3.2 适配器(Adapter)模式


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

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

3.2.1 模式的定义与特点

适配器模式的定义如下:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起功能的那些类能一起工作。适配器模式分为类结构型模式对象结构型模式两种,前者之间的耦合度比后者高,其要求程序员了解现有组件库中的相关的内部结构,所以应对相对较少些。

该模式的主要优点如下:

● 客户端通过适配器可以透明地调用目标接口。

● 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。

● 将目标和适配这类解耦,解决了目标类和适配类接口不一致问题。

缺点是:

对类适配器来说,更换适配器的实现过程比较复杂。

3.2.2 模式的结构与实现

类适配器模式可采用充多充继承方式实现,如 C++ 可以定义一个适配器类来同时继承房钱系统的业务接口和现有组件库中已经存在的组件接口;Java 不支持多继承,但可以定义一个适配器来来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。

对象适配器模式可采用将现有组件库中已经实现的组件引入适配器类,该类同时实现当前系统的业务接口。现在来介绍他们的基本结构。

(1)模式的结构

适配器模式包含以下主要角色。

1)目标接口:当前系统业务所期待的接口,它可以是抽象类或接口。

2)适配类:它是被访问和适配的现存组件库中的组件接口。

3)适配器类:它是一个转换器,通过继承或引用适配者的对象,把适配器接口转换成目标接口,让客户目标接口的可是访问适配者。

类适配器模式的结构图,如图3-2所示:

3-2 类适配器模式的结构图

对象适配器模式的结构图,如图3-3所示:

图3-3对象适配器模式的结构图

(2)模式的实现

1 // 目标接口
2 interface Target {
3     public void request();
4 }
① 类适配器模式的代码如下:
 1 // 适配者接口
 2 class Adaptee {
 3     public void specificRequest() {       
 4         System.out.println("适配者中的业务代码被调用!");
 5     }
 6 }
 7 
 8 // 类适配器类
 9 class ClassAdapter extends Adaptee implements Target {
10     public void request() {
11         specificRequest();
12     }
13 }
1 //客户端代码
2 public class ClassAdapterTest {
3     public static void main(String[] args) {
4         System.out.println("类适配器模式测试:");
5         Target target = new ClassAdapter();
6         target.request();
7     }
8 }

程序运行结果如下:

类适配器模式测试:
适配者中的业务代码被调用!
② 对象适配器模式的代码如下:
 1 // 对象适配器类
 2 class ObjectAdapter implements Target {
 3     private Adaptee adaptee;
 4     public ObjectAdapter(Adaptee adaptee) {
 5         this.adaptee = adaptee;
 6     }
 7     public void request() {
 8         adaptee.specificRequest();
 9     }
10 }
1 // 客户端代码
2 public class ObjectAdapterTest {
3     public static void main(String[] args) {
4         System.out.println("对象适配器模式测试:");
5         Adaptee adaptee = new Adaptee();
6         Target target = new ObjectAdapter(adaptee);
7         target.request();
8     }
9 }

程序运行结果如下:

对象适配器模式测试:
适配者中的业务代码被调用!

说明:对象适配器模式中的“目标接口”和“适配者类”的代码同类适配器模式一样,只要修改适配器类和客户端的代码即可。

3.2.3 模式的应用实例

【例】用适配器模式(Adapter)模式新能源汽车的发动机。

分析:新能源汽车的发动机有电能发动机和光能发动机等,各种发动机的驱动,例如,电能发动机的驱动方法 electricDrive() 是用电能驱动,而光能发动机的驱动方法 opticalDrice() 用光能驱动,它们是适配器模式中被访问的适配器。

客户端希望用统一的发动机驱动方法 drive() 访问这两种发动机,所以必须定义一个统一的目标接口 Motor,然后再定义电能适配器(Electric Adapter)和光能适配器(Optical Adapter)去适配这两种发动机。结构图如图3-4所示。

图3-4 发动机适配器的结构图

程序代码如下:

 1 // 目标:发动机
 2 interface Motor {
 3     public void drive();
 4 }
 5 
 6 // 适配者1:电能发动机
 7 class ElectricMotor {
 8     public void electricDrive()
 9     {
10         System.out.println("电能发动机驱动汽车!");
11     }
12 }
13 
14 // 适配者2:光能发动机
15 class OpticalMotor {
16     public void opticalDrive() {
17         System.out.println("光能发动机驱动汽车!");
18     }
19 }
20 
21 // 电能适配器
22 class ElectricAdapter implements Motor {
23     private ElectricMotor emotor;
24     public ElectricAdapter() {
25         emotor = new ElectricMotor();
26     }
27     public void drive() {
28         emotor.electricDrive();
29     }
30 }
31 
32 // 光能适配器
33 class OpticalAdapter implements Motor {
34     private OpticalMotor omotor;
35     public OpticalAdapter() {
36         omotor = new OpticalMotor();
37     }
38     public void drive() {
39         omotor.opticalDrive();
40     }
41 }
1 public class MotorAdapterTest {
2     public static void main(String[] args) {
3         Motor mEle = new ElectricAdapter();
4         mEle.drive();
5         
6         Motor mOpt =  new OpticalAdapter();
7         mOpt.drive();
8     }
9 }

程序运行结果:

电能发动机驱动汽车!
光能发动机驱动汽车!

3.2.4 模式的应用场景

适配器模式(Adapter)通常适用于以下场景:

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

3.4.5 模式的扩展

适配器模式可扩展为双向适配器模式,双向适配器类既可以把适配者接口转换成目标接口,也可以吧目标接口转换成适配者接口,其结构图如图3-5所示。

图3-5 双向适配器模式的结构图

程序代码如下:

 1 //目标接口
 2 interface TwoWayTarget {
 3     public void request();
 4 }
 5 
 6 //目标实现
 7 class TargetRealize implements TwoWayTarget {
 8     public void request() {       
 9         System.out.println("目标代码被调用!");
10     }
11 }
 1 // 适配者接口
 2 interface TwoWayAdaptee {
 3     public void specificRequest();
 4 }
 5 
 6 // 适配者实现
 7 class AdapteeRealize implements TwoWayAdaptee {
 8     public void specificRequest() {       
 9         System.out.println("适配者代码被调用!");
10     }
11 }
 1 // 双向适配器
 2 class TwoWayAdapter implements TwoWayTarget, TwoWayAdaptee {
 3     private TwoWayTarget target;
 4     public TwoWayAdapter(TwoWayTarget target) {
 5         this.target = target;
 6     }
 7     public void specificRequest() {       
 8         target.request();
 9     }
10 
11     private TwoWayAdaptee adaptee;
12     public TwoWayAdapter(TwoWayAdaptee adaptee) {
13         this.adaptee = adaptee;
14     }
15     public void request() {
16         adaptee.specificRequest();
17     }
18 }
 1 // 客户端代码
 2 public class TwoWayAdapterTest {
 3     public static void main(String[] args) {
 4 
 5         System.out.println("目标通过双向适配器访问适配者:");
 6         TwoWayAdaptee adaptee = new AdapteeRealize();
 7         TwoWayTarget target = new TwoWayAdapter(adaptee);
 8         target.request();
 9 
10         System.out.println("-------------------");
11 
12         System.out.println("适配者通过双向适配器访问目标:");
13         target = new TargetRealize();
14         adaptee = new TwoWayAdapter(target);
15         adaptee.specificRequest();
16     }
17 }

程序运行结果如下:

目标通过双向适配器访问适配者:
适配者代码被调用!
-------------------
适配者通过双向适配器访问目标:
目标代码被调用!

3.3 桥接(Bridge)模式


在现实生活中,默写具有两个或多个维度的变化,如图像即可按形状分,又可按颜色分。如果设计类似于 Photoshop 这样的软件,如何去画不同形状和不同颜色的图像呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。

当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车、不同性别和职业的男女。。如果用桥接模式就能很好地解决这些问题。

3.3.1 桥接模式的定义和特点

桥接模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

桥接模式的优点

● 由于抽象与实现分离,所以扩展能力强。

● 其实现细节对客户透明。

缺点是:由于聚合关系建立在抽象层,要求开发者针对抽象画进行设计与编程,这增加了系统的理解与设计难度。

3.3.2 桥接模式的结构与实现

可以将抽象化部分与实现化部分分来,取消二者的继承关系,改用组合关系。

(1)模式的结构

桥接模式包含以下主要角色:

1)抽象化角色:定义抽象类,并包含一个对实现化对象的引用。

2)扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。

3)实现化角色:定义是实现化角色的接口,供扩展抽象化角色调用。

4)具体实现化角色:给出实现化角色接口的具体实现。

其结构图如图3-6所示。

图3-6 桥接模式的结构图

3.3.3 桥接模式的应用实例

【例】用桥接(Bridge)模式模拟女士皮包的选购。

分析:女士皮包有很多种,可以按用途分,按皮质分、按品牌分、按颜色分、按大小分、存在读个维度的变化,所以采用桥接模式来实现女士皮包的选购比较合适。

本实例按用途分为:钱包(Wallet)和挎包(HandBag),按颜色分为:黄色(Yellow)和红色(Red)。可以按两个维度定义为颜色类和包类。

颜色类(Color)是一个维度,定义为实现化角色,它有两个具体实现化角色:黄色和红色,通过 getColor() 方法可以选择颜色。

堡垒(Bag)是另一个维度,定义抽象化角色,它有两个扩展抽象画角色:钱包和挎包,它包含了颜色类对象,通过 getName() 方法可以选择相关颜色的钱包和挎包。

图 3-7 女士皮包选的结构图

程序代码如下:

 1 // 实现化角色:颜色
 2 interface Color {
 3     String getColor();
 4 }
 5 
 6 // 具体实现化角色:黄色
 7 class Yellow implements Color {
 8     public String getColor() {
 9         return "yellow";
10     }
11 }
12 
13 // 具体实现化角色:红色
14 class Red implements Color {
15     public String getColor() {
16         return "red";
17     }
18 }
 1 // 抽象化角色:包
 2 abstract class Bag {
 3     protected Color color;
 4     public void setColor(Color color) {
 5         this.color = color;
 6     }   
 7     public abstract String getName();
 8 }
 9 
10 // 扩展抽象化角色:挎包
11 class HandBag extends Bag {
12     public String getName() {
13         return color.getColor() + "-HandBag";
14     }
15 }
16 
17 // 扩展抽象化角色:钱包
18 class Wallet extends Bag {
19     public String getName() {
20         return color.getColor() + "-Wallet";
21     }   
22 }
1 public class BridgeTest {
2     public static void main(String[] args) {
3         Color yellow = new Yellow();
4         Bag bag = new Wallet();
5         bag.setColor(yellow);
6         System.out.println(bag.getName());
7     }
8 }

程序运行结果:

yellow-Wallet

3.3.4 桥接模式的应用场景

桥接模式通常适用于以下场景:

  • 当一个类存在两个独立变化的维度,其这两个维度都需要进行扩展时。
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  • 当一个系统需要在结构的抽象画角色和具体化角色之间增加更多的灵活性时。

3.3.5 桥接模式的扩展

在软件开发中,有时桥接模式可以适配器模式联合使用。当桥接模式的实现化角色的接口与现有类的接口不一致时,可以二者中间定义一个适配器将二者连接起来,其具体结构图如图3-8所示。

图3-8 桥接模式与适配器模式联用的结构图

3.4 装饰(Decorator)模式


在现实生活中,常常需要对心有产品增加新的功能或美化其外观,如房子装修、相片加相框等。在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰模式来实现。

3.4.1 装饰模式的定义与特点

装饰模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。

装饰模式的主要优点有:

● 采用装饰模式扩展比采用继承方法更加灵活。

● 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。

其主要缺点是:装饰模式增加了许多子类,如果过度使用会使程序变得很复杂。

(1)模式的结构

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

1)抽象构件角色:定义一个抽象接口以规范准备接收附加责任的对象。

2)具体构件角色:实现抽象构件,通过装饰角色为其添加一些职责。

3)抽象装饰角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。

4)具体装饰角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

装饰模式的具体结构图如图3-9所示:

图3-9 装饰模式结构图

(2)模式的实现

装饰模式的实现代码如下:

 1 public class DecoratorPattern {
 2     public static void main(String[] args) {
 3         Component p = new ConcreteComponent();
 4         p.operation();
 5         System.out.println("---------------------------------");
 6         Component d = new ConcreteDecorator(p);
 7         d.operation();
 8     }
 9 }
10 
11 // 抽象构件角色
12 interface Component {
13     public void operation();
14 }
15 
16 // 具体构件角色
17 class ConcreteComponent implements Component {
18     public ConcreteComponent() {
19         System.out.println("创建具体构件角色");       
20     }   
21     public void operation() {
22         System.out.println("调用具体构件角色的方法operation()");           
23     }
24 }
25 
26 // 抽象装饰角色
27 class Decorator implements Component {
28     private Component component;   
29     public Decorator(Component component) {
30         this.component = component;
31     }   
32     public void operation() {
33         component.operation();
34     }
35 }
36 // 具体装饰角色
37 class ConcreteDecorator extends Decorator {
38     public ConcreteDecorator(Component component) {
39         super(component);
40     }   
41     public void operation() {
42         super.operation();
43         addedFunction();
44     }
45     public void addedFunction() {
46         System.out.println("为具体构件角色增加额外的功能addedFunction()");           
47     }
48 }

程序运行结果如下:

创建具体构件角色
调用具体构件角色的方法operation()
---------------------------------
调用具体构件角色的方法operation()
为具体构件角色增加额外的功能addedFunction()

3.4.2 装饰模式的应用实例

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

图 3-10 所示其结构图:

图 3-10 游戏角色 “莫莉卡·安斯兰” 的结构图

程序代码如下:

 1 //抽象构件角色:莫莉卡
 2 interface  Morrigan {
 3     public void display();
 4 }
 5 
 6 //具体构件角色:原身
 7 class original extends JFrame implements Morrigan {
 8     private static final long serialVersionUID = 1L;
 9     private String t="Morrigan0.jpg";
10     
11     public original() {
12         super("《恶魔战士》中的莫莉卡·安斯兰");                
13     }
14 
15     public void setImage(String t) {
16         this.t = t;           
17     }
18 
19     public void display() {   
20         this.setLayout(new FlowLayout());
21         JLabel l1=new JLabel(new ImageIcon("src/decorator/"+t));
22         this.add(l1);   
23         this.pack();       
24         this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  
25         this.setVisible(true);
26     }
27 }
28 
29 // 抽象装饰角色:变形
30 class Changer implements Morrigan {
31     Morrigan m;   
32 
33     public Changer(Morrigan m) {
34         this.m = m;
35     }   
36 
37     public void display() {
38         m.display();
39     }
40 }
41 
42 // 具体装饰角色:女妖
43 class Succubus extends Changer {
44     public Succubus(Morrigan m) {
45         super(m);
46     }   
47 
48     public void display() {
49         setChanger();
50         super.display();   
51     }
52 
53     public void setChanger() {
54         ((original) super.m).setImage("Morrigan1.jpg");           
55     }
56 }
57 
58 // 具体装饰角色:少女
59 class Girl extends Changer {
60     public Girl(Morrigan m) {
61         super(m);
62     }   
63 
64     public void display() {
65         setChanger();
66         super.display();   
67     }
68 
69     public void setChanger() {
70         ((original) super.m).setImage("Morrigan2.jpg");           
71     }
72 }
 1 public class MorriganAensland {
 2     public static void main(String[] args) {
 3         Morrigan m0=new original();
 4         m0.display();
 5 
 6         Morrigan m1=new Succubus(m0);
 7         m1.display();
 8 
 9         Morrigan m2=new Girl(m0);
10         m2.display();
11     }
12 }

程序运行结果如下:

图 3-11 游戏角色“莫莉卡·安斯兰”的变身

3.4.3 装饰模式的应用场景

前面讲解了关于装饰模式的结构与特点,下面介绍其适用的应用场景,装饰模式通常在以下几种情况使用。

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

装饰模式在 Java 语言中的最著名的应用莫过于 Java I/O 标准库的设计了。

例如,InputStream 的子类 FilterInputStream,OutputStream 的子类 FilterOutputStream,Reader 的子类 BufferedReader 以及 FilterReader,还有 Writer 的子类 BufferedWriter、FilterWriter 以及 PrintWriter 等,它们都是抽象装饰类。

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

1 BufferedReader in = new BufferedReader(new FileReader("filename.txt"));
2 String str = in.readLine();

3.4.4 装饰模式的扩展

装饰模式所包含的 4 个角色不是任何时候都要存在的,在有些应用环境下模式是可以简化的,如以下两种情况。

(1) 如果只有一个具体构件而没有抽象构件时,可以让抽象装饰继承具体构件,其结构图如图 4 所示。

图 3-12 只有一个具体构件的装饰模式

(2)如果只有一个具体装饰时,可以将抽象装饰和具体装饰合并,其结构图如图 3-13 所示:

图 3-13 只有一个具体装饰的装饰模式

3.5 外观(Facade)模式


在现实生活中,常常存在办事较复杂的例子,如办房产证或注册一家公司,有时要同多个部门联系,这时要是有一个综合部门能解决一切手续问题就好了。

*软件设计也是这样,当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这时如果系统内部发生改变,客户端也要跟着改变,这违背了“开闭原则”,也违背了“迪米特法则”,所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度,这就是外观模式的目标。

图 3-14 给出了客户去当地房产局办理房产证过户要遇到的相关部门。

图 3-15 办理房产证过户的相关部门

3.5.1 外观模式的定义与特点

外观(Facade)模式的定义:是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。

外观(Facade)模式是 “迪米特法则” 的典型应用,它有以下主要优点

**1)**降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。

**2)**对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。

**3)**降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。

外观(Facade)模式的主要缺点如下。

**1)**不能很好地限制客户使用子系统类。

**2)**增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。

3.5.2 外观模式的结构与实现

外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过它访问各个子系统的功能。现在来分析其基本结构和实现方法。

(1)模式的结构

外观(Facade)模式包含以下主要角色。

1)外观(Facade)角色:为多个子系统对外提供一个共同的接口。

2)子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。

3)客户(Client)角色:通过一个外观角色访问各个子系统的功能。

其结构图如图 3-16 所示。

图 3-16 外观模式的结构图

(2)模式的实现

外观模式的实现代码如下:

 1 // 外观角色
 2 class Facade {
 3     private SubSystem01 obj1 = new SubSystem01();
 4     private SubSystem02 obj2 = new SubSystem02();
 5     private SubSystem03 obj3 = new SubSystem03();
 6     public void method() {
 7         obj1.method1();
 8         obj2.method2();
 9         obj3.method3();
10     }
11 }
12 
13 // 子系统角色
14 class SubSystem01 {
15     public  void method1() {
16         System.out.println("子系统01的method1()被调用!");
17     }   
18 }
19 
20 //子系统角色
21 class SubSystem02 {
22     public void method2() {
23         System.out.println("子系统02的method2()被调用!");
24     }   
25 }
26 
27 // 子系统角色
28 class SubSystem03 {
29     public  void method3() {
30         System.out.println("子系统03的method3()被调用!");
31     }   
32 }
33 
34 public class FacadePattern {
35     public static void main(String[] args) {
36         Facade f = new Facade();
37         f.method();
38     }
39 }

程序运行结果如下:

子系统01的method1()被调用!
子系统02的method2()被调用!
子系统03的method3()被调用!

3.5.3 外观模式的应用实例

【例】用“外观模式”设计一个婺源特产的选购界面。

分析:本实例的外观角色 WySpecialty 是 JPanel 的子类,它拥有 8 个子系统角色 Specialty1~Specialty8,它们是图标类(ImageIcon)的子类对象,用来保存该婺源特产的图标。

外观类(WySpecialty)用 JTree 组件来管理婺源特产的名称,并定义一个事件处理方法 valueClianged(TreeSelectionEvent e),当用户从树中选择特产时,该特产的图标对象保存在标签(JLabd)对象中。

客户窗体对象用分割面板来实现,左边放外观角色的目录树,右边放显示所选特产图像的标签。

其结构图如图 3–17 所示。

图 3-17 婺源特产管理界面的结构图

程序代码如下:

  1 class WySpecialty extends JPanel implements TreeSelectionListener {
  2     private static final long serialVersionUID=1L;
  3     final JTree tree;
  4     JLabel label;
  5     private Specialty1 s1=new Specialty1();
  6     private Specialty2 s2=new Specialty2();
  7     private Specialty3 s3=new Specialty3();
  8     private Specialty4 s4=new Specialty4();
  9     private Specialty5 s5=new Specialty5();
 10     private Specialty6 s6=new Specialty6();
 11     private Specialty7 s7=new Specialty7();
 12     private Specialty8 s8=new Specialty8();
 13     
 14     WySpecialty() {       
 15         DefaultMutableTreeNode top = new DefaultMutableTreeNode("婺源特产");
 16 
 17         DefaultMutableTreeNode node1 = null, node2 = null, tempNode = null; 
 18         
 19         // 四大特产
 20         node1 = new DefaultMutableTreeNode("婺源四大特产(红、绿、黑、白)");
 21 
 22         tempNode = new DefaultMutableTreeNode("婺源荷包红鲤鱼");
 23         node1.add(tempNode);
 24 
 25         tempNode=new DefaultMutableTreeNode("婺源绿茶");
 26         node1.add(tempNode);
 27 
 28         tempNode=new DefaultMutableTreeNode("婺源龙尾砚");
 29         node1.add(tempNode);
 30 
 31         tempNode=new DefaultMutableTreeNode("婺源江湾雪梨");
 32         node1.add(tempNode);
 33 
 34         top.add(node1);
 35 
 36         //  其他特产
 37         node2 = new DefaultMutableTreeNode("婺源其它土特产");
 38 
 39         tempNode = new DefaultMutableTreeNode("婺源酒糟鱼");
 40         node2.add(tempNode);
 41 
 42         tempNode = new DefaultMutableTreeNode("婺源糟米子糕");
 43         node2.add(tempNode);
 44 
 45         tempNode = new DefaultMutableTreeNode("婺源清明果");
 46         node2.add(tempNode);
 47 
 48         tempNode = new DefaultMutableTreeNode("婺源油煎灯");
 49         node2.add(tempNode);
 50 
 51         top.add(node2); 
 52 
 53         //  树目录
 54         tree = new JTree(top);
 55         tree.addTreeSelectionListener(this);
 56         label = new JLabel();
 57     }   
 58 
 59     public void valueChanged(TreeSelectionEvent e) {
 60         if(e.getSource() == tree) {
 61             DefaultMutableTreeNode node=(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
 62             if(node==null) return;
 63 
 64             if(node.isLeaf()) {
 65                 Object object = node.getUserObject();
 66                 String sele = object.toString();
 67                 label.setText(sele);
 68                 label.setHorizontalTextPosition(JLabel.CENTER);
 69                 label.setVerticalTextPosition(JLabel.BOTTOM);
 70                 sele = sele.substring(2,4);
 71 
 72                 if(sele.equalsIgnoreCase("荷包")) label.setIcon(s1);
 73                 else if(sele.equalsIgnoreCase("绿茶")) label.setIcon(s2);
 74                 else if(sele.equalsIgnoreCase("龙尾")) label.setIcon(s3);
 75                 else if(sele.equalsIgnoreCase("江湾")) label.setIcon(s4);
 76                 else if(sele.equalsIgnoreCase("酒糟")) label.setIcon(s5);
 77                 else if(sele.equalsIgnoreCase("糟米")) label.setIcon(s6);
 78                 else if(sele.equalsIgnoreCase("清明")) label.setIcon(s7);
 79                 else if(sele.equalsIgnoreCase("油煎")) label.setIcon(s8);
 80 
 81                 label.setHorizontalAlignment(JLabel.CENTER);
 82             }
 83         }               
 84     }
 85 }
 86 
 87 class Specialty1 extends ImageIcon {
 88     private static final long serialVersionUID=1L;
 89     Specialty1() {
 90         super("src/facade/WyImage/Specialty11.jpg");
 91     }
 92 }
 93 
 94 class Specialty2 extends ImageIcon {
 95     private static final long serialVersionUID=1L;
 96     Specialty2() {
 97         super("src/facade/WyImage/Specialty12.jpg");
 98     }
 99 }
100 
101 class Specialty3 extends ImageIcon {
102     private static final long serialVersionUID=1L;
103     Specialty3() {
104         super("src/facade/WyImage/Specialty13.jpg");
105     }
106 }
107 
108 class Specialty4 extends ImageIcon {
109     private static final long serialVersionUID=1L;
110     Specialty4() {
111         super("src/facade/WyImage/Specialty14.jpg");
112     }
113 }
114 
115 class Specialty5 extends ImageIcon {
116     private static final long serialVersionUID=1L;
117     Specialty5() {
118         super("src/facade/WyImage/Specialty21.jpg");
119     }
120 }
121 
122 class Specialty6 extends ImageIcon {
123     private static final long serialVersionUID=1L;
124     Specialty6() {
125         super("src/facade/WyImage/Specialty22.jpg");
126     }
127 }
128 
129 class Specialty7 extends ImageIcon {
130     private static final long serialVersionUID=1L;
131     Specialty7() {
132         super("src/facade/WyImage/Specialty23.jpg");
133     }
134 }
135 
136 class Specialty8 extends ImageIcon {
137     private static final long serialVersionUID=1L;
138     Specialty8() {
139         super("src/facade/WyImage/Specialty24.jpg");
140     }
141 }
 1 public class WySpecialtyFacade {
 2     public static void main(String[] args) {
 3         JFrame f = new JFrame ("外观模式: 婺源特产选择测试");
 4         Container cp = f.getContentPane();       
 5         WySpecialty wys = new WySpecialty();       
 6         JScrollPane treeView = new JScrollPane(wys.tree);
 7         JScrollPane scrollpane = new JScrollPane(wys.label);  
 8         // 分隔面板
 9         JSplitPane splitpane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,true,treeView,scrollpane);
10         splitpane.setDividerLocation(230);     // 设置splitpane的分隔线位置
11         splitpane.setOneTouchExpandable(true); // 设置splitpane可以展开或收起 
12 
13         cp.add(splitpane);
14         f.setSize(650,350);
15         f.setVisible(true);   
16         f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17     }
18 }

程序运行结果如图 3-18所示:

图 3-18 婺源特产管理界面的运行结果

3.5.4 外观模式的场景应用

通常在以下情况下可以考虑使用外观模式。

**1)**对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。

**2)**当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。

**3)**当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。

3.5.5 外观模式的扩展

在外观模式中,当增加或移除子系统时需要修改外观类,这违背了“开闭原则”。如果引入抽象外观类,则在一定程度上解决了该问题。

其结构图如图 3-19 所示:

图 3-19 引入抽象外观类的外观模式的结构图

3.6 享元(Flyweight)模式


在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。

3.6.1 享元模式的定义与特点

**享元模式的定义:**运用共享技术来有効地支持大量细粒度对象的复用。它通过共享已经存在的又橡来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

享元模式的主要优点是:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

其主要缺点是:

**1)**为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。

**2)**读取享元模式的外部状态会使得运行时间稍微变长。

3.6.2 享元模式的结构与实现

享元模式中存在以下两种状态:

1)内部状态,即不会随着环境的改变而改变的可共享部分;

2)外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。下面来分析其基本结构和实现方法。

(1)模式的结构

享元模式的主要角色有如下:

**1)抽象享元(Flyweight)*角色*:**是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。

2)具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。

3)非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。

4)享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

如图 3-20 所示,是享元模式的结构图,图中的 UnsharedConcreteFlyweight 是与淳元角色,里面包含了非共享的外部状态信息 info;

而 Flyweight 是抽象享元角色,里面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部状态以参数的形式通过该方法传入;

ConcreteFlyweight 是具体享元角色,包含了关键字 key,它实现了抽象享元接口;

FlyweightFactory 是享元工厂角色,它逝关键字 key 来管理具体享元;

客户角色通过享元工厂获取具体享元,并访问具体享元的相关方法。

图 3-20 享元模式的结构图

(2)模式的实现

享元模式的实现代码如下:

 1 // 非享元角色
 2 class UnsharedConcreteFlyweight {
 3     private String info;
 4 
 5     UnsharedConcreteFlyweight(String info) {
 6         this.info=info;
 7     }
 8 
 9     public String getInfo() {
10         return info;
11     }
12 
13     public void setInfo(String info) {
14         this.info=info;
15     }
16 }
17 
18 // 抽象享元角色
19 interface Flyweight {
20     public void operation(UnsharedConcreteFlyweight state);
21 }
22 
23 // 具体享元角色
24 class ConcreteFlyweight implements Flyweight {
25     private String key;
26 
27     ConcreteFlyweight(String key) {
28         this.key = key;
29         System.out.println("具体享元" + key + "被创建!");
30     }
31 
32     public void operation(UnsharedConcreteFlyweight outState) {
33         System.out.print("具体享元" + key + "被调用,");
34         System.out.println("非享元信息是:" + outState.getInfo());
35     }
36 }
37 
38 // 享元工厂角色
39 class FlyweightFactory {
40     private HashMap<String, Flyweight> flyweights = new HashMap<String, Flyweight>();
41 
42     public Flyweight getFlyweight(String key) {
43         Flyweight flyweight=(Flyweight)flyweights.get(key);
44 
45         if(flyweight != null) {
46             System.out.println("具体享元"+key+"已经存在,被成功获取!");
47         } else {
48             flyweight=new ConcreteFlyweight(key);
49             flyweights.put(key, flyweight);
50         }
51         return flyweight;
52     }
53 }
 1 public class FlyweightPattern {
 2     public static void main(String[] args) {
 3         FlyweightFactory factory=new FlyweightFactory();
 4         Flyweight f01 = factory.getFlyweight("a");
 5         Flyweight f02 = factory.getFlyweight("a");
 6         Flyweight f03 = factory.getFlyweight("a");
 7 
 8         Flyweight f11 = factory.getFlyweight("b");
 9         Flyweight f12 = factory.getFlyweight("b");
10 
11         f01.operation(new UnsharedConcreteFlyweight("第1次调用a。"));       
12         f02.operation(new UnsharedConcreteFlyweight("第2次调用a。"));       
13         f03.operation(new UnsharedConcreteFlyweight("第3次调用a。"));
14 
15         f11.operation(new UnsharedConcreteFlyweight("第1次调用b。"));       
16         f12.operation(new UnsharedConcreteFlyweight("第2次调用b。"));
17     }
18 }

程序运行结果如下:

具体享元a被创建!
具体享元a已经存在,被成功获取!
具体享元a已经存在,被成功获取!
具体享元b被创建!
具体享元b已经存在,被成功获取!
具体享元a被调用,非享元信息是:第1次调用a。
具体享元a被调用,非享元信息是:第2次调用a。
具体享元a被调用,非享元信息是:第3次调用a。
具体享元b被调用,非享元信息是:第1次调用b。
具体享元b被调用,非享元信息是:第2次调用b。

3.6.3 享元模式的应用实例

【例】享元模式在五子棋游戏中的应用。
分析:五子棋同围棋一样,包含多个“黑”或“白”颜色的棋子,所以用享元模式比较好。
本实例中的棋子(ChessPieces)类是抽象享元角色,它包含了一个落子的 DownPieces(Graphics g,Point pt) 方法;白子(WhitePieces)和黑子(BlackPieces)类是具体享元角色,它实现了落子方法;Point 是非享元角色,它指定了落子的位置;WeiqiFactory 是享元工厂角色,它通过 ArrayList 来管理棋子,并且提供了获取白子或者黑子的 getChessPieces(String type) 方法;客户类(Chessboard)利用 Graphics 组件在框架窗体中绘制一个棋盘,并实现 mouseClicked(MouseEvent e) 事件处理方法,该方法根据用户的选择从享元工厂中获取白子或者黑子并落在棋盘上。

图 3-21 所示是其结构图。

图 3-21 五子棋游戏的结构图

程序代码如下:

  1 // 棋盘
  2 class Chessboard extends MouseAdapter {
  3     WeiqiFactory wf;
  4     JFrame frame;   
  5     Graphics g;
  6     JRadioButton wz;
  7     JRadioButton bz;
  8     private final int x = 50;
  9     private final int y = 50;
 10     private final int w = 40;    // 小方格宽度和高度
 11     private final int rw = 400;  // 棋盘宽度和高度
 12 
 13     Chessboard() {
 14         wf = new WeiqiFactory();
 15 
 16         frame = new JFrame("享元模式在五子棋游戏中的应用");
 17         frame.setBounds(100,100,500,550);
 18         frame.setVisible(true);       
 19         frame.setResizable(false);
 20         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 21 
 22         JPanel SouthJP = new JPanel();
 23         frame.add("South",SouthJP);
 24 
 25         wz = new JRadioButton("白子");
 26         bz = new JRadioButton("黑子", true);
 27 
 28         ButtonGroup group = new ButtonGroup();
 29         group.add(wz);
 30         group.add(bz);
 31 
 32         SouthJP.add(wz);
 33         SouthJP.add(bz); 
 34 
 35         JPanel CenterJP = new JPanel();
 36         CenterJP.setLayout(null);
 37         CenterJP.setSize(500, 500);
 38         CenterJP.addMouseListener(this);
 39         frame.add("Center", CenterJP);
 40 
 41         try {
 42             Thread.sleep(500);
 43         } catch(InterruptedException e) {
 44             e.printStackTrace();
 45         } 
 46 
 47         g = CenterJP.getGraphics();
 48         g.setColor(Color.BLUE);   
 49         g.drawRect(x, y, rw, rw);
 50         for(int i=1; i<10; i++) {
 51             // 绘制第i条竖直线
 52             g.drawLine(x+(i*w), y, x+(i*w), y+rw);
 53             // 绘制第i条水平线
 54             g.drawLine(x, y+(i*w), x+rw, y+(i*w));
 55         }   
 56     }
 57 
 58     public void mouseClicked(MouseEvent e) {
 59         Point pt = new Point(e.getX() - 15, e.getY() - 15);
 60         if(wz.isSelected()) {
 61             ChessPieces c1=wf.getChessPieces("w");
 62             c1.DownPieces(g,pt);
 63         } else if(bz.isSelected()) {
 64             ChessPieces c2=wf.getChessPieces("b");       
 65             c2.DownPieces(g,pt);  
 66         }
 67     }
 68 }
 69 
 70 // 抽象享元角色:棋子
 71 interface ChessPieces {
 72     public void DownPieces(Graphics g, Point pt);    //下子
 73 }
 74 
 75 // 具体享元角色:白子
 76 class WhitePieces implements ChessPieces {
 77     public void DownPieces(Graphics g, Point pt) {       
 78         g.setColor(Color.WHITE);
 79         g.fillOval(pt.x,pt.y,30,30);
 80     }
 81 }
 82 
 83 // 具体享元角色:黑子
 84 class BlackPieces implements ChessPieces {
 85     public void DownPieces(Graphics g, Point pt) {
 86         g.setColor(Color.BLACK);
 87         g.fillOval(pt.x, pt.y, 30, 30);
 88     }
 89 }
 90 
 91 // 享元工厂角色
 92 class WeiqiFactory {
 93     private ArrayList<ChessPieces> qz;   
 94 
 95     public WeiqiFactory() {
 96         qz = new ArrayList<ChessPieces>();
 97         ChessPieces w = new WhitePieces();
 98         qz.add(w);
 99         ChessPieces b = new BlackPieces();
100         qz.add(b);
101     }
102 
103     public ChessPieces getChessPieces(String type) {
104         if(type.equalsIgnoreCase("w")) {
105             return (ChessPieces)qz.get(0);
106         } else if(type.equalsIgnoreCase("b")) {
107             return (ChessPieces)qz.get(1);
108         } else {
109             return null;
110         }
111     }
112 }
1 public class WzqGame {
2     public static void main(String[] args) {
3         new Chessboard();
4     }
5 }

程序运行结果如下图所示:

图 3-22 五子棋游戏的运行结果

3.6.4 享元模式的应用场景

前面分析了享元模式的结构与特点,下面分析它适用的应用场景。享元模式是通过减少内存中对象的数量来节省内存空间的,所以以下几种情形适合采用享元模式。

1)系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。

2)大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。

3)由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。

3.6.5 享元模式的扩展

在前面介绍的享元模式中,其结构图通常包含可以共享的部分和不可以共享的部分。在实际使用过程中,有时候会稍加改变,即存在两种特殊的享元模式:单纯享元模式和复合享元模式,下面分别对它们进行简单介绍。

(1)单纯享元模式,这种享元模式中的所有的具体享元类都是可以共享的,不存在非共享的具体享元类,其结构图如图 3-23 所示。

图3-23 单享模式的结构图

(2)复合享元模式,这种享元模式中的有些享元对象是由一些单纯享元对象组合而成的,它们就是复合享元对象。虽然复合享元对象本身不能共享,但它们可以分解成单纯享元对象再被共享,其结构图如图 3-24 所示。

图 3-24 复合享元模式的结构图

3.7 组合(Composite)模式


在现实生活中,存在很多“部分-整体”的关系,例如,大学中的部门与学院、总公司中的部门与分公司、学习用品中的书与书包、生活用品中的衣月艮与衣柜以及厨房中的锅碗瓢盆等。在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。

3.7.1 组合模式的定义与特点

组合(Composite)模式的定义:有时又叫作部分-整体模式,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。

组合模式的主要优点有:

**1)**组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;

**2)**更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;

其主要缺点是:

**1)**设计较复杂,客户端需要花更多时间理清类之间的层次关系;

**2)**不容易限制容器中的构件;

**3)**不容易用继承的方法来增加构件的新功能;

3.7.2 组合模式的结构与实现

组合模式的结构不是很复杂,下面对它的结构和实现进行分析。

(1)模式的结构

组合模式包含以下主要角色。

1)抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。

2)树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于实现抽象构件角色中 声明的公共接口。

3)树枝构件(Composite)角色:是组合中的分支节点对象,它有子节点。它实现了抽象构件角色中声明的接口,它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。

组合模式分为透明式的组合模式和安全式的组合模式。

① 透明方式:在该方式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。但其缺点是:树叶构件本来没有 Add()、Remove() 及 GetChild() 方法,却要实现它们(空实现或抛异常),这样会带来一些安全性问题。

其结构图如图 3-25 所示。

图 3-25 透明式的组合模式的结构图

**② 安全方式:**在该方式中,将管理子构件的方法移到树枝构件中,抽象构件和树叶构件没有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户端在调用时要知道树叶对象和树枝对象的存在,所以失去了透明性。

其结构图如图 3-26 所示。

图 3-36 安全式4的组合模式的结构图

(2)模式的实现

假如要访问集合 c0 = {leaf1, {leaf2, leaf3}} 中的元素,其对应的树状图如图 3-37 所示。

图 3-37 集合 c0 的树状图

下面给出透明式的组合模式的实现代码,与安全式的组合模式的实现代码类似,只要对其做简单修改就可以了。

 1 // 抽象构件
 2 interface Component {
 3     public void add(Component c);
 4     public void remove(Component c);
 5     public Component getChild(int i);
 6     public void operation();
 7 }
 8 
 9 / / 树叶构件
10 class Leaf implements Component {
11     private String name;
12 
13     public Leaf(String name) {
14         this.name=name;
15     }
16 
17     public void add(Component c){} 
18        
19     public void remove(Component c){}   
20 
21     public Component getChild(int i) {
22         return null;
23     }
24 
25     public void operation() {
26         System.out.println("树叶" + name + ":被访问!"); 
27     }
28 }
29 
30 // 树枝构件
31 class Composite implements Component {
32     private ArrayList<Component> children = new ArrayList<Component>();
33 
34     public void add(Component c) {
35         children.add(c);
36     } 
37 
38     public void remove(Component c) {
39         children.remove(c);
40     } 
41 
42     public Component getChild(int i) {
43         return children.get(i);
44     } 
45 
46     public void operation() {
47         for(Object obj:children) {
48             ((Component)obj).operation();
49         }
50     }    
51 
 1 public class CompositePattern {
 2 
 3     public static void main(String[] args) {
 4         Component c0 = new Composite(); 
 5 
 6         // 添加树叶
 7         Component leaf1 = new Leaf("1"); 
 8         c0.add(leaf1); 
 9 
10         // 添加树枝
11         Component c1 = new Composite(); 
12         c0.add(c1);
13 
14         Component leaf2=new Leaf("2"); 
15         Component leaf3=new Leaf("3");
16         c1.add(leaf2); 
17         c1.add(leaf3);  
18         
19         c0.operation(); 
20     }
21 }     

程序运行结果如下:

树叶1:被访问!
树叶2:被访问!
树叶3:被访问!

3.7.3 组合模式的应用实例

【例】用组合模式实现当用户在商店购物后,显示其所选商品信息,并计算所选商品总价的功能。
说明:假如李先生到韶关“天街e角”生活用品店购物:

用 1 个红色小袋子装了 2 包婺源特产(单价 7.9 元)、1 张婺源地图(单价 9.9 元);

用 1 个白色小袋子装了 2 包韶关香藉(单价 68 元)和 3 包韶关红茶(单价 180 元);

用 1 个中袋子装了前面的红色小袋子和 1 个景德镇瓷器(单价 380 元);

用 1 个大袋子装了前面的中袋子、白色小袋子和 1 双李宁牌运动鞋(单价 198 元)。

最后“大袋子”中的内容有:

{

1 双李宁牌运动鞋(单价 198 元),

白色小袋子 {

2 包韶关香菇(单价 68 元),3 包韶关红茶(单价 180 元)

},

中袋子 {

1 个景德镇瓷器(单价 380 元),

红色小袋子 {

2 包婺源特产(单价 7.9 元),1 张婺源地图(单价 9.9 元)

}

}

},

现在要求编程显示李先生放在大袋子中的所有商品信息并计算要支付的总价。

本实例可按安全组合模式设计,其结构图如图 3-38 所示。

图 3-38 韶关“天街e角”店购物的结构图

程序代码如下:

 1 // 抽象构件:物品
 2 interface Articles {
 3     public float calculation(); //计算
 4     public void show();
 5 }
 6 
 7 // 树叶构件:商品
 8 class Goods implements Articles {
 9     private String name;     // 名字
10     private int quantity;    // 数量
11     private float unitPrice; // 单价
12 
13     public Goods(String name, int quantity, float unitPrice) {
14         this.name = name;
15         this.quantity = quantity;
16         this.unitPrice = unitPrice;
17     }
18 
19     public float calculation() {
20         return quantity * unitPrice; 
21     }
22 
23     public void show() {
24         System.out.println(name + "(数量:" + quantity + ",单价:" + unitPrice + "元)");
25     }
26 }
27 
28 // 树枝构件:袋子
29 class Bags implements Articles {
30     private String name;     // 名字   
31     private ArrayList<Articles> arrBags = new ArrayList<>(); 
32   
33     public Bags(String name) {
34         this.name = name;
35     }
36 
37     public void add(Articles c) {
38         arrrBags.add(c);
39     }   
40 
41     public void remove(Articles c) {
42         arrBags.remove(c);
43     } 
44 
45     public Articles getChild(int i) {
46         return arrBags.get(i);
47     } 
48 
49     public float calculation() {
50         float s = 0;
51         for(Object obj : arrBags) {
52             s += ((Articles)obj).calculation();
53         }
54         return s;
55     }
56 
57     public void show() {
58         for(Object obj : arrBags) {
59             ((Articles)obj).show();
60         }
61     }
62 }
 1 public class ShoppingTest {
 2     public static void main(String[] args) {
 3         Bags BigBag, mediumBag, smallRedBag, smallWhiteBag;
 4         Goods sp;
 5         
 6         smallRedBag = new Bags("红色小袋子");
 7         sp = new Goods("婺源特产", 2, 7.9f);
 8         smallRedBag.add(sp);
 9         sp = new Goods("婺源地图", 1, 9.9f);
10         smallRedBag.add(sp); 
11 
12         smallWhiteBag = new Bags("白色小袋子"); 
13         sp = new Goods("韶关香菇", 2, 68);
14         smallWhiteBag.add(sp);
15         sp = new Goods("韶关红茶", 3, 180);
16         smallWhiteBag.add(sp); 
17 
18         mediumBag = new Bags("中袋子");
19         sp = new Goods("景德镇瓷器", 1, 380);
20         mediumBag.add(sp);
21         mediumBag.add(smallRedBag); 
22 
23         BigBag = new Bags("大袋子");
24         sp = new Goods("李宁牌运动鞋", 1, 198);
25         BigBag.add(sp);
26         BigBag.add(smallWhiteBag);
27         BigBag.add(mediumBag);
28 
29         System.out.println("您选购的商品有:");
30         BigBag.show();
31 
32         float s = BigBag.calculation();       
33         System.out.println("要支付的总价是:" + s + "元");
34     }
35 }

程序运行结果如下:

您选购的商品有:
李宁牌运动鞋(数量:1,单价:198.0元)
韶关香菇(数量:2,单价:68.0元)
韶关红茶(数量:3,单价:180.0元)
景德镇瓷器(数量:1,单价:380.0元)
婺源特产(数量:2,单价:7.9元)
婺源地图(数量:1,单价:9.9元)
要支付的总价是:1279.7元

3.7.4 组合模式的应用场景

前面分析了组合模式的结构与特点,下面分析它适用的以下应用场景。

**1)**在需要表示一个对象整体与部分的层次结构的场合。

**2)**要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合。

3.7.5 组合模式的扩展

如果对前面介绍的组合模式中的树叶节点和树枝节点进行抽象,也就是说树叶节点和树枝节点还有子节点,这时组合模式就扩展成复杂的组合模式了,如 Java AWT/Swing 中的简单组件 JTextComponent 有子类 JTextField、JTextArea,容器组件 Container 也有子类 Window、Panel。复杂的组合模式的结构图如图 3-39 所示。

图 3-39 复杂的组合模式的结构图