当前位置:   article > 正文

Java中的23种设计模式的介绍

Java中的23种设计模式的介绍

设计模式

设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。

1995 年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了 23 种设计模式,从此树立了软件设计模式领域的里程碑,人称「GoF设计模式」。

这 23 种设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。

当然,软件设计模式只是一个引导,在实际的软件开发中,必须根据具体的需求来选择:

  • 对于简单的程序,可能写一个简单的算法要比引入某种设计模式更加容易;
  • 但是对于大型项目开发或者框架设计,用设计模式来组织代码显然更好。

GoF 的 23 种设计模式的分类和功能 

设计模式有两种分类方法,即根据模式的目的来分和根据模式的作用的范围来分。

1. 根据目的来分

根据模式是用来完成什么工作来划分,这种方式可分为创建型模式、结构型模式和行为型模式 3 种。

  1. 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF 中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
  2. 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,GoF 中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
  3. 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。GoF 中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。

2. 根据作用范围来分

根据模式是主要用于类上还是主要用于对象上来分,这种方式可分为类模式和对象模式两种。

  1. 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。GoF中的工厂方法、(类)适配器、模板方法、解释器属于该模式。
  2. 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。GoF 中除了以上 4 种,其他的都是对象模式。

3. GoF的23种设计模式的功能

前面说明了 GoF 的 23 种设计模式的分类,现在对各个模式的功能进行介绍。

  1. 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
  2. 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
  3. 工厂方法(Factory Method)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
  4. 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
  5. 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
  6. 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
  7. 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
  8. 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
  9. 装饰(Decorator)模式:动态的给对象增加一些职责,即增加其额外的功能。
  10. 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
  11. 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
  12. 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
  13. 模板方法(TemplateMethod)模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
  14. 策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
  15. 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
  16. 职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
  17. 状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
  18. 观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
  19. 中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
  20. 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
  21. 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
  22. 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
  23. 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。


必须指出,这 23 种设计模式不是孤立存在的,很多模式之间存在一定的关联关系,在大的系统开发中常常同时使用多种设计模式,希望读者认真学好它们。

在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。

创建型模式的特点和分类

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。就像我们去商场购买商品时,不需要知道商品是怎么生产出来一样,因为它们由专门的厂商生产。

创建型模式分为以下几种。

  • 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
  • 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
  • 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
  • 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
  • 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。


以上 5 种创建型模式,除了工厂方法模式属于类创建型模式,其他的全部属于对象创建型模式,我们将在之后的教程中详细地介绍它们的特点、结构与应用。

单例模式的定义与特点

单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。

单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。J2EE 标准中的 ServletContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。

单例模式有 3 个特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点。

单例模式的优点和缺点

单例模式的优点:

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。


单例模式的缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

单例模式看起来非常简单,实现起来也非常简单。单例模式在面试中是一个高频面试题。希望大家能够认真学习,掌握单例模式,提升核心竞争力,给面试加分,顺利拿到 Offer。

单例模式的应用场景

对于Java来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
  • 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

单例模式的结构与实现

单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

下面来分析其基本结构和实现方法。

1. 单例模式的结构

单例模式的主要角色如下。

  • 单例类:包含一个实例且能自行创建这个实例的类。
  • 访问类:使用单例的类。


其结构如图所示。

2. 单例模式的实现

Singleton 模式通常有两种实现形式。

第 1 种:懒汉式单例

该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。代码如下:

  1. public class LazySingleton {
  2. private static volatile LazySingleton instance = null; //保证 instance 在所有线程中同步
  3. private LazySingleton() {
  4. } //private 避免类在外部被实例化
  5. public static synchronized LazySingleton getInstance() {
  6. //getInstance 方法前加同步
  7. if (instance == null) {
  8. instance = new LazySingleton();
  9. }
  10. return instance;
  11. }
  12. }

注意:如果编写的是多线程程序,则不要删除上例代码中的关键字 volatile 和 synchronized,否则将存在线程非安全的问题。如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。

第 2 种:饿汉式单例

该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。

  1. public class HungrySingleton {
  2. private static final HungrySingleton instance = new HungrySingleton();
  3. private HungrySingleton() {
  4. }
  5. public static HungrySingleton getInstance() {
  6. return instance;
  7. }
  8. }

饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。

单例模式的应用实例

【例1】用懒汉式单例模式模拟产生美国当今总统对象。

分析:在每一届任期内,美国的总统只有一人,所以本实例适合用单例模式实现,图所示是用懒汉式单例实现的结构图。


程序代码如下:

  1. public class SingletonLazy {
  2. public static void main(String[] args) {
  3. President zt1 = President.getInstance();
  4. zt1.getName(); //输出经理的名字
  5. President zt2 = President.getInstance();
  6. zt2.getName(); //输出经理的名字
  7. if (zt1 == zt2) {
  8. System.out.println("他们是同一人!");
  9. } else {
  10. System.out.println("他们不是同一人!");
  11. }
  12. }
  13. }
  14. class President {
  15. private static volatile President instance = null; //保证instance在所有线程中同步
  16. //private避免类在外部被实例化
  17. private President() {
  18. System.out.println("产生一个经理!");
  19. }
  20. public static synchronized President getInstance() {
  21. //在getInstance方法上加同步
  22. if (instance == null) {
  23. instance = new President();
  24. } else {
  25. System.out.println("已经有一个经理,不能产生新经理!");
  26. }
  27. return instance;
  28. }
  29. public void getName() {
  30. System.out.println("我是酒店经理:王小二。");
  31. }
  32. }

程序运行结果如下:

产生一个经理!
我是酒店经理:王小二。
已经有一个经理,不能产生新经理!
我是酒店经理:王小二。
他们是同一人!


【例2】用饿汉式单例模式模拟产生猪八戒对象。

分析:同上例类似,猪八戒也只有一个,所以本实例同样适合用单例模式实现。本实例由于要显示猪八戒的图像(点此下载该程序所要显示的猪八戒图片),所以用到了框架窗体 JFrame 组件,这里的猪八戒类是单例类,可以将其定义成面板 JPanel 的子类,里面包含了标签,用于保存猪八戒的图像,客户窗体可以获得猪八戒对象,并显示它。图所示是用饿汉式单例实现的结构图。
 

 

程序代码如下:

  1. import java.awt.*;
  2. import javax.swing.*;
  3. public class SingletonEager {
  4. public static void main(String[] args) {
  5. JFrame jf = new JFrame("饿汉单例模式测试");
  6. jf.setLayout(new GridLayout(1, 2));
  7. Container contentPane = jf.getContentPane();
  8. Bajie obj1 = Bajie.getInstance();
  9. contentPane.add(obj1);
  10. Bajie obj2 = Bajie.getInstance();
  11. contentPane.add(obj2);
  12. if (obj1 == obj2) {
  13. System.out.println("他们是同一人!");
  14. } else {
  15. System.out.println("他们不是同一人!");
  16. }
  17. jf.pack();
  18. jf.setVisible(true);
  19. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  20. }
  21. }
  22. class Bajie extends JPanel {
  23. private static Bajie instance = new Bajie();
  24. private Bajie() {
  25. JLabel l1 = new JLabel(new ImageIcon("src/Bajie.jpg"));
  26. this.add(l1);
  27. }
  28. public static Bajie getInstance() {
  29. return instance;
  30. }
  31. }

程序运行结果如图所示。 

单例模式的扩展

单例模式可扩展为有限的多例(Multitcm)模式,这种模式可生成有限个实例并保存在 ArrayList 中,客户需要时可随机获取,其结构图如图所示。

在有些系统中,存在大量相同或相似对象的创建问题,如果用传统的构造函数来创建对象,会比较复杂且耗时耗资源,用原型模式生成对象就很高效,就像孙悟空拔下猴毛轻轻一吹就变出很多孙悟空一样简单。

原型模式的定义与特点

原型(Prototype)模式的定义如下:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。例如,Windows 操作系统的安装通常较耗时,如果复制就快了很多。在生活中复制的例子非常多,这里不一一列举了。

原型模式的优点:

  • Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
  • 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。

原型模式的缺点:

  • 需要为每一个类都配置一个 clone 方法
  • clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
  • 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。

原型模式的结构与实现

由于 Java 提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单。

1. 模式的结构

原型模式包含以下主要角色。

  1. 抽象原型类:规定了具体原型对象必须实现的接口。
  2. 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
  3. 访问类:使用具体原型类中的 clone() 方法来复制新的对象。


其结构图如图所示。

 

2. 模式的实现

原型模式的克隆分为浅克隆和深克隆。

  • 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
  • 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。


Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆,这里的 Cloneable 接口就是抽象原型类。其代码如下:

  1. //具体原型类
  2. class Realizetype implements Cloneable {
  3. Realizetype() {
  4. System.out.println("具体原型创建成功!");
  5. }
  6. public Object clone() throws CloneNotSupportedException {
  7. System.out.println("具体原型复制成功!");
  8. return (Realizetype) super.clone();
  9. }
  10. }
  11. //原型模式的测试类
  12. public class PrototypeTest {
  13. public static void main(String[] args) throws CloneNotSupportedException {
  14. Realizetype obj1 = new Realizetype();
  15. Realizetype obj2 = (Realizetype) obj1.clone();
  16. System.out.println("obj1==obj2?" + (obj1 == obj2));
  17. }
  18. }

程序的运行结果如下:

具体原型创建成功!
具体原型复制成功!
obj1==obj2?false

原型模式的应用实例

【例1】用原型模式模拟“孙悟空”复制自己。

分析:孙悟空拔下猴毛轻轻一吹就变出很多孙悟空,这实际上是用到了原型模式。这里的孙悟空类 SunWukong 是具体原型类,而 Java 中的 Cloneable 接口是抽象原型类。

同前面介绍的猪八戒实例一样,由于要显示孙悟空的图像(点击此处下载该程序所要显示的孙悟空的图片),所以将孙悟空类定义成面板 JPanel 的子类,里面包含了标签,用于保存孙悟空的图像。

另外,重写了 Cloneable 接口的 clone() 方法,用于复制新的孙悟空。访问类可以通过调用孙悟空的 clone() 方法复制多个孙悟空,并在框架窗体 JFrame 中显示。图所示是其结构图。

 程序代码如下:

  1. import java.awt.*;
  2. import javax.swing.*;
  3. class SunWukong extends JPanel implements Cloneable {
  4. private static final long serialVersionUID = 5543049531872119328L;
  5. public SunWukong() {
  6. JLabel l1 = new JLabel(new ImageIcon("src/Wukong.jpg"));
  7. this.add(l1);
  8. }
  9. public Object clone() {
  10. SunWukong w = null;
  11. try {
  12. w = (SunWukong) super.clone();
  13. } catch (CloneNotSupportedException e) {
  14. System.out.println("拷贝悟空失败!");
  15. }
  16. return w;
  17. }
  18. }
  19. public class ProtoTypeWukong {
  20. public static void main(String[] args) {
  21. JFrame jf = new JFrame("原型模式测试");
  22. jf.setLayout(new GridLayout(1, 2));
  23. Container contentPane = jf.getContentPane();
  24. SunWukong obj1 = new SunWukong();
  25. contentPane.add(obj1);
  26. SunWukong obj2 = (SunWukong) obj1.clone();
  27. contentPane.add(obj2);
  28. jf.pack();
  29. jf.setVisible(true);
  30. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  31. }
  32. }

程序的运行结果如图所示。

用原型模式除了可以生成相同的对象,还可以生成相似的对象,请看以下实例。

【例2】用原型模式生成“三好学生”奖状。

分析:同一学校的“三好学生”奖状除了获奖人姓名不同,其他都相同,属于相似对象的复制,同样可以用原型模式创建,然后再做简单修改就可以了。图 4 所示是三好学生奖状生成器的结构图。


程序代码如下:

  1. public class ProtoTypeCitation {
  2. public static void main(String[] args) throws CloneNotSupportedException {
  3. citation obj1 = new citation("张三", "同学:在2016学年第一学期中表现优秀,被评为三好学生。", "韶关学院");
  4. obj1.display();
  5. citation obj2 = (citation) obj1.clone();
  6. obj2.setName("李四");
  7. obj2.display();
  8. }
  9. }
  10. //奖状类
  11. class citation implements Cloneable {
  12. String name;
  13. String info;
  14. String college;
  15. citation(String name, String info, String college) {
  16. this.name = name;
  17. this.info = info;
  18. this.college = college;
  19. System.out.println("奖状创建成功!");
  20. }
  21. void setName(String name) {
  22. this.name = name;
  23. }
  24. String getName() {
  25. return (this.name);
  26. }
  27. void display() {
  28. System.out.println(name + info + college);
  29. }
  30. public Object clone() throws CloneNotSupportedException {
  31. System.out.println("奖状拷贝成功!");
  32. return (citation) super.clone();
  33. }
  34. }


程序运行结果如下:

奖状创建成功!
张三同学:在2016学年第一学期中表现优秀,被评为三好学生。韶关学院
奖状拷贝成功!
李四同学:在2016学年第一学期中表现优秀,被评为三好学生。韶关学院

原型模式的应用场景

原型模式通常适用于以下场景。

  • 对象之间相同或相似,即只是个别的几个属性不同的时候。
  • 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
  • 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
  • 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。


在 Spring 中,原型模式应用的非常广泛,例如 scope='prototype'、JSON.parseObject() 等都是原型模式的具体应用。

原型模式的扩展

原型模式可扩展为带原型管理器的原型模式,它在原型模式的基础上增加了一个原型管理器 PrototypeManager 类。该类用 HashMap 保存多个复制的原型,Client 类可以通过管理器的 get(String id) 方法从中获取复制的原型。其结构图如图所示。

【例3】用带原型管理器的原型模式来生成包含“圆”和“正方形”等图形的原型,并计算其面积。分析:本实例中由于存在不同的图形类,例如,“圆”和“正方形”,它们计算面积的方法不一样,所以需要用一个原型管理器来管理它们,图所示是其结构图。
 

程序代码如下: 

  1. import java.util.*;
  2. interface Shape extends Cloneable {
  3. public Object clone(); //拷贝
  4. public void countArea(); //计算面积
  5. }
  6. class Circle implements Shape {
  7. public Object clone() {
  8. Circle w = null;
  9. try {
  10. w = (Circle) super.clone();
  11. } catch (CloneNotSupportedException e) {
  12. System.out.println("拷贝圆失败!");
  13. }
  14. return w;
  15. }
  16. public void countArea() {
  17. int r = 0;
  18. System.out.print("这是一个圆,请输入圆的半径:");
  19. Scanner input = new Scanner(System.in);
  20. r = input.nextInt();
  21. System.out.println("该圆的面积=" + 3.1415 * r * r + "\n");
  22. }
  23. }
  24. class Square implements Shape {
  25. public Object clone() {
  26. Square b = null;
  27. try {
  28. b = (Square) super.clone();
  29. } catch (CloneNotSupportedException e) {
  30. System.out.println("拷贝正方形失败!");
  31. }
  32. return b;
  33. }
  34. public void countArea() {
  35. int a = 0;
  36. System.out.print("这是一个正方形,请输入它的边长:");
  37. Scanner input = new Scanner(System.in);
  38. a = input.nextInt();
  39. System.out.println("该正方形的面积=" + a * a + "\n");
  40. }
  41. }
  42. class ProtoTypeManager {
  43. private HashMap<String, Shape> ht = new HashMap<String, Shape>();
  44. public ProtoTypeManager() {
  45. ht.put("Circle", new Circle());
  46. ht.put("Square", new Square());
  47. }
  48. public void addshape(String key, Shape obj) {
  49. ht.put(key, obj);
  50. }
  51. public Shape getShape(String key) {
  52. Shape temp = ht.get(key);
  53. return (Shape) temp.clone();
  54. }
  55. }
  56. public class ProtoTypeShape {
  57. public static void main(String[] args) {
  58. ProtoTypeManager pm = new ProtoTypeManager();
  59. Shape obj1 = (Circle) pm.getShape("Circle");
  60. obj1.countArea();
  61. Shape obj2 = (Shape) pm.getShape("Square");
  62. obj2.countArea();
  63. }
  64. }

运行结果如下所示:

这是一个圆,请输入圆的半径:3
该圆的面积=28.2735

这是一个正方形,请输入它的边长:3
该正方形的面积=9

简单工厂模式

现实生活中,原始社会自给自足(没有工厂),农耕社会小作坊(简单工厂,民间酒坊),工业革命流水线(工厂方法,自产自销),现代产业链代工厂(抽象工厂,富士康)。我们的项目代码同样是由简到繁一步一步迭代而来的,但对于调用者来说,却越来越简单。

在日常开发中,凡是需要生成复杂对象的地方,都可以尝试考虑使用工厂模式来代替。

注意:上述复杂对象指的是类的构造函数参数过多等对类的构造有影响的情况,因为类的构造过于复杂,如果直接在其他业务类内使用,则两者的耦合过重,后续业务更改,就需要在任何引用该类的源代码内进行更改,光是查找所有依赖就很消耗时间了,更别说要一个一个修改了。

工厂模式的定义:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。

按实际业务场景划分,工厂模式有 3 种不同的实现方式,分别是简单工厂模式、工厂方法模式和抽象工厂模式。

我们把被创建的对象称为“产品”,把创建产品的对象称为“工厂”。如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。

在简单工厂模式中创建实例的方法通常为静态(static)方法,因此简单工厂模式(Simple Factory Pattern)又叫作静态工厂方法模式(Static Factory Method Pattern)。

简单来说,简单工厂模式有一个具体的工厂类,可以生成多个不同的产品,属于创建型设计模式。简单工厂模式不在 GoF 23 种设计模式之列。

简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。

“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。

优点和缺点

优点:

  1. 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
  2. 客户端无需知道所创建具体产品的类名,只需知道参数即可。
  3. 也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。

缺点:

  1. 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则。
  2. 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
  3. 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
  4. 简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构。

应用场景

对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。

模式的结构与实现

简单工厂模式的主要角色如下:

  • 简单工厂(SimpleFactory):是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
  • 抽象产品(Product):是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
  • 具体产品(ConcreteProduct):是简单工厂模式的创建目标。


其结构图如下图所示。

简单工厂模式的结构图


图 1 简单工厂模式的结构图

根据上图写出该模式的代码如下:

  1. public class Client {
  2. public static void main(String[] args) {
  3. }
  4. //抽象产品
  5. public interface Product {
  6. void show();
  7. }
  8. //具体产品:ProductA
  9. static class ConcreteProduct1 implements Product {
  10. public void show() {
  11. System.out.println("具体产品1显示...");
  12. }
  13. }
  14. //具体产品:ProductB
  15. static class ConcreteProduct2 implements Product {
  16. public void show() {
  17. System.out.println("具体产品2显示...");
  18. }
  19. }
  20. final class Const {
  21. static final int PRODUCT_A = 0;
  22. static final int PRODUCT_B = 1;
  23. static final int PRODUCT_C = 2;
  24. }
  25. static class SimpleFactory {
  26. public static Product makeProduct(int kind) {
  27. switch (kind) {
  28. case Const.PRODUCT_A:
  29. return new ConcreteProduct1();
  30. case Const.PRODUCT_B:
  31. return new ConcreteProduct2();
  32. }
  33. return null;
  34. }
  35. }
  36. }

 工厂方法模式(详解版)

在现实生活中社会分工越来越细,越来越专业化。各种产品有专门的工厂生产,彻底告别了自给自足的小农经济时代,这大大缩短了产品的生产周期,提高了生产效率。同样,在软件开发中能否做到软件对象的生产和使用相分离呢?能否在满足“开闭原则”的前提下,客户随意增删或改变对软件相关对象的使用呢?这就是本节要讨论的问题。

在《简单工厂模式》一节我们介绍了简单工厂模式,提到了简单工厂模式违背了开闭原则,而“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程。
  • 灵活性增强,对于新产品的创建,只需多写一个相应的工厂类。
  • 典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。

缺点:

  • 类的个数容易过多,增加复杂度
  • 增加了系统的抽象性和理解难度
  • 抽象产品只能生产一种产品,此弊端可使用抽象工厂模式解决。

应用场景:

  • 客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等。
  • 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
  • 客户不关心创建产品的细节,只关心产品的品牌

模式的结构与实现

工厂方法模式由抽象工厂、具体工厂、抽象产品和具体产品等4个要素构成。本节来分析其基本结构和实现方法。

1. 模式的结构

工厂方法模式的主要角色如下。

  1. 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
  2. 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  3. 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  4. 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。


其结构图如图 1 所示。
 

工厂方法模式的结构图


图1 工厂方法模式的结构图

2. 模式的实现

根据图 1 写出该模式的代码如下:

  1. package FactoryMethod;
  2. public class AbstractFactoryTest {
  3. public static void main(String[] args) {
  4. try {
  5. Product a;
  6. AbstractFactory af;
  7. af = (AbstractFactory) ReadXML1.getObject();
  8. a = af.newProduct();
  9. a.show();
  10. } catch (Exception e) {
  11. System.out.println(e.getMessage());
  12. }
  13. }
  14. }
  15. //抽象产品:提供了产品的接口
  16. interface Product {
  17. public void show();
  18. }
  19. //具体产品1:实现抽象产品中的抽象方法
  20. class ConcreteProduct1 implements Product {
  21. public void show() {
  22. System.out.println("具体产品1显示...");
  23. }
  24. }
  25. //具体产品2:实现抽象产品中的抽象方法
  26. class ConcreteProduct2 implements Product {
  27. public void show() {
  28. System.out.println("具体产品2显示...");
  29. }
  30. }
  31. //抽象工厂:提供了厂品的生成方法
  32. interface AbstractFactory {
  33. public Product newProduct();
  34. }
  35. //具体工厂1:实现了厂品的生成方法
  36. class ConcreteFactory1 implements AbstractFactory {
  37. public Product newProduct() {
  38. System.out.println("具体工厂1生成-->具体产品1...");
  39. return new ConcreteProduct1();
  40. }
  41. }
  42. //具体工厂2:实现了厂品的生成方法
  43. class ConcreteFactory2 implements AbstractFactory {
  44. public Product newProduct() {
  45. System.out.println("具体工厂2生成-->具体产品2...");
  46. return new ConcreteProduct2();
  47. }
  48. }
  49. package FactoryMethod;
  50. import javax.xml.parsers.*;
  51. import org.w3c.dom.*;
  52. import java.io.*;
  53. class ReadXML1 {
  54. //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
  55. public static Object getObject() {
  56. try {
  57. //创建文档对象
  58. DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
  59. DocumentBuilder builder = dFactory.newDocumentBuilder();
  60. Document doc;
  61. doc = builder.parse(new File("src/FactoryMethod/config1.xml"));
  62. //获取包含类名的文本节点
  63. NodeList nl = doc.getElementsByTagName("className");
  64. Node classNode = nl.item(0).getFirstChild();
  65. String cName = "FactoryMethod." + classNode.getNodeValue();
  66. //System.out.println("新类名:"+cName);
  67. //通过类名生成实例对象并将其返回
  68. Class<?> c = Class.forName(cName);
  69. Object obj = c.newInstance();
  70. return obj;
  71. } catch (Exception e) {
  72. e.printStackTrace();
  73. return null;
  74. }
  75. }
  76. }

 注意:该程序中用到了 XML 文件,如果想要获取该文件,请点击“下载”,就可以对其进行下载。

程序运行结果如下:

具体工厂1生成-->具体产品1...
具体产品1显示...


如果将 XML 配置文件中的 ConcreteFactory1 改为 ConcreteFactory2,则程序运行结果如下:

具体工厂2生成-->具体产品2...
具体产品2显示...

模式的应用实例

【例1】用工厂方法模式设计畜牧场。

分析:有很多种类的畜牧场,如养马场用于养马,养牛场用于养牛,所以该实例用工厂方法模式比较适合。

对养马场和养牛场等具体工厂类,只要定义一个生成动物的方法 newAnimal() 即可。由于要显示马类和牛类等具体产品类的图像,所以它们的构造函数中用到了 JPanel、JLabd 和 ImageIcon 等组件,并定义一个 show() 方法来显示它们。

客户端程序通过对象生成器类 ReadXML2 读取 XML 配置文件中的数据来决定养马还是养牛。其结构图如图 2 所示。
 

畜牧场结构图


图2 畜牧场结构图


注意:该程序中用到了 XML 文件,并且要显示马类和牛类等具体产品类的图像,如果想要获取 HTML 文件和图片,请点击“下载”,就可以对其进行下载。

程序代码如下:

  1. package FactoryMethod;
  2. import java.awt.*;
  3. import javax.swing.*;
  4. public class AnimalFarmTest {
  5. public static void main(String[] args) {
  6. try {
  7. Animal a;
  8. AnimalFarm af;
  9. af = (AnimalFarm) ReadXML2.getObject();
  10. a = af.newAnimal();
  11. a.show();
  12. } catch (Exception e) {
  13. System.out.println(e.getMessage());
  14. }
  15. }
  16. }
  17. //抽象产品:动物类
  18. interface Animal {
  19. public void show();
  20. }
  21. //具体产品:马类
  22. class Horse implements Animal {
  23. JScrollPane sp;
  24. JFrame jf = new JFrame("工厂方法模式测试");
  25. public Horse() {
  26. Container contentPane = jf.getContentPane();
  27. JPanel p1 = new JPanel();
  28. p1.setLayout(new GridLayout(1, 1));
  29. p1.setBorder(BorderFactory.createTitledBorder("动物:马"));
  30. sp = new JScrollPane(p1);
  31. contentPane.add(sp, BorderLayout.CENTER);
  32. JLabel l1 = new JLabel(new ImageIcon("src/A_Horse.jpg"));
  33. p1.add(l1);
  34. jf.pack();
  35. jf.setVisible(false);
  36. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //用户点击窗口关闭
  37. }
  38. public void show() {
  39. jf.setVisible(true);
  40. }
  41. }
  42. //具体产品:牛类
  43. class Cattle implements Animal {
  44. JScrollPane sp;
  45. JFrame jf = new JFrame("工厂方法模式测试");
  46. public Cattle() {
  47. Container contentPane = jf.getContentPane();
  48. JPanel p1 = new JPanel();
  49. p1.setLayout(new GridLayout(1, 1));
  50. p1.setBorder(BorderFactory.createTitledBorder("动物:牛"));
  51. sp = new JScrollPane(p1);
  52. contentPane.add(sp, BorderLayout.CENTER);
  53. JLabel l1 = new JLabel(new ImageIcon("src/A_Cattle.jpg"));
  54. p1.add(l1);
  55. jf.pack();
  56. jf.setVisible(false);
  57. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //用户点击窗口关闭
  58. }
  59. public void show() {
  60. jf.setVisible(true);
  61. }
  62. }
  63. //抽象工厂:畜牧场
  64. interface AnimalFarm {
  65. public Animal newAnimal();
  66. }
  67. //具体工厂:养马场
  68. class HorseFarm implements AnimalFarm {
  69. public Animal newAnimal() {
  70. System.out.println("新马出生!");
  71. return new Horse();
  72. }
  73. }
  74. //具体工厂:养牛场
  75. class CattleFarm implements AnimalFarm {
  76. public Animal newAnimal() {
  77. System.out.println("新牛出生!");
  78. return new Cattle();
  79. }
  80. }

  1. package FactoryMethod;
  2. import javax.xml.parsers.*;
  3. import org.w3c.dom.*;
  4. import java.io.*;
  5. class ReadXML2 {
  6. public static Object getObject() {
  7. try {
  8. DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
  9. DocumentBuilder builder = dFactory.newDocumentBuilder();
  10. Document doc;
  11. doc = builder.parse(new File("src/FactoryMethod/config2.xml"));
  12. NodeList nl = doc.getElementsByTagName("className");
  13. Node classNode = nl.item(0).getFirstChild();
  14. String cName = "FactoryMethod." + classNode.getNodeValue();
  15. System.out.println("新类名:" + cName);
  16. Class<?> c = Class.forName(cName);
  17. Object obj = c.newInstance();
  18. return obj;
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. return null;
  22. }
  23. }
  24. }

程序的运行结果如图 3 所示。
 

畜牧场养殖的运行结果


图3 畜牧场养殖的运行结果


