软件模式

  1. 软件模式是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式并非仅限于设计模式,还包括架构模式、分析模式和过程模式等,实际上,在软件生存期的每一个阶段都存在着一些被认同的模式
  2. 软件模式可以认为是对软件开发这一特定“问题”的“解法”的某种统一表示软件模式等于一定条件下的出现的问题以及解法。软件模式的基础结构由 4 个部分构成:问题描述、前提条件(环境或约束条件)、解法和效果
  • 能讲清为什么使用某个模式
  • 软件模式与具体的应用领域无关,在模式发现过程中需要遵循大三律(Rule of Three),即只有经过三个以上不同类型(或不同领域)的系统的校验,一个解决方案才能从候选模式升格为模式。

设计模式

设计模式的定义

设计模式(Design Pattern)是一套被反复使用多数人知晓的经过分类编目的代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

设计模式的基本要素

设计模式一般有如下几个基本要素:模式名称、问题、目的、解决方案、效果、实例代码和相关设计模式,其中的关键元素包括以下四个方面:

  • 模式名称(Pattern name)
  • 问题(Problem)
  • 解决方案(Solution)
  • 效果(Consequences)

设计模式的分类

  • 根据其目的(模式是用来做什么的)可分为**创建型(Creational),结构型(Structural)和行为型(Behavioral)**三种:

    • 创建型模式主要用于创建对象
    • 结构型模式主要用于处理类或对象的组合
    • 行为型模式主要用于描述对类或对象怎样交互和怎样分配职责
  • 根据范围,即模式主要是用于处理类之间关系还是处理对象之间的关系,可分为类模式对象模式两种:

    • 类模式处理类和子类之间的关系,这些关系通过继承建立,在编译时刻就被确定下来,是属于静态的。
    • 对象模式处理对象间的关系,这些关系在运行时刻变化,更具动态性。
      alt text

    模板方法为什么是行为型模式:子类控制父类的行为

设计模式与类库框架

alt text

设计原则

  • 对于面向对象的软件系统设计来说,在支持可维护性的同时,需要提高系统的可复用性。
  • 软件的复用可以提高软件的开发效率,提高软件质量,节约开发成本,恰当的复用还可以改善系统的可维护性。
  1. 单一职责原则:要求在软件系统中,一个类只负责一个功能领域中的相应职责。

  2. 开闭原则:要求一个软件实体应当对扩展开放,对修改关闭,即在不修改源代码的基础上扩展一个系统的行为。

  3. 里氏代换原则:可以通俗表述为在软件中如果能够使用基类对象,那么一定能够使用其子类对象。

    因此在程序中尽量使用基类类型来对对象进行定义,

    而在运行时再确定其子类类型,用子类对象来替换父类对象

    实现开闭原则的重要方式之一

  4. 依赖倒转原则:要求抽象不应该依赖于细节,细节应该依赖于抽象;要针对接口编程,不要针对实现编程。

  5. 接口隔离原则:要求客户端不应该依赖那些它不需要的接口,即将一些大的接口细化成一些小的接口供客户端使用。

    使用接口隔离原则拆分接口时,首先必须满足单一职责原则

  6. 合成复用原则:要求复用时尽量使用对象组合,而不使用继承。

    通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分

    新对象通过委派调用已有对象的方法达到复用其已有功能的目的

  7. 迪米特法则最小知识原则):要求一个软件实体应当尽可能少的与其他实体发生相互作用。

设计原则之间的关系

  • 目标:开闭原则
  • 指导:迪米特法则(最小知识原则)
  • 基础:单一职责原则、可变性封装原则
  • 实现:依赖倒转原则、合成复用原则、里氏代换原则、接口隔离原则

表驱动法(不考)

  • 目标
  • 原理
  • 如何快速从表中查询条目
    • 直接访问
    • 索引访问
    • 阶梯访问

创建型模式

工厂方法模式(类模式)

整理不动了,去看ppt或者博客吧

软件详细设计-03-工厂模式 | EagleBear2002 的博客

抽象工厂模式(对象模式)

整理不动了,去看ppt或者博客吧

软件详细设计-03-工厂模式 | EagleBear2002 的博客

原型模式 Prototype Pattern

模式定义

  • 原型模式是一种对象创建型模式,用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。原型模式允许一个对象再创建另外一个可定制的对象,无须知道任何创建的细节。

模式结构

  • 模式结构
    alt text
  • 包含如下角色
    • Prototype:抽象原型类
    • ConcretePrototype:具体原型类
    • Client:客户类

模式分析

  • 在原型模式结构中定义了一个抽象原型类,所有的 Java 类都继承自 java.lang.Object,而 Object 类提供一个 clone() 方法,可以将一个 Java 对象复制一份。
  • 能够实现克隆的 Java 类必须实现一个标识接口 Cloneable,表示这个 Java 类支持复制。如果一个类没有实现这个接口但是调用了 clone() 方法,Java 编译器将抛出一个 CloneNotSupportedException 异常。
  • 通原型模式可以分为两种形式:深克隆【成员对象也克隆】和浅克隆【成员对象不克隆】。
  • Java 语言提供的 clone() 方法将对象复制了一份并返回给调用者。一般而言,clone() 方法满足:
    • 对任何的对象 x,都有 x.clone() != x,即克隆对象与原对象不是同一个对象。
    • 对任何的对象 x,都有 x.clone().getClass()==x.getClass(),即克隆对象与原对象的类型一样。
    • 如果对象 xequals() 方法定义恰当,那么 x.clone().equals(x) 应该成立。

实例分析

实例一:邮件复制(浅克隆)
  • 由于邮件对象包含的内容较多(如发送者、接收者、标题、内容、日期、附件等),某系统中现需要提供一个邮件复制功能,对于已经创建好的邮件对象,可以通过复制的方式创建一个新的邮件对象,如果需要改变某部分内容,无须修改原始的邮件对象,只需要修改复制后得到的邮件对象即可。使用原型模式设计该系统。在本实例中使用浅克隆实现邮件复制,即复制邮件(Email)的同时不复制附件(Attachment)。
    alt text

优缺点

  • 优点:
    • 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过一个已有实例可以提高新实例的创建效率。
    • 可以动态增加或减少产品类。
    • 原型模式提供了简化的创建结构。
    • 可以使用深克隆的方式保存对象的状态。
  • 缺点:
    • 需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。
    • 在实现深克隆时需要编写较为复杂的代码。
  • 适用环境:
    • 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其属性稍作修改。
    • 如果系统要保存对象的状态而对象的状态变化很小,或者对象本身占内存不大的时候,也可以使用原型模式配合备忘录模式来应用。相反,如果对象的状态变化很大,或者对象占用的内存很大,那么采用状态模式会比原型模式更好。
    • 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。

