设计模式

三种类型,23种

  1. 创建型

    单例模式、抽象工厂模式、工厂模式、原型模式、建造者模式

  2. 结构型

    适配器模式、桥接模式、装饰者模式、组合模式、外观模式、享元模式、代理模式

  3. 行为型

    模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式

UML

简介

UML是一种标准的图形化建模语言,它是面向对象分析与设计的一种标准表示。

类图

类图是静态视图的图形表达方式,表示声明静态的模型元素,如:类、类型和其内容,以及它们的相互关系。也就是说,类图是用来描述类以及类与类之间关系的一种UML图

类图的基本表示

1635670058270

  1. 类名定义

    没有特殊的要求,任何合法的名称都可以。

  2. 属性定义的基本语法

    属性用来描述类所具有的特征。描述属性的语法格式为:

    可见性属性名:类型名=初值

    对于可见性:+表示public,-表示private,#表示protected,没有符号就表示默认的可见性。

    -age:int=20

    -name:String

  3. 操作定义的基本语法

    操作用来描述类能干些什么事情,也就是我们通常说的方法。描述操作的语法格式为:

    可见性 操作名(参数列表):返回值类型

    +run(speed: double, durableTime: double): void

  4. Java中的static的表示

    添加一条下划线表示static

    1635670410138

抽象类和接口

抽象类的表示是类名倾斜,抽象操作的表示是整条操作定义都倾斜

1635670503016

接口是一种特殊的抽象类,归根结底还是类,所以接口的表达基本语法和抽象类是一样的

关系

1635670753546

  1. 关联关系

    用来描述类和类的连接。类与类之间有多种连接方式,每种连接的含义都是不同的,虽然语义不同,但是外部表象类似,因此统称为关联

    关联关系一般都是双向的,但是也有单向的关联。

    根据不同的含义,把关联分成普通关联、递归关联、限定关联、或关联、有序关联、三元关联和聚合7种

  2. 泛化关系

    又称通用化或继承,用来描述一个通用元素的所有信息能被另一个具体元素继承的机制。

    1635671091889

  3. 实现关系

    描述类实现接口的关系

    1635671178377

  4. 依赖关系

    如果某个对象的行为和实现,需要受到另外对象的影响,那么就说这个对象依赖于其他对象。最常用的依赖关系是“使用”,意思是如果A使用了B,那么就说A依赖于B

    1635671286121

顺序图

设计原则

7大原则

单一职责原则

一个类只负责一个功能领域中的相应职责

单一职责原则是实现高内聚、低耦合的指导方针

开闭原则

一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展

里氏代换原则

跟多态联系起来

在软件中将一个基类对象替换成它的子类对象,程序不会产生任何错误,反过来则不成立

如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物。

(在代码中尽量使用基类作为参数)

依赖倒置原则

依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。

接口隔离原则

使用多个专门的接口,而不是用单一的总接口,即客户端不应该依赖那些它不需要的接口

合成复用原则

复用时要尽量使用组合/聚合关系(关联关系),少用继承。

迪米特原则

也叫最少知识原则

只和你的朋友谈话

哪些对象可以作为朋友:

  1. 当前对象本身
  2. 通过方法的参数传递进来的对象
  3. 当前对象所创建的对象
  4. 当前对象的实例变量所引用的对象
  5. 方法内所创建或实例化的对象

概览