注意:当需要生成的产品不多且不会增加,一个具体工厂类就可以完成任务时,可删除抽象工厂类。这时工厂方法模式将退化到简单工厂模式。

抽象工厂模式(详解版) 

前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机、计算机软件学院只培养计算机软件专业的学生等。

同种类称为同等级,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如农场里既养动物又种植物,电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。

本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,图 1 所示的是海尔工厂和 TCL 工厂所生产的电视机与空调对应的关系图。
 

电器工厂的产品等级与产品族


图1 电器工厂的产品等级与产品族

模式的定义与特点

抽象工厂(AbstractFactory)模式的定义:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。

使用抽象工厂模式一般要满足以下条件。

  • 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
  • 系统一次只可能消费其中某一族产品,即同族的产品一起使用。


抽象工厂模式除了具有工厂方法模式的优点外,其他主要优点如下。

  • 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
  • 当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。
  • 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。


其缺点是:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。

模式的结构与实现

抽象工厂模式同工厂方法模式一样,也是由抽象工厂、具体工厂、抽象产品和具体产品等 4 个要素构成,但抽象工厂中方法个数不同,抽象产品的个数也不同。现在我们来分析其基本结构和实现方法。

1. 模式的结构

抽象工厂模式的主要角色如下。

  1. 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
  2. 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  3. 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  4. 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。


抽象工厂模式的结构图如图 2 所示。
 

抽象工厂模式的结构图


图2 抽象工厂模式的结构图

2. 模式的实现

从图 2 可以看出抽象工厂模式的结构同工厂方法模式的结构相似,不同的是其产品的种类不止一个,所以创建产品的方法也不止一个。下面给出抽象工厂和具体工厂的代码。

(1) 抽象工厂:提供了产品的生成方法。

  1. interface AbstractFactory {
  2. public Product1 newProduct1();
  3. public Product2 newProduct2();
  4. }

(2) 具体工厂:实现了产品的生成方法。

  1. class ConcreteFactory1 implements AbstractFactory {
  2. public Product1 newProduct1() {
  3. System.out.println("具体工厂 1 生成-->具体产品 11...");
  4. return new ConcreteProduct11();
  5. }
  6. public Product2 newProduct2() {
  7. System.out.println("具体工厂 1 生成-->具体产品 21...");
  8. return new ConcreteProduct21();
  9. }
  10. }

模式的应用实例

【例1】用抽象工厂模式设计农场类。

分析:农场中除了像畜牧场一样可以养动物,还可以培养植物,如养马、养牛、种菜、种水果等,所以本实例比前面介绍的畜牧场类复杂,必须用抽象工厂模式来实现。

本例用抽象工厂模式来设计两个农场,一个是韶关农场用于养牛和种菜,一个是上饶农场用于养马和种水果,可以在以上两个农场中定义一个生成动物的方法 newAnimal() 和一个培养植物的方法 newPlant()。

对马类、牛类、蔬菜类和水果类等具体产品类,由于要显示它们的图像(点此下载图片),所以它们的构造函数中用到了 JPanel、JLabel 和 ImageIcon 等组件,并定义一个 show() 方法来显示它们。

客户端程序通过对象生成器类 ReadXML 读取 XML 配置文件中的数据来决定养什么动物和培养什么植物(点此下载 XML 文件)。其结构图如图 3 所示。
 

农场类的结构图


图3 农场类的结构图


程序代码如下:

  1. package AbstractFactory;
  2. import java.awt.*;
  3. import javax.swing.*;
  4. public class FarmTest {
  5. public static void main(String[] args) {
  6. try {
  7. Farm f;
  8. Animal a;
  9. Plant p;
  10. f = (Farm) ReadXML.getObject();
  11. a = f.newAnimal();
  12. p = f.newPlant();
  13. a.show();
  14. p.show();
  15. } catch (Exception e) {
  16. System.out.println(e.getMessage());
  17. }
  18. }
  19. }
  20. //抽象产品:动物类
  21. interface Animal {
  22. public void show();
  23. }
  24. //具体产品:马类
  25. class Horse implements Animal {
  26. JScrollPane sp;
  27. JFrame jf = new JFrame("抽象工厂模式测试");
  28. public Horse() {
  29. Container contentPane = jf.getContentPane();
  30. JPanel p1 = new JPanel();
  31. p1.setLayout(new GridLayout(1, 1));
  32. p1.setBorder(BorderFactory.createTitledBorder("动物:马"));
  33. sp = new JScrollPane(p1);
  34. contentPane.add(sp, BorderLayout.CENTER);
  35. JLabel l1 = new JLabel(new ImageIcon("src/A_Horse.jpg"));
  36. p1.add(l1);
  37. jf.pack();
  38. jf.setVisible(false);
  39. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//用户点击窗口关闭
  40. }
  41. public void show() {
  42. jf.setVisible(true);
  43. }
  44. }
  45. //具体产品:牛类
  46. class Cattle implements Animal {
  47. JScrollPane sp;
  48. JFrame jf = new JFrame("抽象工厂模式测试");
  49. public Cattle() {
  50. Container contentPane = jf.getContentPane();
  51. JPanel p1 = new JPanel();
  52. p1.setLayout(new GridLayout(1, 1));
  53. p1.setBorder(BorderFactory.createTitledBorder("动物:牛"));
  54. sp = new JScrollPane(p1);
  55. contentPane.add(sp, BorderLayout.CENTER);
  56. JLabel l1 = new JLabel(new ImageIcon("src/A_Cattle.jpg"));
  57. p1.add(l1);
  58. jf.pack();
  59. jf.setVisible(false);
  60. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//用户点击窗口关闭
  61. }
  62. public void show() {
  63. jf.setVisible(true);
  64. }
  65. }
  66. //抽象产品:植物类
  67. interface Plant {
  68. public void show();
  69. }
  70. //具体产品:水果类
  71. class Fruitage implements Plant {
  72. JScrollPane sp;
  73. JFrame jf = new JFrame("抽象工厂模式测试");
  74. public Fruitage() {
  75. Container contentPane = jf.getContentPane();
  76. JPanel p1 = new JPanel();
  77. p1.setLayout(new GridLayout(1, 1));
  78. p1.setBorder(BorderFactory.createTitledBorder("植物:水果"));
  79. sp = new JScrollPane(p1);
  80. contentPane.add(sp, BorderLayout.CENTER);
  81. JLabel l1 = new JLabel(new ImageIcon("src/P_Fruitage.jpg"));
  82. p1.add(l1);
  83. jf.pack();
  84. jf.setVisible(false);
  85. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//用户点击窗口关闭
  86. }
  87. public void show() {
  88. jf.setVisible(true);
  89. }
  90. }
  91. //具体产品:蔬菜类
  92. class Vegetables implements Plant {
  93. JScrollPane sp;
  94. JFrame jf = new JFrame("抽象工厂模式测试");
  95. public Vegetables() {
  96. Container contentPane = jf.getContentPane();
  97. JPanel p1 = new JPanel();
  98. p1.setLayout(new GridLayout(1, 1));
  99. p1.setBorder(BorderFactory.createTitledBorder("植物:蔬菜"));
  100. sp = new JScrollPane(p1);
  101. contentPane.add(sp, BorderLayout.CENTER);
  102. JLabel l1 = new JLabel(new ImageIcon("src/P_Vegetables.jpg"));
  103. p1.add(l1);
  104. jf.pack();
  105. jf.setVisible(false);
  106. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//用户点击窗口关闭
  107. }
  108. public void show() {
  109. jf.setVisible(true);
  110. }
  111. }
  112. //抽象工厂:农场类
  113. interface Farm {
  114. public Animal newAnimal();
  115. public Plant newPlant();
  116. }
  117. //具体工厂:韶关农场类
  118. class SGfarm implements Farm {
  119. public Animal newAnimal() {
  120. System.out.println("新牛出生!");
  121. return new Cattle();
  122. }
  123. public Plant newPlant() {
  124. System.out.println("蔬菜长成!");
  125. return new Vegetables();
  126. }
  127. }
  128. //具体工厂:上饶农场类
  129. class SRfarm implements Farm {
  130. public Animal newAnimal() {
  131. System.out.println("新马出生!");
  132. return new Horse();
  133. }
  134. public Plant newPlant() {
  135. System.out.println("水果长成!");
  136. return new Fruitage();
  137. }
  138. }

  1. package AbstractFactory;
  2. import javax.xml.parsers.*;
  3. import org.w3c.dom.*;
  4. import java.io.*;
  5. class ReadXML {
  6. public static Object getObject() {
  7. try {
  8. DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
  9. DocumentBuilder builder = dFactory.newDocumentBuilder();
  10. Document doc;
  11. doc = builder.parse(new File("src/AbstractFactory/config.xml"));
  12. NodeList nl = doc.getElementsByTagName("className");
  13. Node classNode = nl.item(0).getFirstChild();
  14. String cName = "AbstractFactory." + classNode.getNodeValue();
  15. System.out.println("新类名:" + cName);
  16. Class<?> c = Class.forName(cName);
  17. Object obj = c.newInstance();
  18. return obj;
  19. } catch (Exception e) {
  20. e.printStackTrace();
  21. return null;
  22. }
  23. }
  24. }

程序运行结果如图 4 所示。
 

农场养殖的运行结果


图4 农场养殖的运行结果

模式的应用场景

抽象工厂模式最早的应用是用于创建属于不同操作系统的视窗构件。如 Java 的 AWT 中的 Button 和 Text 等构件在 Windows 和 UNIX 中的本地实现是不同的。

抽象工厂模式通常适用于以下场景:

  1. 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
  2. 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
  3. 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。

模式的扩展

抽象工厂模式的扩展有一定的“开闭原则”倾斜性:

  1. 当增加一个新的产品族时只需增加一个新的具体工厂,不需要修改原代码,满足开闭原则。
  2. 当产品族中需要增加一个新种类的产品时,则所有的工厂类都需要进行修改,不满足开闭原则。


另一方面,当系统中只存在一个等级结构的产品时,抽象工厂模式将退化到工厂方法模式。

建造者模式(Bulider模式)详解 

在软件开发过程中有时需要创建一个复杂的对象,这个复杂对象通常由多个子部件按一定的步骤组合而成。例如,计算机是由 CPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的,采购员不可能自己去组装计算机,而是将计算机的配置要求告诉计算机销售公司,计算机销售公司安排技术人员去组装计算机,然后再交给要买计算机的采购员。

生活中这样的例子很多,如游戏中的不同角色,其性别、个性、能力、脸型、体型、服装、发型等特性都有所差异;还有汽车中的方向盘、发动机、车架、轮胎等部件也多种多样;每封电子邮件的发件人、收件人、主题、内容、附件等内容也各不相同。

以上所有这些产品都是由多个部件构成的,各个部件可以灵活选择,但其创建步骤都大同小异。这类产品的创建无法用前面介绍的工厂模式描述,只有建造者模式可以很好地描述该类产品的创建。

模式的定义与特点

建造者(Builder)模式的定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。

该模式的主要优点如下:

  1. 封装性好,构建和表示分离。
  2. 扩展性好,各个具体的建造者相互独立,有利于系统的解耦。
  3. 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。


其缺点如下:

  1. 产品的组成部分必须相同,这限制了其使用范围。
  2. 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。


建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。

模式的结构与实现

建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个要素构成,现在我们来分析其基本结构和实现方法。

1. 模式的结构

建造者(Builder)模式的主要角色如下。

  1. 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
  2. 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
  3. 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
  4. 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。


其结构图如图 1 所示。
 

建造者模式的结构图


图1 建造者模式的结构图

2. 模式的实现

图 1 给出了建造者(Builder)模式的主要结构,其相关类的代码如下。

(1) 产品角色:包含多个组成部件的复杂对象。

  1. class Product {
  2. private String partA;
  3. private String partB;
  4. private String partC;
  5. public void setPartA(String partA) {
  6. this.partA = partA;
  7. }
  8. public void setPartB(String partB) {
  9. this.partB = partB;
  10. }
  11. public void setPartC(String partC) {
  12. this.partC = partC;
  13. }
  14. public void show() {
  15. //显示产品的特性
  16. }
  17. }

 (2) 抽象建造者:包含创建产品各个子部件的抽象方法。

  1. abstract class Builder {
  2. //创建产品对象
  3. protected Product product = new Product();
  4. public abstract void buildPartA();
  5. public abstract void buildPartB();
  6. public abstract void buildPartC();
  7. //返回产品对象
  8. public Product getResult() {
  9. return product;
  10. }
  11. }

(3) 具体建造者:实现了抽象建造者接口。

  1. public class ConcreteBuilder extends Builder {
  2. public void buildPartA() {
  3. product.setPartA("建造 PartA");
  4. }
  5. public void buildPartB() {
  6. product.setPartB("建造 PartB");
  7. }
  8. public void buildPartC() {
  9. product.setPartC("建造 PartC");
  10. }
  11. }

(4) 指挥者:调用建造者中的方法完成复杂对象的创建。

  1. class Director {
  2. private Builder builder;
  3. public Director(Builder builder) {
  4. this.builder = builder;
  5. }
  6. //产品构建与组装方法
  7. public Product construct() {
  8. builder.buildPartA();
  9. builder.buildPartB();
  10. builder.buildPartC();
  11. return builder.getResult();
  12. }
  13. }

(5) 客户类。

  1. public class Client {
  2. public static void main(String[] args) {
  3. Builder builder = new ConcreteBuilder();
  4. Director director = new Director(builder);
  5. Product product = director.construct();
  6. product.show();
  7. }
  8. }

模式的应用实例

【例1】用建造者(Builder)模式描述客厅装修。

分析:客厅装修是一个复杂的过程,它包含墙体的装修、电视机的选择、沙发的购买与布局等。客户把装修要求告诉项目经理,项目经理指挥装修工人一步步装修,最后完成整个客厅的装修与布局,所以本实例用建造者模式实现比较适合。

这里客厅是产品,包括墙、电视和沙发等组成部分。具体装修工人是具体建造者,他们负责装修与墙、电视和沙发的布局。项目经理是指挥者,他负责指挥装修工人进行装修。

另外,客厅类中提供了 show() 方法,可以将装修效果图显示出来(点此下载装修效果图的图片)。客户端程序通过对象生成器类 ReadXML 读取 XML 配置文件中的装修方案数据(点此下载 XML 文件),调用项目经理进行装修。其类图如图 2 所示。
 

客厅装修的结构图


图2 客厅装修的结构图


程序代码如下:

  1. package Builder;
  2. import java.awt.*;
  3. import javax.swing.*;
  4. public class ParlourDecorator {
  5. public static void main(String[] args) {
  6. try {
  7. Decorator d;
  8. d = (Decorator) ReadXML.getObject();
  9. ProjectManager m = new ProjectManager(d);
  10. Parlour p = m.decorate();
  11. p.show();
  12. } catch (Exception e) {
  13. System.out.println(e.getMessage());
  14. }
  15. }
  16. }
  17. //产品:客厅
  18. class Parlour {
  19. private String wall; //墙
  20. private String TV; //电视
  21. private String sofa; //沙发
  22. public void setWall(String wall) {
  23. this.wall = wall;
  24. }
  25. public void setTV(String TV) {
  26. this.TV = TV;
  27. }
  28. public void setSofa(String sofa) {
  29. this.sofa = sofa;
  30. }
  31. public void show() {
  32. JFrame jf = new JFrame("建造者模式测试");
  33. Container contentPane = jf.getContentPane();
  34. JPanel p = new JPanel();
  35. JScrollPane sp = new JScrollPane(p);
  36. String parlour = wall + TV + sofa;
  37. JLabel l = new JLabel(new ImageIcon("src/" + parlour + ".jpg"));
  38. p.setLayout(new GridLayout(1, 1));
  39. p.setBorder(BorderFactory.createTitledBorder("客厅"));
  40. p.add(l);
  41. contentPane.add(sp, BorderLayout.CENTER);
  42. jf.pack();
  43. jf.setVisible(true);
  44. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  45. }
  46. }
  47. //抽象建造者:装修工人
  48. abstract class Decorator {
  49. //创建产品对象
  50. protected Parlour product = new Parlour();
  51. public abstract void buildWall();
  52. public abstract void buildTV();
  53. public abstract void buildSofa();
  54. //返回产品对象
  55. public Parlour getResult() {
  56. return product;
  57. }
  58. }
  59. //具体建造者:具体装修工人1
  60. class ConcreteDecorator1 extends Decorator {
  61. public void buildWall() {
  62. product.setWall("w1");
  63. }
  64. public void buildTV() {
  65. product.setTV("TV1");
  66. }
  67. public void buildSofa() {
  68. product.setSofa("sf1");
  69. }
  70. }
  71. //具体建造者:具体装修工人2
  72. class ConcreteDecorator2 extends Decorator {
  73. public void buildWall() {
  74. product.setWall("w2");
  75. }
  76. public void buildTV() {
  77. product.setTV("TV2");
  78. }
  79. public void buildSofa() {
  80. product.setSofa("sf2");
  81. }
  82. }
  83. //指挥者:项目经理
  84. class ProjectManager {
  85. private Decorator builder;
  86. public ProjectManager(Decorator builder) {
  87. this.builder = builder;
  88. }
  89. //产品构建与组装方法
  90. public Parlour decorate() {
  91. builder.buildWall();
  92. builder.buildTV();
  93. builder.buildSofa();
  94. return builder.getResult();
  95. }
  96. }
  97. package Builder;
  98. import org.w3c.dom.Document;
  99. import org.w3c.dom.Node;
  100. import org.w3c.dom.NodeList;
  101. import javax.xml.parsers.DocumentBuilder;
  102. import javax.xml.parsers.DocumentBuilderFactory;
  103. import java.io.File;
  104. class ReadXML {
  105. public static Object getObject() {
  106. try {
  107. DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
  108. DocumentBuilder builder = dFactory.newDocumentBuilder();
  109. Document doc;
  110. doc = builder.parse(new File("src/Builder/config.xml"));
  111. NodeList nl = doc.getElementsByTagName("className");
  112. Node classNode = nl.item(0).getFirstChild();
  113. String cName = "Builder." + classNode.getNodeValue();
  114. System.out.println("新类名:" + cName);
  115. Class<?> c = Class.forName(cName);
  116. Object obj = c.newInstance();
  117. return obj;
  118. } catch (Exception e) {
  119. e.printStackTrace();
  120. return null;
  121. }
  122. }
  123. }

 程序运行结果如图 3 所示。
 

客厅装修的运行结果


图3 客厅装修的运行结果

模式的应用场景

建造者模式唯一区别于工厂模式的是针对复杂对象的创建。也就是说,如果创建简单对象,通常都是使用工厂模式进行创建,而如果创建复杂对象,就可以考虑使用建造者模式。

当需要创建的产品具备复杂创建过程时,可以抽取出共性创建过程,然后交由具体实现类自定义创建流程,使得同样的创建行为可以生产出不同的产品,分离了创建与表示,使创建产品的灵活性大大增加。

建造者模式主要适用于以下应用场景:

  • 相同的方法,不同的执行顺序,产生不同的结果。
  • 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
  • 产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
  • 初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。

建造者模式和工厂模式的区别

通过前面的学习,我们已经了解了建造者模式,那么它和工厂模式有什么区别呢?

  • 建造者模式更加注重方法的调用顺序,工厂模式注重创建对象。
  • 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的对象都一样
  • 关注重点不一样,工厂模式只需要把对象创建出来就可以了,而建造者模式不仅要创建出对象,还要知道对象由哪些部件组成。
  • 建造者模式根据建造过程中的顺序不一样,最终对象部件组成也不一样。

模式的扩展

建造者(Builder)模式在应用过程中可以根据需要改变,如果创建的产品种类只有一种,只需要一个具体建造者,这时可以省略掉抽象建造者,甚至可以省略掉指挥者角色。

创建型模式应用实验 

创建型模式(Creational Pattern)的主要特点是将对象的创建与使用分离,根据对象的创建与组合方式的不同,创建型模式可分为单例(Singleton)模式原型(Prototype)模式工厂方法(Factory Method)模式抽象工厂(Abstract Factory)模式建造者(Builder)模式 5 种。

本实验的主要目的如下。

  1. 了解 5 种“创建型模式”的定义、特点和工作原理。
  2. 理解 5 种“创建型模式”的结构、实现和应用场景。
  3. 学会应用 5 种“创建型模式”进行软件开发。

实验原理

1. 创建型模式的工作原理

创建型模式隐藏了对象的创建细节,对象的创建由相关的工厂来完成,使用者不需要关注对象的创建细节,这样可以降低系统的耦合度。

创建型模式共 5 种,它们分别是单例模式原型模式工厂方法模式抽象工厂模式建造者模式,其工作原理在各自的教程中也都有详细的介绍,每种模式的实验大概要花 2 个学时,大家可以根据实验计划来选做若干个实验,下面以工厂方法模式为例,介绍其实验过程。

2. 工厂方法模式的工作原理

工厂方法模式(Factory Method Pattern),也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成。其结构图如图 1 所示。
 

工厂方法模式的类图


图1 工厂方法模式的类图


工厂方法模式包含如下角色。

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品(Concrete Product):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一 一对应。


工厂方法模式的特点是当系统扩展需要添加新的产品对象时,仅仅需要添加一个具体产品对象以及一个具体工厂对象,原有工厂对象不需要进行任何修改,也不需要修改客户端,很好地符合了“开闭原则”。

实验内容

(1) 用工厂方法模式设计一个电动自行车工厂的模拟程序。

要求:要为每种品牌的电动自行车提供一个子工厂,如爱玛工厂专门负责生产爱玛(Aima)牌电动自行车,雅迪工厂专门负责生产雅迪(Yadea)牌电动自行车。如果今后需要生产台铃(Tailg)牌电动自行车,只需要增加一个新的台铃电动自行车工厂即可,无须修改原有代码,使得整个系统具有更强的灵活性和可扩展性。(点此下载程序运行后所要显示的图片)。

(2) 按照以上要求设计类图和编写 Java 源程序。

实验要求

所设计的实验程序要满足以下两点。

  1. 体现“工厂方法模式”的工作原理。
  2. 符合面向对象中的“开闭原则”。

实验步骤

(1) 用 UML 设计“电动自行车工厂模拟程序”的结构图。“电动自行车工厂模拟程序”的结构图如图 2 所示。
 

电动自行车工厂模拟程序的结构图


图2 电动自行车工厂模拟程序的结构图


(2) 根据结构图写出“电动自行车工厂模拟程序”的源代码。

① 电动自行车工厂模拟程序的源代码如下。

  1. package FactoryMethod;
  2. import java.awt.*;
  3. import javax.swing.*;
  4. public class BicycleFactoryTest {
  5. public static void main(String[] args) {
  6. try {
  7. Bicycle a;
  8. BicycleFactory bf;
  9. bf = (BicycleFactory) ReadXML.getObject();
  10. a = bf.produce();
  11. a.show();
  12. } catch (Exception e) {
  13. System.out.println(e.getMessage());
  14. }
  15. }
  16. }
  17. //抽象产品:自行车
  18. interface Bicycle {
  19. public void show();
  20. }
  21. //具体产品:爱玛自行车
  22. class AimaBicycle implements Bicycle {
  23. JScrollPane sp;
  24. JFrame jf = new JFrame("工厂方法模式测试");
  25. public AimaBicycle() {
  26. JPanel p1 = new JPanel();
  27. p1.setLayout(new GridLayout(1, 1));
  28. p1.setBorder(BorderFactory.createTitledBorder("爱玛自行车"));
  29. JLabel l1 = new JLabel(new ImageIcon("src/FactoryMethod/AIMABicycle.jpg"));
  30. p1.add(l1);
  31. sp = new JScrollPane(p1);
  32. Container contentPane = jf.getContentPane();
  33. contentPane.add(sp, BorderLayout.CENTER);
  34. jf.pack();
  35. jf.setVisible(false);
  36. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //用户点击窗口关闭
  37. }
  38. public void show() {
  39. jf.setVisible(true);
  40. }
  41. }
  42. //具体产品:雅迪自行车
  43. class YadeaBicycle implements Bicycle {
  44. JScrollPane sp;
  45. JFrame jf = new JFrame("工厂方法模式测试");
  46. public YadeaBicycle() {
  47. JPanel p1 = new JPanel();
  48. p1.setLayout(new GridLayout(1, 1));
  49. p1.setBorder(BorderFactory.createTitledBorder("雅迪自行车"));
  50. JLabel l1 = new JLabel(new ImageIcon("src/FactoryMethod/YadeaBicycle.jpg"));
  51. p1.add(l1);
  52. Container contentPane = jf.getContentPane();
  53. sp = new JScrollPane(p1);
  54. contentPane.add(sp, BorderLayout.CENTER);
  55. jf.pack();
  56. jf.setVisible(false);
  57. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //用户点击窗口关闭
  58. }
  59. public void show() {
  60. jf.setVisible(true);
  61. }
  62. }
  63. //抽象工厂:自行车工厂
  64. interface BicycleFactory {
  65. public Bicycle produce();
  66. }
  67. //具体工厂:爱玛工厂
  68. class AimaFactory implements BicycleFactory {
  69. public Bicycle produce() {
  70. System.out.println("爱玛自行车生产了!");
  71. return new AimaBicycle();
  72. }
  73. }
  74. //具体工厂:雅迪工厂
  75. class YadeaFactory implements BicycleFactory {
  76. public Bicycle produce() {
  77. System.out.println("雅迪自行车生产了!");
  78. return new YadeaBicycle();
  79. }
  80. }

② 对象生成器的源代码如下。

  1. package FactoryMethod;
  2. import org.w3c.dom.Document;
  3. import org.w3c.dom.Node;
  4. import org.w3c.dom.NodeList;
  5. import javax.xml.parsers.DocumentBuilder;
  6. import javax.xml.parsers.DocumentBuilderFactory;
  7. import java.io.File;
  8. class ReadXML {
  9. public static Object getObject() {
  10. try {
  11. DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
  12. DocumentBuilder builder = dFactory.newDocumentBuilder();
  13. Document doc;
  14. doc = builder.parse(new File("src/FactoryMethod/config.xml"));
  15. NodeList nl = doc.getElementsByTagName("className");
  16. Node classNode = nl.item(0).getFirstChild();
  17. String cName = "FactoryMethod." + classNode.getNodeValue();
  18. System.out.println("新类名:" + cName);
  19. Class<?> c = Class.forName(cName);
  20. Object obj = c.newInstance();
  21. return obj;
  22. } catch (Exception e) {
  23. e.printStackTrace();
  24. return null;
  25. }
  26. }
  27. }

③XML 配置文件的代码如下。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <config>
  3. <className>AimaFactory</className>
  4. </config>

(3) 上机测试程序,查看运行结果。

“电动自行车工厂模拟程序”的运行结果如图 3 所示。
 

电动自行车工厂模拟程序的运行结果


图3 电动自行车工厂模拟程序的运行结果


(4) 按同样的步骤设计其他“创建型模式”的程序实例。

 结构型模式概述(结构型模式的分类)

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

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

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

  1. 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
  2. 适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
  3. 桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
  4. 装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
  5. 外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
  6. 享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
  7. 组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。


以上 7 种结构型模式,除了适配器模式分为类结构型模式和对象结构型模式两种,其他的全部属于对象结构型模式,下面我们会分别、详细地介绍它们的特点、结构与应用。

代理模式(代理设计模式)详解

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

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

代理模式的定义与特点

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

代理模式的主要优点有:

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


其主要缺点是:

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

那么如何解决以上提到的缺点呢?答案是可以使用动态代理方式

代理模式的结构与实现

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

1. 模式的结构

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

  1. 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  2. 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  3. 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。


其结构图如图 1 所示。
 

代理模式的结构图


图1 代理模式的结构图


在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。

根据代理的创建时期,代理模式分为静态代理和动态代理。

  • 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
  • 动态:在程序运行时,运用反射机制动态创建而成