适用环境

  • 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其属性稍作修改。
  • 如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占内存不大的时候,也可以使用原型模式配合备忘录模式来应用。相反,如果对象的状态变化很大,或者对象占用的内存很大,那么采用状态模式会比原型模式更好。
  • 需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。

模式扩展

带原型管理器的原型模式

img

相似对象的复制

很多情况下,复制所得到的对象与原型对象并不是完全相同的,它们的某些属性值存在异同。通过原型模式获得相同对象后可以再对其属性进行修改,从而获取所需对象。如多个学生对象的信息的区别在于性别、姓名和年龄,而专业、学院、学校等信息都相同,为了简化创建过程,可以通过原型模式来实现相似对象的复制。

模式应用

  • 原型模式应用于很多软件中,如果每次创建一个对象要花大量时间,原型模式是最好的解决方案。很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的应用,复制得到的对象与原型对象是两个类型相同但内存地址不同的对象,通过原型模式可以大大提高对象的创建效率。
  • 在 Struts2 中为了保证线程的安全性,Action 对象的创建使用了原型模式,访问一个已经存在的 Action 对象时将通过克隆的方式创建出一个新的对象,从而保证其中定义的变量无须进行加锁实现同步,每一个 Action 中都有自己的成员变量,避免 Struts1 因使用单例模式而导致的并发和同步问题。
  • 在 Spring 中,用户也可以采用原型模式来创建新的 bean 实例,从而实现每次获取的是通过克隆生成的新实例,对其进行修改时对原有实例对象不造成任何影响。

结构型模式

适配器模式 Adapter Pattern

模式动机

  • 在软件开发中采用类似于电源适配器的设计和编码技巧被称为适配器模式。
  • 通常情况下,客户端可以通过目标类的接口访问它所提供的服务。有时,现有的类可以满足客户类的功能需要,但是它所提供的接口不一定是客户类所期望的,这可能是因为现有类中方法名与目标类中定义的方法名不一致等原因所导致的。
  • 在这种情况下,现有的接口需要转化为客户类期望的接口,这样保证了对现有类的重用。如果不进行这样的转化,客户类就不能利用现有类所提供的功能,适配器模式可以完成这样的转化。
  • 在适配器模式中可以定义一个包装类,包装不兼容接口的对象,这个包装类指的就是适配器(Adapter),它所包装的对象就是适配者(Adaptee),即被适配的类。
  • 适配器提供客户类需要的接口,适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作。这就是适配器模式的模式动机。

模式定义

  • Adapter Pattern:将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

模式结构

  • 模式结构:
    • 对象适配器:
      alt text
    • 类适配器:
      alt text
  • 适配器模式包含如下角色:
    1. Target:目标抽象类
    2. Adapter:适配器类
    3. Adaptee:适配者类
    4. Client:客户类

模式分析

  • 典型的类适配器代码:

    1
    2
    3
    4
    5
    public class Adapter extends Adaptee implements Target{
    public void request(){
    specificRequest();
    }
    }
  • 典型的对象适配器代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Adapter extends Target {
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
    this.adaptee = adaptee;
    }

    public void request() {
    adaptee.specificRequest();
    }
    }

实例分析

仿生机器人

现需要设计一个可以模拟各种动物行为的机器人,在机器人中定义了一系列方法,如机器人叫喊方法 cry()、机器人移动方法 move() 等。如果希望在不修改已有代码的基础上使得机器人能够像狗一样叫,像狗一样跑,使用适配器模式进行系统设计。

alt text

加密适配器

某系统需要提供一个加密模块,将用户信息(如密码等机密信息)加密之后再存储在数据库中,系统已经定义好了数据库操作类。为了提高开发效率,现需要重用已有的加密算法,这些算法封装在一些由第三方提供的类中,有些甚至没有源代码。使用适配器模式设计该加密模块,实现在不修改现有类的基础上重用第三方加密方法。

alt text

优缺点

  • 适配器模式优点:

    • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
    • 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
    • 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
  • 类适配器模式还具有如下优点:

    • 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
  • 类适配器模式的缺点如下:

    • 对于 Java、C# 等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。
  • 对象适配器模式还具有如下优点:

    • 一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口
  • 对象适配器模式的缺点如下:

    • 与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

适用环境

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

模式应用

  • 在 JDK 类库中也定义了一系列适配器类,如在 com.sun.imageio.plugins.common 包中定义的InputStreamAdapter类,用于包装 ImageInputStream 接口及其子类对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class InputStreamAdapter extends InputStream {
    ImageInputStream stream;

    public InputStreamAdapter(ImageInputStream stream) {
    super();
    this.stream = stream;
    }

    public int read() throws IOException {
    return stream.read();
    }

    public int read(byte b[], int off, int len) throws IOException {
    return stream.read(b, off, len);
    }
    }

组合模式 Composite Pattern

模式动机

  • 对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象,如子文件夹和文件)并调用执行。(递归调用)
  • 由于容器对象和叶子对象在功能上的区别,在使用这些对象的客户端代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下客户端希望一致地处理它们因为对于这些对象的区别对待将会使得程序非常复杂
  • 组合模式描述了如何将容器对象和叶子对象进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器对象和叶子对象,这就是组合模式的模式动机。

模式定义

  • 组合模式(Composite Pattern):组合多个对象形成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(即叶子对象)组合对象(即容器对象)的使用具有一致性
  • 组合模式又可以称为整体-部分(Part-Whole)模式,属于对象的结构模式,它将对象组织到树结构中,可以用来描述整体与部分的关系

模式结构

alt text

  • 组合模式包含如下角色:
    1. Component:抽象构件
    2. Leaf:叶子构件
    3. Composite:容器构件
    4. Client:客户类

模式分析

  • 组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。

  • 同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构

  • 文件系统组合模式结构图:
    alt text

  • 典型的抽象构件角色代码:

    1
    2
    3
    4
    5
    6
    7
    public abstract class Component
    {
    public abstract void add(Component c);
    public abstract void remove(Component c);
    public abstract Component getChild(int i);
    public abstract void operation();
    }
  • 典型的叶子构件角色代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Leaf extends Component
    {
    public void add(Component c)
    { //异常处理或错误提示
    }
    public void remove(Component c)
    { //异常处理或错误提示
    }
    public Component getChild(int i)
    { //异常处理或错误提示
    }
    public void operation()
    {
    //实现代码
    }
    }
  • 典型的容器构件角色代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class Composite extends Component {
    private ArrayList list = new ArrayList();

    public void add(Component c) {
    list.add(c);
    }

    public void remove(Component c) {
    list.remove(c);
    }

    public Component getChild(int i) {
    (Component) list.get(i);
    }

    public void operation() {
    for (Object obj : list) {
    ((Component) obj).operation();
    }
    }
    }

