肾莫是设计模式

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

软件模式是将模式的一般概念应用于软件开发领域,即软件开发的总体指导思路或参照样板。软件模式除了设计模式,还包括架构模式、分析模式和过程模式等。

分类

  • 创建型模式

对象实例化的模式,创建型模式用于解耦对象的实例化过程。

  • 结构型模式

把类或对象结合在一起形成一个更大的结构。

  • 行为型模式

类和对象如何交互,及划分责任和算法。以及对象之间的通信问题。

创建型模式

单例模式

确保某一个类只有一个实例,并且提供一个全局访问点。

场景:一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。

实现

  • 饿汉式
1
2
3
4
5
6
7
public class Singleton {  
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

比较常用。它基于classloader 机制避免了多线程的同步问题,没有加锁,效率高。但类加载时就初始化,浪费内存。

  • 双重校验锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

采用双锁机制,安全且在多线程情况下能保持高性能。

  • 登记式/静态内部类
1
2
3
4
5
6
7
8
9
public class Singleton {  
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。

优点

  • 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

工厂模式

工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个,也就是说工厂方法模式让实例化推迟到子类。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public interface Shape {
void draw();
}

public class Rectangle implements Shape {

@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}

public class Circle implements Shape {

@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}

public class ShapeFactory {
public Shape getShape(String shapeType){
if(shapeType == null){
return null;
}
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
} else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
}
return null;
}
}

抽象工厂模式

提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。

通俗地讲,就是围绕一个抽象工厂创建具体工厂。这样创建的产品可以使多个维度的。这里以创建“形状”与“颜色”两个维度举例:

多个产品的接口及其实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Shape接口、Rectangle类、Circle类同上

// 颜色接口及其实现类
public interface Color {
void fill();
}

public class Red implements Color {

@Override
public void fill() {
System.out.println("Inside Red::fill() method.");
}
}

public class Green implements Color {

@Override
public void fill() {
System.out.println("Inside Green::fill() method.");
}
}

提到的抽象工厂,只要负责定义多个维度的产品的创建接口:

1
2
3
4
public abstract class AbstractFactory {
public abstract Color getColor(String color);
public abstract Shape getShape(String shape) ;
}

抽象工厂的具体实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ShapeFactory 同上

public class ColorFactory extends AbstractFactory {

@Override
public Shape getShape(String shapeType){
return null;
}

@Override
public Color getColor(String color) {
if(color == null){
return null;
}
if(color.equalsIgnoreCase("RED")){
return new Red();
} else if(color.equalsIgnoreCase("BLUE")){
return new Blue();
}
return null;
}
}

创建一个工厂创造器/生成器类,通过传递形状或颜色信息来获取工厂:

1
2
3
4
5
6
7
8
9
10
public class FactoryProducer {
public static AbstractFactory getFactory(String choice){
if(choice.equalsIgnoreCase("SHAPE")){
return new ShapeFactory();
} else if(choice.equalsIgnoreCase("COLOR")){
return new ColorFactory();
}
return null;
}
}

使用:

1
2
3
4
5
//获取形状工厂
AbstractFactory shapeFactory = FactoryProducer.getFactory("SHAPE");

//获取形状为 Circle 的对象
Shape shape1 = shapeFactory.getShape("CIRCLE");

缺点

  • 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。
  • 开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)。

建造者模式

使用多个简单的对象一步一步构建成一个复杂的对象。将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。

StringBuilder就采用本模式实现。

本模式包含如下角色:

  • Builder:抽象建造者
  • ConcreteBuilder:具体建造者,创建和提供实例
  • Director:指挥者,管理建造出来的实例的依赖关系
  • Product:产品角色

实现

建造者:

1
2
3
4
5
public abstract class Builder {
public abstract void buildPart1();
public abstract void buildPart2();
public abstract void buildPart3();
}

具体的建造者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ConcreteBuilder1 extends Builder {
private StringBuffer buffer = new StringBuffer();//假设 buffer.toString() 就是最终生成的产品

@Override
public void buildPart1() {//实现构建最终实例需要的所有方法
buffer.append("Builder1 : Part1\n");
}

@Override
public void buildPart2() {
buffer.append("Builder1 : Part2\n");
}

@Override
public void buildPart3() {
buffer.append("Builder1 : Part3\n");
}

public String getResult() {//定义获取最终生成实例的方法
return buffer.toString();
}
}