类型 模式名称 学习难度 使用频率
创建型模式 Creational Pattern 单例模式 Singleton Pattern ★☆☆☆☆ ★★★★☆
创建型模式 Creational Pattern 简单工厂模式 Simple Factory Pattern ★★☆☆☆ ★★★☆☆
创建型模式 Creational Pattern 工厂方法模式 Factory Method Pattern ★★☆☆☆ ★★★★★
创建型模式 Creational Pattern 抽象工厂模式 Abstract Factory Pattern ★★★★☆ ★★★★★
创建型模式 Creational Pattern 原型模式 Prototype Pattern ★★★☆☆ ★★★☆☆
创建型模式 Creational Pattern 建造者模式 Builder Pattern ★★★★☆ ★★☆☆☆
结构型模式 Structural Pattern 适配器模式 Adapter Pattern ★★☆☆☆ ★★★★☆
结构型模式 Structural Pattern 桥接模式 Bridge Pattern ★★★☆☆ ★★★☆☆
结构型模式 Structural Pattern 组合模式 Composite Pattern ★★★☆☆ ★★★★☆
结构型模式 Structural Pattern 装饰模式 Decorator Pattern ★★★☆☆ ★★★☆☆
结构型模式 Structural Pattern 外观模式 Façade Pattern ★☆☆☆☆ ★★★★★
结构型模式 Structural Pattern 享元模式 Flyweight Pattern ★★★★☆ ★☆☆☆☆
结构型模式 Structural Pattern 代理模式 Proxy Pattern ★★★☆☆ ★★★★☆
行为型模式 Behavioral Pattern 职责链模式 Chain of Responsibility Pattern ★★★☆☆ ★★☆☆☆
行为型模式 Behavioral Pattern 命令模式 Command Pattern ★★★☆☆ ★★★★☆
行为型模式 Behavioral Pattern 解释器模式 Interpreter Pattern ★★★★★ ★☆☆☆☆
行为型模式 Behavioral Pattern 迭代器模式 Iterator Pattern ★★★☆☆ ★★★★★
行为型模式 Behavioral Pattern 中介者模式 Mediator Pattern ★★★☆☆ ★★☆☆☆
行为型模式 Behavioral Pattern 备忘录模式 Memento Pattern ★★☆☆☆ ★★☆☆☆
行为型模式 Behavioral Pattern 观察者模式 Observer Pattern ★★★☆☆ ★★★★★
行为型模式 Behavioral Pattern 状态模式 State Pattern ★★★☆☆ ★★★☆☆
行为型模式 Behavioral Pattern 策略模式 Strategy Pattern ★☆☆☆☆ ★★★★☆
行为型模式 Behavioral Pattern 模板方法模式 Template Method Pattern ★★☆☆☆ ★★★☆☆
行为型模式 Behavioral Pattern 访问者模式 Visitor Pattern ★★★★☆ ★☆☆☆☆

单例模式(Singleton)

8种方法

  1. 饿汉式(静态常量) 内存问题

  2. 饿汉式(静态代码块) 内存问题

  3. 懒汉式(线程不安全) 实际开发不能用

  4. 懒汉式(线程安全,同步方法) 每次都要同步,效率低,实际开发不推荐

  5. 懒汉式(线程安全,同步代码块)

  6. 双重检查

    问题:线程A对象未完成创建,线程B在外层if判断可能不为null了(编译器指令重排导致分配了内存,指向该空间,但是实际对象还没有创建),导致可能拿不到对象实例

    重点:volatile:防止指令重排

    指令重排:

    是编译器在不改变执行效果的前提下,对指令顺序进行调整,从而提高执行效率的过程。

    例子:

    1
    2
    int a = 1;
    String b = "b";

    因为不影响效果,可能指令重排成

    1
    2
    String b = "b";
    int a = 1;

    正常创建对象的步骤

    1. 分配一块内存空间
    2. 在这块内存上初始化一个DoubleCheckLock的实例
    3. 将声明的引用instance指向这块内存

    指令重排后可能变成

    1. 分配一块内存空间
    2. 将声明的引用instance指向这块内存
    3. 在这块内存上初始化一个DoubleCheckLock的实例
  7. 静态内部类

    在外部类装载的时候不会立即实例化,而是在需要实例化的时候才调用getInstance方法,才会装载内部类,且静态属性只会加载一次,JVM已经解决了线程安全的问题

  8. 枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SingletonTest08 {
    public static void main(String[] args) {
    Singleton instance1 = Singleton.INSTANCE;
    Singleton instance2 = Singleton.INSTANCE;
    System.out.println(instance1 == instance2);
    }
    }

    enum Singleton {
    INSTANCE; // 属性
    }

Java实例

Runtime

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Runtime {
// 饿汉式
private static Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
return currentRuntime;
}

/** Don't let anyone else instantiate this class */
private Runtime() {}

...
}

简单工厂模式(Simple Factory)

案例

披萨订购例子

使用简单工厂模式设计一个可以创建不同几何形状(如圆形、方形和三角形等)的绘图工具,每个几何图形都具有绘制draw()和擦除erase()两个方法,要求在绘制不支持的几何图形时,提示一个UnSupportedShapeException