实例分析

水果盘
  • 在水果盘(Plate)中有一些水果,如苹果(Apple)、香蕉(Banana)、梨子(Pear),当然大水果盘中还可以有小水果盘,现需要对盘中的水果进行遍历(吃),当然如果对一个水果盘执行”吃”方法,实际上就是吃其中的水果。使用组合模式模拟该场景。
    alt text
文件浏览
  • 文件有不同类型,不同类型的文件其浏览方式有所区别,如文本文件和图片文件的浏览方式就不相同。对文件夹的浏览实际上就是对其中所包含文件的浏览,而客户端可以一致地对文件和文件夹进行操作,无须关心它们的区别。使用组合模式来模拟文件的浏览操作。
    alt text

优缺点

  • 优点:

    • 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
    • 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象
    • 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构
    • 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。组合模式
  • 缺点:

    • 使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
    • 增加新构件时可能会产生一些问题,很难对容器中的构件类型进行限制

适用环境

  • 需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
  • 让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节
  • 对象的结构是动态的并且复杂程度不一样,但客户需要一致地处理它们

模式应用

  • XML 文档解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?xml version="1.0"?>
    <books>
    <book>
    <author>Carson</author>
    <price format="dollar">31.95</price>
    <pubdate>05/01/2001</pubdate>
    </book>
    <pubinfo>
    <publisher>MSPress</publisher>
    <state>WA</state>
    </pubinfo>
    </books>

    img

  • 操作系统中的目录结构是一个树形结构,因此在对文件和文件夹进行操作时可以应用组合模式,例如杀毒软件在查毒或杀毒时,既可以针对一个具体文件,也可以针对一个目录。如果是对目录查毒或杀毒,将递归处理目录中的每一个子目录和文件。

  • JDK 的 AWT/Swing 是组合模式在 Java 类库中的一个典型实际应用。

    img

模式扩展

  • 透明组合模式:所有组件(无论是叶子节点还是组合节点)都共享相同的接口。
    alt text
  • 安全组合模式(违反了里氏代换原则):组件接口被分为两类:一个用于叶子节点,一个用于组合节点。
    alt text

装饰模式 Decorator Pattern

模式动机

  • 一般有两种方式可以实现给一个类或对象增加行为:
    1. 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。
    2. 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为。
  • 装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰模式的模式动机。

模式定义

  • Decorator Pattern:动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式

模式结构

  • 模式结构:
    alt text

  • 装饰模式包含如下角色:

    1. Component:抽象构件
    2. ConcreteComponent:具体构件
    3. Decorator:抽象装饰类
    4. ConcreteDecorator:具体装饰类

模式分析

  • 与继承关系相比,关联关系的主要优势在于不会破坏类的封装性,而且继承是一种耦合度较大的静态关系,无法在程序运行时动态扩展。在软件开发阶段,关联关系虽然不会比继承关系减少编码量,但是到了软件维护阶段,由于关联关系使系统具有较好的松耦合性,因此使得系统更加容易维护。当然,关联关系的缺点是比继承关系要创建更多的对象

  • 使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。

  • 典型的抽象装饰类代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Decorator extends Component {
    private Component component;

    public Decorator(Component component) {
    this.component = component;
    }

    public void operation() {
    component.operation();
    }
    }
  • 典型的具体装饰类代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
    super(component);
    }

    public void operation() {
    super.operation();
    addedBehavior();
    }

    public void addedBehavior() {
    // 新增方法
    }
    }

实例分析

变形金刚(半透明装饰模式)
  • 变形金刚在变形之前是一辆汽车,它可以在陆地上移动。当它变成机器人之后除了能够在陆地上移动之外,还可以说话;如果需要,它还可以变成飞机,除了在陆地上移动还可以在天空中飞翔。

    Q:为什么 Changer 在继承自 Transform 的同时还要有一个 Transform

    A:因为我们希望 Changer 能够装饰 Car 或其他的 Transform (如果有,例如 Bicycle),因此需要组合一个 transform

    alt text
多重加密系统(透明装饰模式)
  • 某系统提供了一个数据加密功能,可以对字符串进行加密。最简单的加密算法通过对字母进行移位来实现,同时还提供了稍复杂的逆向输出加密,还提供了更为高级的求模加密。用户先使用最简单的加密算法对字符串进行加密,如果觉得还不够可以对加密之后的结果使用其他加密算法进行二次加密,当然也可以进行第三次加密。现使用装饰模式设计该多重加密系统。
    alt text

优缺点

  • 优点:

    • 装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性
    • 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
    • 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
    • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,**符合”开闭原则”**。
  • 缺点:

    • 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
    • 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐

适用环境

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  • 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如 final 类)。

模式应用

  • javax.swing 包中,可以通过装饰模式动态给一些构件增加新的行为或改善其外观显示。

    • 如 JList 构件本身并不支持直接滚动,即没有滚动条,要创建可以滚动的列表,可以使用如下代码实现:

      1
      2
      JList list = new JList();
      JScrollPane sp = new JScrollPane(list);
  • 装饰模式在 JDK 中最经典的实例是 Java IO。

    • InputStream 为例:
      img
  • 抽象装饰类:FilterInputStream

    1
    2
    3
    4
    5
    protected volatile InputStream in;

    protected FilterInputStream(InputStream in) {
    this.in = in;
    }
  • 角色分配:

    1. 抽象构件类:InputStream
    2. 具体构件类:FileInputStreamByteArrayInputStream
    3. 抽象装饰类:FilterInputStream
    4. 具体装饰类:BufferedInputStreamDataInputStream
  • 客户端使用:

    1
    2
    3
    4
    5
    FileInputStream inFS = new FileInputStream("temp/fileSrc.txt");
    BufferedInputStream inBS = new BufferedInputStream(inFS);
    // 定义一个字节数组,用于存放缓冲数据
    byte[] data = new byte[1024];
    inBS.read(data);