指挥者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Director {    // 将一个复杂的构建过程与其表示相分离
private Builder builder; // 针对接口编程,而不是针对实现编程
public Director(Builder builder) {
this.builder = builder;
}
public void setBuilder(Builder builder) {
this.builder = builder;
}

public void construct() { // 控制(定义)一个复杂的构建过程
builder.buildPart1();
for (int i = 0; i < 5; i++) { // 提示:如果想在运行过程中替换构建算法,可以考虑结合策略模式。
builder.buildPart2();
}
builder.buildPart3();
}
}

缺点

  • 如果产品之间的差异性很大,则不适合使用建造者模式。
  • 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。

与抽象工厂模式对比

  • 在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在建造者模式中,客户端可以不直接调用建造者的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象。
  • 如果将抽象工厂模式看成 汽车配件生产工厂 ,生产一个产品族的产品,那么建造者模式就是一个 汽车组装工厂 ,通过对部件的组装可以返回一辆完整的汽车。

结构型模式

适配器模式(Adapter)

将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

我们可能需要将两个不同接口的类来进行通信,在不修改这两个的前提下,我们可能会需要某个中间件来完成这个衔接的过程。这个中间件就是适配器。所谓适配器模式就是将一个类的接口,转换成客户期望的另一个接口。

实现

例:电脑只有读SD卡的接口,现在要读TF卡:

  1. 先创建一个SD卡与TF卡的接口与其实现类:
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
/* -- SD card -- */
public interface SDCard {
String readSD();
int writeSD(String msg);
}

public class SDCardImpl implements SDCard {
@Override
public String readSD() {
String msg = "sdcard read a msg :hello word SD";
return msg;
}
@Override
public int writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
return 1;
}
}
/* -- TF card -- */
public interface TFCard {
String readTF();
int writeTF(String msg);
}

public class TFCardImpl implements TFCard {
@Override
public String readTF() {
String msg ="tf card reade msg : hello word tf card";
return msg;
}
@Override
public int writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
return 1;
}
}

创建计算机接口与计算机实例,计算机提供读取SD卡方法:

1
2
3
4
5
6
7
8
9
10
11
12
public interface Computer {    
String readSD(SDCard sdCard);
}

public class ThinkpadComputer implements Computer {
@Override
public String readSD(SDCard sdCard) {
if(sdCard == null)
throw new NullPointerException("sd card null");
return sdCard.readSD();
}
}

此时我们希望在不改变计算机读取SD卡接口的情况下,通过适配器模式读取TF卡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    /* -- 创建SD适配TF (也可以说是SD兼容TF,相当于读卡器):  -- */
// 实现SDCard接口,并将要适配的对象作为适配器的属性引入。
public class SDAdapterTF implements SDCard {
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
@Override
public String readSD() {
System.out.println("adapter read tf card ");
return tfCard.readTF();
}
@Override
public int writeSD(String msg) {
System.out.println("adapter write tf card");
return tfCard.writeTF(msg);
}
}

这样就可以读取TF卡:

1
2
3
4
5
6
7
8
public class ComputerReadDemo {    
public static void main(String[] args) {
Computer computer = new ThinkpadComputer();
TFCard tfCard = new TFCardImpl();
SDCard tfCardAdapterSD = new SDAdapterTF(tfCard);
System.out.println(computer.readSD(tfCardAdapterSD));
}
}

组合模式(Composite )

将对象组合成树形结构以表示”部分-整体”的层次结构。

看代码很容易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.ArrayList;
import java.util.List;

public class Employee {
private String name;
private String dept;
private int salary;
private List<Employee> subordinates;

//构造函数
public Employee(String name,String dept, int sal) {
this.name = name;
this.dept = dept;
this.salary = sal;
subordinates = new ArrayList<Employee>();
}

public void add(Employee e) {
subordinates.add(e);
}
}

外观模式(Facade )

外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面