2. 模式的实现

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

  1. package proxy;
  2. public class ProxyTest {
  3. public static void main(String[] args) {
  4. Proxy proxy = new Proxy();
  5. proxy.Request();
  6. }
  7. }
  8. //抽象主题
  9. interface Subject {
  10. void Request();
  11. }
  12. //真实主题
  13. class RealSubject implements Subject {
  14. public void Request() {
  15. System.out.println("访问真实主题方法...");
  16. }
  17. }
  18. //代理
  19. class Proxy implements Subject {
  20. private RealSubject realSubject;
  21. public void Request() {
  22. if (realSubject == null) {
  23. realSubject = new RealSubject();
  24. }
  25. preRequest();
  26. realSubject.Request();
  27. postRequest();
  28. }
  29. public void preRequest() {
  30. System.out.println("访问真实主题之前的预处理。");
  31. }
  32. public void postRequest() {
  33. System.out.println("访问真实主题之后的后续处理。");
  34. }
  35. }

 程序运行的结果如下:

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

代理模式的应用实例

【例1】韶关“天街e角”公司是一家婺源特产公司的代理公司,用代理模式实现。

分析:本实例中的“婺源特产公司”经营许多婺源特产,它是真实主题,提供了显示特产的 display() 方法,可以用窗体程序实现(点此下载该实例所要显示的图片)。而韶关“天街e角”公司是婺源特产公司特产的代理,通过调用婺源特产公司的 display() 方法显示代理产品,当然它可以增加一些额外的处理,如包裝或加价等。客户可通过“天街e角”代理公司间接访问“婺源特产公司”的产品,图 2 所示是公司的结构图。
 

韶关“天街e角”公园的结构图


图2 韶关“天街e角”公司的结构图


程序代码如下:

  1. package proxy;
  2. import java.awt.*;
  3. import javax.swing.*;
  4. public class WySpecialtyProxy {
  5. public static void main(String[] args) {
  6. SgProxy proxy = new SgProxy();
  7. proxy.display();
  8. }
  9. }
  10. //抽象主题:特产
  11. interface Specialty {
  12. void display();
  13. }
  14. //真实主题:婺源特产
  15. class WySpecialty extends JFrame implements Specialty {
  16. private static final long serialVersionUID = 1L;
  17. public WySpecialty() {
  18. super("韶关代理婺源特产测试");
  19. this.setLayout(new GridLayout(1, 1));
  20. JLabel l1 = new JLabel(new ImageIcon("src/proxy/WuyuanSpecialty.jpg"));
  21. this.add(l1);
  22. this.pack();
  23. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  24. }
  25. public void display() {
  26. this.setVisible(true);
  27. }
  28. }
  29. //代理:韶关代理
  30. class SgProxy implements Specialty {
  31. private WySpecialty realSubject = new WySpecialty();
  32. public void display() {
  33. preRequest();
  34. realSubject.display();
  35. postRequest();
  36. }
  37. public void preRequest() {
  38. System.out.println("韶关代理婺源特产开始。");
  39. }
  40. public void postRequest() {
  41. System.out.println("韶关代理婺源特产结束。");
  42. }
  43. }

 程序运行结果如图 3 所示。

韶关“天街e角”公司的代理产品


图3 韶关“天街e角”公司的代理产品

代理模式的应用场景

当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。

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

  • 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
  • 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
  • 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
  • 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
  • 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。

代理模式的扩展

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

  1. 真实主题与代理主题一一对应,增加真实主题也要增加代理。
  2. 设计代理以前真实主题必须事先存在,不太灵活。采用动态代理模式可以解决以上问题,如 SpringAOP,其结构图如图 4 所示。

动态代理模式的结构图


图4 动态代理模式的结构图

适配器模式(Adapter模式)详解

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

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

模式的定义与特点

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

该模式的主要优点如下。

  • 客户端通过适配器可以透明地调用目标接口。
  • 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
  • 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
  • 在很多业务场景中符合开闭原则。


其缺点是:

  • 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
  • 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。

模式的结构与实现

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

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

1. 模式的结构

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

  1. 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  2. 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  3. 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。


类适配器模式的结构图如图 1 所示。
 

类适配器模式的结构图


图1 类适配器模式的结构图


对象适配器模式的结构图如图 2 所示。
 

对象适配器模式的结构图


图2 对象适配器模式的结构图

2. 模式的实现

(1) 类适配器模式的代码如下。

  1. package adapter;
  2. //目标接口
  3. interface Target
  4. {
  5. public void request();
  6. }
  7. //适配者接口
  8. class Adaptee
  9. {
  10. public void specificRequest()
  11. {
  12. System.out.println("适配者中的业务代码被调用!");
  13. }
  14. }
  15. //类适配器类
  16. class ClassAdapter extends Adaptee implements Target
  17. {
  18. public void request()
  19. {
  20. specificRequest();
  21. }
  22. }
  23. //客户端代码
  24. public class ClassAdapterTest
  25. {
  26. public static void main(String[] args)
  27. {
  28. System.out.println("类适配器模式测试:");
  29. Target target = new ClassAdapter();
  30. target.request();
  31. }
  32. }

程序的运行结果如下:

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


(2)对象适配器模式的代码如下。

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

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

程序的运行结果如下:

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

模式的应用实例

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

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

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

我们把客户端想访问的新能源发动机的适配器的名称放在 XML 配置文件中(点此下载 XML 文件),客户端可以通过对象生成器类 ReadXML 去读取。这样,客户端就可以通过 Motor 接口随便使用任意一种新能源发动机去驱动汽车,图 3 所示是其结构图。
 

发动机适配器的结构图


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


程序代码如下:

  1. package adapter;
  2. //目标:发动机
  3. interface Motor
  4. {
  5. public void drive();
  6. }
  7. //适配者1:电能发动机
  8. class ElectricMotor
  9. {
  10. public void electricDrive()
  11. {
  12. System.out.println("电能发动机驱动汽车!");
  13. }
  14. }
  15. //适配者2:光能发动机
  16. class OpticalMotor
  17. {
  18. public void opticalDrive()
  19. {
  20. System.out.println("光能发动机驱动汽车!");
  21. }
  22. }
  23. //电能适配器
  24. class ElectricAdapter implements Motor
  25. {
  26. private ElectricMotor emotor;
  27. public ElectricAdapter()
  28. {
  29. emotor=new ElectricMotor();
  30. }
  31. public void drive()
  32. {
  33. emotor.electricDrive();
  34. }
  35. }
  36. //光能适配器
  37. class OpticalAdapter implements Motor
  38. {
  39. private OpticalMotor omotor;
  40. public OpticalAdapter()
  41. {
  42. omotor=new OpticalMotor();
  43. }
  44. public void drive()
  45. {
  46. omotor.opticalDrive();
  47. }
  48. }
  49. //客户端代码
  50. public class MotorAdapterTest
  51. {
  52. public static void main(String[] args)
  53. {
  54. System.out.println("适配器模式测试:");
  55. Motor motor=(Motor)ReadXML.getObject();
  56. motor.drive();
  57. }
  58. }

  1. package adapter;
  2. import javax.xml.parsers.*;
  3. import org.w3c.dom.*;
  4. import java.io.*;
  5. class ReadXML
  6. {
  7. public static Object getObject()
  8. {
  9. try
  10. {
  11. DocumentBuilderFactory dFactory=DocumentBuilderFactory.newInstance();
  12. DocumentBuilder builder=dFactory.newDocumentBuilder();
  13. Document doc;
  14. doc=builder.parse(new File("src/adapter/config.xml"));
  15. NodeList nl=doc.getElementsByTagName("className");
  16. Node classNode=nl.item(0).getFirstChild();
  17. String cName="adapter."+classNode.getNodeValue();
  18. Class<?> c=Class.forName(cName);
  19. Object obj=c.newInstance();
  20. return obj;
  21. }
  22. catch(Exception e)
  23. {
  24. e.printStackTrace();
  25. return null;
  26. }
  27. }
  28. }

程序的运行结果如下:

适配器模式测试:
电能发动机驱动汽车!


注意:如果将配置文件中的 ElectricAdapter 改为 OpticalAdapter,则运行结果如下:

适配器模式测试:
光能发动机驱动汽车!

模式的应用场景

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

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

模式的扩展

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

双向适配器模式的结构图


图4 双向适配器模式的结构图


程序代码如下:

  1. package adapter;
  2. //目标接口
  3. interface TwoWayTarget
  4. {
  5. public void request();
  6. }
  7. //适配者接口
  8. interface TwoWayAdaptee
  9. {
  10. public void specificRequest();
  11. }
  12. //目标实现
  13. class TargetRealize implements TwoWayTarget
  14. {
  15. public void request()
  16. {
  17. System.out.println("目标代码被调用!");
  18. }
  19. }
  20. //适配者实现
  21. class AdapteeRealize implements TwoWayAdaptee
  22. {
  23. public void specificRequest()
  24. {
  25. System.out.println("适配者代码被调用!");
  26. }
  27. }
  28. //双向适配器
  29. class TwoWayAdapter implements TwoWayTarget,TwoWayAdaptee
  30. {
  31. private TwoWayTarget target;
  32. private TwoWayAdaptee adaptee;
  33. public TwoWayAdapter(TwoWayTarget target)
  34. {
  35. this.target=target;
  36. }
  37. public TwoWayAdapter(TwoWayAdaptee adaptee)
  38. {
  39. this.adaptee=adaptee;
  40. }
  41. public void request()
  42. {
  43. adaptee.specificRequest();
  44. }
  45. public void specificRequest()
  46. {
  47. target.request();
  48. }
  49. }
  50. //客户端代码
  51. public class TwoWayAdapterTest
  52. {
  53. public static void main(String[] args)
  54. {
  55. System.out.println("目标通过双向适配器访问适配者:");
  56. TwoWayAdaptee adaptee=new AdapteeRealize();
  57. TwoWayTarget target=new TwoWayAdapter(adaptee);
  58. target.request();
  59. System.out.println("-------------------");
  60. System.out.println("适配者通过双向适配器访问目标:");
  61. target=new TargetRealize();
  62. adaptee=new TwoWayAdapter(target);
  63. adaptee.specificRequest();
  64. }
  65. }

 程序的运行结果如下:

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

桥接模式(Bridge模式)详解

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

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

桥接模式的定义与特点

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

通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。

桥接(Bridge)模式的优点是:

  • 抽象与实现分离,扩展能力强
  • 符合开闭原则
  • 符合合成复用原则
  • 其实现细节对客户透明


缺点是:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。

桥接模式的结构与实现

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

1. 模式的结构

桥接(Bridge)模式包含以下主要角色。

  1. 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
  2. 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  3. 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
  4. 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。


其结构图如图 1 所示。
 

桥接模式的结构图


图1 桥接模式的结构图

2. 模式的实现

桥接模式的代码如下:

  1. package bridge;
  2. public class BridgeTest {
  3. public static void main(String[] args) {
  4. Implementor imple = new ConcreteImplementorA();
  5. Abstraction abs = new RefinedAbstraction(imple);
  6. abs.Operation();
  7. }
  8. }
  9. //实现化角色
  10. interface Implementor {
  11. public void OperationImpl();
  12. }
  13. //具体实现化角色
  14. class ConcreteImplementorA implements Implementor {
  15. public void OperationImpl() {
  16. System.out.println("具体实现化(Concrete Implementor)角色被访问");
  17. }
  18. }
  19. //抽象化角色
  20. abstract class Abstraction {
  21. protected Implementor imple;
  22. protected Abstraction(Implementor imple) {
  23. this.imple = imple;
  24. }
  25. public abstract void Operation();
  26. }
  27. //扩展抽象化角色
  28. class RefinedAbstraction extends Abstraction {
  29. protected RefinedAbstraction(Implementor imple) {
  30. super(imple);
  31. }
  32. public void Operation() {
  33. System.out.println("扩展抽象化(Refined Abstraction)角色被访问");
  34. imple.OperationImpl();
  35. }
  36. }

 程序的运行结果如下:

扩展抽象化(Refined Abstraction)角色被访问
具体实现化(Concrete Implementor)角色被访问

桥接模式的应用实例

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

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

本实例按用途分可选钱包(Wallet)和挎包(HandBag),按颜色分可选黄色(Yellow)和红色(Red)。可以按两个维度定义为颜色类和包类。(点此下载本实例所要显示的包的图片)。

颜色类(Color)是一个维度,定义为实现化角色,它有两个具体实现化角色:黄色和红色,通过 getColor() 方法可以选择颜色;包类(Bag)是另一个维度,定义为抽象化角色,它有两个扩展抽象化角色:挎包和钱包,它包含了颜色类对象,通过 getName() 方法可以选择相关颜色的挎包和钱包。

客户类通过 ReadXML 类从 XML 配置文件中获取包信息(点此下载 XML 配置文件),并把选到的产品通过窗体显示出现,图 2 所示是其结构图。
 

女士皮包选购的结构图


图2 女士皮包选购的结构图


程序代码如下:

  1. package bridge;
  2. import org.w3c.dom.NodeList;
  3. import javax.swing.*;
  4. import javax.xml.parsers.DocumentBuilder;
  5. import javax.xml.parsers.DocumentBuilderFactory;
  6. import java.awt.*;
  7. public class BagManage {
  8. public static void main(String[] args) {
  9. Color color;
  10. Bag bag;
  11. color = (Color) ReadXML.getObject("color");
  12. bag = (Bag) ReadXML.getObject("bag");
  13. bag.setColor(color);
  14. String name = bag.getName();
  15. show(name);
  16. }
  17. public static void show(String name) {
  18. JFrame jf = new JFrame("桥接模式测试");
  19. Container contentPane = jf.getContentPane();
  20. JPanel p = new JPanel();
  21. JLabel l = new JLabel(new ImageIcon("src/bridge/" + name + ".jpg"));
  22. p.setLayout(new GridLayout(1, 1));
  23. p.setBorder(BorderFactory.createTitledBorder("女士皮包"));
  24. p.add(l);
  25. contentPane.add(p, BorderLayout.CENTER);
  26. jf.pack();
  27. jf.setVisible(true);
  28. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  29. }
  30. }
  31. //实现化角色:颜色
  32. interface Color {
  33. String getColor();
  34. }
  35. //具体实现化角色:黄色
  36. class Yellow implements Color {
  37. public String getColor() {
  38. return "yellow";
  39. }
  40. }
  41. //具体实现化角色:红色
  42. class Red implements Color {
  43. public String getColor() {
  44. return "red";
  45. }
  46. }
  47. //抽象化角色:包
  48. abstract class Bag {
  49. protected Color color;
  50. public void setColor(Color color) {
  51. this.color = color;
  52. }
  53. public abstract String getName();
  54. }
  55. //扩展抽象化角色:挎包
  56. class HandBag extends Bag {
  57. public String getName() {
  58. return color.getColor() + "HandBag";
  59. }
  60. }
  61. //扩展抽象化角色:钱包
  62. class Wallet extends Bag {
  63. public String getName() {
  64. return color.getColor() + "Wallet";
  65. }
  66. }
  67. package bridge;
  68. import javax.xml.parsers.*;
  69. import org.w3c.dom.*;
  70. import java.io.*;
  71. class ReadXML {
  72. public static Object getObject(String args) {
  73. try {
  74. DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
  75. DocumentBuilder builder = dFactory.newDocumentBuilder();
  76. Document doc;
  77. doc = builder.parse(new File("src/bridge/config.xml"));
  78. NodeList nl = doc.getElementsByTagName("className");
  79. Node classNode = null;
  80. if (args.equals("color")) {
  81. classNode = nl.item(0).getFirstChild();
  82. } else if (args.equals("bag")) {
  83. classNode = nl.item(1).getFirstChild();
  84. }
  85. String cName = "bridge." + classNode.getNodeValue();
  86. Class<?> c = Class.forName(cName);
  87. Object obj = c.newInstance();
  88. return obj;
  89. } catch (Exception e) {
  90. e.printStackTrace();
  91. return null;
  92. }
  93. }
  94. }

 程序的运行结果如图 3 所示。
 

女士皮包选购的运行结果1


图3 女士皮包选购的运行结果1


如果将 XML 配置文件按如下修改:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <config>
  3. <className>Red</className>
  4. <className>Wallet</className>
  5. </config>

 则程序的运行结果如图 4 所示。
 

女士皮包选购的运行结果2


图4 女士皮包选购的运行结果2

桥接模式的应用场景

当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。

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

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


桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。

因为父类拥有的方法,子类也会继承得到,无论子类需不需要,这说明继承具备强侵入性(父类代码侵入子类),同时会导致子类臃肿。因此,在设计模式中,有一个原则为优先使用组合/聚合,而不是继承。
 


很多时候,我们分不清该使用继承还是组合/聚合或其他方式等,其实可以从现实语义进行思考。因为软件最终还是提供给现实生活中的人使用的,是服务于人类社会的,软件是具备现实场景的。当我们从纯代码角度无法看清问题时,现实角度可能会提供更加开阔的思路。

桥接模式模式的扩展

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

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


图5 桥接模式与适配器模式联用的结构图

装饰器模式(装饰设计模式)详解

上班族大多都有睡懒觉的习惯,每天早上上班时间都很紧张,于是很多人为了多睡一会,就会用方便的方式解决早餐问题。有些人早餐可能会吃煎饼,煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么“加码”,都还是一个煎饼。在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等,都是装饰器模式。

在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰器模式来实现。

装饰器模式的定义与特点

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

装饰器模式的主要优点有:

  • 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
  • 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果
  • 装饰器模式完全遵守开闭原则


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

装饰器模式的结构与实现

通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰器模式的目标。下面来分析其基本结构和实现方法。

1. 模式的结构

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

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


装饰器模式的结构图如图 1 所示。
 

装饰模式的结构图


图1 装饰器模式的结构图

2. 模式的实现

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

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

 程序运行结果如下:

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

装饰器模式的应用实例

【例1】用装饰器模式实现游戏角色“莫莉卡·安斯兰”的变身。

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

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


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


程序代码如下:

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

 程序运行结果如图 3 所示。
 

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


图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 s = in.readLine();

装饰器模式的扩展

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

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

只有一个具体构件的装饰模式


图4 只有一个具体构件的装饰器模式


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

只有一个具体装饰的装饰模式


图5 只有一个具体装饰的装饰器模式

外观模式(Facade模式)详解

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

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

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

办理房产证过户的相关部门


图1 办理房产证过户的相关部门

外观模式的定义与特点

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

在日常编码工作中,我们都在有意无意的大量使用外观模式。只要是高层模块需要调度多个子系统(2个以上的类对象),我们都会自觉地创建一个新的类封装这些子系统,提供精简的接口,让高层模块可以更加容易地间接调用这些子系统的功能。尤其是现阶段各种第三方SDK、开源类库,很大概率都会使用外观模式。

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

  1. 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  2. 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
  3. 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。


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

  1. 不能很好地限制客户使用子系统类,很容易带来未知风险。
  2. 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。

外观模式的结构与实现

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

1. 模式的结构

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

  1. 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  2. 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
  3. 客户(Client)角色:通过一个外观角色访问各个子系统的功能。


其结构图如图 2 所示。
 

外观模式的结构图


图2 外观(Facade)模式的结构图

2. 模式的实现

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

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

程序运行结果如下:

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

外观模式的应用实例

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

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

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

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

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


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


程序代码如下:

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

程序运行结果如图 4 所示。
 

婺源特产管理界面的运行结果


图4 婺源特产管理界面的运行结果

外观模式的应用场景

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

  1. 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
  2. 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
  3. 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。

外观模式的扩展

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

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


图5 引入抽象外观类的外观模式的结构图

享元模式(详解版)

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。

例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。

享元模式的定义与特点

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

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

其主要缺点是:

  1. 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
  2. 读取享元模式的外部状态会使得运行时间稍微变长。

享元模式的结构与实现

享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。

  • 内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
  • 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。


比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。

享元模式的本质是缓存共享对象,降低内存消耗。

1. 模式的结构

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

  1. 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  2. 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
  3. 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
  4. 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。


图 1 是享元模式的结构图,其中:

  • UnsharedConcreteFlyweight 是非享元角色,里面包含了非共享的外部状态信息 info;
  • Flyweight 是抽象享元角色,里面包含了享元方法 operation(UnsharedConcreteFlyweight state),非享元的外部状态以参数的形式通过该方法传入;
  • ConcreteFlyweight 是具体享元角色,包含了关键字 key,它实现了抽象享元接口;
  • FlyweightFactory 是享元工厂角色,它是关键字 key 来管理具体享元;
  • 客户角色通过享元工厂获取具体享元,并访问具体享元的相关方法。

享元模式的结构图


图1 享元模式的结构图

2. 模式的实现

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

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

 程序运行结果如下:

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

享元模式的应用实例

【例1】享元模式在五子棋游戏中的应用。

分析:五子棋同围棋一样,包含多个“黑”或“白”颜色的棋子,所以用享元模式比较好。

本实例中:

  • 棋子(ChessPieces)类是抽象享元角色,它包含了一个落子的 DownPieces(Graphics g,Point pt) 方法;
  • 白子(WhitePieces)和黑子(BlackPieces)类是具体享元角色,它实现了落子方法;
  • Point 是非享元角色,它指定了落子的位置;
  • WeiqiFactory 是享元工厂角色,它通过 ArrayList 来管理棋子,并且提供了获取白子或者黑子的 getChessPieces(String type) 方法;
  • 客户类(Chessboard)利用 Graphics 组件在框架窗体中绘制一个棋盘,并实现 mouseClicked(MouseEvent e) 事件处理方法,该方法根据用户的选择从享元工厂中获取白子或者黑子并落在棋盘上。


图 2 所示是其结构图。
 

五子棋游戏的结构图


图2 五子棋游戏的结构图


程序代码如下:

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

 程序运行结果如图 3 所示。

五子棋游戏的运行结果


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

享元模式的应用场景

当系统中多处需要同一组信息时,可以把这些信息封装到一个对象中,然后对该对象进行缓存,这样,一个对象就可以提供给多出需要使用的地方,避免大量同一对象的多次创建,降低大量内存空间的消耗。

享元模式其实是工厂方法模式的一个改进机制,享元模式同样要求创建一个或一组对象,并且就是通过工厂方法模式生成对象的,只不过享元模式为工厂方法模式增加了缓存这一功能。

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

  1. 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
  2. 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
  3. 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。

享元模式的扩展

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

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

单纯享元模式的结构图


图4 单纯享元模式的结构图


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

复合享元模式的结构图


图5 复合享元模式的结构图

组合模式(详解版)

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

组合模式的定义与特点

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

组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点,树形结构图如下。

组合模式树形结构图


由上图可以看出,其实根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于用一种类型。但是在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。

这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。

组合模式的主要优点有:

  1. 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
  2. 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;


其主要缺点是:

  1. 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
  2. 不容易限制容器中的构件;
  3. 不容易用继承的方法来增加构件的新功能;

组合模式的结构与实现

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

1. 模式的结构

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

  1. 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
  2. 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件。
  3. 树枝构件(Composite)角色 / 中间构件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。


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

(1) 透明方式

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

透明式的组合模式的结构图


图1 透明式的组合模式的结构图

(2) 安全方式

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

安全式的组合模式的结构图


图2 安全式的组合模式的结构图

2. 模式的实现

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

集合c0的树状图


                                                            图3 集合c0的树状图

透明组合模式

下面为透明式的组合模式的实现代码。

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

程序运行结果如下:

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

安全组合模式

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

首先修改 Component 代码,只保留层次的公共行为。

  1. interface Component {
  2. public void operation();
  3. }

然后修改客户端代码,将树枝构件类型更改为 Composite 类型,以便获取管理子类操作的方法。

  1. public class CompositePattern {
  2. public static void main(String[] args) {
  3. Composite c0 = new Composite();
  4. Composite c1 = new Composite();
  5. Component leaf1 = new Leaf("1");
  6. Component leaf2 = new Leaf("2");
  7. Component leaf3 = new Leaf("3");
  8. c0.add(leaf1);
  9. c0.add(c1);
  10. c1.add(leaf2);
  11. c1.add(leaf3);
  12. c0.operation();
  13. }
  14. }

组合模式的应用实例

【例1】用组合模式实现当用户在商店购物后,显示其所选商品信息,并计算所选商品总价的功能。

说明:假如李先生到韶关“天街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 元)}}},现在要求编程显示李先生放在大袋子中的所有商品信息并计算要支付的总价。

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

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


图4 韶关“天街e角”店购物的结构图


程序代码如下:

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

 程序运行结果如下:

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

组合模式的应用场景

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

  1. 在需要表示一个对象整体与部分的层次结构的场合。
  2. 要求对用户隐藏组合对象与单个对象的不同,用户可以用统一的接口使用组合结构中的所有对象的场合。

组合模式的扩展

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

复杂的组合模式的结构图


图5 复杂的组合模式的结构图

 结构型模式应用实验

结构型模式(Structural Pattern)描述如何将类或者对象结合在一起形成更大的结构,就像搭积木, 可以通过简单积木的组合形成复杂的、功能更为强大的结构。结构型模式可以分为类结构型模式和对象结构型模式,也可分为代理模式(Proxy)、适配器模式(Adapter)、桥接模式(Bridge)、装饰模式 (Decorator )、外观模式(Facade)、享元模式(Flyweight)和组合模式(Composite)等 7 类。

实验目的

本实验的主要目的如下。

  1. 了解 7 种“结构型模式”的定义、特点和工作原理。
  2. 理解 7 种“结构型模式”的结构、实现和应用场景。
  3. 学会应用 7 种“结构型模式”进行软件开发。

实验原理

1. 结构型模式的工作原理

结构型模式重点考虑类或对象的布局方式,其目的是将现有类或对象组成更大的结构。按照其显示方式的不同,结构型模式可分为类结构型模式和对象结构型模式。前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。

由于组合关系和或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。如果按目的来分,结构型模式共 7 种,每种模式的实验大概要花 2 个学时,大家可以根据实验计划来选做若干个实验。下面以代理(Proxy)模式为例,介绍其实验过程。

2. 代理模式的工作原理

代理模式是在访问对象和目标对象之间增加一个代理对象,该对象起到中介作用和保护目标对象的作用。另外,它还可以扩展目标对象的功能,并且将客户端与目标对象分离,这在一定程度上降低了系统的耦合度。

代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,其结构图如图 1 所示。
 

代理模式的结构图


图1 代理模式的结构图


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

  1. 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  2. 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  3. 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问或控制或扩展真实主题的功能。

实验内容

(1) 用代理模式设计一个房产中介的模拟程序。

要求:房产中介有介绍和代售韶关碧桂园房子的权利,以及登记购房者信息权利。这里的房产中介是代理者,韶关碧桂园是真实主题。

(2) 按照以上要求设计类图和编写 Java 源程序。

实验要求

所设计的实验程序要满足以下两点。

  1. 体现“代理模式”的工作原理。
  2. 符合面向对象中的“开闭原则”和“里氏替换原则”。

实验步骤

(1) 用 UML 设计“房产中介”模拟程序的结构图。

“房产中介”模拟程序的结构图如图 2 所示。
 

房产中介模拟程序的结构图


图2 房产中介模拟程序的结构图


(2) 根据结构图写出“房产中介”模拟程序的源代码。

房产中介模拟程序的源代码如下。

  1. package proxy;
  2. import java.awt.*;
  3. import javax.swing.*;
  4. public class SgHouseProxy {
  5. public static void main(String[] args) {
  6. HouseProxy proxy = new HouseProxy();
  7. proxy.display();
  8. }
  9. }
  10. //抽象主题:房主
  11. interface HouseOwner {
  12. void display();
  13. }
  14. //真实主题:韶关碧桂园
  15. class SgBiguiyuan extends JFrame implements HouseOwner {
  16. private static final long serialVersionUID = 1L;
  17. public SgBiguiyuan() {
  18. super("房产中介代售韶关碧桂园房子");
  19. }
  20. public void display() {
  21. this.setLayout(new GridLayout(1, 1));
  22. JLabel l1 = new JLabel(new ImageIcon("src/proxy/SgBiguiyuan.jpg"));
  23. this.add(l1);
  24. this.pack();
  25. this.setVisible(true);
  26. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  27. }
  28. }
  29. //代理:房产中介
  30. class HouseProxy implements HouseOwner {
  31. private SgBiguiyuan realSubject = new SgBiguiyuan();
  32. public void display() {
  33. preRequest();
  34. realSubject.display();
  35. postRequest();
  36. }
  37. public void preRequest() {
  38. System.out.println("房产中介介绍韶关碧桂园房子。");
  39. }
  40. public void postRequest() {
  41. System.out.println("房产中介登记购房者信息。");
  42. }
  43. }