分析

优点:

  1. 客户端不需要知道所创建的具体产品类的类名
  2. 引入配置文件,可以在不修改代码的情况下更换和增加新的具体产品类

缺点:

  1. 工厂类集中了所有产品的创建逻辑,职责过重了,一旦不能正常工作,整个系统都要收到影响
  2. 势必增加系统中类的个数
  3. 系统扩展困难,一旦添加新产品就必须修改工厂的逻辑

适用场景:

  1. 工厂类负责创建的对象比较少
  2. 客户端不关心具体对象类型与创建过程

工厂方法模式(Factory Method)

引入工厂等级结构解决简单工厂模式中工厂类职责太重的问题

抽象工厂类提供抽象的工厂方法,由具体的子工厂类实现工厂方法

组成部分

在工厂方法模式结构图中包含如下几个角色:

● Product(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。

● ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。

● Factory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。

● ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。

案例

使用工厂方法模式设计一个程序来读取各种不同类型的图片格式,针对每一种图片格式都设计一个图片读取器,如GIF图片读取器用于读取GIF格式的图片、JPG图片读取器用于读取JPG格式的图片。需充分考虑系统的灵活性和可扩展性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
ImageReader imageReader = new GifImageReader();
Image gifImage = imageReader.readImage();
System.out.println(gifImage);
imageReader = new JpgImageReader();
Image jpgImage = imageReader.readImage();
System.out.println(jpgImage);

/*
相比于使用简单工厂,如果我要创建读取多个gif图片,那么多次调用GifImageReader来创建即可
但是如果是简单工厂模式,那么会有更多的冗余代码,另外,如果需要新增图片的类型,工厂方法只需要添加相应的具体工厂类即可,而简单工厂则需要违反开闭原则修改源代码
*/

}
}

分析

优点

  1. 客户端只需要关心所需要的产品对应的工厂,无需关心创建的细节
  2. 相比于简单工厂,在加入新产品的时候,无需修改抽象工厂和抽象产品提供的接口,即不需要修改原来的代码,只需要添加新的具体产品以及对应的具体工厂类就可以了

缺点

  1. 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加(类爆炸)

适用场景

大部分客户端不需要知道创建细节的时候都可以使用的

抽象工厂模式(Abstract Factory)

考虑将一些相关的产品组成一个产品族,由同一个工厂来生产,减少类的数量

有时候我们希望一个工厂可以提供多个产品对象,而不是单一的产品对象,如一个电器工厂,它可以生产电视机、电冰箱、空调等多种电器,而不是只生产某一种电器。

为了更好地理解抽象工厂模式,我们先引入两个概念:

(1) 产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。

(2) 产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中,海尔电视机、海尔电冰箱构成了一个产品族。

产品等级结构与产品族示意图如图所示:

1635728277621

抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建。

1635728406223

如图,如果使用工厂方法,那么需要15个工厂类,如果使用抽象工厂,那么只需要5个工厂类就可以了,一个工厂类可以生产一个产品族,即生产相同颜色的不同的图形

组成部分

在抽象工厂模式结构图中包含如下几个角色:

● AbstractFactory(抽象工厂):它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。

● ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。

● AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。

● ConcreteProduct(具体产品):它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。

案例

Sunny软件公司欲推出一款新的手机游戏软件,该软件能够支持Symbian、Android和Windows Mobile等多个智能手机操作系统平台,针对不同的手机操作系统,该游戏软件提供了不同的游戏操作控制(OperationController)类和游戏界面控制(InterfaceController)类,并提供相应的工厂类来封装这些类的初始化过程。软件要求具有较好的扩展性以支持新的操作系统平台,为了满足上述需求,试采用抽象工厂模式对其进行设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {
public static void main(String[] args) {
Factory symbianFactory = new SymbianFactory();
Factory androidFactory = new AndroidFactory();
System.out.println(symbianFactory.getOperationController());
System.out.println(symbianFactory.getInterfaceController());
System.out.println(androidFactory.getOperationController());
System.out.println(androidFactory.getInterfaceController());

/*
如果使用工厂方法,那么对于每一种具体的Controller,我都需要一个对应的工厂类来生产
*/
}
}

Java实例

Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式

DAO和抽象工厂的关系