我们知道类与类之间的耦合越低,那么可复用性就越好,如果两个类不必彼此通信,那么就不要让这两个类发生直接的相互关系,如果需要调用里面的方法,可以通过第三者来转发调用。外观模式非常好的诠释了这段话。外观模式提供了一个统一的接口,用来访问子系统中的一群接口,实现了客户与子系统之间的松耦合。

实现

电脑的外观:打开开关按钮,就可以开机(启动CPU,内存,硬盘…)

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/** * 电脑接口 */
public interface Computer {
void open();
}

/** * CPU类 */
class Cpu implements Computer {
@Override
public void open() {
System.out.println("启动CPU");
}
}

/** * 内存类 */
class Ddr implements Computer {
@Override
public void open() {
System.out.println("启动内存");
}
}

/** * 硬盘类 */
class Ssd implements Computer {
@Override
public void open() {
System.out.println("启动硬盘");
}
}
/** * 外观类 */
public class Facade {
private Computer cpu;
private Computer ddr;
private Computer ssd;

public Facade() {
cpu = new Cpu();
ddr = new Ddr();
ssd = new Ssd();
}
/* 启动电脑 */
public void onComputer() {
cpu.open();
ddr.open();
ssd.open();
}
}

public class FacadeTest {
public static void main(String[] args) {
Facade facade = new Facade();
facade.onComputer();
}
}

代理模式(Proxy )

创建具有现有对象的对象,以便向外界提供功能接口。

某些情况下,一个客户不想或不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。

结构

  • Subject: 抽象主题角色
  • Proxy: 代理主题角色
  • RealSubject: 真实主题角色

实现

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
39
40
41
42
public interface Image {
void display();
}

/* RealSubject */
public class RealImage implements Image {

private String fileName;

public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk(fileName);
}

@Override
public void display() {
System.out.println("Displaying " + fileName);
}

private void loadFromDisk(String fileName) {
System.out.println("Loading " + fileName);
}
}

/* Proxy */
public class ProxyImage implements Image {

private RealImage realImage;
private String fileName;

public ProxyImage(String fileName) {
this.fileName = fileName;
}

@Override
public void display() {
if(realImage == null){
realImage = new RealImage(fileName);
}
realImage.display();
}
}

使用:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Image image = new ProxyImage("test_10mb.jpg");

// 图像将从磁盘加载
image.display();
System.out.println("");
// 图像不需要从磁盘加载
image.display();
}

适用环境

根据代理模式的使用目的,常见的代理模式有以下几种类型:

  • 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个本地 的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在 另一台主机中,远程代理又叫做大使(Ambassador)。
  • 虚拟(Virtual)代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
  • 图片代理:一个很常见的代理模式的应用实例就是对大图浏览的控制。用户通过浏览器访问网页时先在代理对象的方法中,使用一个线程向客户端浏览器加载一个小图片,然后在后台使用另一个线程来调用大图加载方法将大图片加载到客户端。当需要浏览大图片时,再将大图片在新网页中显示。如果此时加载工作还没完成,可以再启动一个线程来显示相应的提示信息。通过代理技术结合多线程编程将真实图片的加载放到后台来操作,不影响前台图片的浏览。
  • 动态代理:在事先不知道真实主题角色的情况下使用代理主题角色。

对于动态代理,JDK有自带的包:

1
2
java.lang.reflect.Proxy: 生成动态代理类和对象;
java.lang.reflect.InvocationHandler(处理器接口):可以通过invoke方法实现

行为型模式

职责链模式(Chain of Responsibility)

它将对象组成一条链,发送者将请求发给链的第一个接收者,并且沿着这条链传递,直到有一个对象来处理它或者直到最后也没有对象处理而留在链末尾端。

实现

消息记录器,判断消息等级小于自己的等级就打印消息,并传递到下一个链节点。

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
public abstract class AbstractLogger {
public static int INFO = 1;
public static int DEBUG = 2;
public static int ERROR = 3;

protected int level;

//责任链中的下一个元素
protected AbstractLogger nextLogger;

public void setNextLogger(AbstractLogger nextLogger){
this.nextLogger = nextLogger;
}

public void logMessage(int level, String message){
if(this.level <= level){
write(message);
}
if(nextLogger !=null){
nextLogger.logMessage(level, message);
}
}

abstract protected void write(String message);

}