(3) 上机测试程序,查看运行结果。

“房产中介”模拟程序的运行结果如图 3 所示。
 

房产中介模拟程序的运行结果


图3 房产中介模拟程序的运行结果


(4) 按同样的步骤设计其他“结构型模式”的程序实例。

行为型模式概述(行为型模式的分类) 

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。

行为型模式是 GoF 设计模式中最为庞大的一类,它包含以下 11 种模式。

  1. 模板方法(Template Method)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使得子类在可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
  2. 策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
  3. 命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
  4. 职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
  5. 状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
  6. 观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
  7. 中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
  8. 迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
  9. 访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
  10. 备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
  11. 解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。


以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式,下面我们将详细介绍它们的特点、结构与应用。

模板方法模式(模板方法设计模式)详解

在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。

例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

这样的例子在生活中还有很多,例如,一个人每天会起床、吃饭、做事、睡觉等,其中“做事”的内容每天可能不同。我们把这些规定了流程或格式的实例定义成模板,允许使用者根据自己的需求去更新它,例如,简历模板、论文模板、Word 中模板文件等。

以下介绍的模板方法模式将解决以上类似的问题。

模式的定义与特点

模板方法(Template Method)模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。

该模式的主要优点如下。

  1. 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  2. 它在父类中提取了公共的部分代码,便于代码复用。
  3. 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。


该模式的主要缺点如下。

  1. 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
  2. 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
  3. 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。

模式的结构与实现

模板方法模式需要注意抽象类与具体子类之间的协作。它用到了虚函数的多态性技术以及“不用调用我,让我来调用你”的反向控制技术。现在来介绍它们的基本结构。

1. 模式的结构

模板方法模式包含以下主要角色。

1)抽象类/抽象模板(Abstract Class)

抽象模板类,负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下。

① 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。

② 基本方法:是整个算法中的一个步骤,包含以下几种类型。

  • 抽象方法:在抽象类中声明,由具体子类实现。
  • 具体方法:在抽象类中已经实现,在具体子类中可以继承或重写它。
  • 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。

2)具体子类/具体实现(Concrete Class)

具体实现类,实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的一个组成步骤。

模板方法模式的结构图如图 1 所示。
 

模板方法模式的结构图


图1 模板方法模式的结构图

2. 模式的实现

模板方法模式的代码如下:

  1. public class TemplateMethodPattern {
  2. public static void main(String[] args) {
  3. AbstractClass tm = new ConcreteClass();
  4. tm.TemplateMethod();
  5. }
  6. }
  7. //抽象类
  8. abstract class AbstractClass {
  9. //模板方法
  10. public void TemplateMethod() {
  11. SpecificMethod();
  12. abstractMethod1();
  13. abstractMethod2();
  14. }
  15. //具体方法
  16. public void SpecificMethod() {
  17. System.out.println("抽象类中的具体方法被调用...");
  18. }
  19. //抽象方法1
  20. public abstract void abstractMethod1();
  21. //抽象方法2
  22. public abstract void abstractMethod2();
  23. }
  24. //具体子类
  25. class ConcreteClass extends AbstractClass {
  26. public void abstractMethod1() {
  27. System.out.println("抽象方法1的实现被调用...");
  28. }
  29. public void abstractMethod2() {
  30. System.out.println("抽象方法2的实现被调用...");
  31. }
  32. }

 程序的运行结果如下:

抽象类中的具体方法被调用...
抽象方法1的实现被调用...
抽象方法2的实现被调用...

模式的应用实例

【例1】用模板方法模式实现出国留学手续设计程序。

分析:出国留学手续一般经过以下流程:索取学校资料,提出入学申请,办理因私出国护照、出境卡和公证,申请签证,体检、订机票、准备行装,抵达目标学校等,其中有些业务对各个学校是一样的,但有些业务因学校不同而不同,所以比较适合用模板方法模式来实现。

在本实例中,我们先定义一个出国留学的抽象类 StudyAbroad,里面包含了一个模板方法 TemplateMethod(),该方法中包含了办理出国留学手续流程中的各个基本方法,其中有些方法的处理由于各国都一样,所以在抽象类中就可以实现,但有些方法的处理各国是不同的,必须在其具体子类(如美国留学类 StudyInAmerica)中实现。如果再增加一个国家,只要增加一个子类就可以了,图 2 所示是其结构图。
 

出国留学手续设计程序的结构图


图2 出国留学手续设计程序的结构图


程序代码如下:

  1. public class StudyAbroadProcess {
  2. public static void main(String[] args) {
  3. StudyAbroad tm = new StudyInAmerica();
  4. tm.TemplateMethod();
  5. }
  6. }
  7. //抽象类: 出国留学
  8. abstract class StudyAbroad {
  9. public void TemplateMethod() //模板方法
  10. {
  11. LookingForSchool(); //索取学校资料
  12. ApplyForEnrol(); //入学申请
  13. ApplyForPassport(); //办理因私出国护照、出境卡和公证
  14. ApplyForVisa(); //申请签证
  15. ReadyGoAbroad(); //体检、订机票、准备行装
  16. Arriving(); //抵达
  17. }
  18. public void ApplyForPassport() {
  19. System.out.println("三.办理因私出国护照、出境卡和公证:");
  20. System.out.println(" 1)持录取通知书、本人户口簿或身份证向户口所在地公安机关申请办理因私出国护照和出境卡。");
  21. System.out.println(" 2)办理出生公证书,学历、学位和成绩公证,经历证书,亲属关系公证,经济担保公证。");
  22. }
  23. public void ApplyForVisa() {
  24. System.out.println("四.申请签证:");
  25. System.out.println(" 1)准备申请国外境签证所需的各种资料,包括个人学历、成绩单、工作经历的证明;个人及家庭收入、资金和财产证明;家庭成员的关系证明等;");
  26. System.out.println(" 2)向拟留学国家驻华使(领)馆申请入境签证。申请时需按要求填写有关表格,递交必需的证明材料,缴纳签证。有的国家(比如美国、英国、加拿大等)在申请签证时会要求申请人前往使(领)馆进行面试。");
  27. }
  28. public void ReadyGoAbroad() {
  29. System.out.println("五.体检、订机票、准备行装:");
  30. System.out.println(" 1)进行身体检查、免疫检查和接种传染病疫苗;");
  31. System.out.println(" 2)确定机票时间、航班和转机地点。");
  32. }
  33. public abstract void LookingForSchool();//索取学校资料
  34. public abstract void ApplyForEnrol(); //入学申请
  35. public abstract void Arriving(); //抵达
  36. }
  37. //具体子类: 美国留学
  38. class StudyInAmerica extends StudyAbroad {
  39. @Override
  40. public void LookingForSchool() {
  41. System.out.println("一.索取学校以下资料:");
  42. System.out.println(" 1)对留学意向国家的政治、经济、文化背景和教育体制、学术水平进行较为全面的了解;");
  43. System.out.println(" 2)全面了解和掌握国外学校的情况,包括历史、学费、学制、专业、师资配备、教学设施、学术地位、学生人数等;");
  44. System.out.println(" 3)了解该学校的住宿、交通、医疗保险情况如何;");
  45. System.out.println(" 4)该学校在中国是否有授权代理招生的留学中介公司?");
  46. System.out.println(" 5)掌握留学签证情况;");
  47. System.out.println(" 6)该国政府是否允许留学生合法打工?");
  48. System.out.println(" 8)毕业之后可否移民?");
  49. System.out.println(" 9)文凭是否受到我国认可?");
  50. }
  51. @Override
  52. public void ApplyForEnrol() {
  53. System.out.println("二.入学申请:");
  54. System.out.println(" 1)填写报名表;");
  55. System.out.println(" 2)将报名表、个人学历证明、最近的学习成绩单、推荐信、个人简历、托福或雅思语言考试成绩单等资料寄往所申请的学校;");
  56. System.out.println(" 3)为了给签证办理留有充裕的时间,建议越早申请越好,一般提前1年就比较从容。");
  57. }
  58. @Override
  59. public void Arriving() {
  60. System.out.println("六.抵达目标学校:");
  61. System.out.println(" 1)安排住宿;");
  62. System.out.println(" 2)了解校园及周边环境。");
  63. }
  64. }

 程序的运行结果如下:

一.索取学校以下资料:
  1)对留学意向国家的政治、经济、文化背景和教育体制、学术水平进行较为全面的了解;
  2)全面了解和掌握国外学校的情况,包括历史、学费、学制、专业、师资配备、教学设施、学术地位、学生人数等;
  3)了解该学校的住宿、交通、医疗保险情况如何;
  4)该学校在中国是否有授权代理招生的留学中介公司?
  5)掌握留学签证情况;
  6)该国政府是否允许留学生合法打工?
  8)毕业之后可否移民?
  9)文凭是否受到我国认可?
二.入学申请:
  1)填写报名表;
  2)将报名表、个人学历证明、最近的学习成绩单、推荐信、个人简历、托福或雅思语言考试成绩单等资料寄往所申请的学校;
  3)为了给签证办理留有充裕的时间,建议越早申请越好,一般提前1年就比较从容。
三.办理因私出国护照、出境卡和公证:
  1)持录取通知书、本人户口簿或身份证向户口所在地公安机关申请办理因私出国护照和出境卡。
  2)办理出生公证书,学历、学位和成绩公证,经历证书,亲属关系公证,经济担保公证。
四.申请签证:
  1)准备申请国外境签证所需的各种资料,包括个人学历、成绩单、工作经历的证明;个人及家庭收入、资金和财产证明;家庭成员的关系证明等;
  2)向拟留学国家驻华使(领)馆申请入境签证。申请时需按要求填写有关表格,递交必需的证明材料,缴纳签证。有的国家(比如美国、英国、加拿大等)在申请签证时会要求申请人前往使(领)馆进行面试。
五.体检、订机票、准备行装:
  1)进行身体检查、免疫检查和接种传染病疫苗;
  2)确定机票时间、航班和转机地点。
六.抵达目标学校:
  1)安排住宿;
  2)了解校园及周边环境。

模式的应用场景

模板方法模式通常适用于以下场景。

  1. 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  2. 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  3. 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。

模式的扩展

在模板方法模式中,基本方法包含:抽象方法、具体方法和钩子方法,正确使用“钩子方法”可以使得子类控制父类的行为。如下面例子中,可以通过在具体子类中重写钩子方法 HookMethod1() 和 HookMethod2() 来改变抽象父类中的运行结果,其结构图如图 3 所示。
 

含钩子方法的模板方法模式的结构图


图3 含钩子方法的模板方法模式的结构图


程序代码如下:

  1. public class HookTemplateMethod {
  2. public static void main(String[] args) {
  3. HookAbstractClass tm = new HookConcreteClass();
  4. tm.TemplateMethod();
  5. }
  6. }
  7. //含钩子方法的抽象类
  8. abstract class HookAbstractClass {
  9. //模板方法
  10. public void TemplateMethod() {
  11. abstractMethod1();
  12. HookMethod1();
  13. if (HookMethod2()) {
  14. SpecificMethod();
  15. }
  16. abstractMethod2();
  17. }
  18. //具体方法
  19. public void SpecificMethod() {
  20. System.out.println("抽象类中的具体方法被调用...");
  21. }
  22. //钩子方法1
  23. public void HookMethod1() {
  24. }
  25. //钩子方法2
  26. public boolean HookMethod2() {
  27. return true;
  28. }
  29. //抽象方法1
  30. public abstract void abstractMethod1();
  31. //抽象方法2
  32. public abstract void abstractMethod2();
  33. }
  34. //含钩子方法的具体子类
  35. class HookConcreteClass extends HookAbstractClass {
  36. public void abstractMethod1() {
  37. System.out.println("抽象方法1的实现被调用...");
  38. }
  39. public void abstractMethod2() {
  40. System.out.println("抽象方法2的实现被调用...");
  41. }
  42. public void HookMethod1() {
  43. System.out.println("钩子方法1被重写...");
  44. }
  45. public boolean HookMethod2() {
  46. return false;
  47. }
  48. }

程序的运行结果如下:

抽象方法1的实现被调用...
钩子方法1被重写...
抽象方法2的实现被调用...

如果钩子方法 HookMethod1() 和钩子方法 HookMethod2() 的代码改变,则程序的运行结果也会改变。

策略模式(策略设计模式)详解

在现实生活中常常遇到实现某种目标存在多种策略可供选择的情况,例如,出行旅游可以乘坐飞机、乘坐火车、骑自行车或自己开私家车等,超市促销可以釆用打折、送商品、送积分等方法。

在软件开发中也常常遇到类似的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有冒泡排序、选择排序、插入排序、二叉树排序等。

如果使用多重条件转移语句实现(即硬编码),不但使条件语句变得很复杂,而且增加、删除或更换算法要修改原代码,不易维护,违背开闭原则。如果采用策略模式就能很好解决该问题。

策略模式的定义与特点

策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

策略模式的主要优点如下。

  1. 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if...else 语句、switch...case 语句。
  2. 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
  3. 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
  4. 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
  5. 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。


其主要缺点如下。

  1. 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
  2. 策略模式造成很多的策略类,增加维护难度。

策略模式的结构与实现

策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性,现在我们来分析其基本结构和实现方法。

1. 模式的结构

策略模式的主要角色如下。

  1. 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
  2. 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
  3. 环境(Context)类:持有一个策略类的引用,最终给客户端调用。


其结构图如图 1 所示。
 

策略模式的结构图


图1 策略模式的结构图

2. 模式的实现

策略模式的实现代码如下:

  1. public class StrategyPattern {
  2. public static void main(String[] args) {
  3. Context c = new Context();
  4. Strategy s = new ConcreteStrategyA();
  5. c.setStrategy(s);
  6. c.strategyMethod();
  7. System.out.println("-----------------");
  8. s = new ConcreteStrategyB();
  9. c.setStrategy(s);
  10. c.strategyMethod();
  11. }
  12. }
  13. //抽象策略类
  14. interface Strategy {
  15. public void strategyMethod(); //策略方法
  16. }
  17. //具体策略类A
  18. class ConcreteStrategyA implements Strategy {
  19. public void strategyMethod() {
  20. System.out.println("具体策略A的策略方法被访问!");
  21. }
  22. }
  23. //具体策略类B
  24. class ConcreteStrategyB implements Strategy {
  25. public void strategyMethod() {
  26. System.out.println("具体策略B的策略方法被访问!");
  27. }
  28. }
  29. //环境类
  30. class Context {
  31. private Strategy strategy;
  32. public Strategy getStrategy() {
  33. return strategy;
  34. }
  35. public void setStrategy(Strategy strategy) {
  36. this.strategy = strategy;
  37. }
  38. public void strategyMethod() {
  39. strategy.strategyMethod();
  40. }
  41. }

程序运行结果如下:

具体策略A的策略方法被访问!
-----------------
具体策略B的策略方法被访问!

策略模式的应用实例

【例1】策略模式在“大闸蟹”做菜中的应用。

分析:关于大闸蟹的做法有很多种,我们以清蒸大闸蟹和红烧大闸蟹两种方法为例,介绍策略模式的应用。

首先,定义一个大闸蟹加工的抽象策略类(CrabCooking),里面包含了一个做菜的抽象方法 CookingMethod();然后,定义清蒸大闸蟹(SteamedCrabs)和红烧大闸蟹(BraisedCrabs)的具体策略类,它们实现了抽象策略类中的抽象方法;由于本程序要显示做好的结果图(点此下载要显示的结果图),所以将具体策略类定义成 JLabel 的子类;最后,定义一个厨房(Kitchen)环境类,它具有设置和选择做菜策略的方法;客户类通过厨房类获取做菜策略,并把做菜结果图在窗体中显示出来,图 2 所示是其结构图。
 

大闸蟹做菜策略的结构图


图2 大闸蟹做菜策略的结构图


程序代码如下:

  1. import java.awt.*;
  2. import java.awt.event.*;
  3. import javax.swing.*;
  4. public class CrabCookingStrategy implements ItemListener {
  5. private JFrame f;
  6. private JRadioButton qz, hs;
  7. private JPanel CenterJP, SouthJP;
  8. private Kitchen cf; //厨房
  9. private CrabCooking qzx, hsx; //大闸蟹加工者
  10. CrabCookingStrategy() {
  11. f = new JFrame("策略模式在大闸蟹做菜中的应用");
  12. f.setBounds(100, 100, 500, 400);
  13. f.setVisible(true);
  14. f.setResizable(false);
  15. f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  16. SouthJP = new JPanel();
  17. CenterJP = new JPanel();
  18. f.add("South", SouthJP);
  19. f.add("Center", CenterJP);
  20. qz = new JRadioButton("清蒸大闸蟹");
  21. hs = new JRadioButton("红烧大闸蟹");
  22. qz.addItemListener(this);
  23. hs.addItemListener(this);
  24. ButtonGroup group = new ButtonGroup();
  25. group.add(qz);
  26. group.add(hs);
  27. SouthJP.add(qz);
  28. SouthJP.add(hs);
  29. //---------------------------------
  30. cf = new Kitchen(); //厨房
  31. qzx = new SteamedCrabs(); //清蒸大闸蟹类
  32. hsx = new BraisedCrabs(); //红烧大闸蟹类
  33. }
  34. public void itemStateChanged(ItemEvent e) {
  35. JRadioButton jc = (JRadioButton) e.getSource();
  36. if (jc == qz) {
  37. cf.setStrategy(qzx);
  38. cf.CookingMethod(); //清蒸
  39. } else if (jc == hs) {
  40. cf.setStrategy(hsx);
  41. cf.CookingMethod(); //红烧
  42. }
  43. CenterJP.removeAll();
  44. CenterJP.repaint();
  45. CenterJP.add((Component) cf.getStrategy());
  46. f.setVisible(true);
  47. }
  48. public static void main(String[] args) {
  49. new CrabCookingStrategy();
  50. }
  51. }
  52. //抽象策略类:大闸蟹加工类
  53. interface CrabCooking {
  54. public void CookingMethod(); //做菜方法
  55. }
  56. //具体策略类:清蒸大闸蟹
  57. class SteamedCrabs extends JLabel implements CrabCooking {
  58. private static final long serialVersionUID = 1L;
  59. public void CookingMethod() {
  60. this.setIcon(new ImageIcon("src/strategy/SteamedCrabs.jpg"));
  61. this.setHorizontalAlignment(CENTER);
  62. }
  63. }
  64. //具体策略类:红烧大闸蟹
  65. class BraisedCrabs extends JLabel implements CrabCooking {
  66. private static final long serialVersionUID = 1L;
  67. public void CookingMethod() {
  68. this.setIcon(new ImageIcon("src/strategy/BraisedCrabs.jpg"));
  69. this.setHorizontalAlignment(CENTER);
  70. }
  71. }
  72. //环境类:厨房
  73. class Kitchen {
  74. private CrabCooking strategy; //抽象策略
  75. public void setStrategy(CrabCooking strategy) {
  76. this.strategy = strategy;
  77. }
  78. public CrabCooking getStrategy() {
  79. return strategy;
  80. }
  81. public void CookingMethod() {
  82. strategy.CookingMethod(); //做菜
  83. }
  84. }

程序运行结果如图 3 所示。
 

大闸蟹做菜结果


图3 大闸蟹做菜结果


【例2】用策略模式实现从韶关去婺源旅游的出行方式。

分析:从韶关去婺源旅游有以下几种出行方式:坐火车、坐汽车和自驾车,所以该实例用策略模式比较适合,图 4 所示是其结构图。
 

婺源旅游结构图


图4 婺源旅游结构图

策略模式的应用场景

策略模式在很多地方用到,如 Java SE 中的容器布局管理就是一个典型的实例,Java SE 中的每个容器都存在多种布局供用户选择。在程序设计中,通常在以下几种情况中使用策略模式较多。

  1. 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
  2. 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
  3. 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
  4. 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构
  5. 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

策略模式的扩展

在一个使用策略模式的系统中,当存在的策略很多时,客户端管理所有策略算法将变得很复杂,如果在环境类中使用策略工厂模式来管理这些策略类将大大减少客户端的工作复杂度,其结构图如图 5 所示。
 

策略工厂模式的结构图


图5 策略工厂模式的结构图

命令模式(详解版)

在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。

在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。

再比如,我们去餐厅吃饭,菜单不是等到客人来了之后才定制的,而是已经预先配置好的。这样,客人来了就只需要点菜,而不是任由客人临时定制。餐厅提供的菜单就相当于把请求和处理进行了解耦,这就是命令模式的体现。

命令模式的定义与特点

命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。

命令模式的主要优点如下。

  1. 通过引入中间件(抽象接口)降低系统的耦合度。
  2. 扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
  3. 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  4. 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
  5. 可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。


其缺点是:

  1. 可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
  2. 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。

命令模式的结构与实现

可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离,其结构如下。

1. 模式的结构

命令模式包含以下主要角色。

  1. 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
  2. 具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
  3. 实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
  4. 调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。


其结构图如图 1 所示。
 

命令模式的结构图


图1 命令模式的结构图

2. 模式的实现

命令模式的代码如下:

  1. package command;
  2. public class CommandPattern {
  3. public static void main(String[] args) {
  4. Command cmd = new ConcreteCommand();
  5. Invoker ir = new Invoker(cmd);
  6. System.out.println("客户访问调用者的call()方法...");
  7. ir.call();
  8. }
  9. }
  10. //调用者
  11. class Invoker {
  12. private Command command;
  13. public Invoker(Command command) {
  14. this.command = command;
  15. }
  16. public void setCommand(Command command) {
  17. this.command = command;
  18. }
  19. public void call() {
  20. System.out.println("调用者执行命令command...");
  21. command.execute();
  22. }
  23. }
  24. //抽象命令
  25. interface Command {
  26. public abstract void execute();
  27. }
  28. //具体命令
  29. class ConcreteCommand implements Command {
  30. private Receiver receiver;
  31. ConcreteCommand() {
  32. receiver = new Receiver();
  33. }
  34. public void execute() {
  35. receiver.action();
  36. }
  37. }
  38. //接收者
  39. class Receiver {
  40. public void action() {
  41. System.out.println("接收者的action()方法被调用...");
  42. }
  43. }

 程序的运行结果如下:

客户访问调用者的call()方法...
调用者执行命令command...
接收者的action()方法被调用...

命令模式的应用实例

【例1】用命令模式实现客户去餐馆吃早餐的实例。

分析:客户去餐馆可选择的早餐有肠粉、河粉和馄饨等,客户可向服务员选择以上早餐中的若干种,服务员将客户的请求交给相关的厨师去做。这里的点早餐相当于“命令”,服务员相当于“调用者”,厨师相当于“接收者”,所以用命令模式实现比较合适。

  • 首先,定义一个早餐类(Breakfast),它是抽象命令类,有抽象方法 cooking(),说明要做什么;
  • 再定义其子类肠粉类(ChangFen)、馄饨类(HunTun)和河粉类(HeFen),它们是具体命令类,实现早餐类的 cooking() 方法,但它们不会具体做,而是交给具体的厨师去做;
  • 具体厨师类有肠粉厨师(ChangFenChef)、馄饨厨师(HunTunChef)和河粉厨师(HeFenChef),他们是命令的接收者。


由于本实例要显示厨师做菜的效果图(点此下载要显示的效果图),所以把每个厨师类定义为 JFrame 的子类;最后,定义服务员类(Waiter),它接收客户的做菜请求,并发出做菜的命令。客户类是通过服务员类来点菜的,图 2 所示是其结构图。
 

客户在餐馆吃早餐的结构图


图2 客户在餐馆吃早餐的结构图


程序代码如下:

  1. package command;
  2. import javax.swing.*;
  3. public class CookingCommand {
  4. public static void main(String[] args) {
  5. Breakfast food1 = new ChangFen();
  6. Breakfast food2 = new HunTun();
  7. Breakfast food3 = new HeFen();
  8. Waiter fwy = new Waiter();
  9. fwy.setChangFen(food1);//设置肠粉菜单
  10. fwy.setHunTun(food2); //设置河粉菜单
  11. fwy.setHeFen(food3); //设置馄饨菜单
  12. fwy.chooseChangFen(); //选择肠粉
  13. fwy.chooseHeFen(); //选择河粉
  14. fwy.chooseHunTun(); //选择馄饨
  15. }
  16. }
  17. //调用者:服务员
  18. class Waiter {
  19. private Breakfast changFen, hunTun, heFen;
  20. public void setChangFen(Breakfast f) {
  21. changFen = f;
  22. }
  23. public void setHunTun(Breakfast f) {
  24. hunTun = f;
  25. }
  26. public void setHeFen(Breakfast f) {
  27. heFen = f;
  28. }
  29. public void chooseChangFen() {
  30. changFen.cooking();
  31. }
  32. public void chooseHunTun() {
  33. hunTun.cooking();
  34. }
  35. public void chooseHeFen() {
  36. heFen.cooking();
  37. }
  38. }
  39. //抽象命令:早餐
  40. interface Breakfast {
  41. public abstract void cooking();
  42. }
  43. //具体命令:肠粉
  44. class ChangFen implements Breakfast {
  45. private ChangFenChef receiver;
  46. ChangFen() {
  47. receiver = new ChangFenChef();
  48. }
  49. public void cooking() {
  50. receiver.cooking();
  51. }
  52. }
  53. //具体命令:馄饨
  54. class HunTun implements Breakfast {
  55. private HunTunChef receiver;
  56. HunTun() {
  57. receiver = new HunTunChef();
  58. }
  59. public void cooking() {
  60. receiver.cooking();
  61. }
  62. }
  63. //具体命令:河粉
  64. class HeFen implements Breakfast {
  65. private HeFenChef receiver;
  66. HeFen() {
  67. receiver = new HeFenChef();
  68. }
  69. public void cooking() {
  70. receiver.cooking();
  71. }
  72. }
  73. //接收者:肠粉厨师
  74. class ChangFenChef extends JFrame {
  75. private static final long serialVersionUID = 1L;
  76. JLabel l = new JLabel();
  77. ChangFenChef() {
  78. super("煮肠粉");
  79. l.setIcon(new ImageIcon("src/command/ChangFen.jpg"));
  80. this.add(l);
  81. this.setLocation(30, 30);
  82. this.pack();
  83. this.setResizable(false);
  84. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  85. }
  86. public void cooking() {
  87. this.setVisible(true);
  88. }
  89. }
  90. //接收者:馄饨厨师
  91. class HunTunChef extends JFrame {
  92. private static final long serialVersionUID = 1L;
  93. JLabel l = new JLabel();
  94. HunTunChef() {
  95. super("煮馄饨");
  96. l.setIcon(new ImageIcon("src/command/HunTun.jpg"));
  97. this.add(l);
  98. this.setLocation(350, 50);
  99. this.pack();
  100. this.setResizable(false);
  101. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  102. }
  103. public void cooking() {
  104. this.setVisible(true);
  105. }
  106. }
  107. //接收者:河粉厨师
  108. class HeFenChef extends JFrame {
  109. private static final long serialVersionUID = 1L;
  110. JLabel l = new JLabel();
  111. HeFenChef() {
  112. super("煮河粉");
  113. l.setIcon(new ImageIcon("src/command/HeFen.jpg"));
  114. this.add(l);
  115. this.setLocation(200, 280);
  116. this.pack();
  117. this.setResizable(false);
  118. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  119. }
  120. public void cooking() {
  121. this.setVisible(true);
  122. }
  123. }

 程序的运行结果如图 2 所示。
 

客户在餐馆吃早餐的运行结果


图2 客户在餐馆吃早餐的运行结果

命令模式的应用场景

当系统的某项操作具备命令语义,且命令实现不稳定(变化)时,可以通过命令模式解耦请求与实现。使用抽象命令接口使请求方的代码架构稳定,封装接收方具体命令的实现细节。接收方与抽象命令呈现弱耦合(内部方法无需一致),具备良好的扩展性。