在实现DAO模式的时候,最常见的实现策略就是使用工厂的策略,而且多是通过抽象工厂模式来实现的,当然也可以结合工厂方法模式

分析

优点:

  1. 增加新的产品族很方便,只需要新增一个具体工厂类就可以了,比如我要新增windows端,那么只需要新增windows的具体工厂类以及对象的windows端controller就可以了,即我不需要修改已有的代码(符合开闭)

缺点:

  1. 新增新的产品等级结构麻烦,比如我要新增一种controller,那么我需要修改抽象工厂类,新增一个抽象方法,当然就要修改其他的具体工厂类(违反开闭)

适用场景

  1. 客户端不关心产品实例创建细节
  2. 系统中有多于一个产品族,而每次只使用其中一个产品族
  3. 产品等级结构稳定,设计完成之后,不会向系统增加或删除产品结构(开闭原则)

建造者模式(Builder)

建造者模式是较为复杂的创建型模式,它将客户端与包含多个组成部分(或部件)的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性。

建造者模式一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。

组成部分

在建造者模式结构图中包含如下几个角色:

● Builder(抽象建造者):它为创建一个产品Product对象的各个部件指定抽象接口,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。

●ConcreteBuilder(具体建造者):它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象。

●Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。

● Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。

高级应用

有些情况下,为了简化系统结构,可以将Director和抽象建造者Builder合并,将其定义为Builder类中的静态方法

可以通过钩子方法(isXXX)来控制是否产品的构建,如加入isBareHeaded表示是否光头,则在construct中就可以通过判断该函数返回结果选择是否建造头发的组件,通过具体的产品来覆盖函数就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DevilBuilder extends ActorBuilder // 恶魔没有头发
{
@Override
public boolean isBareheaded()
{
return true;
}
}

class ActorController
{
public Actor construct(ActorBuilder ab)
{
// ...

//通过钩子方法来控制产品的构建
if(!ab.isBareheaded())
{
ab. buildHairstyle();
}
// ...
}
}

案例

Sunny软件公司欲开发一个视频播放软件,为了给用户使用提供方便,该播放软件提供多种界面显示模式,如完整模式、精简模式、记忆模式、网络模式等。在不同的显示模式下主界面的组成元素有所差异,如在完整模式下将显示菜单、播放列表、主窗口、控制条等,在精简模式下只显示主窗口和控制条,而在记忆模式下将显示主窗口、控制条、收藏列表等。尝试使用建造者模式设计该软件。

Java实例

分析

优点:

  1. 客户端不必知道产品内部组成的细节,将产品本身与产品创建的过程解耦,使得相同的创建过程可以创建不同的产品对象

!原型模式(Prototype)

使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。

组成部分

在原型模式结构图中包含如下几个角色:

●Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。

● ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。

● Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。

Java实例

案例

分析

职责链模式(Chain Of Responsibility)

避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。

组成部分

在职责链模式结构图中包含如下几个角色:

● Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。

● ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。

高级应用

纯与不纯的职责链

一个纯的职责链模式要求一个具体处理者对象只能在两个行为中选择一个:要么承担全部责任,要么将责任推给下家,不允许出现某一个具体处理者对象在承担了一部分或全部责任后又将责任向下传递的情况。而且在纯的职责链模式中,要求一个请求必须被某一个处理者对象所接收,不能出现某个请求未被任何一个处理者对象处理的情况。在前面的采购单审批实例中应用的是纯的职责链模式。

在一个不纯的职责链模式中允许某个请求被一个具体处理者部分处理后再向下传递,或者一个具体处理者处理完某请求后其后继处理者可以继续处理该请求,而且一个请求可以最终不被任何处理者对象所接收。

Java AWT 1.0中的事件处理模型应用的是不纯的职责链模式,其基本原理如下:由于窗口组件(如按钮、文本框等)一般都位于容器组件中,因此当事件发生在某一个组件上时,先通过组件对象的handleEvent()方法将事件传递给相应的事件处理方法,该事件处理方法将处理此事件,然后决定是否将该事件向上一级容器组件传播;上级容器组件在接到事件之后可以继续处理此事件并决定是否继续向上级容器组件传播,如此反复,直到事件到达顶层容器组件为止;如果一直传到最顶层容器仍没有处理方法,则该事件不予处理。