实体类:将ConsoleLogger->FileLogger->ErrorLogger串联

1
2
3
4
5
6
7
8
9
10
11
12
public class ConsoleLogger extends AbstractLogger{

public ConsoleLogger() {
this.level = Level.INFO;
//指定nextLogger为FileLogger
setNextLogger(new FileLogger());
}
@Override
public void log(String message) {
System.out.println("Console logger: " + message);
}
}
1
2
3
4
5
6
7
8
9
10
11
public class FileLogger extends AbstractLogger{

public FileLogger() {
this.level = Level.DEBUG;
setNextLogger(new ErrorLogger());
}
@Override
public void log(String message) {
System.out.println("File logger: " + message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ErrorLogger extends AbstractLogger {
public ErrorLogger() {
this.level = Level.ERROR;
//nextLogger设置为null
//扩展时将此更改为新的Logger
setNextLogger(null);
}

@Override
public void log(String message) {
System.out.println("Error logger: " + message);
}
}

测试:

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
public class ChainPatternDemo {
public static void main(String[] args) {

/**
* 暴露最低等级既可
*/
AbstractLogger consoleLogger = new ConsoleLogger();

consoleLogger.logMegger(3, "错误信息");
System.out.println();
consoleLogger.logMegger(2, "测试信息");
System.out.println();
consoleLogger.logMegger(1, "控制台信息");
}
}
/*
Console logger: 错误信息
File logger: 错误信息
Error logger: 错误信息

Console logger: 测试信息
File logger: 测试信息

Console logger: 控制台信息
*/

优缺点

  • 在职责链模式中,使得每一个对象都有可能来处理请求,从而实现了请求的发送者和接收者之间的解耦。
  • 同时职责链模式简化了对象的结构,它使得每个对象都只需要引用它的后继者即可,而不必了解整条链,这样既提高了系统的灵活性也使得增加新的请求处理类也比较方便。
  • 但是在职责链中我们不能保证所有的请求都能够被处理,而且不利于观察运行时特征。

命令模式(Command)

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

一个编辑器的例子:

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
public class TextEditor {
private StringBuilder buffer = new StringBuilder();

public void copy() {
...
}

public void paste() {
String text = getFromClipBoard();
add(text);
}

public void add(String s) {
buffer.append(s);
}

public void delete() {
if (buffer.length() > 0) {
buffer.deleteCharAt(buffer.length() - 1);
}
}

public String getState() {
return buffer.toString();
}
}

我们可以用一个StringBuilder模拟一个文本编辑器,它支持copy()、paste()、add()、delete()等方法。

直接调用方法,调用方需要了解TextEditor的所有接口信息:

1
2
3
4
5
TextEditor editor = new TextEditor();
editor.add("Command pattern in text editor.\n");
editor.copy();
editor.paste();
System.out.println(editor.getState());

命令需要把调用方发送命令和执行方执行命令分开。引入一个Command接口:

1
2
3
public interface Command {
void execute();
}

调用方创建一个对应的Command,然后执行,并不关心内部是如何具体执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CopyCommand implements Command {
// 持有执行者对象:
private TextEditor receiver;

public CopyCommand(TextEditor receiver) {
this.receiver = receiver;
}

public void execute() {
receiver.copy();
}
}

public class PasteCommand implements Command {
private TextEditor receiver;

public PasteCommand(TextEditor receiver) {
this.receiver = receiver;
}

public void execute() {
receiver.paste();
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
TextEditor editor = new TextEditor();
editor.add("Command pattern in text editor.\n");
// 执行一个CopyCommand:
Command copy = new CopyCommand(editor);
copy.execute();
editor.add("----\n");
// 执行一个PasteCommand:
Command paste = new PasteCommand(editor);
paste.execute();
System.out.println(editor.getState());

本例的需求很简单,那么直接调用显然更直观而且更简单。但如果TextEditor复杂到一定程度,例如需要支持Undo、Redo的功能时,就需要使用命令模式,因为我们可以给每个命令增加undo():

1
2
3
4
public interface Command {
void execute();
void undo();
}

把执行的一系列命令用List保存起来,就既能支持Undo,又能支持Redo。这个时候,我们又需要一个Invoker对象,负责执行命令并保存历史命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────┐
│ Client │
└─────────────┘




┌─────────────┐
│ Invoker │
├─────────────┤ ┌───────┐
│List commands│─ ─>│Command│
│invoke(c) │ └───────┘
│undo() │ │ ┌──────────────┐
└─────────────┘ ├─>│ CopyCommand │
│ ├──────────────┤
│ │editor.copy() │─ ┐
│ └──────────────┘
│ │ ┌────────────┐
│ ┌──────────────┐ ─>│ TextEditor │
└─>│ PasteCommand │ │ └────────────┘
├──────────────┤
│editor.paste()│─ ┘
└──────────────┘

解释器模式

给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

例如,使用正则表达式来匹配字符串时,正则表达式本身就是一个字符串,但要把正则表达式解析为语法树,然后再匹配指定的字符串,就需要一个解释器。

实现一个完整的正则表达式的解释器非常复杂,但是使用解释器模式却很简单:

1
2
String s = "+861012345678";
System.out.println(s.matches("^\\+\\d+$"));

类似的,当我们使用JDBC时,执行的SQL语句虽然是字符串,但最终需要数据库服务器的SQL解释器来把SQL“翻译”成数据库服务器能执行的代码,这个执行引擎也非常复杂,但对于使用者来说,仅仅需要写出SQL字符串即可。

迭代器模式(Iterator)

提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

实现一个自定义的集合,通过Iterator模式实现倒序遍历:

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
public class ReverseArrayCollection<T> implements Iterable<T> {
private T[] array;

public ReverseArrayCollection(T... objs) {
this.array = Arrays.copyOfRange(objs, 0, objs.length);
}

public Iterator<T> iterator() {
return new ReverseIterator();
}

class ReverseIterator implements Iterator<T> {
// 索引位置:
int index;

public ReverseIterator() {
// 创建Iterator时,索引在数组末尾:
this.index = ReverseArrayCollection.this.array.length;
}

public boolean hasNext() {
// 如果索引大于0,那么可以移动到下一个元素(倒序往前移动):
return index > 0;
}

public T next() {
// 将索引移动到下一个元素并返回(倒序往前移动):
index--;
return array[index];
}
}
}

内部类隐含地持有一个它所在对象的this引用,可以通过ReverseArrayCollection.this引用到它所在的集合。

观察者模式(Observer)

一种通知机制,让发送通知的一方(被观察方)和接收通知的一方(观察者)能彼此分离,互不影响。

例如,一个商品网站,当发生商品变动时,需要通知管理员与消费者,就可以抽象出一个ProductObserver接口,任何人想要观察Store,只要实现该接口,并且把自己注册到Store即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Store {
private List<ProductObserver> observers = new ArrayList<>();
private Map<String, Product> products = new HashMap<>();

// 注册观察者:
public void addObserver(ProductObserver observer) {
this.observers.add(observer);
}

// 取消注册:
public void removeObserver(ProductObserver observer) {
this.observers.remove(observer);
}

public void addNewProduct(String name, double price) {
Product p = new Product(name, price);
products.put(p.getName(), p);
// 通知观察者:
observers.forEach(o -> o.onPublished(p));
}

public void setProductPrice(String name, double price) {
Product p = products.get(name);
p.setPrice(price);
// 通知观察者:
observers.forEach(o -> o.onPriceChanged(p));
}
}

然后,AdminConsumer就可以通过实现该接口并注册即可,无需在函数中对于每个不同类的对象调用一次通知方法。

状态模式(State)

将不同状态的逻辑分离到不同的状态类中,从而使得增加新状态更容易;

例如,我们设计一个聊天机器人,它有两个状态:未连线;已连线。

对于未连线状态,我们收到消息也不回复:

1
2
3
4
5
6
7
8
9
public class DisconnectedState implements State {
public String init() {
return "Bye!";
}

public String reply(String input) {
return "";
}
}

对于已连线状态,我们回应收到的消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConnectedState implements State {
public String init() {
return "Hello, I'm Bob.";
}

public String reply(String input) {
if (input.endsWith("?")) {
return "Yes. " + input.substring(0, input.length() - 1) + "!";
}
if (input.endsWith(".")) {
return input.substring(0, input.length() - 1) + "!";
}
return input.substring(0, input.length() - 1) + "?";
}
}

状态模式的关键设计思想在于状态切换,我们引入一个BotContext完成状态切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BotContext {
private State state = new DisconnectedState();

public String chat(String input) {
if ("hello".equalsIgnoreCase(input)) {
// 收到hello切换到在线状态:
state = new ConnectedState();
return state.init();
} else if ("bye".equalsIgnoreCase(input)) {
// 收到bye切换到离线状态:
state = new DisconnectedState();
return state.init();
}
return state.reply(input);
}
}

效果:

1
2
3
4
5
6
7
8
> hello
< Hello, I'm Bob.
> Nice to meet you.
< Nice to meet you!
> Today is cold?
< Yes. Today is cold!
> bye
< Bye!

策略模式(Strategy)

核心思想是在一个计算方法中把容易变化的算法抽出来作为“策略”参数传进去,从而使得新增策略不必修改原有逻辑。

策略模式在Java标准库中应用非常广泛,例如,实现忽略大小写排序:

1
2
3
4
String[] array = { "apple", "Pear", "Banana", "orange" };
Arrays.sort(array, String::compareToIgnoreCase);
System.out.println(Arrays.toString(array));
// [apple, Banana, orange, Pear]

想倒序排序,就传入(s1, s2) -> -s1.compareTo(s2),这个比较两个元素大小的算法就是策略。

在 OO 课中,我们将电梯调度算法进行的封装也是策略模式思想的体现。或是商城的折扣算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DiscountContext {
// 持有某个策略:
private DiscountStrategy strategy = new UserDiscountStrategy();

// 允许客户端设置新策略:
public void setStrategy(DiscountStrategy strategy) {
this.strategy = strategy;
}

public BigDecimal calculatePrice(BigDecimal total) {
return total.subtract(this.strategy.getDiscount(total)).setScale(2);
}
}
/* --- 调用 --- */
DiscountContext ctx = new DiscountContext();

// 默认使用普通会员折扣:
BigDecimal pay1 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay1);

// 使用满减折扣:
ctx.setStrategy(new OverDiscountStrategy());
BigDecimal pay2 = ctx.calculatePrice(BigDecimal.valueOf(105));
System.out.println(pay2);

模板方法模式(Template Method)

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

例:实现一个支持数据库查询数据、对数据进行缓存的类。如何缓存没想好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractSetting {
public final String getSetting(String key) {
String value = lookupCache(key);
if (value == null) {
value = readFromDatabase(key);
putIntoCache(key, value);
}
return value;
}

protected abstract String lookupCache(String key);

protected abstract void putIntoCache(String key, String value);
}

假设我们希望用一个Map做缓存,那么可以写一个LocalSetting:

1
2
3
4
5
6
7
8
9
10
11
public class LocalSetting extends AbstractSetting {
private Map<String, String> cache = new HashMap<>();

protected String lookupCache(String key) {
return cache.get(key);
}

protected void putIntoCache(String key, String value) {
cache.put(key, value);
}
}

如果我们要使用Redis做缓存,那么可以再写一个RedisSetting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RedisSetting extends AbstractSetting {
private RedisClient client = RedisClient.create("redis://localhost:6379");

protected String lookupCache(String key) {
try (StatefulRedisConnection<String, String> connection = client.connect()) {
RedisCommands<String, String> commands = connection.sync();
return commands.get(key);
}
}

protected void putIntoCache(String key, String value) {
try (StatefulRedisConnection<String, String> connection = client.connect()) {
RedisCommands<String, String> commands = connection.sync();
commands.set(key, value);
}
}
}

注:Redis是一个数据库,其具备如下特性:

  • 基于内存运行,性能高效
  • 支持分布式,理论上可以无限扩展
  • key-value存储系统
  • 开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API