命令模式通常适用于以下场景。

  1. 请求调用者需要与请求接收者解耦时,命令模式可以使调用者和接收者不直接交互。
  2. 系统随机请求命令或经常增加、删除命令时,命令模式可以方便地实现这些功能。
  3. 当系统需要执行一组操作时,命令模式可以定义宏命令来实现该功能。
  4. 当系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作时,可以将命令对象存储起来,采用备忘录模式来实现。

命令模式的扩展

在软件开发中,有时将命令模式与前面学的组合模式联合使用,这就构成了宏命令模式,也叫组合命令模式。宏命令包含了一组命令,它充当了具体命令与调用者的双重角色,执行它时将递归调用它所包含的所有命令,其具体结构图如图 3 所示。
 

组合命令模式的结构图


图3 组合命令模式的结构图


程序代码如下:

  1. package command;
  2. import java.util.ArrayList;
  3. public class CompositeCommandPattern {
  4. public static void main(String[] args) {
  5. AbstractCommand cmd1 = new ConcreteCommand1();
  6. AbstractCommand cmd2 = new ConcreteCommand2();
  7. CompositeInvoker ir = new CompositeInvoker();
  8. ir.add(cmd1);
  9. ir.add(cmd2);
  10. System.out.println("客户访问调用者的execute()方法...");
  11. ir.execute();
  12. }
  13. }
  14. //抽象命令
  15. interface AbstractCommand {
  16. public abstract void execute();
  17. }
  18. //树叶构件: 具体命令1
  19. class ConcreteCommand1 implements AbstractCommand {
  20. private CompositeReceiver receiver;
  21. ConcreteCommand1() {
  22. receiver = new CompositeReceiver();
  23. }
  24. public void execute() {
  25. receiver.action1();
  26. }
  27. }
  28. //树叶构件: 具体命令2
  29. class ConcreteCommand2 implements AbstractCommand {
  30. private CompositeReceiver receiver;
  31. ConcreteCommand2() {
  32. receiver = new CompositeReceiver();
  33. }
  34. public void execute() {
  35. receiver.action2();
  36. }
  37. }
  38. //树枝构件: 调用者
  39. class CompositeInvoker implements AbstractCommand {
  40. private ArrayList<AbstractCommand> children = new ArrayList<AbstractCommand>();
  41. public void add(AbstractCommand c) {
  42. children.add(c);
  43. }
  44. public void remove(AbstractCommand c) {
  45. children.remove(c);
  46. }
  47. public AbstractCommand getChild(int i) {
  48. return children.get(i);
  49. }
  50. public void execute() {
  51. for (Object obj : children) {
  52. ((AbstractCommand) obj).execute();
  53. }
  54. }
  55. }
  56. //接收者
  57. class CompositeReceiver {
  58. public void action1() {
  59. System.out.println("接收者的action1()方法被调用...");
  60. }
  61. public void action2() {
  62. System.out.println("接收者的action2()方法被调用...");
  63. }
  64. }

程序的运行结果如下:

客户访问调用者的execute()方法...
接收者的action1()方法被调用...
接收者的action2()方法被调用...


当然,命令模式还可以同备忘录(Memento)模式组合使用,这样就变成了可撤销的命令模式,这将在后面介绍。

责任链模式(职责链模式)详解

在现实生活中,一个事件需要经过多个对象处理是很常见的场景。例如,采购审批流程、请假流程等。公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据需要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这无疑增加了难度。

在计算机软硬件中也有相关例子,如总线网中数据报传送,每台计算机根据目标地址是否同自己的地址相同来决定是否接收;还有异常处理中,处理程序根据异常的类型决定自己是否处理该异常;还有 Struts2 的拦截器、JSP 和 Servlet 的 Filter 等,所有这些,都可以考虑使用责任链模式来实现。

模式的定义与特点

责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

注意:责任链模式也叫职责链模式。

在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。

责任链模式是一种对象行为型模式,其主要优点如下。

  1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
  3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。


其主要缺点如下。

  1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

模式的结构与实现

通常情况下,可以通过数据链表来实现职责链模式的数据结构

1. 模式的结构

职责链模式主要包含以下角色。

  1. 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  2. 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  3. 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。


责任链模式的本质是解耦请求与处理,让请求在处理链中能进行传递与被处理;理解责任链模式应当理解其模式,而不是其具体实现。责任链模式的独到之处是将其节点处理者组合成了链式结构,并允许节点自身决定是否进行请求处理或转发,相当于让请求流动起来。

其结构图如图 1 所示。客户端可按图 2 所示设置责任链。
 

责任链模式的结构图


图1 责任链模式的结构图

责任链


图2 责任链

2. 模式的实现

职责链模式的实现代码如下:

  1. package chainOfResponsibility;
  2. public class ChainOfResponsibilityPattern {
  3. public static void main(String[] args) {
  4. //组装责任链
  5. Handler handler1 = new ConcreteHandler1();
  6. Handler handler2 = new ConcreteHandler2();
  7. handler1.setNext(handler2);
  8. //提交请求
  9. handler1.handleRequest("two");
  10. }
  11. }
  12. //抽象处理者角色
  13. abstract class Handler {
  14. private Handler next;
  15. public void setNext(Handler next) {
  16. this.next = next;
  17. }
  18. public Handler getNext() {
  19. return next;
  20. }
  21. //处理请求的方法
  22. public abstract void handleRequest(String request);
  23. }
  24. //具体处理者角色1
  25. class ConcreteHandler1 extends Handler {
  26. public void handleRequest(String request) {
  27. if (request.equals("one")) {
  28. System.out.println("具体处理者1负责处理该请求!");
  29. } else {
  30. if (getNext() != null) {
  31. getNext().handleRequest(request);
  32. } else {
  33. System.out.println("没有人处理该请求!");
  34. }
  35. }
  36. }
  37. }
  38. //具体处理者角色2
  39. class ConcreteHandler2 extends Handler {
  40. public void handleRequest(String request) {
  41. if (request.equals("two")) {
  42. System.out.println("具体处理者2负责处理该请求!");
  43. } else {
  44. if (getNext() != null) {
  45. getNext().handleRequest(request);
  46. } else {
  47. System.out.println("没有人处理该请求!");
  48. }
  49. }
  50. }
  51. }

程序运行结果如下:

具体处理者2负责处理该请求!

在上面代码中,我们把消息硬编码为 String 类型,而在真实业务中,消息是具备多样性的,可以是 int、String 或者自定义类型。因此,在上面代码的基础上,可以对消息类型进行抽象 Request,增强了消息的兼容性。

模式的应用实例

【例1】用责任链模式设计一个请假条审批模块。

分析:假如规定学生请假小于或等于 2 天,班主任可以批准;小于或等于 7 天,系主任可以批准;小于或等于 10 天,院长可以批准;其他情况不予批准;这个实例适合使用职责链模式实现。

首先,定义一个领导类(Leader),它是抽象处理者,包含了一个指向下一位领导的指针 next 和一个处理假条的抽象处理方法 handleRequest(int LeaveDays);然后,定义班主任类(ClassAdviser)、系主任类(DepartmentHead)和院长类(Dean),它们是抽象处理者的子类,是具体处理者,必须根据自己的权力去实现父类的 handleRequest(int LeaveDays) 方法,如果无权处理就将假条交给下一位具体处理者,直到最后;客户类负责创建处理链,并将假条交给链头的具体处理者(班主任)。图 3 所示是其结构图。
 

请假条审批模块的结构图


图3 请假条审批模块的结构图


程序代码如下:

  1. package chainOfResponsibility;
  2. public class LeaveApprovalTest {
  3. public static void main(String[] args) {
  4. //组装责任链
  5. Leader teacher1 = new ClassAdviser();
  6. Leader teacher2 = new DepartmentHead();
  7. Leader teacher3 = new Dean();
  8. //Leader teacher4=new DeanOfStudies();
  9. teacher1.setNext(teacher2);
  10. teacher2.setNext(teacher3);
  11. //teacher3.setNext(teacher4);
  12. //提交请求
  13. teacher1.handleRequest(8);
  14. }
  15. }
  16. //抽象处理者:领导类
  17. abstract class Leader {
  18. private Leader next;
  19. public void setNext(Leader next) {
  20. this.next = next;
  21. }
  22. public Leader getNext() {
  23. return next;
  24. }
  25. //处理请求的方法
  26. public abstract void handleRequest(int LeaveDays);
  27. }
  28. //具体处理者1:班主任类
  29. class ClassAdviser extends Leader {
  30. public void handleRequest(int LeaveDays) {
  31. if (LeaveDays <= 2) {
  32. System.out.println("班主任批准您请假" + LeaveDays + "天。");
  33. } else {
  34. if (getNext() != null) {
  35. getNext().handleRequest(LeaveDays);
  36. } else {
  37. System.out.println("请假天数太多,没有人批准该假条!");
  38. }
  39. }
  40. }
  41. }
  42. //具体处理者2:系主任类
  43. class DepartmentHead extends Leader {
  44. public void handleRequest(int LeaveDays) {
  45. if (LeaveDays <= 7) {
  46. System.out.println("系主任批准您请假" + LeaveDays + "天。");
  47. } else {
  48. if (getNext() != null) {
  49. getNext().handleRequest(LeaveDays);
  50. } else {
  51. System.out.println("请假天数太多,没有人批准该假条!");
  52. }
  53. }
  54. }
  55. }
  56. //具体处理者3:院长类
  57. class Dean extends Leader {
  58. public void handleRequest(int LeaveDays) {
  59. if (LeaveDays <= 10) {
  60. System.out.println("院长批准您请假" + LeaveDays + "天。");
  61. } else {
  62. if (getNext() != null) {
  63. getNext().handleRequest(LeaveDays);
  64. } else {
  65. System.out.println("请假天数太多,没有人批准该假条!");
  66. }
  67. }
  68. }
  69. }
  70. //具体处理者4:教务处长类
  71. class DeanOfStudies extends Leader {
  72. public void handleRequest(int LeaveDays) {
  73. if (LeaveDays <= 20) {
  74. System.out.println("教务处长批准您请假" + LeaveDays + "天。");
  75. } else {
  76. if (getNext() != null) {
  77. getNext().handleRequest(LeaveDays);
  78. } else {
  79. System.out.println("请假天数太多,没有人批准该假条!");
  80. }
  81. }
  82. }
  83. }

程序运行结果如下:

院长批准您请假8天。


假如增加一个教务处长类,可以批准学生请假 20 天,也非常简单,代码如下:

  1. //具体处理者4:教务处长类
  2. class DeanOfStudies extends Leader {
  3. public void handleRequest(int LeaveDays) {
  4. if (LeaveDays <= 20) {
  5. System.out.println("教务处长批准您请假" + LeaveDays + "天。");
  6. } else {
  7. if (getNext() != null) {
  8. getNext().handleRequest(LeaveDays);
  9. } else {
  10. System.out.println("请假天数太多,没有人批准该假条!");
  11. }
  12. }
  13. }
  14. }

模式的应用场景

前边已经讲述了关于责任链模式的结构与特点,下面介绍其应用场景,责任链模式通常在以下几种情况使用。

  1. 多个对象可以处理一个请求,但具体由哪个对象处理该请求在运行时自动确定。
  2. 可动态指定一组对象处理请求,或添加新的处理者。
  3. 需要在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。

模式的扩展

职责链模式存在以下两种情况。

  1. 纯的职责链模式:一个请求必须被某一个处理者对象所接收,且一个具体处理者对某个请求的处理只能采用以下两种行为之一:自己处理(承担责任);把责任推给下家处理。
  2. 不纯的职责链模式:允许出现某一个具体处理者对象在承担了请求的一部分责任后又将剩余的责任传给下家的情况,且一个请求可以最终不被任何接收端对象所接收。

状态模式(详解版)

在软件开发过程中,应用程序中的部分对象可能会根据不同的情况做出不同的行为,我们把这种对象称为有状态的对象,而把影响对象行为的一个或多个动态变化的属性称为状态。当有状态的对象与外部事件产生互动时,其内部状态就会发生改变,从而使其行为也发生改变。如人都有高兴和伤心的时候,不同的情绪有不同的行为,当然外界也会影响其情绪变化。

对这种有状态的对象编程,传统的解决方案是:将这些所有可能发生的情况全都考虑到,然后使用 if-else 或 switch-case 语句来做状态判断,再进行不同情况的处理。但是显然这种做法对复杂的状态判断存在天然弊端,条件判断语句会过于臃肿,可读性差,且不具备扩展性,维护难度也大。且增加新的状态时要添加新的 if-else 语句,这违背了“开闭原则”,不利于程序的扩展。

以上问题如果采用“状态模式”就能很好地得到解决。状态模式的解决思想是:当控制一个对象状态转换的条件表达式过于复杂时,把相关“判断逻辑”提取出来,用各个不同的类进行表示,系统处于哪种情况,直接使用相应的状态类对象进行处理,这样能把原来复杂的逻辑判断简单化,消除了 if-else、switch-case 等冗余语句,代码更有层次性,并且具备良好的扩展力。

状态模式的定义与特点

状态(State)模式的定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。

状态模式是一种对象行为型模式,其主要优点如下。

  1. 结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
  2. 将状态转换显示化,减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
  3. 状态类职责明确,有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。


状态模式的主要缺点如下。

  1. 状态模式的使用必然会增加系统的类与对象的个数。
  2. 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
  3. 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。

状态模式的结构与实现

状态模式把受环境改变的对象行为包装在不同的状态对象里,其意图是让一个对象在其内部状态改变的时候,其行为也随之改变。现在我们来分析其基本结构和实现方法。

1. 模式的结构

状态模式包含以下主要角色。

  1. 环境类(Context)角色:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
  2. 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
  3. 具体状态(Concrete State)角色:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。


其结构图如图 1 所示。

状态模式的结构图


图1 状态模式的结构图

2. 模式的实现

状态模式的实现代码如下:

  1. public class StatePatternClient {
  2. public static void main(String[] args) {
  3. Context context = new Context(); //创建环境
  4. context.Handle(); //处理请求
  5. context.Handle();
  6. context.Handle();
  7. context.Handle();
  8. }
  9. }
  10. //环境类
  11. class Context {
  12. private State state;
  13. //定义环境类的初始状态
  14. public Context() {
  15. this.state = new ConcreteStateA();
  16. }
  17. //设置新状态
  18. public void setState(State state) {
  19. this.state = state;
  20. }
  21. //读取状态
  22. public State getState() {
  23. return (state);
  24. }
  25. //对请求做处理
  26. public void Handle() {
  27. state.Handle(this);
  28. }
  29. }
  30. //抽象状态类
  31. abstract class State {
  32. public abstract void Handle(Context context);
  33. }
  34. //具体状态A类
  35. class ConcreteStateA extends State {
  36. public void Handle(Context context) {
  37. System.out.println("当前状态是 A.");
  38. context.setState(new ConcreteStateB());
  39. }
  40. }
  41. //具体状态B类
  42. class ConcreteStateB extends State {
  43. public void Handle(Context context) {
  44. System.out.println("当前状态是 B.");
  45. context.setState(new ConcreteStateA());
  46. }
  47. }

程序运行结果如下:

当前状态是 A.
当前状态是 B.
当前状态是 A.
当前状态是 B.

状态模式的应用实例

【例1】用“状态模式”设计一个学生成绩的状态转换程序。

分析:本实例包含了“不及格”“中等”和“优秀” 3 种状态,当学生的分数小于 60 分时为“不及格”状态,当分数大于等于 60 分且小于 90 分时为“中等”状态,当分数大于等于 90 分时为“优秀”状态,我们用状态模式来实现这个程序。

首先,定义一个抽象状态类(AbstractState),其中包含了环境属性、状态名属性和当前分数属性,以及加减分方法 addScore(intx) 和检查当前状态的抽象方法 checkState()。

然后,定义“不及格”状态类 LowState、“中等”状态类 MiddleState 和“优秀”状态类 HighState,它们是具体状态类,实现 checkState() 方法,负责检査自己的状态,并根据情况转换。

最后,定义环境类(ScoreContext),其中包含了当前状态对象和加减分的方法 add(int score),客户类通过该方法来改变成绩状态。图 2 所示是其结构图。
 

学生成绩的状态转换程序的结构图


图2 学生成绩的状态转换程序的结构图


程序代码如下:

  1. public class ScoreStateTest {
  2. public static void main(String[] args) {
  3. ScoreContext account = new ScoreContext();
  4. System.out.println("学生成绩状态测试:");
  5. account.add(30);
  6. account.add(40);
  7. account.add(25);
  8. account.add(-15);
  9. account.add(-25);
  10. }
  11. }
  12. //环境类
  13. class ScoreContext {
  14. private AbstractState state;
  15. ScoreContext() {
  16. state = new LowState(this);
  17. }
  18. public void setState(AbstractState state) {
  19. this.state = state;
  20. }
  21. public AbstractState getState() {
  22. return state;
  23. }
  24. public void add(int score) {
  25. state.addScore(score);
  26. }
  27. }
  28. //抽象状态类
  29. abstract class AbstractState {
  30. protected ScoreContext hj; //环境
  31. protected String stateName; //状态名
  32. protected int score; //分数
  33. public abstract void checkState(); //检查当前状态
  34. public void addScore(int x) {
  35. score += x;
  36. System.out.print("加上:" + x + "分,\t当前分数:" + score);
  37. checkState();
  38. System.out.println("分,\t当前状态:" + hj.getState().stateName);
  39. }
  40. }
  41. //具体状态类:不及格
  42. class LowState extends AbstractState {
  43. public LowState(ScoreContext h) {
  44. hj = h;
  45. stateName = "不及格";
  46. score = 0;
  47. }
  48. public LowState(AbstractState state) {
  49. hj = state.hj;
  50. stateName = "不及格";
  51. score = state.score;
  52. }
  53. public void checkState() {
  54. if (score >= 90) {
  55. hj.setState(new HighState(this));
  56. } else if (score >= 60) {
  57. hj.setState(new MiddleState(this));
  58. }
  59. }
  60. }
  61. //具体状态类:中等
  62. class MiddleState extends AbstractState {
  63. public MiddleState(AbstractState state) {
  64. hj = state.hj;
  65. stateName = "中等";
  66. score = state.score;
  67. }
  68. public void checkState() {
  69. if (score < 60) {
  70. hj.setState(new LowState(this));
  71. } else if (score >= 90) {
  72. hj.setState(new HighState(this));
  73. }
  74. }
  75. }
  76. //具体状态类:优秀
  77. class HighState extends AbstractState {
  78. public HighState(AbstractState state) {
  79. hj = state.hj;
  80. stateName = "优秀";
  81. score = state.score;
  82. }
  83. public void checkState() {
  84. if (score < 60) {
  85. hj.setState(new LowState(this));
  86. } else if (score < 90) {
  87. hj.setState(new MiddleState(this));
  88. }
  89. }
  90. }

 程序运行结果如下:

学生成绩状态测试:
加上:30分,    当前分数:30分,    当前状态:不及格
加上:40分,    当前分数:70分,    当前状态:中等
加上:25分,    当前分数:95分,    当前状态:优秀
加上:-15分,    当前分数:80分,    当前状态:中等
加上:-25分,    当前分数:55分,    当前状态:不及格

【例2】用“状态模式”设计一个多线程的状态转换程序。

分析:多线程存在 5 种状态,分别为新建状态、就绪状态、运行状态、阻塞状态和死亡状态,各个状态当遇到相关方法调用或事件触发时会转换到其他状态,其状态转换规律如图 3 所示。
 

线程状态转换图


图3 线程状态转换图


现在先定义一个抽象状态类(TheadState),然后为图 3 所示的每个状态设计一个具体状态类,它们是新建状态(New)、就绪状态(Runnable )、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Dead),每个状态中有触发它们转变状态的方法,环境类(ThreadContext)中先生成一个初始状态(New),并提供相关触发方法,图 4 所示是线程状态转换程序的结构图。
 

线程状态转换程序的结构图


图4 线程状态转换程序的结构图


程序代码如下:

  1. public class ScoreStateTest {
  2. public static void main(String[] args) {
  3. ThreadContext context = new ThreadContext();
  4. context.start();
  5. context.getCPU();
  6. context.suspend();
  7. context.resume();
  8. context.getCPU();
  9. context.stop();
  10. }
  11. }
  12. //环境类
  13. class ThreadContext {
  14. private ThreadState state;
  15. ThreadContext() {
  16. state = new New();
  17. }
  18. public void setState(ThreadState state) {
  19. this.state = state;
  20. }
  21. public ThreadState getState() {
  22. return state;
  23. }
  24. public void start() {
  25. ((New) state).start(this);
  26. }
  27. public void getCPU() {
  28. ((Runnable) state).getCPU(this);
  29. }
  30. public void suspend() {
  31. ((Running) state).suspend(this);
  32. }
  33. public void stop() {
  34. ((Running) state).stop(this);
  35. }
  36. public void resume() {
  37. ((Blocked) state).resume(this);
  38. }
  39. }
  40. //抽象状态类:线程状态
  41. abstract class ThreadState {
  42. protected String stateName; //状态名
  43. }
  44. //具体状态类:新建状态
  45. class New extends ThreadState {
  46. public New() {
  47. stateName = "新建状态";
  48. System.out.println("当前线程处于:新建状态.");
  49. }
  50. public void start(ThreadContext hj) {
  51. System.out.print("调用start()方法-->");
  52. if (stateName.equals("新建状态")) {
  53. hj.setState(new Runnable());
  54. } else {
  55. System.out.println("当前线程不是新建状态,不能调用start()方法.");
  56. }
  57. }
  58. }
  59. //具体状态类:就绪状态
  60. class Runnable extends ThreadState {
  61. public Runnable() {
  62. stateName = "就绪状态";
  63. System.out.println("当前线程处于:就绪状态.");
  64. }
  65. public void getCPU(ThreadContext hj) {
  66. System.out.print("获得CPU时间-->");
  67. if (stateName.equals("就绪状态")) {
  68. hj.setState(new Running());
  69. } else {
  70. System.out.println("当前线程不是就绪状态,不能获取CPU.");
  71. }
  72. }
  73. }
  74. //具体状态类:运行状态
  75. class Running extends ThreadState {
  76. public Running() {
  77. stateName = "运行状态";
  78. System.out.println("当前线程处于:运行状态.");
  79. }
  80. public void suspend(ThreadContext hj) {
  81. System.out.print("调用suspend()方法-->");
  82. if (stateName.equals("运行状态")) {
  83. hj.setState(new Blocked());
  84. } else {
  85. System.out.println("当前线程不是运行状态,不能调用suspend()方法.");
  86. }
  87. }
  88. public void stop(ThreadContext hj) {
  89. System.out.print("调用stop()方法-->");
  90. if (stateName.equals("运行状态")) {
  91. hj.setState(new Dead());
  92. } else {
  93. System.out.println("当前线程不是运行状态,不能调用stop()方法.");
  94. }
  95. }
  96. }
  97. //具体状态类:阻塞状态
  98. class Blocked extends ThreadState {
  99. public Blocked() {
  100. stateName = "阻塞状态";
  101. System.out.println("当前线程处于:阻塞状态.");
  102. }
  103. public void resume(ThreadContext hj) {
  104. System.out.print("调用resume()方法-->");
  105. if (stateName.equals("阻塞状态")) {
  106. hj.setState(new Runnable());
  107. } else {
  108. System.out.println("当前线程不是阻塞状态,不能调用resume()方法.");
  109. }
  110. }
  111. }
  112. //具体状态类:死亡状态
  113. class Dead extends ThreadState {
  114. public Dead() {
  115. stateName = "死亡状态";
  116. System.out.println("当前线程处于:死亡状态.");
  117. }
  118. }


程序运行结果如下:

当前线程处于:新建状态.
调用start()方法-->当前线程处于:就绪状态.
获得CPU时间-->当前线程处于:运行状态.
调用suspend()方法-->当前线程处于:阻塞状态.
调用resume()方法-->当前线程处于:就绪状态.
获得CPU时间-->当前线程处于:运行状态.
调用stop()方法-->当前线程处于:死亡状态.

状态模式的应用场景

通常在以下情况下可以考虑使用状态模式。

  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

状态模式的扩展

在有些情况下,可能有多个环境对象需要共享一组状态,这时需要引入享元模式,将这些具体状态对象放在集合中供程序共享,其结构图如图 5 所示。
 

共享状态模式的结构图


图5 共享状态模式的结构图


分析:共享状态模式的不同之处是在环境类中增加了一个 HashMap 来保存相关状态,当需要某种状态时可以从中获取,其程序代码如下:

  1. package state;
  2. import java.util.HashMap;
  3. public class FlyweightStatePattern {
  4. public static void main(String[] args) {
  5. ShareContext context = new ShareContext(); //创建环境
  6. context.Handle(); //处理请求
  7. context.Handle();
  8. context.Handle();
  9. context.Handle();
  10. }
  11. }
  12. //环境类
  13. class ShareContext {
  14. private ShareState state;
  15. private HashMap<String, ShareState> stateSet = new HashMap<String, ShareState>();
  16. public ShareContext() {
  17. state = new ConcreteState1();
  18. stateSet.put("1", state);
  19. state = new ConcreteState2();
  20. stateSet.put("2", state);
  21. state = getState("1");
  22. }
  23. //设置新状态
  24. public void setState(ShareState state) {
  25. this.state = state;
  26. }
  27. //读取状态
  28. public ShareState getState(String key) {
  29. ShareState s = (ShareState) stateSet.get(key);
  30. return s;
  31. }
  32. //对请求做处理
  33. public void Handle() {
  34. state.Handle(this);
  35. }
  36. }
  37. //抽象状态类
  38. abstract class ShareState {
  39. public abstract void Handle(ShareContext context);
  40. }
  41. //具体状态1类
  42. class ConcreteState1 extends ShareState {
  43. public void Handle(ShareContext context) {
  44. System.out.println("当前状态是: 状态1");
  45. context.setState(context.getState("2"));
  46. }
  47. }
  48. //具体状态2类
  49. class ConcreteState2 extends ShareState {
  50. public void Handle(ShareContext context) {
  51. System.out.println("当前状态是: 状态2");
  52. context.setState(context.getState("1"));
  53. }
  54. }

 程序运行结果如下:

当前状态是: 状态1
当前状态是: 状态2
当前状态是: 状态1
当前状态是: 状态2

拓展

状态模式与责任链模式的区别

状态模式和责任链模式都能消除 if-else 分支过多的问题。但在某些情况下,状态模式中的状态可以理解为责任,那么在这种情况下,两种模式都可以使用。

从定义来看,状态模式强调的是一个对象内在状态的改变,而责任链模式强调的是外部节点对象间的改变。

从代码实现上来看,两者最大的区别就是状态模式的各个状态对象知道自己要进入的下一个状态对象,而责任链模式并不清楚其下一个节点处理对象,因为链式组装由客户端负责。

状态模式与策略模式的区别

状态模式和策略模式的 UML 类图架构几乎完全一样,但两者的应用场景是不一样的。策略模式的多种算法行为择其一都能满足,彼此之间是独立的,用户可自行更换策略算法,而状态模式的各个状态间存在相互关系,彼此之间在一定条件下存在自动切换状态的效果,并且用户无法指定状态,只能设置初始状态。

观察者模式(Observer模式)详解

在现实世界中,许多对象并不是独立存在的,其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如,某种商品的物价上涨时会导致部分商家高兴,而消费者伤心;还有,当我们开车到交叉路口时,遇到红灯会停,遇到绿灯会行。这样的例子还有很多,例如,股票价格与股民、微信公众号与微信用户、气象局的天气预报与听众、小偷与警察等。

在软件世界也是这样,例如,Excel 中的数据与折线图、饼状图、柱状图之间的关系;MVC 模式中的模型与视图的关系;事件模型中的事件源与事件处理者。所有这些,如果用观察者模式来实现就非常方便。

模式的定义与特点

观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

观察者模式是一种对象行为型模式,其主要优点如下。

  1. 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
  2. 目标与观察者之间建立了一套触发机制。


它的主要缺点如下。

  1. 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
  2. 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

模式的结构与实现

实现观察者模式时要注意具体目标对象和具体观察者对象之间不能直接调用,否则将使两者之间紧密耦合起来,这违反了面向对象的设计原则。

1. 模式的结构