Java实例

案例

Sunny软件公司的OA系统需要提供一个假条审批模块:如果员工请假天数小于3天,主任可以审批该假条;如果员工请假天数大于等于3天,小于10天,经理可以审批;如果员工请假天数大于等于10天,小于30天,总经理可以审批;如果超过30天,总经理也不能审批,提示相应的拒绝信息。试用职责链模式设计该假条审批模块。

流水线产品质量检测

分析

优点:

  1. 一个对象无需知道其他对象如何处理请求,接收者和发送者都没有对方的明确信息,且链中的对象不需要知道链的结构,由客户端负责链的创建,降低系统耦合度(只做份内事)
  2. 将职责分离封装起来,达到单一职责,代码复用性高
  3. 新增处理逻辑的时候,只需要客户端调整链的结构,增加新的处理者就可以了,符合开闭原则

缺点:

  1. 一个请求没有明确的接收者,那么不保证一定会被处理,也可能因为链没有被正确配置而得不到处理
  2. 职责链长的时候,可能设计多个处理对象,对性能有所影响
  3. 建链不当会导致死循环

适用场景:

  1. 有多个对象可以处理同一个请求,具体哪个对象处理该请求待运行时刻再确定,客户端只需将请求提交到链上,而无须关心请求的处理对象是谁以及它是如何处理的。
  2. 可动态指定一组对象处理请求,客户端可以动态创建职责链来处理请求,还可以改变链中处理者之间的先后次序。

适配器模式(Adapter)

适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无须修改原来的适配者接口和抽象目标类接口。

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

组成部分

在对象适配器模式结构图中包含如下几个角色:

● Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。

● Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。

● Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。

高级应用

  1. 类适配器

    适配器与适配者之间是继承(实现)关系

    由于Java语言并不支持多继承,且如果适配者类是Final类,那么就无法使用类适配器,所以在Java中大多数情况还是使用的对象适配器

  2. 对象适配器(使用更多)

    适配器与适配者之间是关联关系

双向适配器,即适配器中组合了目标类以及适配者类,目标类可以通过适配器间接调用适配者类,适配者类也可以通过适配器间接调用目标类

缺省适配器

是适配器模式的一种变体,应用比较广泛

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

在缺省适配器模式中,包含如下三个角色:

● ServiceInterface(适配者接口):它是一个接口,通常在该接口中声明了大量的方法。

● AbstractServiceClass(缺省适配器类):它是缺省适配器模式的核心类,使用空方法的形式实现了在ServiceInterface接口中声明的方法。通常将它定义为抽象类,因为对它进行实例化没有任何意义。

● ConcreteServiceClass(具体业务类):它是缺省适配器类的子类,在没有引入适配器之前,它需要实现适配者接口,因此需要实现在适配者接口中定义的所有方法,而对于一些无须使用的方法也不得不提供空实现。在有了缺省适配器之后,可以直接继承该适配器类,根据需要有选择性地覆盖在适配器类中定义的方法。

Java实例

在JDK类库的事件处理包java.awt.event中广泛使用了缺省适配器模式,如WindowAdapter、KeyAdapter、MouseAdapter等。下面我们以处理窗口事件为例来进行说明:在Java语言中,一般我们可以使用两种方式来实现窗口事件处理类,一种是通过实现WindowListener接口,另一种是通过继承WindowAdapter适配器类。如果是使用第一种方式,直接实现WindowListener接口,事件处理类需要实现在该接口中定义的七个方法,而对于大部分需求可能只需要实现一两个方法,其他方法都无须实现,但由于语言特性我们不得不为其他方法也提供一个简单的实现(通常是空实现),这给使用带来了麻烦。而使用缺省适配器模式就可以很好地解决这一问题,在JDK中提供了一个适配器类WindowAdapter来实现WindowListener接口,该适配器类为接口中的每一个方法都提供了一个空实现,此时事件处理类可以继承WindowAdapter类,而无须再为接口中的每个方法都提供实现。

案例

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

分析

优点:

  1. 将目标类和适配者类解耦,通过引入一个适配器来重用现有的适配者类,无需修改原有结构
  2. 增加类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的

缺点:

对于类适配器

  1. 对于Java等不支持多继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者
  2. 适配者类不能为最终类,如在Java中不能为final类