模式扩展

  • 装饰模式的简化-需要注意的问题:
    • 一个装饰类的接口必须与被装饰类的接口保持相同,对于客户端来说无论是装饰之前的对象还是装饰之后的对象都可以一致对待。
    • 尽量保持具体构件类 Component 作为一个”轻”类,也就是说不要把太多的逻辑和状态放在具体构件类中,可以通过装饰类对其进行扩展。
    • 如果只有一个具体构件类而没有抽象构件类,那么抽象装饰类可以作为具体构件类的直接子类
  • 装饰模式的简化
    alt text
  • 透明装饰模式(多重加密系统):
    • 透明装饰模式中,要求客户端完全针对抽象编程,装饰模式的透明性要求客户端程序不应该声明具体构件类型和具体装饰类型,而应该全部声明为抽象构件类型。
  • 半透明装饰模式(变形金刚)
    • 半透明(semi-transparent)的装饰模式允许用户在客户端声明具体装饰者类型的对象,调用在具体装饰者中新增的方法。

外观模式 Facade Pattern

模式动机

  • 引入外观角色之后,用户只需要直接与外观角色交互用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度。外观模式

模式定义

  • Facade Pattern:外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式
    1. 简化了接口

    2. 接口相关模式:适配器模式、外观模式,变化决定了要使用哪个模式

模式结构

  • 使用的设计原则:迪米特原则、单一职责原则
    alt text

  • 外观模式包含如下角色:

    1. Facade:外观角色
    2. SubSystem:子系统角色

模式分析

  • 根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口

  • 外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度

  • 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。

  • 外观模式的目的在于降低系统的复杂程度

  • 外观模式从很大程度上提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节,通过外观角色即可调用相关功能。

  • 典型的外观角色代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Facade {
    private SubSystemA obj1 = new SubSystemA();
    private SubSystemB obj2 = new SubSystemB();
    private SubSystemC obj3 = new SubSystemC();

    public void method() {
    obj1.method();
    obj2.method();
    obj3.method();
    }
    }

实例分析

电源总开关
  • 现在考察一个电源总开关的例子,以便进一步说明外观模式。为了使用方便,一个电源总开关可以控制四盏灯、一个风扇、一台空调和一台电视机的启动和关闭。通过该电源总开关可以同时控制上述所有电器设备,使用外观模式设计该系统。
    alt text
文件加密
  • 某系统需要提供一个文件加密模块,加密流程包括三个操作,分别是读取源文件、加密、保存加密之后的文件。读取文件和保存文件使用流来实现,这三个操作相对独立,其业务代码封装在三个不同的类中。现在需要提供一个统一的加密外观类,用户可以直接使用该加密外观类完成文件的读取、加密和保存三个操作,而不需要与每一个类进行交互,使用外观模式设计该加密模块。
    alt text
  1. 可以使用模板方法模式实现吗?子类提供不同的实现,复用父类方法的步骤
  2. 什么时候外观?如果每一个部分都不变,只是提供接口

优缺点

  • 优点:

    • 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入外观模式,客户代码将变得很简单,与之关联的对象也很少。
    • 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。
    • 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
    • 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类
  • 缺点:

    • 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。
    • 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码违背了“开闭原则”

适用环境

  • 当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
  • 客户程序与多个子系统之间存在很大的依赖性。引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性。
  • 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度

模式应用

  • 外观模式应用于 JDBC 数据库操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class JDBCFacade {
    private Connection conn = null;
    private Statement statement = null;

    public void open(String driver, String jdbcUrl, String userName, String userPwd) {
    ......
    }

    public int executeUpdate(String sql) {
    ......
    }

    public ResultSet executeQuery(String sql) {
    ......
    }

    public void close() {
    ......
    }
    }
  • Session 外观模式是外观模式在 Java EE 框架中的应用。
    img

模式扩展

  • 在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互。

  • 不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的。

  • 外观模式与迪米特法则:外观模式创造出一个外观对象,将客户端所涉及的属于一个子系统的协作伙伴的数量减到最少,使得客户端与子系统内部的对象的相互作用被外观对象所取代

  • 抽象外观类的引入:外观模式最大的缺点在于违背了“开闭原则”当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。
    alt text

行为型模式

策略模式

整理不动了,去看ppt或者博客吧

软件详细设计-02-策略模式 | EagleBear2002 的博客

状态模式 State Pattern