观察者模式的主要角色如下。

  1. 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
  2. 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
  3. 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
  4. 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。


观察者模式的结构图如图 1 所示。
 

观察者模式的结构图


图1 观察者模式的结构图

2. 模式的实现

观察者模式的实现代码如下:

  1. package net.biancheng.c.observer;
  2. import java.util.*;
  3. public class ObserverPattern {
  4. public static void main(String[] args) {
  5. Subject subject = new ConcreteSubject();
  6. Observer obs1 = new ConcreteObserver1();
  7. Observer obs2 = new ConcreteObserver2();
  8. subject.add(obs1);
  9. subject.add(obs2);
  10. subject.notifyObserver();
  11. }
  12. }
  13. //抽象目标
  14. abstract class Subject {
  15. protected List<Observer> observers = new ArrayList<Observer>();
  16. //增加观察者方法
  17. public void add(Observer observer) {
  18. observers.add(observer);
  19. }
  20. //删除观察者方法
  21. public void remove(Observer observer) {
  22. observers.remove(observer);
  23. }
  24. public abstract void notifyObserver(); //通知观察者方法
  25. }
  26. //具体目标
  27. class ConcreteSubject extends Subject {
  28. public void notifyObserver() {
  29. System.out.println("具体目标发生改变...");
  30. System.out.println("--------------");
  31. for (Object obs : observers) {
  32. ((Observer) obs).response();
  33. }
  34. }
  35. }
  36. //抽象观察者
  37. interface Observer {
  38. void response(); //反应
  39. }
  40. //具体观察者1
  41. class ConcreteObserver1 implements Observer {
  42. public void response() {
  43. System.out.println("具体观察者1作出反应!");
  44. }
  45. }
  46. //具体观察者1
  47. class ConcreteObserver2 implements Observer {
  48. public void response() {
  49. System.out.println("具体观察者2作出反应!");
  50. }
  51. }

 程序运行结果如下:

具体目标发生改变...
--------------
具体观察者1作出反应!
具体观察者2作出反应!

模式的应用实例

【例1】利用观察者模式设计一个程序,分析“人民币汇率”的升值或贬值对进口公司进口产品成本或出口公司的出口产品收入以及公司利润率的影响。

分析:当“人民币汇率”升值时,进口公司的进口产品成本降低且利润率提升,出口公司的出口产品收入降低且利润率降低;当“人民币汇率”贬值时,进口公司的进口产品成本提升且利润率降低,出口公司的出口产品收入提升且利润率提升。

这里的汇率(Rate)类是抽象目标类,它包含了保存观察者(Company)的 List 和增加/删除观察者的方法,以及有关汇率改变的抽象方法 change(int number);而人民币汇率(RMBrate)类是具体目标, 它实现了父类的 change(int number) 方法,即当人民币汇率发生改变时通过相关公司;公司(Company)类是抽象观察者,它定义了一个有关汇率反应的抽象方法 response(int number);进口公司(ImportCompany)类和出口公司(ExportCompany)类是具体观察者类,它们实现了父类的 response(int number) 方法,即当它们接收到汇率发生改变的通知时作为相应的反应。图 2 所示是其结构图。
 

人民币汇率分析程序的结构图


图2 人民币汇率分析程序的结构图


程序代码如下:

  1. package net.biancheng.c.observer;
  2. import java.util.*;
  3. public class RMBrateTest {
  4. public static void main(String[] args) {
  5. Rate rate = new RMBrate();
  6. Company watcher1 = new ImportCompany();
  7. Company watcher2 = new ExportCompany();
  8. rate.add(watcher1);
  9. rate.add(watcher2);
  10. rate.change(10);
  11. rate.change(-9);
  12. }
  13. }
  14. //抽象目标:汇率
  15. abstract class Rate {
  16. protected List<Company> companys = new ArrayList<Company>();
  17. //增加观察者方法
  18. public void add(Company company) {
  19. companys.add(company);
  20. }
  21. //删除观察者方法
  22. public void remove(Company company) {
  23. companys.remove(company);
  24. }
  25. public abstract void change(int number);
  26. }
  27. //具体目标:人民币汇率
  28. class RMBrate extends Rate {
  29. public void change(int number) {
  30. for (Company obs : companys) {
  31. ((Company) obs).response(number);
  32. }
  33. }
  34. }
  35. //抽象观察者:公司
  36. interface Company {
  37. void response(int number);
  38. }
  39. //具体观察者1:进口公司
  40. class ImportCompany implements Company {
  41. public void response(int number) {
  42. if (number > 0) {
  43. System.out.println("人民币汇率升值" + number + "个基点,降低了进口产品成本,提升了进口公司利润率。");
  44. } else if (number < 0) {
  45. System.out.println("人民币汇率贬值" + (-number) + "个基点,提升了进口产品成本,降低了进口公司利润率。");
  46. }
  47. }
  48. }
  49. //具体观察者2:出口公司
  50. class ExportCompany implements Company {
  51. public void response(int number) {
  52. if (number > 0) {
  53. System.out.println("人民币汇率升值" + number + "个基点,降低了出口产品收入,降低了出口公司的销售利润率。");
  54. } else if (number < 0) {
  55. System.out.println("人民币汇率贬值" + (-number) + "个基点,提升了出口产品收入,提升了出口公司的销售利润率。");
  56. }
  57. }
  58. }

 程序运行结果如下:

人民币汇率升值10个基点,降低了进口产品成本,提升了进口公司利润率。
人民币汇率升值10个基点,降低了出口产品收入,降低了出口公司的销售利润率。
人民币汇率贬值9个基点,提升了进口产品成本,降低了进口公司利润率。
人民币汇率贬值9个基点,提升了出口产品收入,提升了出口公司的销售利润率。


观察者模式在软件幵发中用得最多的是窗体程序设计中的事件处理,窗体中的所有组件都是“事件源”,也就是目标对象,而事件处理程序类的对象是具体观察者对象。下面以一个学校铃声的事件处理程序为例,介绍 Windows 中的“事件处理模型”的工作原理。

【例2】利用观察者模式设计一个学校铃声的事件处理程序。

分析:在本实例中,学校的“铃”是事件源和目标,“老师”和“学生”是事件监听器和具体观察者,“铃声”是事件类。学生和老师来到学校的教学区,都会注意学校的铃,这叫事件绑定;当上课时间或下课时间到,会触发铃发声,这时会生成“铃声”事件;学生和老师听到铃声会开始上课或下课,这叫事件处理。这个实例非常适合用观察者模式实现,图 3 给出了学校铃声的事件模型。
 

学校铃声的事件模型图


图3 学校铃声的事件模型图


现在用“观察者模式”来实现该事件处理模型。

首先,定义一个铃声事件(RingEvent)类,它记录了铃声的类型(上课铃声/下课铃声)。

再定义一个学校的铃(BellEventSource)类,它是事件源,是观察者目标类,该类里面包含了监听器容器 listener,可以绑定监听者(学生或老师),并且有产生铃声事件和通知所有监听者的方法。

然后,定义铃声事件监听者(BellEventListener)类,它是抽象观察者,它包含了铃声事件处理方法 heardBell(RingEvent e)。

最后,定义老师类(TeachEventListener)和学生类(StuEventListener),它们是事件监听器,是具体观察者,听到铃声会去上课或下课。图 4 给出了学校铃声事件处理程序的结构。
 

学校铃声事件处理程序的结构图


图4 学校铃声事件处理程序的结构图


程序代码如下:

  1. package net.biancheng.c.observer;
  2. import java.util.*;
  3. public class BellEventTest {
  4. public static void main(String[] args) {
  5. BellEventSource bell = new BellEventSource(); //铃(事件源)
  6. bell.addPersonListener(new TeachEventListener()); //注册监听器(老师)
  7. bell.addPersonListener(new StuEventListener()); //注册监听器(学生)
  8. bell.ring(true); //打上课铃声
  9. System.out.println("------------");
  10. bell.ring(false); //打下课铃声
  11. }
  12. }
  13. //铃声事件类:用于封装事件源及一些与事件相关的参数
  14. class RingEvent extends EventObject {
  15. private static final long serialVersionUID = 1L;
  16. private boolean sound; //true表示上课铃声,false表示下课铃声
  17. public RingEvent(Object source, boolean sound) {
  18. super(source);
  19. this.sound = sound;
  20. }
  21. public void setSound(boolean sound) {
  22. this.sound = sound;
  23. }
  24. public boolean getSound() {
  25. return this.sound;
  26. }
  27. }
  28. //目标类:事件源,铃
  29. class BellEventSource {
  30. private List<BellEventListener> listener; //监听器容器
  31. public BellEventSource() {
  32. listener = new ArrayList<BellEventListener>();
  33. }
  34. //给事件源绑定监听器
  35. public void addPersonListener(BellEventListener ren) {
  36. listener.add(ren);
  37. }
  38. //事件触发器:敲钟,当铃声sound的值发生变化时,触发事件。
  39. public void ring(boolean sound) {
  40. String type = sound ? "上课铃" : "下课铃";
  41. System.out.println(type + "响!");
  42. RingEvent event = new RingEvent(this, sound);
  43. notifies(event); //通知注册在该事件源上的所有监听器
  44. }
  45. //当事件发生时,通知绑定在该事件源上的所有监听器做出反应(调用事件处理方法)
  46. protected void notifies(RingEvent e) {
  47. BellEventListener ren = null;
  48. Iterator<BellEventListener> iterator = listener.iterator();
  49. while (iterator.hasNext()) {
  50. ren = iterator.next();
  51. ren.heardBell(e);
  52. }
  53. }
  54. }
  55. //抽象观察者类:铃声事件监听器
  56. interface BellEventListener extends EventListener {
  57. //事件处理方法,听到铃声
  58. public void heardBell(RingEvent e);
  59. }
  60. //具体观察者类:老师事件监听器
  61. class TeachEventListener implements BellEventListener {
  62. public void heardBell(RingEvent e) {
  63. if (e.getSound()) {
  64. System.out.println("老师上课了...");
  65. } else {
  66. System.out.println("老师下课了...");
  67. }
  68. }
  69. }
  70. //具体观察者类:学生事件监听器
  71. class StuEventListener implements BellEventListener {
  72. public void heardBell(RingEvent e) {
  73. if (e.getSound()) {
  74. System.out.println("同学们,上课了...");
  75. } else {
  76. System.out.println("同学们,下课了...");
  77. }
  78. }
  79. }

程序运行结果如下:

上课铃响!
老师上课了...
同学们,上课了...
------------
下课铃响!
老师下课了...
同学们,下课了...

模式的应用场景

在软件系统中,当系统一方行为依赖另一方行为的变动时,可使用观察者模式松耦合联动双方,使得一方的变动可以通知到感兴趣的另一方对象,从而让另一方对象对此做出响应。

通过前面的分析与应用实例可知观察者模式适合以下几种情形。

  1. 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  2. 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时,可将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
  3. 实现类似广播机制的功能,不需要知道具体收听者,只需分发广播,系统中感兴趣的对象会自动接收该广播。
  4. 多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域(跨越两种观察者类型)通知。

模式的扩展

在 Java 中,通过 java.util.Observable 类和 java.util.Observer 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。

1. Observable类

Observable 类是抽象目标类,它有一个 Vector 向量,用于保存所有要通知的观察者对象,下面来介绍它最重要的 3 个方法。

  1. void addObserver(Observer o) 方法:用于将新的观察者对象添加到向量中。
  2. void notifyObservers(Object arg) 方法:调用向量中的所有观察者对象的 update() 方法,通知它们数据发生改变。通常越晚加入向量的观察者越先得到通知。
  3. void setChange() 方法:用来设置一个 boolean 类型的内部标志位,注明目标对象发生了变化。当它为真时,notifyObservers() 才会通知观察者。

2. Observer 接口

Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 void update(Observable o,Object arg) 方法,进行相应的工作。

【例3】利用 Observable 类和 Observer 接口实现原油期货的观察者模式实例。

分析:当原油价格上涨时,空方伤心,多方局兴;当油价下跌时,空方局兴,多方伤心。本实例中的抽象目标(Observable)类在 Java 中已经定义,可以直接定义其子类,即原油期货(OilFutures)类,它是具体目标类,该类中定义一个 SetPriCe(float price) 方法,当原油数据发生变化时调用其父类的 notifyObservers(Object arg) 方法来通知所有观察者;另外,本实例中的抽象观察者接口(Observer)在 Java 中已经定义,只要定义其子类,即具体观察者类(包括多方类 Bull 和空方类 Bear),并实现 update(Observable o,Object arg) 方法即可。图 5 所示是其结构图。
 

原油期货的观察者模式实例的结构图


图5 原油期货的观察者模式实例的结构图


程序代码如下:

  1. package net.biancheng.c.observer;
  2. import java.util.Observer;
  3. import java.util.Observable;
  4. public class CrudeOilFutures {
  5. public static void main(String[] args) {
  6. OilFutures oil = new OilFutures();
  7. Observer bull = new Bull(); //多方
  8. Observer bear = new Bear(); //空方
  9. oil.addObserver(bull);
  10. oil.addObserver(bear);
  11. oil.setPrice(10);
  12. oil.setPrice(-8);
  13. }
  14. }
  15. //具体目标类:原油期货
  16. class OilFutures extends Observable {
  17. private float price;
  18. public float getPrice() {
  19. return this.price;
  20. }
  21. public void setPrice(float price) {
  22. super.setChanged(); //设置内部标志位,注明数据发生变化
  23. super.notifyObservers(price); //通知观察者价格改变了
  24. this.price = price;
  25. }
  26. }
  27. //具体观察者类:多方
  28. class Bull implements Observer {
  29. public void update(Observable o, Object arg) {
  30. Float price = ((Float) arg).floatValue();
  31. if (price > 0) {
  32. System.out.println("油价上涨" + price + "元,多方高兴了!");
  33. } else {
  34. System.out.println("油价下跌" + (-price) + "元,多方伤心了!");
  35. }
  36. }
  37. }
  38. //具体观察者类:空方
  39. class Bear implements Observer {
  40. public void update(Observable o, Object arg) {
  41. Float price = ((Float) arg).floatValue();
  42. if (price > 0) {
  43. System.out.println("油价上涨" + price + "元,空方伤心了!");
  44. } else {
  45. System.out.println("油价下跌" + (-price) + "元,空方高兴了!");
  46. }
  47. }
  48. }

 程序运行结果如下:

油价上涨10.0元,空方伤心了!
油价上涨10.0元,多方高兴了!
油价下跌8.0元,空方高兴了!
油价下跌8.0元,多方伤心了!

中介者模式(详解版)

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵一发而动全身”,非常复杂。

如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。这样的例子还有很多,例如,你刚刚参加工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。

在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。

模式的定义与特点

中介者(Mediator)模式的定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。

中介者模式是一种对象行为型模式,其主要优点如下。

  1. 类之间各司其职,符合迪米特法则。
  2. 降低了对象之间的耦合性,使得对象易于独立地被复用。
  3. 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。


其主要缺点是:中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。

模式的结构与实现

中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。

1. 模式的结构

中介者模式包含以下主要角色。

  1. 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
  2. 具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
  3. 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
  4. 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。


中介者模式的结构图如图 1 所示。
 

中介者模式的结构图


图1 中介者模式的结构图

2. 模式的实现

中介者模式的实现代码如下:

  1. package net.biancheng.c.mediator;
  2. import java.util.*;
  3. public class MediatorPattern {
  4. public static void main(String[] args) {
  5. Mediator md = new ConcreteMediator();
  6. Colleague c1, c2;
  7. c1 = new ConcreteColleague1();
  8. c2 = new ConcreteColleague2();
  9. md.register(c1);
  10. md.register(c2);
  11. c1.send();
  12. System.out.println("-------------");
  13. c2.send();
  14. }
  15. }
  16. //抽象中介者
  17. abstract class Mediator {
  18. public abstract void register(Colleague colleague);
  19. public abstract void relay(Colleague cl); //转发
  20. }
  21. //具体中介者
  22. class ConcreteMediator extends Mediator {
  23. private List<Colleague> colleagues = new ArrayList<Colleague>();
  24. public void register(Colleague colleague) {
  25. if (!colleagues.contains(colleague)) {
  26. colleagues.add(colleague);
  27. colleague.setMedium(this);
  28. }
  29. }
  30. public void relay(Colleague cl) {
  31. for (Colleague ob : colleagues) {
  32. if (!ob.equals(cl)) {
  33. ((Colleague) ob).receive();
  34. }
  35. }
  36. }
  37. }
  38. //抽象同事类
  39. abstract class Colleague {
  40. protected Mediator mediator;
  41. public void setMedium(Mediator mediator) {
  42. this.mediator = mediator;
  43. }
  44. public abstract void receive();
  45. public abstract void send();
  46. }
  47. //具体同事类
  48. class ConcreteColleague1 extends Colleague {
  49. public void receive() {
  50. System.out.println("具体同事类1收到请求。");
  51. }
  52. public void send() {
  53. System.out.println("具体同事类1发出请求。");
  54. mediator.relay(this); //请中介者转发
  55. }
  56. }
  57. //具体同事类
  58. class ConcreteColleague2 extends Colleague {
  59. public void receive() {
  60. System.out.println("具体同事类2收到请求。");
  61. }
  62. public void send() {
  63. System.out.println("具体同事类2发出请求。");
  64. mediator.relay(this); //请中介者转发
  65. }
  66. }

 程序的运行结果如下:

具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。

模式的应用实例

【例1】用中介者模式编写一个“韶关房地产交流平台”程序。

说明:韶关房地产交流平台是“房地产中介公司”提供给“卖方客户”与“买方客户”进行信息交流的平台,比较适合用中介者模式来实现。

首先,定义一个中介公司(Medium)接口,它是抽象中介者,它包含了客户注册方法 register(Customer member) 和信息转发方法 relay(String from,String ad);再定义一个韶关房地产中介(EstateMedium)公司,它是具体中介者类,它包含了保存客户信息的 List 对象,并实现了中介公司中的抽象方法。

然后,定义一个客户(Customer)类,它是抽象同事类,其中包含了中介者的对象,和发送信息的 send(String ad) 方法与接收信息的 receive(String from,String ad) 方法的接口,由于本程序是窗体程序,所以本类继承 JPmme 类,并实现动作事件的处理方法 actionPerformed(ActionEvent e)。

最后,定义卖方(Seller)类和买方(Buyer)类,它们是具体同事类,是客户(Customer)类的子类,它们实现了父类中的抽象方法,通过中介者类进行信息交流,其结构图如图 2 所示。
 

韶关房地产交流平台的结构图


图2 韶关房地产交流平台的结构图


程序代码如下:

  1. package net.biancheng.c.mediator;
  2. import javax.swing.*;
  3. import java.awt.*;
  4. import java.awt.event.ActionEvent;
  5. import java.awt.event.ActionListener;
  6. import java.util.ArrayList;
  7. import java.util.List;
  8. public class DatingPlatform {
  9. public static void main(String[] args) {
  10. Medium md = new EstateMedium(); //房产中介
  11. Customer member1, member2;
  12. member1 = new Seller("张三(卖方)");
  13. member2 = new Buyer("李四(买方)");
  14. md.register(member1); //客户注册
  15. md.register(member2);
  16. }
  17. }
  18. //抽象中介者:中介公司
  19. interface Medium {
  20. void register(Customer member); //客户注册
  21. void relay(String from, String ad); //转发
  22. }
  23. //具体中介者:房地产中介
  24. class EstateMedium implements Medium {
  25. private List<Customer> members = new ArrayList<Customer>();
  26. public void register(Customer member) {
  27. if (!members.contains(member)) {
  28. members.add(member);
  29. member.setMedium(this);
  30. }
  31. }
  32. public void relay(String from, String ad) {
  33. for (Customer ob : members) {
  34. String name = ob.getName();
  35. if (!name.equals(from)) {
  36. ((Customer) ob).receive(from, ad);
  37. }
  38. }
  39. }
  40. }
  41. //抽象同事类:客户
  42. abstract class Customer extends JFrame implements ActionListener {
  43. private static final long serialVersionUID = -7219939540794786080L;
  44. protected Medium medium;
  45. protected String name;
  46. JTextField SentText;
  47. JTextArea ReceiveArea;
  48. public Customer(String name) {
  49. super(name);
  50. this.name = name;
  51. }
  52. void ClientWindow(int x, int y) {
  53. Container cp;
  54. JScrollPane sp;
  55. JPanel p1, p2;
  56. cp = this.getContentPane();
  57. SentText = new JTextField(18);
  58. ReceiveArea = new JTextArea(10, 18);
  59. ReceiveArea.setEditable(false);
  60. p1 = new JPanel();
  61. p1.setBorder(BorderFactory.createTitledBorder("接收内容:"));
  62. p1.add(ReceiveArea);
  63. sp = new JScrollPane(p1);
  64. cp.add(sp, BorderLayout.NORTH);
  65. p2 = new JPanel();
  66. p2.setBorder(BorderFactory.createTitledBorder("发送内容:"));
  67. p2.add(SentText);
  68. cp.add(p2, BorderLayout.SOUTH);
  69. SentText.addActionListener(this);
  70. this.setLocation(x, y);
  71. this.setSize(250, 330);
  72. this.setResizable(false); //窗口大小不可调整
  73. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  74. this.setVisible(true);
  75. }
  76. public void actionPerformed(ActionEvent e) {
  77. String tempInfo = SentText.getText().trim();
  78. SentText.setText("");
  79. this.send(tempInfo);
  80. }
  81. public String getName() {
  82. return name;
  83. }
  84. public void setMedium(Medium medium) {
  85. this.medium = medium;
  86. }
  87. public abstract void send(String ad);
  88. public abstract void receive(String from, String ad);
  89. }
  90. //具体同事类:卖方
  91. class Seller extends Customer {
  92. private static final long serialVersionUID = -1443076716629516027L;
  93. public Seller(String name) {
  94. super(name);
  95. ClientWindow(50, 100);
  96. }
  97. public void send(String ad) {
  98. ReceiveArea.append("我(卖方)说: " + ad + "\n");
  99. //使滚动条滚动到最底端
  100. ReceiveArea.setCaretPosition(ReceiveArea.getText().length());
  101. medium.relay(name, ad);
  102. }
  103. public void receive(String from, String ad) {
  104. ReceiveArea.append(from + "说: " + ad + "\n");
  105. //使滚动条滚动到最底端
  106. ReceiveArea.setCaretPosition(ReceiveArea.getText().length());
  107. }
  108. }
  109. //具体同事类:买方
  110. class Buyer extends Customer {
  111. private static final long serialVersionUID = -474879276076308825L;
  112. public Buyer(String name) {
  113. super(name);
  114. ClientWindow(350, 100);
  115. }
  116. public void send(String ad) {
  117. ReceiveArea.append("我(买方)说: " + ad + "\n");
  118. //使滚动条滚动到最底端
  119. ReceiveArea.setCaretPosition(ReceiveArea.getText().length());
  120. medium.relay(name, ad);
  121. }
  122. public void receive(String from, String ad) {
  123. ReceiveArea.append(from + "说: " + ad + "\n");
  124. //使滚动条滚动到最底端
  125. ReceiveArea.setCaretPosition(ReceiveArea.getText().length());
  126. }
  127. }

程序的运行结果如图 3 所示。
 

韶关房地产交流平台的运行结果


图3 韶关房地产交流平台的运行结果

模式的应用场景

前面分析了中介者模式的结构与特点,下面分析其以下应用场景。

  • 当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

模式的扩展

在实际开发中,通常采用以下两种方法来简化中介者模式,使开发变得更简单。

  1. 不定义中介者接口,把具体中介者对象实现成为单例。
  2. 同事对象不持有中介者,而是在需要的时候直接获取中介者对象并调用。


图 4 所示是简化中介者模式的结构图。
 

简化中介者模式的结构图


图4 简化中介者模式的结构图


程序代码如下:

  1. package net.biancheng.c.mediator;
  2. import java.util.*;
  3. public class SimpleMediatorPattern {
  4. public static void main(String[] args) {
  5. SimpleColleague c1, c2;
  6. c1 = new SimpleConcreteColleague1();
  7. c2 = new SimpleConcreteColleague2();
  8. c1.send();
  9. System.out.println("-----------------");
  10. c2.send();
  11. }
  12. }
  13. //简单单例中介者
  14. class SimpleMediator {
  15. private static SimpleMediator smd = new SimpleMediator();
  16. private List<SimpleColleague> colleagues = new ArrayList<SimpleColleague>();
  17. private SimpleMediator() {
  18. }
  19. public static SimpleMediator getMedium() {
  20. return (smd);
  21. }
  22. public void register(SimpleColleague colleague) {
  23. if (!colleagues.contains(colleague)) {
  24. colleagues.add(colleague);
  25. }
  26. }
  27. public void relay(SimpleColleague scl) {
  28. for (SimpleColleague ob : colleagues) {
  29. if (!ob.equals(scl)) {
  30. ((SimpleColleague) ob).receive();
  31. }
  32. }
  33. }
  34. }
  35. //抽象同事类
  36. interface SimpleColleague {
  37. void receive();
  38. void send();
  39. }
  40. //具体同事类
  41. class SimpleConcreteColleague1 implements SimpleColleague {
  42. SimpleConcreteColleague1() {
  43. SimpleMediator smd = SimpleMediator.getMedium();
  44. smd.register(this);
  45. }
  46. public void receive() {
  47. System.out.println("具体同事类1:收到请求。");
  48. }
  49. public void send() {
  50. SimpleMediator smd = SimpleMediator.getMedium();
  51. System.out.println("具体同事类1:发出请求...");
  52. smd.relay(this); //请中介者转发
  53. }
  54. }
  55. //具体同事类
  56. class SimpleConcreteColleague2 implements SimpleColleague {
  57. SimpleConcreteColleague2() {
  58. SimpleMediator smd = SimpleMediator.getMedium();
  59. smd.register(this);
  60. }
  61. public void receive() {
  62. System.out.println("具体同事类2:收到请求。");
  63. }
  64. public void send() {
  65. SimpleMediator smd = SimpleMediator.getMedium();
  66. System.out.println("具体同事类2:发出请求...");
  67. smd.relay(this); //请中介者转发
  68. }
  69. }

程序运行结果如下:

具体同事类1:发出请求...
具体同事类2:收到请求。
-----------------
具体同事类2:发出请求...
具体同事类1:收到请求。

迭代器模式(详解版)

在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。

既然将遍历方法封装在聚合类中不可取,那么聚合类中不提供遍历方法,将遍历方法由用户自己实现是否可行呢?答案是同样不可取,因为这种方式会存在两个缺点:

  1. 暴露了聚合类的内部表示,使其数据不安全;
  2. 增加了客户的负担。


“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”,如 Java 中的 Collection、List、Set、Map 等都包含了迭代器。

迭代器模式在生活中应用的比较广泛,比如:物流系统中的传送带,不管传送的是什么物品,都会被打包成一个个箱子,并且有一个统一的二维码。这样我们不需要关心箱子里是什么,在分发时只需要一个个检查发送的目的地即可。再比如,我们平时乘坐交通工具,都是统一刷卡或者刷脸进站,而不需要关心是男性还是女性、是残疾人还是正常人等信息。

模式的定义与特点

迭代器(Iterator)模式的定义:提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。迭代器模式是一种对象行为型模式,其主要优点如下。

  1. 访问一个聚合对象的内容而无须暴露它的内部表示。
  2. 遍历任务交由迭代器完成,这简化了聚合类。
  3. 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  4. 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  5. 封装性良好,为遍历不同的聚合结构提供一个统一的接口。


其主要缺点是:增加了类的个数,这在一定程度上增加了系统的复杂性。

在日常开发中,我们几乎不会自己写迭代器。除非需要定制一个自己实现的数据结构对应的迭代器,否则,开源框架提供的 API 完全够用。

模式的结构与实现

迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。现在我们来分析其基本结构与实现方法。

1. 模式的结构

迭代器模式主要包含以下角色。

  1. 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
  2. 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  3. 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
  4. 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。


其结构图如图 1 所示。
 

迭代器模式的结构图


图1 迭代器模式的结构图

2. 模式的实现