对于对象适配器

  1. 想要置换适配者类中的方法比较麻烦

适用场景

  1. 系统需要适用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码
  2. 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作

!外观/门面模式(Facade)

外观模式是一种使用频率非常高的结构型设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。

为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

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

组成部分

外观模式包含如下两个角色:

(1) Facade(外观角色):在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。

(2) SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。

Java实例

案例

分析

代理模式(Proxy)

为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,这样做的好处就是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

需要定义接口或者父类,被代理对象和代理对象一起实现相同的接口或者继承相同的父类

组成部分

Subject:接口

RealSubject:被代理对象,实现Subject

ProxySubject:代理对象(实际使用的对象),实现Subject,内部new了一个RealSubject

案例

老师不上课,通过代理老师上课

通过代理的请求方法可以进行功能性的增强,增加日志打印等功能

Java实例

分析

优点:在不修改目标对象功能的前提下,能通过代理对象对目标功能进行扩展

缺点:

  1. 因为代理对象需要与目标对象实现一样的接口,所以会有很多的代理类
  2. 一旦接口新增方法,目标对象与代理对象都需要维护

!迭代器模式(Iterator)

遥控器模式

在软件开发中,也存在大量类似电视机一样的类,它们可以存储多个成员对象(元素),这些类通常称为聚合类(Aggregate Classes),对应的对象称为聚合对象。为了更加方便地操作这些聚合对象,同时可以很灵活地为聚合对象增加不同的遍历方法,我们也需要类似电视机遥控器一样的角色,可以访问一个聚合对象中的元素但又不需要暴露它的内部结构。本章我们将要学习的迭代器模式将为聚合对象提供一个遥控器,通过引入迭代器,客户端无须了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式。

在软件开发中,我们经常需要使用聚合对象来存储一系列数据。聚合对象拥有两个职责:一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;而后者既是可变化的,又是可分离的。因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合“单一职责原则”的要求。

提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。

组成部分

在迭代器模式结构图中包含如下几个角色:

● Iterator(抽象迭代器):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的first()方法,用于访问下一个元素的next()方法,用于判断是否还有下一个元素的hasNext()方法,用于获取当前元素的currentItem()方法等,在具体迭代器中将实现这些方法。

● ConcreteIterator(具体迭代器):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。

● Aggregate(抽象聚合类):它用于存储和管理元素对象,声明一个createIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。

● ConcreteAggregate(具体聚合类):它实现了在抽象聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator实例。

装饰者模式(Decorator)

动态的将新功能附加到对象上,在对象功能扩展方面,比继承更有弹性,体现了开闭原则(ocp)

装饰者包含被装饰者

组成部分

Component:抽象类或接口,如果不需要提供公共功能那么使用接口也可以

ConcreteComponent:待装饰的对象,实现Component

Decorator:抽象装饰器,继承Component

ConcreteDecorator:具体装饰器,实现Decorator,用于装饰对象

案例

星巴克咖啡订单项目,计算不同种咖啡的费用:客户可以单点咖啡,也可以组合调料

Java实例

分析

组合模式(Composite)

组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。

组成部分

在组合模式结构图中包含如下几个角色:

● Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。

● Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。

● Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。

高级应用

在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式安全组合模式两种形式

  1. 透明组合模式

    抽象构件Component声明了所有用于管理成员对象的方法,包括add、remove以及getChild等方法,这样做的好处就是确保所有的构件类都有相同的接口,在客户端看来是一致的。

    1636944165015

    缺点就是不够安全,因为实际上叶子是不可能有下一层级的对象的,也就是没有add、remove和getChild等方法的,但是客户端却是可以调用这些方法的。

  2. 安全组合模式

    安全组合模式中,抽象构件Component中没有声明任何用于管理成员对象的方法,而是在具体的容器类Composite中声明。这种做法是安全的,因为根本根本不向叶子对象提供这些管理成员对象的方法。

    1636944385532

    缺点就是不够透明,因为叶子和容器构件有不同的方法,且容器构件中的管理成员对象的方法没有在抽象构件中定义,这样的客户端就必须区别对待子类,不能完全针对抽象编程。

Java实例

在Java AWT中使用的组合模式就是安全组合模式。

1636944534124