模式动机

  • 一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的(stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。
  • 在 UML 中可以使用状态图来描述对象状态的变化。

模式定义

  • 状态模式(State Pattern):允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
  • 设计主要是面对变化,因此我们在分析开始的时候就应该分析变化:增加新的状态,面向新的状态的部分的行为。
  • 封装变化:封闭 + 委托(组合关系)

模式结构

  • 模式结构
    alt text

  • 状态模式包含如下角色:

    • Context: 环境类
    • State: 抽象状态类
    • ConcreteState: 具体状态类
  • 在结构上策略模式和状态模式是一致的,但是在使用上是很不同的

    • Context 是状态模式关联的上下文环境。

模式分析

alt text

  • 状态模式的关键是引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    if (state == "空闲") {
    if (预订房间) {
    // 预订操作;
    state = "已预订";
    } else if (住进房间) {
    // 入住操作;
    state = "已入住";
    }
    } else if (state == "已预订") {
    if (住进房间) {
    // 入住操作;
    state = "已入住";
    } else if (取消预订) {
    // 取消操作;
    state = "空闲";
    }
    }
  • 如何能够对用户透明,而避免让用户进行状态变更的设置。

  • 使用状态模式重构之后的代码:但是下图中不符合开闭原则,下面如果添加状态则会导致内部的改变。

    1
    2
    3
    4
    5
    6
    7
    8
    // 重构之后的“空闲状态类”示例代码
    if (预订房间) {
    // 预订操作;
    context.setState(new 已预订状态类());
    } else if (住进房间) {
    // 入住操作;
    context.setState(new 已入住状态类());
    }
  • 策略模式是封装好的,而下图中为了追求对用户透明则牺牲了开闭原则。

  • 状态模式要注意数据的分割:类的拆分,尽量将数据放在 context 中,状态中的信息越少越好。

  • 需要理解环境类与抽象状态类的作用

    • 环境类实际上就是拥有状态的对象,环境类有时候可以充当状态管理器(State Manager)的角色,可以在环境类中对状态进行切换操作。
    • 抽象状态类可以是抽象类,也可以是接口,不同状态类就是继承这个父类的不同子类,状态类的产生是由于环境类存在多个状态,同时还满足两个条件:这些状态经常需要切换,在不同的状态下对象的行为不同。

实例分析

论坛用户等级
  • 在某论坛系统中,用户可以发表留言,发表留言将增加积分;用户也可以回复留言,回复留言也将增加积分;用户还可以下载文件,下载文件将扣除积分。该系统用户分为三个等级,分别是新手、高手和专家,这三个等级对应三种不同的状态,这三种状态分别

  • 定义如下:

    1. 如果积分小于 100 分,则为新手状态,用户可以发表留言、回复留言,但是不能下载文件。如果积分大于等于 1000 分,则转换为专家状态;如果积分大于等于 100 分,则转换为高手状态。
    2. 如果积分大于等于 100 分但小于 1000 分,则为高手状态,用户可以发表留言、回复留言,还可以下载文件,而且用户在发表留言时可以获取双倍积分。如果积分小于 100 分,则转换为新手状态;如果积分大于等于 1000 分,则转换为专家状态;如果下载文件后积分小于 0,则不能下载该文件。
    3. 如果积分大于等于 1000 分,则为专家状态,用户可以发表留言、回复留言和下载文件,用户除了在发表留言时可以获取双倍积分外,下载文件只扣除所需积分的一半。如果积分小于 100 分,则转换为新手状态;如果积分小于 1000 分,但大于等于 100,则转换为高手状态;如果下载文件后积分小于 0,则不能下载该文件。

    alt text

  • pointcheckstate 应该放置在外侧

优缺点

  • 优点:

    • 封装了转换规则
    • 枚举可能的状态,在枚举状态之前需要确定状态种类。
    • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
    • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
    • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
  • 缺点:

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

适用环境

  • 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为。
  • 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。在这些条件语句中包含了对象的行为,而且这些条件对应于对象的各种状态。

模式应用

  • 状态模式在工作流或游戏等类型的软件中得以广泛使用,甚至可以用于这些系统的核心功能设计,如在政府 OA 办公系统中,一个批文的状态有多种:尚未办理;正在办理;正在批示;正在审核;已经完成等各种状态,而且批文状态不同时对批文的操作也有所差异。使用状态模式可以描述工作流对象(如批文)的状态转换以及不同状态下它所具有的行为。
  • 在目前主流的 RPG(Role Play Game,角色扮演游戏)中,使用状态模式可以对游戏角色进行控制,游戏角色的升级伴随着其状态的变化和行为的变化。对于游戏程序本身也可以通过状态模式进行总控,一个游戏活动包括开始、运行、结束等状态,通过对状态的控制可以控制系统的行为,决定游戏的各个方面,因此可以使用状态模式对整个游戏的架构进行设计与实现。

模式扩展

共享模式

在有些情况下多个环境对象需要共享同一个状态,如果希望在系统中实现多个环境对象实例共享一个或多个状态对象,那么需要将这些状态对象定义为环境的静态成员对象。

简单状态模式与可切换状态的状态模式
  • 简单状态模式:简单状态模式是指状态都相互独立,状态之间无须进行转换的状态模式,这是最简单的一种状态模式。对于这种状态模式,每个状态类都封装与状态相关的操作,而无须关心状态的切换,可以在客户端直接实例化状态类,然后将状态对象设置到环境类中。如果是这种简单的状态模式,它遵循“开闭原则”,在客户端可以针对抽象状态类进行编程,而将具体状态类写到配置文件中,同时增加新的状态类对原有系统也不造成任何影响。
  • 可切换状态的状态模式:大多数的状态模式都是可以切换状态的状态模式,在实现状态切换时,在具体状态类内部需要调用环境类 ContextsetState() 方法进行状态的转换操作,在具体状态类中可以调用到环境类的方法,因此状态类与环境类之间通常还存在关联关系或者依赖关系。通过在状态类中引用环境类的对象来回调环境类的 setState() 方法实现状态的切换。在这种可以切换状态的状态模式中,增加新的状态类可能需要修改其他某些状态类甚至环境类的源代码,否则系统无法切换到新增状态。

命令模式 Command Pattern

模式动机

  • 我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计,使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。
  • 命令模式可以对发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。这就是命令模式的模式动机。

模式定义

  • 将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。

模式结构

  • 模式结构:
    alt text

  • 包含的角色:

    • Command:抽象命令类
    • ConcreteCommand:具体命令类
    • Invoker:调用者
    • Receiver:接收者
    • Client:客户类

模式分析

  • 命令模式的本质是对命令进行封装将发出命令的责任和执行命令的责任分割开

  • 每一个命令都是一个操作:请求的一方发出请求,要求执行一个操作;接收的一方收到请求,并执行操作。

  • 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。

  • 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。

  • 命令模式的关键在于引入了抽象命令接口,且发送者针对抽象命令接口编程,只有实现了抽象命令接口的具体命令才能与接收者相关联

  • 典型的抽象命令类代码:

    1
    2
    3
    public abstract class Command{
    public abstract void execute();
    }
  • 典型的调用者代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class Invoker {
    private Command command;

    public Invoker(Command command) {
    this.command = command;
    }

    public void setCommand(Command command) {
    this.command = command;
    }

    // 业务方法,用于调用命令类的方法
    public void call() {
    command.execute();
    }
    }
  • 典型的具体命令类代码:

    1
    2
    3
    4
    5
    6
    7
    public class ConcreteCommand extends Command {
    private Receiver receiver;

    public void execute() {
    receiver.action();
    }
    }
  • 典型的请求接收者代码:

    1
    2
    3
    4
    5
    public class Receiver {
    public void action() {
    // 具体操作
    }
    }
  • 命令模式顺序图
    alt text

实例分析

电视机遥控器
  • 电视机是请求的接收者,遥控器是请求的发送者,遥控器上有一些按钮,不同的按钮对应电视机的不同操作。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。显然,电视机遥控器就是一个典型的命令模式应用实例。
    alt text
功能键设置
  • 为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,如功能键 FunctionButton 可以用于退出系统(SystemExitClass),也可以用于打开帮助界面(DisplayHelpClass)。用户可以通过修改配置文件来改变功能键的用途,现使用命令模式来设计该系统,使得功能键类与功能类之间解耦,相同的功能键可以对应不同的功能。
    alt text

优缺点

  • 优点:

    • 降低系统的耦合度。
    • 新的命令可以很容易地加入到系统中。
    • 可以比较容易地设计一个命令队列和宏命令(组合命令)。
    • 可以方便地实现对请求的 Undo 和 Redo。
  • 缺点:

    • 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
  • 模式适用坏境:

    • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
    • 系统需要在不同的时间指定请求、将请求排队和执行请求
    • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作
    • 系统需要将一组操作组合在一起,即支持宏命令。

适用环境

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
  • 系统需要将一组操作组合在一起,即支持宏命令。

模式应用

  • Java 语言使用命令模式实现 AWT/Swing GUI 的委派事件模型 (Delegation Event Model, DEM)。
    • 在 AWT/Swing 中,Frame、Button 等界面组件是请求发送者,而 AWT 提供的事件监听器接口和事件适配器类是抽象命令接口,用户可以自己写抽象命令接口的子类来实现事件处理,即实现具体命令类,而在具体命令类中可以调用业务处理方法来实现该事件的处理。对于界面组件而言,只需要了解命令接口即可,无须关心接口的实现,组件类并不关心实际操作,而操作由用户来实现。
  • 很多系统都提供了宏命令功能,如 UNIX 平台下的 Shell 编程,可以将多条命令封装在一个命令对象中,只需要一条简单的命令即可执行一个命令序列,这也是命令模式的应用实例之一。

模式扩展

撤销操作的实现

alt text

  • 宏命令又称为组合命令,它是命令模式和组合模式联用的产物。
  • 宏命令也是一个具体命令,不过它包含了对其他命令对象的引用,在调用宏命令的 execute() 方法时,将递归调用它所包含的每个成员命令的 execute() 方法,一个宏命令的成员对象可以是简单命令,还可以继续是宏命令。执行一个宏命令将执行多个具体命令,从而实现对命令的批处理。
宏命令

alt text

观察者模式 Observer Pattern

模式动机

  • 建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。
    1. 发生改变的对象称为观察目标
    2. 被通知的对象称为观察者
  • 一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者使得系统更易于扩展,这就是观察者模式的模式动机。

模式结构

  • 模式结构
    alt text
  • 包含的角色:
    • Subject:目标
    • ConcreteSubject:具体目标
    • Observer:观察者
    • ConcreteObserver:具体观察者

模式分析

  • 观察者模式描述了如何建立对象与对象之间的依赖关系,如何构造满足这种需求的系统。

  • 这一模式中的关键对象是观察目标和观察者,一个目标可以有任意数目的与之相依赖的观察者,一旦目标的状态发生改变,所有的观察者都将得到通知。

  • 作为对这个通知的响应,每个观察者都将即时更新自己的状态,以与目标状态同步,这种交互也称为发布-订阅(publish-subscribe)。目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。

  • 松耦合:

    • 当两个对象之间松耦合,它们依然可以交互,但是不太清楚彼此的细节。
    • 观察者模式提供了一种对象设计,让主题和观察者之间松耦合。
    • 改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他们之间的接口仍被遵守,就可以自由地改变他们。
  • 回到 WeatherData

    • alt text

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      public interface Subject {
      public void registerObserver(Observer o);

      public void removeObserver(Observer o);

      public void notifyObservers();
      }

      public interface Observer {
      public void update(float temp, float humidity, float pressure);

      public interface DisplayElement {
      public void display();
      }
      }

      public class CurrentConditionsDisplay implements Observer, DisplayElement {
      private float temperature;
      private float humidity;
      private Subject weatherData;

      public CurrentConditionsDisplay(Subject weatherData) {
      this.weatherData = weatherData;
      weatherData.registerObserver(this);
      }

      public void update(float temperature, float humidity, float pressure) {
      this.temperature = temperature;
      this.humidity = humidity;
      display();
      }

      public void display() {
      System.out.println(
      "Current conditions:" + temperature + "F degrees and " + humidity + "% humidity");

      }
      }

优缺点

  • 优点:

    • 观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。
    • 观察者模式在观察目标和观察者之间建立一个抽象的耦合
    • 观察者模式支持广播通信
    • 观察者模式符合“开闭原则”的要求。
  • 缺点:

    • 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间
    • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃
    • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

适用环境

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  • 一个对象必须通知其他对象,而并不知道这些对象是谁。
  • 需要在系统中创建一个触发链,A 对象的行为将影响 B 对象,B 对象的行为将影响 C 对象……,可以使用观察者模式创建一种链式触发机制。

模式应用

  • JDK1.1 版本及以后的各个版本中,事件处理模型采用基于观察者模式的委派事件模型(Delegation Event Model, DEM)。
    • 在 DEM 中,事件的发布者称为事件源(Event Source),而订阅者叫做事件监听器(Event Listener),在这个过程中还可以通过事件对象(Event Object)来传递与事件相关的信息,可以在事件监听者的实现类中实现事件处理,因此事件监听对象又可以称为事件处理对象。
    • 事件源对象、事件监听对象(事件处理对象)和事件对象构成了 Java 事件处理模型的三要素。
  • 除了 AWT 中的事件处理之外,Java 语言解析 XML 的技术 SAX2 以及 Servlet 技术的事件处理机制都基于 DEM,它们都是观察者模式的应用。
  • 观察者模式在软件开发中应用非常广泛,如某电子商务网站可以在执行发送操作后给用户多个发送商品打折信息,某团队战斗游戏中某队友牺牲将给所有成员提示等等,凡是涉及到一对一或者一对多的对象交互场景都可以使用观察者模式。

模式扩展

Java 语言提供的对观察者模式的支持
  • 在 JDK 的 java.util 包中,提供了 Observable 类以及 Observer 接口,它们构成了 Java 语言对观察者模式的支持。
    alt text
  • Observer 接口: void update(Observable o, Object arg);
  • Observable 类
    1. Observable()
    2. addObserver(Observer o)
    3. deleteObserver (Observer o)
    4. notifyObservers()、notifyObservers(Object arg)
    5. deleteObservers
    6. setChanged()
    7. clearChanged()
    8. hasChanged()
    9. countObservers()
谁触发谁更新
  • 目标和它的观察者依赖于通知机制来保持一致。但到底哪一个对象调用 Notify 来触发更新?此时有两个选择:

    1. Push 更新:由目标对象的状态设定操作在改变目标对象的状态后自动调用 Notify 。 这种方法的优点是客户不需要记住要在目标对象上调用 Notify 缺点是多个连续的操作会产生多次连续的更新 , 可能效率较低。
    2. Pull 更新:让客户负责在适当的时候调用 Notify 。 这样做的优点是客户可以在一系列的状态改变完成后再一次性地触发更新 避免了不必要的中间更新。缺点是给客户增加了触发更新的责任。由于客户可能会忘记调用 Notify 这种方式较易出错。
封装复杂的更新语义
  • 当目标和观察者间的依赖关系特别复杂时 , 可能需要一个维护这些关系的对象。 我们称这样的对象为更改管理器 Change Manager。

  • 它的目的是尽量减少观察者反映其目标的状态变化所需的工作量。例如 , 如果一个操作涉及到对几个相互依赖的目标进行改动 , 就必须保证仅在所有目标都已更改完毕后,才一次性地通知它们的观察者 而不是每个目标都通知观察者。
    alt text

MVC 模式

Java SE Application Design With MVC

  • MVC 模式是一种架构模式,它包含三个角色:模型(Model),视图(View)和控制器(Controller)。观察者模式可以用来实现 MVC 模式,观察者模式中的观察目标就是 MVC 模式中的模型(Model),而观察者就是 MVC 中的视图(View),控制器(Controller)充当两者之间的中介者(Mediator)。当模型层的数据发生改变时,视图层将自动改变其显示内容。
    alt text

中介者模式 Mediator Patter

模式动机

  • 在用户与用户直接聊天的设计方案中,用户对象之间存在很强的关联性,将导致系统出现如下问题:
    1. 系统结构复杂对象之间存在大量的相互关联和调用,若有一个对象发生变化,则需要跟踪和该对象关联的其他所有对象,并进行适当处理。
    2. 对象可重用性差:由于一个对象和其他对象具有很强的关联,若没有其他对象的支持,一个对象很难被另一个系统或模块重用,这些对象表现出来更像一个不可分割的整体,职责较为混乱。
    3. 系统扩展性低:增加一个新的对象需要在原有相关对象上增加引用,增加新的引用关系也需要调整原有对象,系统耦合度很高,对象操作很不灵活,扩展性差。
  • 在面向对象的软件设计与开发过程中,根据“单一职责原则”,我们应该尽量将对象细化,使其只负责或呈现单一的职责
  • 对于一个模块,可能由很多对象构成,而且这些对象之间可能存在相互的引用,为了减少对象两两之间复杂的引用关系使之成为一个松耦合的系统,我们需要使用中介者模式,这就是中介者模式的模式动机。

模式定义

  • Mediator Pattern:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。

模式结构

  • 模式结构:
    alt text
  • 包含角色:
    • Mediator:抽象中介者
    • ConcreteMediator:具体中介者
    • Colleague:抽象同事类
    • ConcreteColleague:具体同事类

模式分析

  • 中介者模式可以使对象之间的关系数量急剧减少

  • 中介者承担两方面的职责:

    1. 中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,通过中介者即可。该中转作用属于中介者在结构上的支持
    2. 协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持
  • 典型的抽象中介者类代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public abstract class Mediator {
    protected ArrayList colleagues;

    public void register(Colleague colleague) {
    colleagues.add(colleague);
    }

    public abstract void operation();
    }
  • 典型的具体中介者类代码:

    1
    2
    3
    4
    5
    6
    7
    public class ConcreteMediator extends Mediator {
    public void operation() {
    ......
    ((Colleague) (colleagues.get(0))).method1();
    ......
    }
    }
  • 典型的抽象同事类代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public abstract class Colleague {
    protected Mediator mediator;

    public Colleague(Mediator mediator) {
    this.mediator = mediator;
    }

    public abstract void method1();

    public abstract void method2();
    }
  • 典型的具体同事类代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class ConcreteColleague extends Colleague {
    public ConcreteColleague(Mediator mediator) {
    super(mediator);
    }

    public void method1() {
    ......
    }

    public void method2() {
    mediator.operation1();
    }
    }

实例分析

虚拟聊天室
  • 某论坛系统欲增加一个虚拟聊天室,允许论坛会员通过该聊天室进行信息交流,普通会员(CommonMember)可以给其他会员发送文本信息,钻石会员(DiamondMember)既可以给其他会员发送文本信息,还可以发送图片信息。该聊天室可以对不雅字符进行过滤,如“日”等字符;还可以对发送的图片大小进行控制。用中介者模式设计该虚拟聊天室。
    alt text

优缺点

  • 优点:

    • 简化了对象之间的交互。
    • 将各同事解耦。
    • 减少子类生成。
    • 可以简化各同事类的设计和实现。
  • 缺点:

    • 在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护

适用环境

  • 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解。
  • 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。
  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的中介者类。

模式应用

  • 中介者模式在事件驱动类软件中应用比较多,在设计 GUI 应用程序时,组件之间可能存在较为复杂的交互关系,一个组件的改变将影响与之相关的其他组件,此时可以使用中介者模式来对组件进行协调。
  • MVC 是 Java EE 的一个基本模式,此时控制器 Controller 作为一种中介者,它负责控制视图对象 View 和模型对象 Model 之间的交互。如在 Struts 中,Action 就可以作为 JSP 页面与业务对象之间的中介者。

模式扩展

  • 中介者模式与迪米特法则
    • 在中介者模式中,通过创造出一个中介者对象,将系统中有关的对象所引用的其他对象数目减少到最少,使得一个对象与其同事之间的相互作用被这个对象与中介者对象之间的相互作用所取代。因此,中介者模式就是迪米特法则的一个典型应用。
  • 中介者模式与 GUI 开发
    • 中介者模式可以方便地应用于图形界面(GUI)开发中,在比较复杂的界面中可能存在多个界面组件之间的交互关系。
    • 对于这些复杂的交互关系,有时候我们可以引入一个中介者类,将这些交互的组件作为具体的同事类,将它们之间的引用和控制关系交由中介者负责,在一定程度上简化系统的交互,这也是中介者模式的常见应用之一。

模板方法模式

模式动机

  • 模板方法模式是基于继承的代码复用基本技术,模板方法模式的结构和用法也是面向对象设计的核心之一。在模板方法模式中,可以将相同的代码放在父类中,而将不同的方法实现放在不同的子类中
  • 在模板方法模式中,我们需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来让子类实现剩余的逻辑不同的子类可以以不同的方式实现这些抽象方法从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。

模式定义

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

模式结构

  • 模式结构
    alt text
  • 包含角色:
    • AbstractClass:抽象类
    • ConcreteClass:具体子类

模式分析

  • 模板方法模式是一种类的行为型模式,在它的结构图中只有类之间的继承关系,没有对象关联关系

  • 在模板方法模式的使用过程中,要求开发抽象类和开发具体子类的设计师之间进行协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。实现这些具体逻辑步骤的方法称为基本方法(Primitive Method),而将这些基本法方法汇总起来的方法称为模板方法(Template Method),模板方法模式的名字从此而来。

  • 模板方法:一个模板方法是定义在抽象类中的、把基本操作方法组合在一起形成一个总算法或一个总行为的方法。

  • 基本方法:基本方法是实现算法各个步骤的方法,是模板方法的组成部分。

    1. 抽象方法(Abstract Method)
    2. 具体方法(Concrete Method)
    3. 钩子方法(Hook Method):“挂钩”方法和空方法
  • 钩子方法(Hook Method)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public void template() {
    open();
    display();
    if (isPrint()) {
    print();
    }
    }

    public boolean isPrint() {
    return true;
    }
  • 典型的抽象类代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public abstract class AbstractClass
    {
    public void templateMethod() //模板方法
    {
    primitiveOperation1();
    primitiveOperation2();
    primitiveOperation3();
    }
    public void primitiveOperation1() //基本方法—具体方法
    {
    //实现代码
    }
    public abstract void primitiveOperation2(); //基本方法—抽象方法
    public void primitiveOperation3() //基本方法—钩子方法
    {
    }
    }
  • 典型的具体子类代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class ConcreteClass extends AbstractClass {
    public void primitiveOperation2() {
    //实现代码
    }

    public void primitiveOperation3() {
    //实现代码
    }
    }
  • 在模板方法模式中,由于面向对象的多态性,子类对象在运行时将覆盖父类对象,子类中定义的方法也将覆盖父类中定义的方法,因此程序在运行时,具体子类的基本方法将覆盖父类中定义的基本方法子类的钩子方法也将覆盖父类的钩子方法,从而可以通过在子类中实现的钩子方法对父类方法的执行进行约束,实现子类对父类行为的反向控制

实例分析

银行业务办理流程
  • 在银行办理业务时,一般都包含几个基本步骤,首先需要取号排队,然后办理具体业务,最后需要对银行工作人员进行评分。无论具体业务是取款、存款还是转账,其基本流程都一样。现使用模板方法模式模拟银行业务办理流程。
    alt text
数据库操作模板
  • 对数据库的操作一般包括连接、打开、使用、关闭等步骤,在数据库操作模板类中我们定义了 connDB()openDB()useDB()closeDB()四个方法分别对应这四个步骤。对于不同类型的数据库(如 SQL Server 和 Oracle),其操作步骤都一致,只是连接数据库 connDB() 方法有所区别,现使用模板方法模式对其进行设计。
    alt text

优缺点

  • 优点:

    • 模板方法模式在一个类中抽象地定义算法,而由它的子类实现细节的处理
    • 模板方法模式是一种代码复用的基本技术。
    • 模板方法模式导致一种反向的控制结构,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,符合“开闭原则”
  • 缺点:

    • 每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,但是更加符合“单一职责原则”,使得类的内聚性得以提高。

适用环境

  • 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。
  • 对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。
  • 控制子类的扩展

模式应用

  • 模板方法模式广泛应用于框架设计(如Spring,Struts等)中,以确保父类控制处理流程的逻辑顺序(如框架的初始化)。
  • Java 单元测试工具 JUnit 中的 TestCase类的设计:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Java 单元测试工具 JUnit 中的 TestCase 类的设计:
    public void runBare() throws Throwable {
    setUp();
    try {
    runTest();
    }
    finally {
    tearDown();
    }
    }

模式扩展

  • 关于继承的讨论:模板方法模式鼓励我们恰当使用继承,此模式可以用来改写一些拥有相同功能的相关类,将可复用的一般性的行为代码移到父类里面,而将特殊化的行为代码移到子类里面。这也进一步说明,虽然继承复用存在一些问题,但是在某些情况下还是可以给开发人员带来方便,模板方法模式就是体现继承优势的模式之一。
  • 好莱坞原则:
    • 在模板方法模式中,子类不显式调用父类的方法,而是通过覆盖父类的方法来实现某些具体的业务逻辑,父类控制对子类的调用,这种机制被称为好莱坞原则(Hollywood Principle),好莱坞原则的定义为:“不要给我们打电话,我们会给你打电话(Don‘t call us, we’ll call you)”。
    • 在模板方法模式中,好莱坞原则体现在:子类不需要调用父类,而通过父类来调用子类,将某些步骤的实现写在子类中,由父类来控制整个过程。
  • 钩子方法的使用
    • 钩子方法的引入使得子类可以控制父类的行为。
    • 最简单的钩子方法就是空方法,也可以在钩子方法中定义一个默认的实现,如果子类不覆盖钩子方法,则执行父类的默认实现代码。
    • 比较复杂一点的钩子方法可以对其他方法进行约束,这种钩子方法通常返回一个 boolean 类型,即返回 true 或 false,用来判断是否执行某一个基本方法。

PPT中问题

在 JDK 中,java.util.Stackjava.util.Vector 类的子类,该设计合理吗?若不合理,请分析解释该设计存在的问题。

在 JDK 中,确实存在 java.util.Stackjava.util.Vector 类的子类的设计。这种设计在早期的 Java 开发中是合理的,但在当前的软件工程实践中,这种继承关系通常被认为是不合理或不推荐的,主要原因如下:

  1. 设计用途的不匹配
    • Vector 是一个动态数组,它支持自动扩容和随机访问,而 Stack 是一种后进先出(LIFO)的数据结构。虽然可以通过 Vector 的方法实现 Stack 的功能,但这并不是 Vector 最主要的设计目标,因此使用 Vector 作为 Stack 的基类可能会导致功能上的混淆。
  2. 接口暴露的问题
    • Vector 是一个线程安全的类,而 Stack 继承自 Vector,因此继承了 Vector 的所有同步方法,这包括对整个栈的操作进行同步处理。然而,在实际应用中,通常并不需要 Stack 的所有操作都是线程安全的,因此这种继承可能会带来额外的开销和复杂性。
  3. 继承与组合的选择
    • 在软件设计中,通常推荐使用组合而不是继承来实现功能的复用。将 Vector 作为 Stack 的基类,意味着 Stack 继承了 Vector 的所有公共方法和实现细节,这可能导致不必要的耦合和限制。更好的做法是使用组合,即在 Stack 内部使用 Vector 或其他合适的数据结构来实现自己的功能,从而更灵活地管理和扩展。
  4. 过时的设计选择
    • 随着时间的推移,Java 在集合框架和并发性能方面有了显著的进步和优化。VectorStack 是 Java 集合框架的旧部分,现在更推荐使用更现代和更高效的集合类,例如 ArrayListArrayDeque,它们分别用于动态数组和栈的实现,并且不会带来 Vector 继承中的同步开销。

综上所述,虽然 Stack 继承自 Vector 在早期的 Java 设计中是合理的,但在现代的软件开发实践中,这种设计不再被推荐。更好的做法是通过组合和选择更合适的数据结构来实现 Stack 的功能,以获得更好的性能和更清晰的设计。

其他设计模式总结

by Zhy

设计模式原则

具体设计模式