迭代器模式的实现代码如下:

  1. package net.biancheng.c.iterator;
  2. import java.util.*;
  3. public class IteratorPattern {
  4. public static void main(String[] args) {
  5. Aggregate ag = new ConcreteAggregate();
  6. ag.add("中山大学");
  7. ag.add("华南理工");
  8. ag.add("韶关学院");
  9. System.out.print("聚合的内容有:");
  10. Iterator it = ag.getIterator();
  11. while (it.hasNext()) {
  12. Object ob = it.next();
  13. System.out.print(ob.toString() + "\t");
  14. }
  15. Object ob = it.first();
  16. System.out.println("\nFirst:" + ob.toString());
  17. }
  18. }
  19. //抽象聚合
  20. interface Aggregate {
  21. public void add(Object obj);
  22. public void remove(Object obj);
  23. public Iterator getIterator();
  24. }
  25. //具体聚合
  26. class ConcreteAggregate implements Aggregate {
  27. private List<Object> list = new ArrayList<Object>();
  28. public void add(Object obj) {
  29. list.add(obj);
  30. }
  31. public void remove(Object obj) {
  32. list.remove(obj);
  33. }
  34. public Iterator getIterator() {
  35. return (new ConcreteIterator(list));
  36. }
  37. }
  38. //抽象迭代器
  39. interface Iterator {
  40. Object first();
  41. Object next();
  42. boolean hasNext();
  43. }
  44. //具体迭代器
  45. class ConcreteIterator implements Iterator {
  46. private List<Object> list = null;
  47. private int index = -1;
  48. public ConcreteIterator(List<Object> list) {
  49. this.list = list;
  50. }
  51. public boolean hasNext() {
  52. if (index < list.size() - 1) {
  53. return true;
  54. } else {
  55. return false;
  56. }
  57. }
  58. public Object first() {
  59. index = 0;
  60. Object obj = list.get(index);
  61. ;
  62. return obj;
  63. }
  64. public Object next() {
  65. Object obj = null;
  66. if (this.hasNext()) {
  67. obj = list.get(++index);
  68. }
  69. return obj;
  70. }
  71. }

 程序运行结果如下:

聚合的内容有:中山大学    华南理工    韶关学院   
First:中山大学

模式的应用实例

【例1】用迭代器模式编写一个浏览婺源旅游风景图的程序。

分析:婺源的名胜古迹较多,要设计一个查看相关景点图片(点此下载本实例所要显示的景点图片)和简介的程序,用“迭代器模式”设计比较合适。

首先,设计一个婺源景点(WyViewSpot)类来保存每张图片的名称与简介;再设计一个景点集(ViewSpotSet)接口,它是抽象聚合类,提供了增加和删除婺源景点的方法,以及获取迭代器的方法。

然后,定义一个婺源景点集(WyViewSpotSet)类,它是具体聚合类,用 ArrayList 来保存所有景点信息,并实现父类中的抽象方法;再定义婺源景点的抽象迭代器(ViewSpotltemtor)接口,其中包含了查看景点信息的相关方法。

最后,定义婺源景点的具体迭代器(WyViewSpotlterator)类,它实现了父类的抽象方法;客户端程序设计成窗口程序,它初始化婺源景点集(ViewSpotSet)中的数据,并实现 ActionListener 接口,它通过婺源景点迭代器(ViewSpotlterator)来査看婺源景点(WyViewSpot)的信息。图 2 所示是其结构图。
 

婺源旅游风景图浏览程序的结构图