在XML解析、组织结构树处理、文件系统设计等领域,组合模式都得到了广泛应用。

案例

软件菜单,一个菜单项可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单很恰当。

1636895914655

Sunny软件公司欲开发一个界面控件库,界面控件分为两大类,一类是单元控件,例如按钮、文本框等,一类是容器控件,例如窗体、中间面板等,试用组合模式设计该界面控件库。

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
final Widget panel = new Panel("主面板");
final Widget button = new Button("按钮1");
final Widget textera = new Textera("文本框1");
panel.add(button);
panel.add(textera);
panel.printName();
}
}

分析

优点:

  1. 可以清楚的定义分层次的复杂对象,表示对象的全部或者部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  2. 客户端可以一致的使用一个组合结构或者其中单个对象,不必关心处理的是单个对象还是整个组合结构
  3. 增加新的容器构件和叶子构件很方便,无须对现有的类进行修改,符合开闭
  4. 为树形结构的面向对象提供了一种灵活的解决方法,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

缺点:

  1. 增加新构件时很难对容器中的构件类型进行限制,不能依赖类型系统来施加这些约束,因为都来自相同的抽象层,这个时候必须通过类型检查来实现了,过程比较复杂。

适用场景:

  1. 需要处理一个树形结构
  2. 系统中能够分离叶子和容器对象,而且它们的类型不固定,需要增加新类型
  3. 在具有整体和部分的层次结构中,客户端希望一致对待

观察者模式(Observer)

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变的时候,所有依赖它的对象都得到通过并被自动更新

组成部分

案例

Java实例

分析

命令模式(Command)

请求发送者与接收者解耦

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

组成部分

在命令模式结构图中包含如下几个角色:

● Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。

● ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。

● Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。

● Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。

案例

顾客点餐后服务员拿到订单放到订单柜台,厨师收到后开始准备餐点

服务员:就是调用者角色,由他来发起命令

厨师:就是接收者角色,真正命令执行的对象

订单:命令中包含订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Client {
public static void main(String[] args) {
// 创建第一个订单对象
Order order1 = new Order();
order1.setDiningTable(1);
order1.putFood("西红柿炒蛋", 1);
order1.putFood("可乐", 2);

// 创建第二个订单对象
Order order2 = new Order();
order2.setDiningTable(2);
order2.putFood("牛肉滑蛋", 1);
order2.putFood("芬达", 1);

// 创建厨师(接收者)
Chef chef = new Chef();
// 创建命令对象
OrderCommand command1 = new OrderCommand(chef, order1);
OrderCommand command2 = new OrderCommand(chef, order2);

// 创建服务员(调用者)
Waiter waiter = new Waiter();
waiter.putCommand(command1);
waiter.putCommand(command2);

// 让服务员发起命令
waiter.orderUp();

}
}

Java实例

JDK中的Runnable就是使用的命令模式

Runnable担当命令角色,Thread充当调用者,start方法就是其执行方法

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
// 命令接口
interface Runnable {
public abstract void run();
}

// 具体命令类
class ConcreteRunnable implements Runnable {
// 聚合接收者
private Receiver receiver;

@Override
public void run() {
recevier.do();
}
}

// 调用者
class Thread {
// 聚合命令
private Runnable runnable;
// 通过构造传入

void start() {
runnable.run();
}
}

分析

优点:

  1. 降低系统的耦合度,命令模式将调用操作的对象与实现该操作的对象解耦
  2. 增加或者删除命令非常方便,采用命令模式增加与删除命令不影响其他类,符合开闭
  3. 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令
  4. 方便实现UndoRedo操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复

缺点:

  1. 命令模式可能会导致系统有过多的具体命令类
  2. 系统结构更加复杂

适用场景

  1. 系统需要将调用者和接收者解耦,使得调用者和接收者不直接交互
  2. 系统不要在不同的时间指定请求,将请求排队和执行请求
  3. 系统需要支持命令的撤销和恢复的操作

策略模式(Strategy)

电影票打折方案

Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

(1) 学生凭学生证可享受票价8折优惠;

(2) 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);

(3) 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。