图2 婺源旅游风景图浏览程序的结构图(点此查看原图


程序代码如下:

  1. package net.biancheng.c.iterator;
  2. import javax.swing.*;
  3. import java.awt.*;
  4. import java.awt.event.ActionEvent;
  5. import java.awt.event.ActionListener;
  6. import java.util.ArrayList;
  7. public class PictureIterator {
  8. public static void main(String[] args) {
  9. new PictureFrame();
  10. }
  11. }
  12. //相框类
  13. class PictureFrame extends JFrame implements ActionListener {
  14. private static final long serialVersionUID = 1L;
  15. ViewSpotSet ag; //婺源景点集接口
  16. ViewSpotIterator it; //婺源景点迭代器接口
  17. WyViewSpot ob; //婺源景点类
  18. PictureFrame() {
  19. super("中国最美乡村“婺源”的部分风景图");
  20. this.setResizable(false);
  21. ag = new WyViewSpotSet();
  22. ag.add(new WyViewSpot("江湾", "江湾景区是婺源的一个国家5A级旅游景区,景区内有萧江宗祠、永思街、滕家老屋、婺源人家、乡贤园、百工坊等一大批古建筑,精美绝伦,做工精细。"));
  23. ag.add(new WyViewSpot("李坑", "李坑村是一个以李姓聚居为主的古村落,是国家4A级旅游景区,其建筑风格独特,是著名的徽派建筑,给人一种安静、祥和的感觉。"));
  24. ag.add(new WyViewSpot("思溪延村", "思溪延村位于婺源县思口镇境内,始建于南宋庆元五年(1199年),当时建村者俞氏以(鱼)思清溪水而名。"));
  25. ag.add(new WyViewSpot("晓起村", "晓起有“中国茶文化第一村”与“国家级生态示范村”之美誉,村屋多为清代建筑,风格各具特色,村中小巷均铺青石,曲曲折折,回环如棋局。"));
  26. ag.add(new WyViewSpot("菊径村", "菊径村形状为山环水绕型,小河成大半圆型,绕村庄将近一周,四周为高山环绕,符合中国的八卦“后山前水”设计,当地人称“脸盆村”。"));
  27. ag.add(new WyViewSpot("篁岭", "篁岭是著名的“晒秋”文化起源地,也是一座距今近六百历史的徽州古村;篁岭属典型山居村落,民居围绕水口呈扇形梯状错落排布。"));
  28. ag.add(new WyViewSpot("彩虹桥", "彩虹桥是婺源颇有特色的带顶的桥——廊桥,其不仅造型优美,而且它可在雨天里供行人歇脚,其名取自唐诗“两水夹明镜,双桥落彩虹”。"));
  29. ag.add(new WyViewSpot("卧龙谷", "卧龙谷是国家4A级旅游区,这里飞泉瀑流泄银吐玉、彩池幽潭碧绿清新、山峰岩石挺拔奇巧,活脱脱一幅天然泼墨山水画。"));
  30. it = ag.getIterator(); //获取婺源景点迭代器
  31. ob = it.first();
  32. this.showPicture(ob.getName(), ob.getIntroduce());
  33. }
  34. //显示图片
  35. void showPicture(String Name, String Introduce) {
  36. Container cp = this.getContentPane();
  37. JPanel picturePanel = new JPanel();
  38. JPanel controlPanel = new JPanel();
  39. String FileName = "src/iterator/Picture/" + Name + ".jpg";
  40. JLabel lb = new JLabel(Name, new ImageIcon(FileName), JLabel.CENTER);
  41. JTextArea ta = new JTextArea(Introduce);
  42. lb.setHorizontalTextPosition(JLabel.CENTER);
  43. lb.setVerticalTextPosition(JLabel.TOP);
  44. lb.setFont(new Font("宋体", Font.BOLD, 20));
  45. ta.setLineWrap(true);
  46. ta.setEditable(false);
  47. //ta.setBackground(Color.orange);
  48. picturePanel.setLayout(new BorderLayout(5, 5));
  49. picturePanel.add("Center", lb);
  50. picturePanel.add("South", ta);
  51. JButton first, last, next, previous;
  52. first = new JButton("第一张");
  53. next = new JButton("下一张");
  54. previous = new JButton("上一张");
  55. last = new JButton("最末张");
  56. first.addActionListener(this);
  57. next.addActionListener(this);
  58. previous.addActionListener(this);
  59. last.addActionListener(this);
  60. controlPanel.add(first);
  61. controlPanel.add(next);
  62. controlPanel.add(previous);
  63. controlPanel.add(last);
  64. cp.add("Center", picturePanel);
  65. cp.add("South", controlPanel);
  66. this.setSize(630, 550);
  67. this.setVisible(true);
  68. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  69. }
  70. @Override
  71. public void actionPerformed(ActionEvent arg0) {
  72. String command = arg0.getActionCommand();
  73. if (command.equals("第一张")) {
  74. ob = it.first();
  75. this.showPicture(ob.getName(), ob.getIntroduce());
  76. } else if (command.equals("下一张")) {
  77. ob = it.next();
  78. this.showPicture(ob.getName(), ob.getIntroduce());
  79. } else if (command.equals("上一张")) {
  80. ob = it.previous();
  81. this.showPicture(ob.getName(), ob.getIntroduce());
  82. } else if (command.equals("最末张")) {
  83. ob = it.last();
  84. this.showPicture(ob.getName(), ob.getIntroduce());
  85. }
  86. }
  87. }
  88. //婺源景点类
  89. class WyViewSpot {
  90. private String Name;
  91. private String Introduce;
  92. WyViewSpot(String Name, String Introduce) {
  93. this.Name = Name;
  94. this.Introduce = Introduce;
  95. }
  96. public String getName() {
  97. return Name;
  98. }
  99. public String getIntroduce() {
  100. return Introduce;
  101. }
  102. }
  103. //抽象聚合:婺源景点集接口
  104. interface ViewSpotSet {
  105. void add(WyViewSpot obj);
  106. void remove(WyViewSpot obj);
  107. ViewSpotIterator getIterator();
  108. }
  109. //具体聚合:婺源景点集
  110. class WyViewSpotSet implements ViewSpotSet {
  111. private ArrayList<WyViewSpot> list = new ArrayList<WyViewSpot>();
  112. public void add(WyViewSpot obj) {
  113. list.add(obj);
  114. }
  115. public void remove(WyViewSpot obj) {
  116. list.remove(obj);
  117. }
  118. public ViewSpotIterator getIterator() {
  119. return (new WyViewSpotIterator(list));
  120. }
  121. }
  122. //抽象迭代器:婺源景点迭代器接口
  123. interface ViewSpotIterator {
  124. boolean hasNext();
  125. WyViewSpot first();
  126. WyViewSpot next();
  127. WyViewSpot previous();
  128. WyViewSpot last();
  129. }
  130. //具体迭代器:婺源景点迭代器
  131. class WyViewSpotIterator implements ViewSpotIterator {
  132. private ArrayList<WyViewSpot> list = null;
  133. private int index = -1;
  134. WyViewSpot obj = null;
  135. public WyViewSpotIterator(ArrayList<WyViewSpot> list) {
  136. this.list = list;
  137. }
  138. public boolean hasNext() {
  139. if (index < list.size() - 1) {
  140. return true;
  141. } else {
  142. return false;
  143. }
  144. }
  145. public WyViewSpot first() {
  146. index = 0;
  147. obj = list.get(index);
  148. return obj;
  149. }
  150. public WyViewSpot next() {
  151. if (this.hasNext()) {
  152. obj = list.get(++index);
  153. }
  154. return obj;
  155. }
  156. public WyViewSpot previous() {
  157. if (index > 0) {
  158. obj = list.get(--index);
  159. }
  160. return obj;
  161. }
  162. public WyViewSpot last() {
  163. index = list.size() - 1;
  164. obj = list.get(index);
  165. return obj;
  166. }
  167. }

 程序运行结果如图 3 所示。
 

婺源旅游风景图浏览程序的运行结果


图3 婺源旅游风景图浏览程序的运行结果(点此查看原图

模式的应用场景

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

  1. 当需要为聚合对象提供多种遍历方式时。
  2. 当需要为遍历不同的聚合结构提供一个统一的接口时。
  3. 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。


由于聚合与迭代器的关系非常密切,所以大多数语言在实现聚合类时都提供了迭代器类,因此大数情况下使用语言中已有的聚合类的迭代器就已经够了。

模式的扩展

迭代器模式常常与组合模式结合起来使用,在对组合模式中的容器构件进行访问时,经常将迭代器潜藏在组合模式的容器构成类中。当然,也可以构造一个外部迭代器来对容器构件进行访问,其结构图如图 4 所示。
 

组合迭代器模式的结构图


图4 组合迭代器模式的结构图

访问者模式(Visitor模式)详解

在现实生活中,有些集合对象存在多种不同的元素,且每种元素也存在多种不同的访问者和处理方式。例如,公园中存在多个景点,也存在多个游客,不同的游客对同一个景点的评价可能不同;医院医生开的处方单中包含多种药元素,査看它的划价员和药房工作人员对它的处理方式也不同,划价员根据处方单上面的药品名和数量进行划价,药房工作人员根据处方单的内容进行抓药。

这样的例子还有很多,例如,电影或电视剧中的人物角色,不同的观众对他们的评价也不同;还有顾客在商场购物时放在“购物车”中的商品,顾客主要关心所选商品的性价比,而收银员关心的是商品的价格和数量。

这些被处理的数据元素相对稳定而访问方式多种多样的数据结构,如果用“访问者模式”来处理比较方便。访问者模式能把处理方法从数据结构中分离出来,并可以根据需要增加新的处理方法,且不用修改原来的程序代码与数据结构,这提高了程序的扩展性和灵活性。

模式的定义与特点

访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。

访问者(Visitor)模式是一种对象行为型模式,其主要优点如下。

  1. 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  2. 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  3. 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  4. 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。


访问者(Visitor)模式的主要缺点如下。

  1. 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  2. 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  3. 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。

模式的结构与实现

访问者(Visitor)模式实现的关键是如何将作用于元素的操作分离出来封装成独立的类,其基本结构与实现方法如下。

1. 模式的结构

访问者模式包含以下主要角色。

  1. 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
  2. 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  3. 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  4. 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  5. 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。


其结构图如图 1 所示。
 

访问者(Visitor)模式的结构图


图1 访问者(Visitor)模式的结构图(点此查看原图

2. 模式的实现

访问者模式的实现代码如下:

  1. package net.biancheng.c.visitor;
  2. import java.util.*;
  3. public class VisitorPattern {
  4. public static void main(String[] args) {
  5. ObjectStructure os = new ObjectStructure();
  6. os.add(new ConcreteElementA());
  7. os.add(new ConcreteElementB());
  8. Visitor visitor = new ConcreteVisitorA();
  9. os.accept(visitor);
  10. System.out.println("------------------------");
  11. visitor = new ConcreteVisitorB();
  12. os.accept(visitor);
  13. }
  14. }
  15. //抽象访问者
  16. interface Visitor {
  17. void visit(ConcreteElementA element);
  18. void visit(ConcreteElementB element);
  19. }
  20. //具体访问者A类
  21. class ConcreteVisitorA implements Visitor {
  22. public void visit(ConcreteElementA element) {
  23. System.out.println("具体访问者A访问-->" + element.operationA());
  24. }
  25. public void visit(ConcreteElementB element) {
  26. System.out.println("具体访问者A访问-->" + element.operationB());
  27. }
  28. }
  29. //具体访问者B类
  30. class ConcreteVisitorB implements Visitor {
  31. public void visit(ConcreteElementA element) {
  32. System.out.println("具体访问者B访问-->" + element.operationA());
  33. }
  34. public void visit(ConcreteElementB element) {
  35. System.out.println("具体访问者B访问-->" + element.operationB());
  36. }
  37. }
  38. //抽象元素类
  39. interface Element {
  40. void accept(Visitor visitor);
  41. }
  42. //具体元素A类
  43. class ConcreteElementA implements Element {
  44. public void accept(Visitor visitor) {
  45. visitor.visit(this);
  46. }
  47. public String operationA() {
  48. return "具体元素A的操作。";
  49. }
  50. }
  51. //具体元素B类
  52. class ConcreteElementB implements Element {
  53. public void accept(Visitor visitor) {
  54. visitor.visit(this);
  55. }
  56. public String operationB() {
  57. return "具体元素B的操作。";
  58. }
  59. }
  60. //对象结构角色
  61. class ObjectStructure {
  62. private List<Element> list = new ArrayList<Element>();
  63. public void accept(Visitor visitor) {
  64. Iterator<Element> i = list.iterator();
  65. while (i.hasNext()) {
  66. ((Element) i.next()).accept(visitor);
  67. }
  68. }
  69. public void add(Element element) {
  70. list.add(element);
  71. }
  72. public void remove(Element element) {
  73. list.remove(element);
  74. }
  75. }

程序的运行结果如下:

具体访问者A访问-->具体元素A的操作。
具体访问者A访问-->具体元素B的操作。
------------------------
具体访问者B访问-->具体元素A的操作。
具体访问者B访问-->具体元素B的操作。

模式的应用实例

【例1】利用“访问者(Visitor)模式”模拟艺术公司与造币公司的功能。

分析:艺术公司利用“铜”可以设计出铜像,利用“纸”可以画出图画;造币公司利用“铜”可以印出铜币,利用“纸”可以印出纸币(点此下载运行该程序后所要显示的图片)。对“铜”和“纸”这两种元素,两个公司的处理方法不同,所以该实例用访问者模式来实现比较适合。

首先,定义一个公司(Company)接口,它是抽象访问者,提供了两个根据纸(Paper)或铜(Cuprum)这两种元素创建作品的方法;再定义艺术公司(ArtCompany)类和造币公司(Mint)类,它们是具体访问者,实现了父接口的方法。

然后,定义一个材料(Material)接口,它是抽象元素,提供了 accept(Company visitor)方法来接受访问者(Company)对象访问;再定义纸(Paper)类和铜(Cuprum)类,它们是具体元素类,实现了父接口中的方法。

最后,定义一个材料集(SetMaterial)类,它是对象结构角色,拥有保存所有元素的容器 List,并提供让访问者对象遍历容器中的所有元素的 accept(Company visitor)方法;客户类设计成窗体程序,它提供材料集(SetMaterial)对象供访问者(Company)对象访问,实现了 ItemListener 接口,处理用户的事件请求。图 2 所示是其结构图。
 

艺术公司与造币公司的结构图


图2 艺术公司与造币公司的结构图


程序代码如下:

  1. package net.biancheng.c.visitor;
  2. import javax.swing.*;
  3. import java.awt.event.ItemEvent;
  4. import java.awt.event.ItemListener;
  5. import java.util.ArrayList;
  6. import java.util.Iterator;
  7. import java.util.List;
  8. public class VisitorProducer {
  9. public static void main(String[] args) {
  10. new MaterialWin();
  11. }
  12. }
  13. //窗体类
  14. class MaterialWin extends JFrame implements ItemListener {
  15. private static final long serialVersionUID = 1L;
  16. JPanel CenterJP;
  17. SetMaterial os; //材料集对象
  18. Company visitor1, visitor2; //访问者对象
  19. String[] select;
  20. MaterialWin() {
  21. super("利用访问者模式设计艺术公司和造币公司");
  22. JRadioButton Art;
  23. JRadioButton mint;
  24. os = new SetMaterial();
  25. os.add(new Cuprum());
  26. os.add(new Paper());
  27. visitor1 = new ArtCompany();//艺术公司
  28. visitor2 = new Mint(); //造币公司
  29. this.setBounds(10, 10, 750, 350);
  30. this.setResizable(false);
  31. CenterJP = new JPanel();
  32. this.add("Center", CenterJP);
  33. JPanel SouthJP = new JPanel();
  34. JLabel yl = new JLabel("原材料有:铜和纸,请选择生产公司:");
  35. Art = new JRadioButton("艺术公司", true);
  36. mint = new JRadioButton("造币公司");
  37. Art.addItemListener(this);
  38. mint.addItemListener(this);
  39. ButtonGroup group = new ButtonGroup();
  40. group.add(Art);
  41. group.add(mint);
  42. SouthJP.add(yl);
  43. SouthJP.add(Art);
  44. SouthJP.add(mint);
  45. this.add("South", SouthJP);
  46. select = (os.accept(visitor1)).split(" "); //获取产品名
  47. showPicture(select[0], select[1]); //显示产品
  48. }
  49. //显示图片
  50. void showPicture(String Cuprum, String paper) {
  51. CenterJP.removeAll(); //清除面板内容
  52. CenterJP.repaint(); //刷新屏幕
  53. String FileName1 = "src/visitor/Picture/" + Cuprum + ".jpg";
  54. String FileName2 = "src/visitor/Picture/" + paper + ".jpg";
  55. JLabel lb = new JLabel(new ImageIcon(FileName1), JLabel.CENTER);
  56. JLabel rb = new JLabel(new ImageIcon(FileName2), JLabel.CENTER);
  57. CenterJP.add(lb);
  58. CenterJP.add(rb);
  59. this.setVisible(true);
  60. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  61. }
  62. @Override
  63. public void itemStateChanged(ItemEvent arg0) {
  64. JRadioButton jc = (JRadioButton) arg0.getSource();
  65. if (jc.isSelected()) {
  66. if (jc.getText() == "造币公司") {
  67. select = (os.accept(visitor2)).split(" ");
  68. } else {
  69. select = (os.accept(visitor1)).split(" ");
  70. }
  71. showPicture(select[0], select[1]); //显示选择的产品
  72. }
  73. }
  74. }
  75. //抽象访问者:公司
  76. interface Company {
  77. String create(Paper element);
  78. String create(Cuprum element);
  79. }
  80. //具体访问者:艺术公司
  81. class ArtCompany implements Company {
  82. public String create(Paper element) {
  83. return "讲学图";
  84. }
  85. public String create(Cuprum element) {
  86. return "朱熹铜像";
  87. }
  88. }
  89. //具体访问者:造币公司
  90. class Mint implements Company {
  91. public String create(Paper element) {
  92. return "纸币";
  93. }
  94. public String create(Cuprum element) {
  95. return "铜币";
  96. }
  97. }
  98. //抽象元素:材料
  99. interface Material {
  100. String accept(Company visitor);
  101. }
  102. //具体元素:纸
  103. class Paper implements Material {
  104. public String accept(Company visitor) {
  105. return (visitor.create(this));
  106. }
  107. }
  108. //具体元素:铜
  109. class Cuprum implements Material {
  110. public String accept(Company visitor) {
  111. return (visitor.create(this));
  112. }
  113. }
  114. //对象结构角色:材料集
  115. class SetMaterial {
  116. private List<Material> list = new ArrayList<Material>();
  117. public String accept(Company visitor) {
  118. Iterator<Material> i = list.iterator();
  119. String tmp = "";
  120. while (i.hasNext()) {
  121. tmp += ((Material) i.next()).accept(visitor) + " ";
  122. }
  123. return tmp; //返回某公司的作品集
  124. }
  125. public void add(Material element) {
  126. list.add(element);
  127. }
  128. public void remove(Material element) {
  129. list.remove(element);
  130. }
  131. }

 程序运行结果如图 3 所示。
 

艺术公司设计的产品


(a)艺术公司设计的产品

造币公司生产的货币


(b)造币公司生产的货币

图3 艺术公司与造币公司的运行结果

模式的应用场景

当系统中存在类型数量稳定(固定)的一类数据结构时,可以使用访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会对数据产生任何副作用(脏数据)。

简而言之,就是当对集合中的不同类型数据(类型数量稳定)进行多种操作时,使用访问者模式。

通常在以下情况可以考虑使用访问者(Visitor)模式。

  1. 对象结构相对稳定,但其操作算法经常变化的程序。
  2. 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  3. 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。

模式的扩展

访问者(Visitor)模式是使用频率较高的一种设计模式,它常常同以下两种设计模式联用。

(1)与“迭代器模式”联用。因为访问者模式中的“对象结构”是一个包含元素角色的容器,当访问者遍历容器中的所有元素时,常常要用迭代器。如【例1】中的对象结构是用 List 实现的,它通过 List 对象的 Iterator() 方法获取迭代器。如果对象结构中的聚合类没有提供迭代器,也可以用迭代器模式自定义一个。

(2)访问者(Visitor)模式同“组合模式”联用。因为访问者(Visitor)模式中的“元素对象”可能是叶子对象或者是容器对象,如果元素对象包含容器对象,就必须用到组合模式,其结构图如图 4 所示。
 

包含组合模式的访问者模式的结构图


图4 包含组合模式的访问者模式的结构图

备忘录模式(详解版)

每个人都有犯错误的时候,都希望有种“后悔药”能弥补自己的过失,让自己重新开始,但现实是残酷的。在计算机应用中,客户同样会常常犯错误,能否提供“后悔药”给他们呢?当然是可以的,而且是有必要的。这个功能由“备忘录模式”来实现。

其实很多应用软件都提供了这项功能,如 Word、记事本、Photoshop、Eclipse 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 IE 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。

模式的定义与特点

备忘录(Memento)模式的定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。

备忘录模式是一种对象行为型模式,其主要优点如下。

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
  • 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
  • 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。


其主要缺点是:资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

模式的结构与实现

备忘录模式的核心是设计备忘录类以及用于管理备忘录的管理者类,现在我们来学习其结构与实现。

1. 模式的结构

备忘录模式的主要角色如下。

  1. 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  2. 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  3. 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。


备忘录模式的结构图如图 1 所示。
 

备忘录模式的结构图


图1 备忘录模式的结构图

2. 模式的实现

备忘录模式的实现代码如下:

  1. package net.biancheng.c.memento;
  2. public class MementoPattern {
  3. public static void main(String[] args) {
  4. Originator or = new Originator();
  5. Caretaker cr = new Caretaker();
  6. or.setState("S0");
  7. System.out.println("初始状态:" + or.getState());
  8. cr.setMemento(or.createMemento()); //保存状态
  9. or.setState("S1");
  10. System.out.println("新的状态:" + or.getState());
  11. or.restoreMemento(cr.getMemento()); //恢复状态
  12. System.out.println("恢复状态:" + or.getState());
  13. }
  14. }
  15. //备忘录
  16. class Memento {
  17. private String state;
  18. public Memento(String state) {
  19. this.state = state;
  20. }
  21. public void setState(String state) {
  22. this.state = state;
  23. }
  24. public String getState() {
  25. return state;
  26. }
  27. }
  28. //发起人
  29. class Originator {
  30. private String state;
  31. public void setState(String state) {
  32. this.state = state;
  33. }
  34. public String getState() {
  35. return state;
  36. }
  37. public Memento createMemento() {
  38. return new Memento(state);
  39. }
  40. public void restoreMemento(Memento m) {
  41. this.setState(m.getState());
  42. }
  43. }
  44. //管理者
  45. class Caretaker {
  46. private Memento memento;
  47. public void setMemento(Memento m) {
  48. memento = m;
  49. }
  50. public Memento getMemento() {
  51. return memento;
  52. }
  53. }

 程序运行的结果如下:

初始状态:S0
新的状态:S1
恢复状态:S0

模式的应用实例

【例1】利用备忘录模式设计相亲游戏。

分析:假如有西施、王昭君、貂蝉、杨玉环四大美女同你相亲,你可以选择其中一位作为你的爱人;当然,如果你对前面的选择不满意,还可以重新选择,但希望你不要太花心;这个游戏提供后悔功能,用“备忘录模式”设计比较合适(点此下载所要显示的四大美女的图片)。

首先,先设计一个美女(Girl)类,它是备忘录角色,提供了获取和存储美女信息的功能;然后,设计一个相亲者(You)类,它是发起人角色,它记录当前时刻的内部状态信息(临时妻子的姓名),并提供创建备忘录和恢复备忘录数据的功能;最后,定义一个美女栈(GirlStack)类,它是管理者角色,负责对备忘录进行管理,用于保存相亲者(You)前面选过的美女信息,不过最多只能保存 4 个,提供后悔功能。

客户类设计成窗体程序,它包含美女栈(GirlStack)对象和相亲者(You)对象,它实现了 ActionListener 接口的事件处理方法 actionPerformed(ActionEvent e),并将 4 大美女图像和相亲者(You)选择的美女图像在窗体中显示出来。图 2 所示是其结构图。
 

相亲游戏的结构图


图2 相亲游戏的结构图


程序代码如下:

  1. package net.biancheng.c.memento;
  2. import javax.swing.*;
  3. import java.awt.*;
  4. import java.awt.event.ActionEvent;
  5. import java.awt.event.ActionListener;
  6. public class DatingGame {
  7. public static void main(String[] args) {
  8. new DatingGameWin();
  9. }
  10. }
  11. //客户窗体类
  12. class DatingGameWin extends JFrame implements ActionListener {
  13. private static final long serialVersionUID = 1L;
  14. JPanel CenterJP, EastJP;
  15. JRadioButton girl1, girl2, girl3, girl4;
  16. JButton button1, button2;
  17. String FileName;
  18. JLabel g;
  19. You you;
  20. GirlStack girls;
  21. DatingGameWin() {
  22. super("利用备忘录模式设计相亲游戏");
  23. you = new You();
  24. girls = new GirlStack();
  25. this.setBounds(0, 0, 900, 380);
  26. this.setResizable(false);
  27. FileName = "src/memento/Photo/四大美女.jpg";
  28. g = new JLabel(new ImageIcon(FileName), JLabel.CENTER);
  29. CenterJP = new JPanel();
  30. CenterJP.setLayout(new GridLayout(1, 4));
  31. CenterJP.setBorder(BorderFactory.createTitledBorder("四大美女如下:"));
  32. CenterJP.add(g);
  33. this.add("Center", CenterJP);
  34. EastJP = new JPanel();
  35. EastJP.setLayout(new GridLayout(1, 1));
  36. EastJP.setBorder(BorderFactory.createTitledBorder("您选择的爱人是:"));
  37. this.add("East", EastJP);
  38. JPanel SouthJP = new JPanel();
  39. JLabel info = new JLabel("四大美女有“沉鱼落雁之容、闭月羞花之貌”,您选择谁?");
  40. girl1 = new JRadioButton("西施", true);
  41. girl2 = new JRadioButton("貂蝉");
  42. girl3 = new JRadioButton("王昭君");
  43. girl4 = new JRadioButton("杨玉环");
  44. button1 = new JButton("确定");
  45. button2 = new JButton("返回");
  46. ButtonGroup group = new ButtonGroup();
  47. group.add(girl1);
  48. group.add(girl2);
  49. group.add(girl3);
  50. group.add(girl4);
  51. SouthJP.add(info);
  52. SouthJP.add(girl1);
  53. SouthJP.add(girl2);
  54. SouthJP.add(girl3);
  55. SouthJP.add(girl4);
  56. SouthJP.add(button1);
  57. SouthJP.add(button2);
  58. button1.addActionListener(this);
  59. button2.addActionListener(this);
  60. this.add("South", SouthJP);
  61. showPicture("空白");
  62. you.setWife("空白");
  63. girls.push(you.createMemento()); //保存状态
  64. }
  65. //显示图片
  66. void showPicture(String name) {
  67. EastJP.removeAll(); //清除面板内容
  68. EastJP.repaint(); //刷新屏幕
  69. you.setWife(name);
  70. FileName = "src/memento/Photo/" + name + ".jpg";
  71. g = new JLabel(new ImageIcon(FileName), JLabel.CENTER);
  72. EastJP.add(g);
  73. this.setVisible(true);
  74. this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  75. }
  76. @Override
  77. public void actionPerformed(ActionEvent e) {
  78. boolean ok = false;
  79. if (e.getSource() == button1) {
  80. ok = girls.push(you.createMemento()); //保存状态
  81. if (ok && girl1.isSelected()) {
  82. showPicture("西施");
  83. } else if (ok && girl2.isSelected()) {
  84. showPicture("貂蝉");
  85. } else if (ok && girl3.isSelected()) {
  86. showPicture("王昭君");
  87. } else if (ok && girl4.isSelected()) {
  88. showPicture("杨玉环");
  89. }
  90. } else if (e.getSource() == button2) {
  91. you.restoreMemento(girls.pop()); //恢复状态
  92. showPicture(you.getWife());
  93. }
  94. }
  95. }
  96. //备忘录:美女
  97. class Girl {
  98. private String name;
  99. public Girl(String name) {
  100. this.name = name;
  101. }
  102. public void setName(String name) {
  103. this.name = name;
  104. }
  105. public String getName() {
  106. return name;
  107. }
  108. }
  109. //发起人:您
  110. class You {
  111. private String wifeName; //妻子
  112. public void setWife(String name) {
  113. wifeName = name;
  114. }
  115. public String getWife() {
  116. return wifeName;
  117. }
  118. public Girl createMemento() {
  119. return new Girl(wifeName);
  120. }
  121. public void restoreMemento(Girl p) {
  122. setWife(p.getName());
  123. }
  124. }
  125. //管理者:美女栈
  126. class GirlStack {
  127. private Girl girl[];
  128. private int top;
  129. GirlStack() {
  130. girl = new Girl[5];
  131. top = -1;
  132. }
  133. public boolean push(Girl p) {
  134. if (top >= 4) {
  135. System.out.println("你太花心了,变来变去的!");
  136. return false;
  137. } else {
  138. girl[++top] = p;
  139. return true;
  140. }
  141. }
  142. public Girl pop() {
  143. if (top <= 0) {
  144. System.out.println("美女栈空了!");
  145. return girl[0];
  146. } else return girl[top--];
  147. }
  148. }

 程序运行结果如图 3 所示。
 

相亲游戏的运行结果


图3 相亲游戏的运行结果(点此查看原图

模式的应用场景

前面学习了备忘录模式的定义与特点、结构与实现,现在来看该模式的以下应用场景。

  1. 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
  2. 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,Eclipse 等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

模式的扩展

在前面介绍的备忘录模式中,有单状态备份的例子,也有多状态备份的例子。下面介绍备忘录模式如何同原型模式混合使用。在备忘录模式中,通过定义“备忘录”来备份“发起人”的信息,而原型模式的 clone() 方法具有自备份功能,所以,如果让发起人实现 Cloneable 接口就有备份自己的功能,这时可以删除备忘录类,其结构图如图 4 所示。
 

带原型的备忘录模式的结构图


图4 带原型的备忘录模式的结构图


实现代码如下:

  1. package net.biancheng.c.memento;
  2. public class PrototypeMemento {
  3. public static void main(String[] args) {
  4. OriginatorPrototype or = new OriginatorPrototype();
  5. PrototypeCaretaker cr = new PrototypeCaretaker();
  6. or.setState("S0");
  7. System.out.println("初始状态:" + or.getState());
  8. cr.setMemento(or.createMemento()); //保存状态
  9. or.setState("S1");
  10. System.out.println("新的状态:" + or.getState());
  11. or.restoreMemento(cr.getMemento()); //恢复状态
  12. System.out.println("恢复状态:" + or.getState());
  13. }
  14. }
  15. //发起人原型
  16. class OriginatorPrototype implements Cloneable {
  17. private String state;
  18. public void setState(String state) {
  19. this.state = state;
  20. }
  21. public String getState() {
  22. return state;
  23. }
  24. public OriginatorPrototype createMemento() {
  25. return this.clone();
  26. }
  27. public void restoreMemento(OriginatorPrototype opt) {
  28. this.setState(opt.getState());
  29. }
  30. public OriginatorPrototype clone() {
  31. try {
  32. return (OriginatorPrototype) super.clone();
  33. } catch (CloneNotSupportedException e) {
  34. e.printStackTrace();
  35. }
  36. return null;
  37. }
  38. }
  39. //原型管理者
  40. class PrototypeCaretaker {
  41. private OriginatorPrototype opt;
  42. public void setMemento(OriginatorPrototype opt) {
  43. this.opt = opt;
  44. }
  45. public OriginatorPrototype getMemento() {
  46. return opt;
  47. }
  48. }

 程序的运行结果如下:

初始状态:S0
新的状态:S1
恢复状态:S0

拓展

由于 JDK、SpringMybatis 中很少有备忘录模式,所以该设计模式不做典型应用源码分析。

Spring Webflow 中 DefaultMessageContext 类实现了 StateManageableMessageContext 接口,查看其源码可以发现其主要逻辑就相当于给 Message 备份。感兴趣的小伙伴可以去阅读学习其源码。

解释器模式(详解版)

在软件开发中,会遇到有些问题多次重复出现,而且有一定的相似性和规律性。如果将它们归纳成一种简单的语言,那么这些问题实例将是该语言的一些句子,这样就可以用“编译原理”中的解释器模式来实现了。

虽然使用解释器模式的实例不是很多,但对于满足以上特点,且对运行效率要求不是很高的应用实例,如果用解释器模式来实现,其效果是非常好的,本文将介绍其工作原理与使用方法。

模式的定义与特点

解释器(Interpreter)模式的定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。

这里提到的文法和句子的概念同编译原理中的描述相同,“文法”指语言的语法规则,而“句子”是语言集中的元素。例如,汉语中的句子有很多,“我是中国人”是其中的一个句子,可以用一棵语法树来直观地描述语言中的句子。

解释器模式是一种类行为型模式,其主要优点如下。

  1. 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
  2. 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。


解释器模式的主要缺点如下。

  1. 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
  2. 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
  3. 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。

模式的结构与实现

解释器模式常用于对简单语言的编译或分析实例中,为了掌握好它的结构与实现,必须先了解编译原理中的“文法、句子、语法树”等相关概念。

1) 文法

文法是用于描述语言的语法结构的形式规则。没有规矩不成方圆,例如,有些人认为完美爱情的准则是“相互吸引、感情专一、任何一方都没有恋爱经历”,虽然最后一条准则较苛刻,但任何事情都要有规则,语言也一样,不管它是机器语言还是自然语言,都有它自己的文法规则。例如,中文中的“句子”的文法如下。

〈句子〉::=〈主语〉〈谓语〉〈宾语〉
〈主语〉::=〈代词〉|〈名词〉
〈谓语〉::=〈动词〉
〈宾语〉::=〈代词〉|〈名词〉
〈代词〉你|我|他
〈名词〉7大学生I筱霞I英语
〈动词〉::=是|学习


注:这里的符号“::=”表示“定义为”的意思,用“〈”和“〉”括住的是非终结符,没有括住的是终结符。

2) 句子

句子是语言的基本单位,是语言集中的一个元素,它由终结符构成,能由“文法”推导出。例如,上述文法可以推出“我是大学生”,所以它是句子。

3) 语法树

语法树是句子结构的一种树型表示,它代表了句子的推导结果,它有利于理解句子语法结构的层次。图 1 所示是“我是大学生”的语法树。
 

句子“我是大学生”的语法树


图1 句子“我是大学生”的语法树


有了以上基础知识,现在来介绍解释器模式的结构就简单了。解释器模式的结构与组合模式相似,不过其包含的组成元素比组合模式多,而且组合模式是对象结构型模式,而解释器模式是类行为型模式。

1. 模式的结构

解释器模式包含以下主要角色。

  1. 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
  2. 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
  3. 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
  4. 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
  5. 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。


解释器模式的结构图如图 2 所示。
 

解释器模式的结构图


图2 解释器模式的结构图

2. 模式的实现

解释器模式实现的关键是定义文法规则、设计终结符类与非终结符类、画出结构图,必要时构建语法树,其代码结构如下:

  1. package net.biancheng.c.interpreter;
  2. //抽象表达式类
  3. interface AbstractExpression {
  4. public void interpret(String info); //解释方法
  5. }
  6. //终结符表达式类
  7. class TerminalExpression implements AbstractExpression {
  8. public void interpret(String info) {
  9. //对终结符表达式的处理
  10. }
  11. }
  12. //非终结符表达式类
  13. class NonterminalExpression implements AbstractExpression {
  14. private AbstractExpression exp1;
  15. private AbstractExpression exp2;
  16. public void interpret(String info) {
  17. //非对终结符表达式的处理
  18. }
  19. }
  20. //环境类
  21. class Context {
  22. private AbstractExpression exp;
  23. public Context() {
  24. //数据初始化
  25. }
  26. public void operation(String info) {
  27. //调用相关表达式类的解释方法
  28. }
  29. }

模式的应用实例

【例1】用解释器模式设计一个“韶粵通”公交车卡的读卡器程序。

说明:假如“韶粵通”公交车读卡器可以判断乘客的身份,如果是“韶关”或者“广州”的“老人” “妇女”“儿童”就可以免费乘车,其他人员乘车一次扣 2 元。

分析:本实例用“解释器模式”设计比较适合,首先设计其文法规则如下。

<expression> ::= <city>的<person>
<city> ::= 韶关|广州
<person> ::= 老人|妇女|儿童


然后,根据文法规则按以下步骤设计公交车卡的读卡器程序的类图。

  • 定义一个抽象表达式(Expression)接口,它包含了解释方法 interpret(String info)。
  • 定义一个终结符表达式(Terminal Expression)类,它用集合(Set)类来保存满足条件的城市或人,并实现抽象表达式接口中的解释方法 interpret(Stringinfo),用来判断被分析的字符串是否是集合中的终结符。
  • 定义一个非终结符表达式(AndExpressicm)类,它也是抽象表达式的子类,它包含满足条件的城市的终结符表达式对象和满足条件的人员的终结符表达式对象,并实现 interpret(String info) 方法,用来判断被分析的字符串是否是满足条件的城市中的满足条件的人员。
  • 最后,定义一个环境(Context)类,它包含解释器需要的数据,完成对终结符表达式的初始化,并定义一个方法 freeRide(String info) 调用表达式对象的解释方法来对被分析的字符串进行解释。其结构图如图 3 所示。

“韶粵通”公交车读卡器程序的结构图


图3 “韶粵通”公交车读卡器程序的结构图


程序代码如下:

  1. package net.biancheng.c.interpreter;
  2. import java.util.*;
  3. /*文法规则
  4. <expression> ::= <city><person>
  5. <city> ::= 韶关|广州
  6. <person> ::= 老人|妇女|儿童
  7. */
  8. public class InterpreterPatternDemo {
  9. public static void main(String[] args) {
  10. Context bus = new Context();
  11. bus.freeRide("韶关的老人");
  12. bus.freeRide("韶关的年轻人");
  13. bus.freeRide("广州的妇女");
  14. bus.freeRide("广州的儿童");
  15. bus.freeRide("山东的儿童");
  16. }
  17. }
  18. //抽象表达式类
  19. interface Expression {
  20. public boolean interpret(String info);
  21. }
  22. //终结符表达式类
  23. class TerminalExpression implements Expression {
  24. private Set<String> set = new HashSet<String>();
  25. public TerminalExpression(String[] data) {
  26. for (int i = 0; i < data.length; i++) set.add(data[i]);
  27. }
  28. public boolean interpret(String info) {
  29. if (set.contains(info)) {
  30. return true;
  31. }
  32. return false;
  33. }
  34. }
  35. //非终结符表达式类
  36. class AndExpression implements Expression {
  37. private Expression city = null;
  38. private Expression person = null;
  39. public AndExpression(Expression city, Expression person) {
  40. this.city = city;
  41. this.person = person;
  42. }
  43. public boolean interpret(String info) {
  44. String s[] = info.split("的");
  45. return city.interpret(s[0]) && person.interpret(s[1]);
  46. }
  47. }
  48. //环境类
  49. class Context {
  50. private String[] citys = {"韶关", "广州"};
  51. private String[] persons = {"老人", "妇女", "儿童"};
  52. private Expression cityPerson;
  53. public Context() {
  54. Expression city = new TerminalExpression(citys);
  55. Expression person = new TerminalExpression(persons);
  56. cityPerson = new AndExpression(city, person);
  57. }
  58. public void freeRide(String info) {
  59. boolean ok = cityPerson.interpret(info);
  60. if (ok) System.out.println("您是" + info + ",您本次乘车免费!");
  61. else System.out.println(info + ",您不是免费人员,本次乘车扣费2元!");
  62. }
  63. }

 程序运行结果如下:

您是韶关的老人,您本次乘车免费!
韶关的年轻人,您不是免费人员,本次乘车扣费2元!
您是广州的妇女,您本次乘车免费!
您是广州的儿童,您本次乘车免费!
山东的儿童,您不是免费人员,本次乘车扣费2元!

模式的应用场景

前面介绍了解释器模式的结构与特点,下面分析它的应用场景。

  1. 当语言的文法较为简单,且执行效率不是关键问题时。
  2. 当问题重复出现,且可以用一种简单的语言来进行表达时。
  3. 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候,如 XML 文档解释。


注意:解释器模式在实际的软件开发中使用比较少,因为它会引起效率、性能以及维护等问题。如果碰到对表达式的解释,在 Java 中可以用 Expression4J 或 Jep 等来设计。

模式的扩展

在项目开发中,如果要对数据表达式进行分析与计算,无须再用解释器模式进行设计了,Java 提供了以下强大的数学公式解析器:Expression4J、MESP(Math Expression String Parser) 和 Jep 等,它们可以解释一些复杂的文法,功能强大,使用简单。

现在以 Jep 为例来介绍该工具包的使用方法。Jep 是 Java expression parser 的简称,即 Java 表达式分析器,它是一个用来转换和计算数学表达式的 Java 库。通过这个程序库,用户可以以字符串的形式输入一个任意的公式,然后快速地计算出其结果。而且 Jep 支持用户自定义变量、常量和函数,它包括许多常用的数学函数和常量。

使用前先下载 Jep 压缩包,解压后,将 jep-x.x.x.jar 文件移到选择的目录中,在 Eclipse 的“Java 构建路径”对话框的“库”选项卡中选择“添加外部 JAR(X)...”,将该 Jep 包添加项目中后即可使用其中的类库。

下面以计算存款利息为例来介绍。存款利息的计算公式是:本金x利率x时间=利息,其相关代码如下:

  1. package net.biancheng.c.interpreter;
  2. import com.singularsys.jep.*;
  3. public class JepDemo {
  4. public static void main(String[] args) throws JepException {
  5. Jep jep = new Jep();
  6. //定义要计算的数据表达式
  7. String 存款利息 = "本金*利率*时间";
  8. //给相关变量赋值
  9. jep.addVariable("本金", 10000);
  10. jep.addVariable("利率", 0.038);
  11. jep.addVariable("时间", 2);
  12. jep.parse(存款利息); //解析表达式
  13. Object accrual = jep.evaluate(); //计算
  14. System.out.println("存款利息:" + accrual);
  15. }
  16. }

程序运行结果如下:

存款利息:760.0

行为型模式应用实验

行为型模式(Behavioral Pattern)是对在不同的对象之间划分责任和算法的抽象化,它是 GoF 设计模式中最为庞大的一类模式,包含以下 11 种:模板方法(Template Method)模式、策略(Strategy)模式、命令(Command)模式、职责链(Chain of Responsibility)模式、状态(State)模式、观察者(Observer)模式、中介者(Mediator)模式、迭代器(Iterator)模式、访问者(Visitor)模式、备忘录(Memento)模式、解释器(Interpreter)模式。

实验目的

本实验的主要目的如下。

  • 了解 11 种“行为型模式”的定义、特点和工作原理。
  • 理解 11 种“行为型模式”的结构、实现和应用场景。
  • 学会应用 11 种“行为型模式”进行软件开发。

实验原理

1. 行为型模式的工作原理

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,它涉及算法与对象间职责的分配。

按照其显示方式的不同,行为型模式可分为类行为模式和对象行为模式,其中类行为型模式使用继承关系在几个类之间分配行为,主要通过多态等方式来分配父类与子类的职责;对象行为型模式则使用对象的组合或聚合关联关系来分配行为,主要是通过对象关联等方式来分配两个或多个类的职责。

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

如果按目的来分,行为型模式共 11 种,每种模式的工作原理在前面的教程中都有详细的介绍,每种模式的实验大概要花 2 个学时,大家可以根据实验计划来选做若干个实验。下面以观察者模式为例,介绍其实验过程。

2. 观察者模式的工作原理

观察者模式是一种对象行为型模式,用于解决多个对象间存在的一对多的依赖关系。在现实世界中,许多对象并不是独立存在的,其中一个对象的状态发生改变可能会导致一个或者多个其他对象也发生改变,如物价与消费者、股价与股民、天气预报与农民、警察与小偷、事件源与事件处理 者等。这种模式有时又称作发布-订阅模式、模型-视图模式,其结构图如图 1 所示。
 

观察者模式的结构图


图1 观察者模式的结构图


观察者模式包含如下角色。

  1. 抽象主题/目标(Subject)角色:提供了一个用于保存观察者对象的聚集类和一个管理观察者对象的接口,它包含了增加、删除和通知所有观察者的抽象方法。
  2. 具体主题/目标(Concrete Subject)角色:实现抽象目标中的方法,当具体目标的内部状态发生改变时,通知所有注册过的观察者对象。
  3. 抽象观察者(Observer)角色:定义一个更新接口,它包含了一个更新自己的抽象方法。
  4. 具体观察者(Concrete Observer)角色:实现抽象观察者定义的更新接口,以便在得到目标更改通知时更新自身的状态。

实验内容

(1) 用观察者模式设计一个交通信号灯的事件处理程序。

分析:“交通信号灯”是事件源和目标,各种“车”是事件监听器和具体观察者,“信号灯颜色” 是事件类。

(2) 按照以上要求设计类图和编写 Java 源程序。

实验要求

所设计的实验程序要满足以下两点。

  1. 体现“观察者模式”的工作原理。
  2. 符合面向对象中的“开闭原则”。

实验步骤

(1) 用 UML 设计“交通信号灯事件处理程序”的结构图。

“交通信号灯事件处理程序”的结构图如图 2 所示。
 

交通信号灯事件处理程序的结构图


图2 交通信号灯事件处理程序的结构图


(2) 根据结构图写出“交通信号灯事件处理程序”的源代码。交通信号灯事件处理程序的源代码如下。

  1. package observer;
  2. import java.util.*;
  3. public class SignalLightEvent {
  4. public static void main(String[] args) {
  5. SignalLight light = new SignalLight();//交通信号灯(事件源)
  6. light.addVehicleListener(new Car()); //注册监听器(轿车)
  7. light.addVehicleListener(new Buses());//注册监听器(公交车)
  8. light.changeColor("红色");
  9. System.out.println("------------");
  10. light.changeColor("绿色");
  11. }
  12. }
  13. //信号灯颜色
  14. class SignalColor extends EventObject {
  15. private String color; //"红色"和"绿色"
  16. public SignalColor(Object source, String color) {
  17. super(source);
  18. this.color = color;
  19. }
  20. public void setColor(String color) {
  21. this.color = color;
  22. }
  23. public String getColor() {
  24. return this.color;
  25. }
  26. }
  27. //目标类:事件源,交通信号灯
  28. class SignalLight {
  29. private List<Vehicle> listener; //监听器容器
  30. public SignalLight() {
  31. listener = new ArrayList<Vehicle>();
  32. }
  33. //给事件源绑定监听器
  34. public void addVehicleListener(Vehicle vehicle) {
  35. listener.add(vehicle);
  36. }
  37. //事件触发器:信号灯改变颜色。
  38. public void changeColor(String color) {
  39. System.out.println(color + "信号灯亮...");
  40. SignalColor event = new SignalColor(this, color);
  41. notifies(event); //通知注册在该事件源上的所有监听器
  42. }
  43. //当事件发生时,通知绑定在该事件源上的所有监听器做出反应(调用事件处理方法)
  44. protected void notifies(SignalColor e) {
  45. Vehicle vehicle = null;
  46. Iterator<Vehicle> iterator = listener.iterator();
  47. while (iterator.hasNext()) {
  48. vehicle = iterator.next();
  49. vehicle.see(e);
  50. }
  51. }
  52. }
  53. //抽象观察者类:车
  54. interface Vehicle extends EventListener {
  55. //事件处理方法,看见
  56. public void see(SignalColor e);
  57. }
  58. //具体观察者类:轿车
  59. class Car implements Vehicle {
  60. public void see(SignalColor e) {
  61. if ("红色".equals(e.getColor())) {
  62. System.out.println("红灯亮,轿车停!");
  63. } else {
  64. System.out.println("绿灯亮,轿车行!");
  65. }
  66. }
  67. }
  68. //具体观察者类: 公交车
  69. class Buses implements Vehicle {
  70. public void see(SignalColor e) {
  71. if ("红色".equals(e.getColor())) {
  72. System.out.println("红灯亮,公交车停!");
  73. } else {
  74. System.out.println("绿灯亮,公交车行!");
  75. }
  76. }
  77. }

(3) 上机测试程序,写出运行结果。

交通信号灯事件处理程序的运行结果如下:

红色信号灯亮...
红灯亮,轿车停!
红灯亮,公交车停!
------------
绿色信号灯亮...
绿灯亮,轿车行!
绿灯亮,公交车行!


(4) 按同样的步骤设计其他“观察者模式”的程序实例。

一句话归纳设计模式

通过《23种设计模式全面解析》教程,我们已经学习完了经典的 23 种设计模式。下面总结一下这 23 种设计模式,以方便小伙伴们日后复习和查阅。
 

分类设计模式简述一句话归纳目的生活案例
创建型设计模式
(简单来说就是用来创建对象的)
工厂模式(Factory Pattern)不同条件下创建不同实例产品标准化,生产更高效封装创建细节实体工厂
单例模式(Singleton Pattern)保证一个类仅有一个实例,并且提供一个全局访问点世上只有一个我保证独一无二CEO
原型模式(Prototype Pattern)通过拷贝原型创建新的对象拔一根猴毛,吹出千万个高效创建对象克隆
建造者模式(Builder Pattern)用来创建复杂的复合对象高配中配和低配,想选哪配就哪配开放个性配置步骤选配
结构型设计模式
(关注类和对象的组合)
代理模式(Proxy Pattern)为其他对象提供一种代理以控制对这个对象的访问没有资源没时间,得找别人来帮忙增强职责媒婆
外观模式(Facade Pattern)对外提供一个统一的接口用来访问子系统打开一扇门,通向全世界统一访问入口前台
装饰器模式(Decorator Pattern)为对象添加新功能他大舅他二舅都是他舅灵活扩展、同宗同源煎饼
享元模式(Flyweight Pattern)使用对象池来减少重复对象的创建优化资源配置,减少重复浪费共享资源池全国社保联网
组合模式(Composite Pattern)将整体与局部(树形结构)进行递归组合,让客户端能够以一种的方式对其进行处理人在一起叫团伙,心在一起叫团队统一整体和个体组织架构树
适配器模式(Adapter Pattern)将原来不兼容的两个类融合在一起万能充电器兼容转换电源适配
桥接模式(Bridge Pattern)将两个能够独立变化的部分分离开来约定优于配置不允许用继承
行为型设计模式
(关注对象之间的通信)
模板模式(Template Pattern)定义一套流程模板,根据需要实现模板中的操作流程全部标准化,需要微调请覆盖逻辑复用把大象装进冰箱
策略模式(Strategy Pattern)封装不同的算法,算法之间能互相替换条条大道通罗马,具体哪条你来定把选择权交给用户选择支付方式
责任链模式(Chain of Responsibility Pattern)拦截的类都实现统一接口,每个接收者都包含对下一个接收者的引用。将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。各人自扫门前雪,莫管他们瓦上霜解耦处理逻辑踢皮球
迭代器模式(Iterator Pattern)提供一种方法顺序访问一个聚合对象中的各个元素流水线上坐一天,每个包裹扫一遍统一对集合的访问方式逐个检票进站
命令模式(Command Pattern)将请求封装成命令,并记录下来,能够撤销与重做运筹帷幄之中,决胜千里之外解耦请求和处理遥控器
状态模式(State Pattern)根据不同的状态做出不同的行为状态驱动行为,行为决定状态绑定状态和行为订单状态跟踪
备忘录模式(Memento Pattern)保存对象的状态,在需要时进行恢复失足不成千古恨,想重来时就重来备份、后悔机制草稿箱
中介者模式(Mediator Pattern)将对象之间的通信关联关系封装到一个中介类中单独处理,从而使其耦合松散联系方式我给你,怎么搞定我不管统一管理网状资源朋友圈
解释器模式(Interpreter Pattern)给定一个语言,定义它的语法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子我想说”方言“,一切解释权都归我实现特定语法解析摩斯密码
观察者模式(Observer Pattern)状态发生改变时通知观察者,一对多的关系到点就通知我解耦观察者与被观察者闹钟
访问者模式(Visitor Pattern)稳定数据结构,定义新的操作行为横看成岭侧成峰,远近高低各不同解耦数据结构和数据操作KPI考核
委派模式(Delegate Pattern)允许对象组合实现与继承相同的代码重用,负责任务的调用和分配这个需求很简单,怎么实现我不管只对结果负责授权委托书
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/593011
推荐阅读
相关标签
  

闽ICP备14008679号