该系统在将来可能还要根据需要引入新的打折方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public double calculate() {  
//学生票折后票价计算
if(this.type.equalsIgnoreCase("student")) {
System.out.println("学生票:");
return this.price * 0.8;
}
//儿童票折后票价计算
else if(this.type.equalsIgnoreCase("children") && this.price >= 20 ) {
System.out.println("儿童票:");
return this.price - 10;
}
//VIP票折后票价计算
else if(this.type.equalsIgnoreCase("vip")) {
System.out.println("VIP票:");
System.out.println("增加积分!");
return this.price * 0.5;
}
else {
return this.price; //如果不满足任何打折要求,则返回原始票价
}
}

如果不采用策略模式,那么大概是以if-else的形式进行分支判断决定打折的方式。

这不是一个完美的方案,存在至少三个问题:

  1. calculate方法过于臃肿,大量的if-else语句不利于测试和维护
  2. 增加新的打折算法需要修改这个类的源代码,违反了开闭原则,系统的灵活性和可扩展性较差
  3. 算法的复用性差,如果在另一个系统中打算重用某些打折算法,只能通过复制粘贴来重用,无法单独重用其中的计算算法

策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类当中,每个策略类封装实现一种算法,使用算法的类针对抽象类进行编程(使用抽象类接收符合依赖倒置原则)。

策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。

组成部分

1640074545774

在策略模式结构图中包含如下几个角色:

● Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。

● Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。

● ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

Java实例

  1. JavaSE的容器布局管理就是策略模式的一个经典应用实例

    在JavaSE开发中,用户需要对容器对象Container中的成员对象如按钮、文本框等GUI控件进行布局(Layout),在程序运行期间由客户端动态决定一个Container对象如何布局,Java在JDK中提供了几种不同的布局方式,封装在不同的类当中,如BorderLayout、FlowLayout、GridLayout等等。Container类充当Context环境的角色,而LayoutManager作为所有布局类的公共父类扮演抽象策略角色,具体的策略就是LayoutManager的子类了。

  2. 在微软公司提供的延时项目PetShop4.0中就使用策略模式来处理同步订单和异步订单的问题

    1640075834426

案例

分析

优点:

  1. 提供了对开闭原则的完美支持
  2. 提供了管理相关算法族的办法,策略类的等级结构定义了一个算法或行为族,使用继承可以把公共代码移到抽象策略类中,从而避免重复代码。
  3. 策略模式可以避免多重条件选择语句
  4. 提供了一种算法复用的机制

缺点:

  1. 客户端必须知道所有的策略类,并自行决定使用哪个策略类,这意味着客户端必须理解这些算法的区别。换言之就是策略模式只适用于客户端知道所有的算法或行为的情况下。
  2. 策略模式将造成系统产生很多具体的策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。

模板方法模式(Template Method)

模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

是一种基于继承的代码复用技术,是一种行为型模式。

组成部分

1645941310984

模板方法模式包含如下两个角色:

  1. AbstractClass(抽象类):在抽象类中定义了一系列基本操作(PrimitiveOperations),这些基本操作可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或实现这些步骤。同时,在抽象类中实现了一个模板方法(Template Method),用于定义一个算法的框架,模板方法不仅可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其他对象中的方法。

  2. ConcreteClass(具体子类):它是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。

Java实例

案例

分析

优点:

  1. 父类中形式化定义一个算法,并由子类来实现细节,子类不会改变算法中的步骤的执行次序
  2. 是一种代码复用技术,鼓励我们恰当使用继承来实现代码复用
  3. 通过子类来覆盖父类基本方法,不同子类不同实现,符合单一职责和开闭原则

缺点:

  1. 需要为每一个基本方法的不同实现提供一个子类,将会导致类个数增加,系统更庞大

实战

面试题

设计模式是什么

从字面上理解,模,就是模型模板的意思,式,就是方式方法的意思,综合起来,所谓的模式就是可以作为模板的方法,设计模式就是设计方面的模板。

设计模式:是指在软件开发中,经过验证的,用于解决在特定环境下、重复出现的、特定问题的解决方案。

代理模式和装饰者模式的区别

代理模式关注点在于控制对对象的访问,客户端直接使用具体的代理类来做到想做的事(无需传入参数),隐藏了一个对象的具体信息

而装饰者模式关注点在于在一个对象上动态的添加方法(增强),客户端在使用的时候需要传入待装饰的对象,而返回的则是装饰后(增强后)的对象