设计模式学习笔记

作者:Grey

原文地址: 设计模式学习笔记

UML和代码

UML图

代码

单例模式

单例模式是创建型模式。

单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是指进程内只允许创建一个对象,也就是说,单例模式创建的对象是进程唯一的(而非线程)

image

饿汉式

类加载的时候就会初始化这个实例, JVM保证唯一实例,线程安全, 但是可以通过反射破坏

方式一

1
2
3
4
5
6
7
8
9
10
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();

private Singleton1() {
}

public static Singleton1 getInstance() {
return INSTANCE;
}
}

方式二

1
2
3
4
5
6
7
8
9
10
11
public class Singleton2 {
private static final Singleton2 INSTANCE;

static {
INSTANCE = new Singleton2();
}

public static Singleton2 getInstance() {
return INSTANCE;
}
}

这种方式不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。

不过,如果初始化耗时长,那最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能,我们可以将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

可以通过如下反射方式破坏

1
2
3
4
Class<?> aClass=Class.forName("singleton.Singleton2",true,Thread.currentThread().getContextClassLoader());
Singleton2 instance1=(Singleton2)aClass.newInstance();
Singleton2 instance2=(Singleton2)aClass.newInstance();
System.out.println(instance1==instance2);

输出:false

懒汉式

虽然可以实现按需初始化,但是线程不安全, 因为在判断INSTANCE == null的时候,如果是多个线程操作的话, 一个线程还没有把INSTANCE初始化好,另外一个线程判断INSTANCE==null 得到true,就会继续初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton3 {
private static Singleton3 INSTANCE;

private Singleton3() {
}

public static Singleton3 getInstance() {
if (INSTANCE == null) {
// 模拟初始化对象需要的耗时操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}

为了防止线程不安全,可以在getInstance方法上加锁,这样既实现了按需初始化,又保证了线程安全,

但是加锁可能会导致一些性能的问题:我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton4 {
private static Singleton4 INSTANCE;

private Singleton4() {
}

public static synchronized Singleton4 getInstance() {
if (INSTANCE == null) {
// 模拟初始化对象需要的耗时操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}

为了提升一点点性能,可以不给getInstance整个方法加锁,而是对INSTANCE判空这段代码加锁, 但是又带来了线程不安全的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton5 {
private static Singleton5 INSTANCE;

private Singleton5() {
}

public static Singleton5 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton5.class) {
// 模拟初始化对象需要的耗时操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}

Double Check Locking模式,就是双加锁检查模式 这种方式中,Volatile是必需的,目的为了防止指令重排,生成一个半初始化的的实例,导致生成两个实例

具体可参考 双重检索(DCL)的思考: 为什么要加volatile?
说了这个问题

实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton6 {
private volatile static Singleton6 INSTANCE;

private Singleton6() {
}

public static Singleton6 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton6.class) {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}

以下两种更为优雅的方式,既保证了线程安全,又实现了按需加载

方式一:静态内部类方式,JVM保证单例,加载外部类时不会加载内部类,这样可以实现懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton7 {
private Singleton7() {
}

public static Singleton7 getInstance() {
return Holder.INSTANCE;
}

private static class Holder {
private static final Singleton7 INSTANCE = new Singleton7();
}

}

方式二: 使用枚举, 这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,这种方式是 Effective Java 作者
Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

1
2
3
public enum Singleton8 {
INSTANCE;
}

为什么要使用单例

  1. 处理资源访问冲突

    比如写日志的类,如果不使用单例,就必须使用锁机制来解决日志被覆盖的问题。

  2. 表示全局唯一类

    比如配置信息类,在系统中,只有一个配置文件,当配置文件加载到内存中,以对象形式存在,也理所应当只有一份。

    唯一ID生成器也是类似的机制。如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。

单例模式的替代方案

使用静态方法

1
2
3
4
5
6
7
8
9
10
// 静态方法实现方式
public class IdGenerator {
private static AtomicLong id = new AtomicLong(0);

public static long getId() {
return id.incrementAndGet();
}
}
// 使用举例
long id = IdGenerator.getId();

使用依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 1. 老的使用方式
public demofunction() {
//...
long id = IdGenerator.getInstance().getId();
//...
}

// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
long id = idGenerator.getId();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

线程单例

通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class IdGenerator {
private AtomicLong id = new AtomicLong(0);

private static final ConcurrentHashMap<Long, IdGenerator> instances
= new ConcurrentHashMap<>();

private IdGenerator() {}

public static IdGenerator getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new IdGenerator());
return instances.get(currentThreadId);
}

public long getId() {
return id.incrementAndGet();
}
}

集群模式下单例

我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。

如何实现一个多例模式

“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。

单例模式的应用举例

  • JDK的Runtime类
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
/**
* Every Java application has a single instance of class
* <code>Runtime</code> that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the <code>getRuntime</code> method.
* <p>
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
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() {}

//....
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
//...
}

工厂模式

工厂模式是创建型模式。

简单工厂

这个模式很简单,比如我们需要制造不同类型的鼠标,我们只需要创建一个鼠标工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MouseFactory {
public static Mouse createMouse(int type) {
switch (type) {
case 1:
return new HpMouse();
case 2:
return new LenovoMouse();
case 0:
default:
return new DellMouse();
}
}

public static void main(String[] args) {
Mouse mouse = MouseFactory.createMouse(1);
mouse.sayHi();
}
}

根据不同的type来创建不同的鼠标即可。这个模式的缺点很明显:违反了开闭原则 ,所以我们引入工厂方法

工厂方法

工厂方法中,我们可以定义对应产品的对应工厂,以上面这个鼠标的例子为例,我们可以增加工厂的接口

1
2
3
public interface MouseFactory {
Mouse createMouse();
}

不同类型的鼠标工厂实现这个工厂即可,以Dell鼠标工厂为例

1
2
3
4
5
6
public class DellMouseFactory implements MouseFactory {
@Override
public Mouse createMouse() {
return new DellMouse();
}
}

主函数在调用的时候,直接指定工厂即可制造对应的产品了:

1
2
3
4
5
6
7
public class FactoryMethodDemo {
public static void main(String[] args) {
MouseFactory mf = new HpMouseFactory();
Mouse mouse = mf.createMouse();
mouse.sayHi();
}
}

工厂方法的优点是符合开闭原则,但是缺点也很明显,就是在增加子类的时候,同时要增加一个子类的工厂,而且,只支持同一类产品的创建,不适用于同一产品族

抽象工厂

举例,现在需要通过工厂来制造交通工具,如果是现代的工厂,制造的就是汽车,如果是古代的工厂,制造的就是马车, 我们可以先把工厂抽象出来,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package factory.abstractfactory;

/**
* @author Grey
* @date 2020/4/13
*/
public abstract class AbstractFactory {
/**
* 子类实现
*
* @return
*/
protected abstract Transportation createTransportation();

/**
* 子类实现
*
* @return
*/
protected abstract WritingInstrument createWritingInstrument();
}

交通工具我们也可以抽象出来

1
2
3
public abstract class Transportation {
protected abstract void go();
}

对于马车和汽车来说,只需要继承这个Transportation类,实现对应的go方法即可,以汽车为例

1
2
3
4
5
6
public class Car extends Transportation {
@Override
protected void go() {
System.out.println("car go");
}
}

对于现代工厂还是古代工厂,我们只需要继承AbstractFactory这个类,实现createTransportation方法即可,以现代工厂为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package factory.abstractfactory;

/**
* @author Grey
* @date 2020/4/13
*/
public class ModernFactory extends AbstractFactory {

@Override
protected Transportation createTransportation() {
return new Car();
}

@Override
protected WritingInstrument createWritingInstrument() {
return new Pen();
}
}

主方法在调用的时候,只需要

1
2
3
4
5
6
public class Main {
public static void main(String[] args) {
AbstractFactory factory = new ModernFactory();
factory.createTransportation().go();
}
}

抽象工厂的UML图如下:

image

Java8以后,提供了Supplier这个函数式接口,我们可以通过这个接口很方便的实现工厂类,举例:

我们可以定义一个MovableFactory,里面的create方法,传入的是一个Supplier,你可以把所有Movable的子类实现传给这个参数

1
2
3
4
5
6
7
8
9
10
public class MovableFactory {
public static Movable create(Supplier<? extends Movable> supplier) {
return supplier.get();
}

public static void main(String[] args) {
MovableFactory.create(Car::new).go();
MovableFactory.create(() -> new Ship()).go();
}
}

注:单例模式就是一种工厂模式(静态工厂)

应用

  • Spring IOC DI
    • 配置解析
    • 对象创建(BeansFactory:通过BeanDifinition来创建对象)
    • 对象生命周期管理(单例/多例,是否支持懒加载,销毁对象时候的清理方法)
  • Hibernate 换数据库只需换方言和驱动就可以切换不同数据库
  • java.util.Calender.getInstance()

建造者模式

建造者模式是创建型模式。

我们在对一个实体类进行属性的get/set的时候,可以通过封装一些常用的构造方法来简化实体类的构造。

比如 Effective Java中文版(第3版) 中举到到这个例子

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
54
55
56
57
58
59
60
61
62
package builder;

// Effective Java 3th examples
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;

public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;

// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;

public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}

public Builder calories(int val) {
calories = val;
return this;
}

public Builder fat(int val) {
fat = val;
return this;
}

public Builder sodium(int val) {
sodium = val;
return this;
}

public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}

public NutritionFacts build() {
return new NutritionFacts(this);
}
}

private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}

其中Builder就是一个内部类,用于构造NutritionFacts的必要信息,外部调用NutritionFacts的构造方法时候,可以这样使用:

1
NutritionFacts cocaCola=new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

image

构造器模式也适用于类层次结构。抽象类有抽象的Builder,具体类有具体的Builder。Effective Java中文版(第3版)
中还有一个例子, 假设我们抽象出一个披萨类,各种各样的披萨均可以继承披萨这个抽象类来实现自己的具体类型的披萨。

Pizza抽象类如下:

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
package builder;

import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

// Effective Java 3th examples
public abstract class Pizza {
public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
final Set<Topping> toppings;

abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}

abstract Pizza build();

// Subclasses must override this method to return "this"
protected abstract T self();
}

Pizza(Builder<?> builder) {
toppings = builder.toppings.clone(); // See Item 50
}
}

其中的Builder方法是abstract的,所以子类需要实现具体的Builder策略,

一种披萨的具体实现:NyPizza

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
import java.util.Objects;

public class NyPizza extends Pizza {
public enum Size {SMALL, MEDIUM, LARGE}

private final Size size;

public static class Builder extends Pizza.Builder<Builder> {
private final Size size;

public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}

@Override
public NyPizza build() {
return new NyPizza(this);
}

@Override
protected Builder self() {
return this;
}
}

private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}

另一种披萨的具体实现Calzone:

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
package builder;

public class Calzone extends Pizza {
private final boolean sauceInside;

public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // Default

public Builder sauceInside() {
sauceInside = true;
return this;
}

@Override
public Calzone build() {
return new Calzone(this);
}

@Override
protected Builder self() {
return this;
}
}

private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}

我们在具体调用的时候,可以通过如下方式:

1
2
NyPizza pizza=new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone=new Calzone.Builder().addTopping(HAM).sauceInside().build();

实际应用有非常多,很多组件都提供这样的构造方式,比如OkHttpClient的构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static OkHttpClient create(long connectTimeOut) {
return new OkHttpClient().newBuilder().connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT)).connectTimeout(connectTimeOut, TimeUnit.SECONDS).readTimeout(30, TimeUnit.SECONDS).writeTimeout(30, TimeUnit.SECONDS).connectionPool(CONNECTION_POOL).retryOnConnectionFailure(true).followRedirects(true).followSslRedirects(true).hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
}).cookieJar(new CookieJar() {
private List<Cookie> cookies;

@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
this.cookies = cookies;
}

@Override
public List<Cookie> loadForRequest(HttpUrl url) {
if (cookies != null) {
return cookies;
}
return Collections.emptyList();
}
}).build();
}

应用

  • JDK中的Calender
1
Calendar calendar = new Calendar.Builder().build();

原型模式

原型模式是创建型模式。

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。

实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者说对于大部分业务系统来说,这点时间完全是可以忽略的。应用一个复杂的模式,只得到一点点的性能提升,这就是所谓的过度设计,得不偿失。但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从 RPC、网络、数据库、文件系统等非常慢速的 IO 中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

原型模式用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象,典型的应用是对象的克隆方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Person implements Cloneable {
String name = "lisa";
int age = 1;
Location loc = new Location("xy", 10);

@Override
protected Object clone() throws CloneNotSupportedException {
Person p = (Person) super.clone();
p.loc = (Location) loc.clone();
return p;
}

@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + ", loc=" + loc + '}';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Location implements Cloneable {
private String street;
private int roomNo;

public Location(String street, int roomNo) {
this.street = street;
this.roomNo = roomNo;
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

@Override
public String toString() {
return "Location{" + "street='" + street + '\'' + ", roomNo=" + roomNo + '}';
}
}
1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person p = new Person();
System.out.println(p);
Person p2 = (Person) p.clone();
System.out.println(p2);
}
}

UML图如下:

image

使用示例

克隆一个巨大的HashMap,如果构建散列表的代价很大,我们可以通过

  1. HashMap的clone方法(注意:默认的clone方法是浅拷贝,需要递归拷贝HashMap里面的内容,直到类型是基础类型为止)
  2. 使用序列化

如果只是增量拷贝,可以通过浅拷贝拿到一个新的HashMap,然后拿到增量的数据单独进行深拷贝即可。

代理模式

代理模式是结构型模式。

静态代理

举例说明,假设我们需要在某个类的某段代码的前后加上日志记录,我们就可以通过静态代理的方式实现

1
2
3
4
5
public class Main {
public static void main(String[] args) {
new Tank().move();
}
}

假设我们需要在move()方法的前后都加上日志记录,我们可以设置一个代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TankLogProxy implements Moveable {
private Moveable m;

public TankLogProxy(Moveable m) {
this.m = m;
}

@Override
public void move() {
System.out.println("log before");
m.move();
System.out.println("log after");
}
}

这样的话,原先的调用就改成了:

1
2
3
4
5
public class Main {
public static void main(String[] args) {
new TankLogProxy(new Tank()).move();
}
}

即可实现在move方法调用前后加入日志记录的操作。

UML图如下:

image

动态代理

JDK自带

如果需要通过动态代理(jdk自带的方式)的方式来完成上述功能,我们可以这样来做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MovableProxy implements InvocationHandler {
private Movable movable;

public MovableProxy(Movable movable) {
this.movable = movable;
}

public void before() {
System.out.println("before , do sth");
}

public void after() {
System.out.println("after , do sth");
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object o = method.invoke(movable, args);
after();
return o;
}
}

主方法调用的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package proxy.dynamic.jdk;

import java.lang.reflect.Proxy;

/**
* @author Grey
* @date 2020/4/15
*/
public class Main {
public static void main(String[] args) {
Movable tank = new Tank();

//reflection 通过二进制字节码分析类的属性和方法

Movable m = (Movable) Proxy.newProxyInstance(Movable.class.getClassLoader(),
new Class[]{Movable.class},
new MovableProxy(tank)
);

m.move();
m.go();
}
}

UML图如下:

image

Cglib

JDK自带的方式实现动态代理需要被代理对象实现一个接口,Cglib不需要,使用示例:

其中被代理的Tank类无需实现接口

1
2
3
4
5
6
7
8
public class Tank {
public void move() {
System.out.println("tank move");
}
public void go() {
System.out.println("tank go");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import net.sf.cglib.proxy.Enhancer;

public class Main {

public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
//设置目标类的字节码文件
enhancer.setSuperclass(Tank.class);
//设置回调函数
enhancer.setCallback(new MyMethodInterceptor());

//这里的creat方法就是正式创建代理类
Tank m = (Tank) enhancer.create();
m.move();
m.go();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object o = proxy.invokeSuper(obj, args);
after();
return o;
}

public void before() {
System.out.println("before , do sth");
}

public void after() {
System.out.println("after , do sth");
}
}

实际应用

  • 在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理。
  • RPC框架可以看成一种代理模式。
  • 为接口增加缓存能力。
  • Spring AOP
  • jdk自带
    • ASM操作二进制码
    • Java Instrumentation
    • 必须面向接口
  • cglib
    • final类不行,代理类的子类 底层也是ASM

桥接模式

桥接模式是一种结构型模式。

使用桥接模式,可以将抽象和具体的发展单独分支(抽象中持有一个具体的引用 )
举例说明:

GG在追MM的时候,可以送书和花两种礼物

1
2
3
4
5
6
7
8
9
10
public class GG {
public void chase(MM mm) {
Gift g = new WarmGift(new Flower());
give(mm, g);
}

public void give(MM mm, Gift g) {
System.out.println(g + "gived!");
}
}

如上代码,Flower被包装成了一个WarmGift送给MM,WarmGift和WildGift都是Gift的一种抽象,Flower和Book都算Gift的一种具体实现, 我们让Gift这个抽象类中,持有一个GiftImpl的引用

1
2
3
public abstract class Gift {
protected GiftImpl impl;
}
1
2
public class Flower extends GiftImpl {
}
1
2
3
4
5
public class WarmGift extends Gift {
public WarmGift(GiftImpl impl) {
this.impl = impl;
}
}

UML示例图如下:

image

如果说代理模式是一个类与另一个类的组合,那么桥接模式是一组类和另外一组类的组合。

桥接模式的应用

  • jdbc驱动配置

    当我们把具体的 Driver 实现类(比如,com.mysql.jdbc.Driver)注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。而 Driver 实现类都实现了相同的接口(java.sql.Driver ),这也是可以灵活切换 Driver 的原因。

装饰器模式

装饰器模式是一种结构型模式。

顾名思义,就是对某个方法或者对象进行装饰,举个简单的例子,有个圆形类(Circle),我需要把这个圆形的涂上红色,其实就是新增一个装饰器来装饰这个圆形类。如果要让装饰器通用一些,可以处理圆形类对应的抽象类
Sharpe,那么对于任意Shape的子类,都可以用红色装饰器来涂红色。

我们先定义Sharp这个抽象类:

1
2
3
public abstract class Sharp {
protected abstract void draw();
}

然后我们定义Sharp的装饰类:SharpDecorator,这个类是所有装饰器类的抽象类,后续的装饰器只需要实现这个抽象类就可以对Sharp进行各种装饰了,

1
2
3
4
5
6
7
public abstract class SharpDecorator extends Sharp {
protected Sharp decoratedSharp;

public SharpDecorator(Sharp decoratedSharp) {
this.decoratedSharp = decoratedSharp;
}
}

红色装饰器实现这个抽象类即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RedSharpDecorator extends SharpDecorator {
public RedSharpDecorator(Sharp decoratedSharp) {
super(decoratedSharp);
}

private static void redIt() {
System.out.println("[RED]");
}

@Override
protected void draw() {
redIt();
this.decoratedSharp.draw();
redIt();
}
}

主方法调用的时候只需要:

1
new RedSharpDecorator(new Circle()).draw();

UML图如下:

image

说明:

  1. 装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
  2. 装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点。符合“组合关系”这种代码结构的设计模式有很多,比如代理模式、桥接模式,还有现在的装饰器模式。尽管它们的代码结构很相似,但是每种设计模式的意图是不同的。就拿比较相似的代理模式和装饰器模式来说吧,代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。

实际上,如果去查看 JDK 的源码,你会发现,BufferedInputStream、DataInputStream 并非继承自 InputStream,而是另外一个叫 FilterInputStream 的类。那这又是出于什么样的设计意图,才引入这样一个类呢?

因为InputStream 是一个抽象类而非接口,而且它的大部分函数(比如 read()、available())都有默认实现,按理来说,我们只需要在 BufferedInputStream 类中重新实现那些需要增加缓存功能的函数就可以了,其他函数继承 InputStream 的默认实现。但实际上,这样做是行不通的。对于即便是不需要增加缓存功能的函数来说,BufferedInputStream 还是必须把它重新实现一遍,简单包裹对 InputStream 对象的函数调用。那 BufferedInputStream 类就无法将最终读取数据的任务,委托给传递进来的 InputStream 对象来完成,DataInputStream 也存在跟 BufferedInputStream 同样的问题。为了避免代码重复,Java IO 抽象出了一个装饰器父类 FilterInputStream,InputStream 的所有的装饰器类(BufferedInputStream、DataInputStream)都继承自这个装饰器父类。这样,装饰器类只需要实现它需要增强的方法就可以了,其他方法继承装饰器父类的默认实现。
应用

  • Java中的IO流, Read/InputStream ,Write/OutputStream
  • JDK中的UnmodifiableCollection

适配器模式

适配器模式是一种结构型模式。

举例说明,假设又一个播放器,需要根据不同格式以及对应的文件来播放,接口设计如下:

1
2
3
public interface MediaPlayer {
void play(String type, String fileName);
}

不同类型的播放器只需要实现这个接口即可,比如我们有一个ClassicMediaPlayer,这个只能播放mp3类型的文件

1
2
3
4
5
6
7
8
9
10
public class ClassicMediaPlayer implements MediaPlayer {
@Override
public void play(String type, String fileName) {
if ("mp3".equalsIgnoreCase(type)) {
System.out.println("play mp3");
} else {
System.out.println("not supported format");
}
}
}

如果我想扩展,我们可以增加一个适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PlayerAdapter implements MediaPlayer {
private AdvanceMediaPlayer advanceMediaPlayer;

public PlayerAdapter(String type) {
if ("mp4".equalsIgnoreCase(type)) {
advanceMediaPlayer = new MP4Player();
} else if ("AVI".equalsIgnoreCase(type)) {
advanceMediaPlayer = new AVIPlayer();
}
}

@Override
public void play(String type, String fileName) {
if ("mp4".equalsIgnoreCase(type)) {
advanceMediaPlayer.playMP4(fileName);
} else if ("AVI".equalsIgnoreCase(type)) {
advanceMediaPlayer.playAVI(fileName);
} else {
new ClassicMediaPlayer().play(type, fileName);
}
}
}

这个适配器就是根据不同类型来构造不同的播放器的,然后定义一个ExtendMediaPlayer,在里面持有PlayAdapter,这样,ExtendMediaPlayer就拥有了播放不同类型文件的能力,所以我们在调用的时候,只需要:

1
2
3
4
ExtendMediaPlayer audioPlayer=new ExtendMediaPlayer();
audioPlayer.play("mp3","beyond the horizon.mp3");
audioPlayer.play("mp4","alone.mp4");
audioPlayer.play("avi","far far away.vlc");

UML图如下:

image

适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

应用

  • java.io
  • jdbc-odbc bridge
  • ASM transformer
  • 老版本的 JDK 提供了 Enumeration 类来遍历容器。新版本的 JDK 用 Iterator 类替代 Enumeration 类来遍历容器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Returns an enumeration over the specified collection. This provides
* interoperability with legacy APIs that require an enumeration
* as input.
*
* @param <T> the class of the objects in the collection
* @param c the collection for which an enumeration is to be returned.
* @return an enumeration over the specified collection.
* @see Enumeration
*/
public static <T> Enumeration<T> enumeration(final Collection<T> c) {
return new Enumeration<T>() {
private final Iterator<T> i = c.iterator();

public boolean hasMoreElements() {
return i.hasNext();
}

public T nextElement() {
return i.next();
}
};
}

门面模式

门面模式是一种结构型模式。

门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。

假设建造一个房子需要有如下三个步骤:

  • 和泥
  • 搬砖
  • 砌墙

如果每次我们制造一个房子都要分别调用这三个方法,就会比较麻烦一些,我们可以设置一个门面,这个门面封装了这三个步骤,后续建造房子,只需要调用这个门面即可。

和泥

1
2
3
4
5
public class Mason {
public void mix() {
System.out.println("我和好泥了!");
}
}

搬砖

1
2
3
4
5
public class BrickWorker {
public void carry() {
System.out.println("我搬好砖了!");
}
}

砌墙

1
2
3
4
5
public class BrickLayer {
public void neat() {
System.out.println("我砌好墙了!");
}
}

门面

1
2
3
4
5
6
7
8
9
10
11
public class LabourConstractor {
private Mason work1 = new Mason();
private BrickWorker work2 = new BrickWorker();
private BrickLayer work3 = new BrickLayer();

public void buildHouse() {
work1.mix();
work2.carry();
work3.neat();
}
}

这样主函数只需要调用门面的buildHourse()方法,就可以建造一个房子了

1
2
3
4
5
6
public class Client {
public static void main(String[] args) {
LabourConstractor labour = new LabourConstractor();
labour.buildHouse();
}
}

门面模式的UML图如下

image

  • 应用

    • Linux的系统调用和Shell脚本

      Linux 系统调用函数就可以看作一种“门面”。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更基础的 Linux 内核调用。再比如,Linux 的 Shell 命令,实际上也可以看作一种门面模式的应用。它继续封装系统调用,提供更加友好、简单的命令,让我们可以直接通过执行命令来跟操作系统交互。

组合模式

组合模式是一种结构型模式。

组合模式中,最常用的一个用法就是目录层级的遍历,话不多说,直接上代码,主方法中

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
BranchNode root = new BranchNode("root");
BranchNode branch1 = new BranchNode("branch1");
BranchNode branch2 = new BranchNode("branch2");
branch1.addNode(new LeafNode("leaf1"));
root.addNode(branch1);
root.addNode(branch2);
tree(root, 0);
}
}

其中,BranchNode为分支节点,LeafNode是叶子节点 达到的效果就是打印如下的形式

1
2
3
4
root
--branch1
----leaf1
--branch2

递归方法

1
2
3
4
5
6
7
8
9
10
11
static void tree(Node node, int depth) {
for (int i = 0; i < depth; i++) {
System.out.print("--");
}
node.print();
if (node instanceof BranchNode) {
for (Node n : ((BranchNode) node).getNodes()) {
tree(n, depth + 1);
}
}
}

其中BranchNode和LeafNode都实现了Node接口,Node接口(也可以为定义抽象类)仅提供了一个属性(content:标识节点内容)和一个打印方法:

1
2
3
4
5
public abstract class Node {
protected String content;

protected abstract void print();
}

BranchNode下可以包含多个Node,因为一个分支下面可以有多个分支(这个分支可以是任意的Node子类)

1
2
3
4
5
6
7
8
9
10
11
12
public class BranchNode extends Node {
private List<Node> nodes = new ArrayList<>();

public BranchNode(String content) {
this.content = content;
}

@Override
public void print() {
System.out.println(content);
} // get..set方法略
}

组合模式的UML图如下:

image

享元模式

享元模式是一种结构型模式。

运用共享技术有效地支持大量细粒度的对象。主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

假设我们有一个子弹类,同时我们设计一个子弹池,子弹池负责提供子弹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BulletPool {
List<Bullet> bullets = new ArrayList<>();
{
for (int i = 0; i < 10; i++) {
bullets.add(new Bullet(true));
}
}
public Bullet getBullet() {
for (int i = 0; i < bullets.size(); i++) {
if (bullets.get(i).living) {
return bullets.get(i);
}
}
return new Bullet(true);
}
}

可以看到getBullet逻辑,如果池子中有子弹,就拿池中的子弹,如果没有,就new一个新的子弹返回

UML图如下

image

应用

  • Java中Boolean的valueOf(boolean b) 方法 ,这个方法返回的Boolean对象不会新new出来,而是复用的同一个, 源码如下:
1
2
3
4
5
public static Boolean valueOf(boolean b){
return(b?TRUE:FALSE);
}
public static final Boolean TRUE=new Boolean(true);
public static final Boolean FALSE=new Boolean(false);
  • 连接池管理

  • IntegerCache 和 String

    在 Java Integer 的实现中,-128 到 127 之间的整型对象会被事先创建好,缓存在 IntegerCache 类中。当我们使用自动装箱或者 valueOf() 来创建这个数值区间的整型对象时,会复用 IntegerCache 类事先创建好的对象。这里的 IntegerCache 类就是享元工厂类,事先创建好的整型对象就是享元对象。在 Java String 类的实现中,JVM 开辟一块存储区专门存储字符串常量,这块存储区叫作字符串常量池,类似于 Integer 中的 IntegerCache。不过,跟 IntegerCache 不同的是,它并非事先创建好需要共享的对象,而是在程序的运行期间,根据需要来创建和缓存字符串常量

    注:Java提供了两个配置IntegerCache的参数

1
2
3
4
//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255

观察者模式

观察者模式是一种行为型模式。在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。

一般可以用做事件处理往往和责任链模式搭配使用, 举个例子 按钮上一般都可以绑定事件,当我们按下按钮的时候,可以触发这些事件的执行,这里就可以用观察者模式来做, 我们先定义按钮这个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Button {
private List<ActionListener> listeners = new ArrayList<>();

public void addActionListener(ActionListener listener) {
this.listeners.add(listener);
}

@Override
public String toString() {
return "Button{" + "listeners=" + listeners + '}';
}

public void buttonPressed() {
ActionEvent event = new ActionEvent(System.currentTimeMillis(), this);
listeners.forEach(item -> item.actionPerformed(event));
}
}

由上可知,Button中持有了一个列表,这个列表里面装的就是所有事件的列表,我们可以把事件绑定到这个按钮的事件列表中,这样就可以实现按钮执行press操作的时候,把对应的事件触发执行了

1
2
3
public interface ActionListener {
void actionPerformed(ActionEvent event);
}

模拟两个监听事件

1
2
3
4
5
6
public class Listener1 implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
System.out.println("Listener 1 listened it source: [" + event.getSource() + "], when is [" + event.getWhen() + "]");
}
}
1
2
3
4
5
6
public class Listener2 implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
System.out.println("Listener 2 listened it source: [" + event.getSource() + "], when is [" + event.getWhen() + "]");
}
}

主方法在调用的时候

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
Button button = new Button();
button.addActionListener(new Listener1());
button.addActionListener(new Listener2());
button.buttonPressed();
}
}

当执行

1
button.buttonPressed()

的时候,对应的listener1和listener2就可以执行了。

UML图如下

image

应用

  • Spring ApplicationEvent
  • 邮件订阅、RSS Feeds,本质上都是观察者模式。
  • Google Guava EventBus

模板方法

模板方法是一种行为型模式。

假设我们要实现一个游戏,这个游戏有初始化,启动,结束三个方法,我们可以定义一个游戏的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Game {
protected abstract void init();

protected abstract void start();

protected abstract void end();

protected final void play() {
init();
start();
end();
}
}

每种类似这样结构(有初始化,启动,结束)的游戏都可以继承这个类来实现这三个方法,比如BasketballGame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BasketballGame extends Game {
@Override
protected void init() {
System.out.println("basketball init");
}

@Override
protected void start() {
System.out.println("basketball start");
}

@Override
protected void end() {
System.out.println("basketball end");
}
}

FootballGame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FootballGame extends Game {
@Override
protected void init() {
System.out.println("football init");
}

@Override
protected void start() {
System.out.println("football start");
}

@Override
protected void end() {
System.out.println("football end");
}
}

主方法在调用的时候,直接:

1
2
3
4
Game basketballGame=new BasketballGame();
basketballGame.play();
Game footballGame=new FootballGame();
footballGame.play();

另外一个例子:

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
public abstract class TestCase {
public void run() {
if (doTest()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}

public abstract boolean doTest();
}

public class JunitApplication {
private static final List<TestCase> testCases = new ArrayList<>();

public static void register(TestCase testCase) {
testCases.add(testCase);
}

public static final void main(String[] args) {
for (TestCase c : testCases) {
c.run();
}
}
}

public class UserServiceTest extends TestCase {

@Override
public boolean doTest() {
System.out.println("do test...");
return false;
}

}

UML图如下:

image

实际应用场景

  • 钩子函数
  • Spring中的RestTemplate /JDBCTemplate
  • Collections.sort()方法也可以看成模板方法。

策略模式

实例: 假设我们有一个猫类,这个类里面有体重和身高这两个属性,给你一个猫的集合,然后需要你按猫的体重从小到大排序

思路: 我们可以把体重从小到大这个看成是一个策略,后续可能衍生其他的策略,比如: 按身高从高到低 按体重从小到大,体重一样的身高从高到低

以身高从低到高排序这个策略为例

1
2
3
4
5
6
public class CatSortStrategy implements Comparator<Cat> {
@Override
public int compare(Cat o1, Cat o2) {
return o1.getHeight() - o2.getHeight();
}
}

假设我们定义猫排序的方法是: sort 那么这个方法必然需要传入一个排序策略的参数(否则我怎么知道要怎么排序猫?) 所以定义的sort方法可以是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Sorter {
public Cat[] sort(Cat[] items, Comparator<Cat> strategy) {
int length = items.length;
for (int i = 0; i < length; i++) {
for (int j = i + 1; j < length; j++) {
if (strategy.compare(items[i], items[j]) > 0) {
Cat tmp = items[i];
items[i] = items[j];
items[j] = tmp;
}
}
}
return items;
}
}

进一步抽象,如果我想让Sorter这个工具类不仅可以对猫进行各种策略的排序(基于比较的排序算法),还可以对狗进行各种策略的排序(基于比较排序算法),可以将Sorter定义成泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Sorter<T> {
public T[] sort(T[] items, Comparator<T> strategy) {
int length = items.length;
for (int i = 0; i < length; i++) {
for (int j = i + 1; j < length; j++) {
if (strategy.compare(items[i], items[j]) > 0) {
T tmp = items[i];
items[i] = items[j];
items[j] = tmp;
}
}
}
return items;
}
}

调用的时候, 泛型版本的Sorter可以对猫和狗都进行基于特定排序策略的排序。

1
2
3
4
Sorter<Cat> sorter=new Sorter<>();
Cat[]sortedCats=sorter.sort(cats,new CatSortStrategy());
Sorter<Dog> sorter=new Sorter<>();
Dog[]sortedCats=sorter.sort(dogs,new DogSortStrategy());

策略模式UML图如下

image

责任链模式

责任链模式是一种行为型模式。

有一段文本需要过滤敏感字,我们可以通过责任链模式来设计这个功能,假设文本是:scripts Hell World! 996

我们有多个过滤规则,比如第一个规则是:过滤 scripts 这个关键字(实际的规则可能很复杂,目前只是举这个简单例子来说明情况)
第二个规则是:过滤 996 这个关键字

我们可以抽象一个Filter接口,各种过滤规则无非就是实现这个接口即可

1
2
3
public interface Filter {
boolean doFilter(Msg msg);
}

过滤 996 的规则:

1
2
3
4
5
6
7
public class SensitiveFilter implements Filter {
@Override
public boolean doFilter(Msg msg) {
msg.setContent(msg.getContent().replace("996", ""));
return true;
}
}

过滤 scripts 的规则:

1
2
3
4
5
6
7
public class HTMLFilter implements Filter {
@Override
public boolean doFilter(Msg msg) {
msg.setContent(msg.getContent().replace("scripts", ""));
return true;
}
}

主方法调用的时候,就直接New 相应的Filter来处理即可:

1
2
3
4
5
6
7
8
Msg msg=new Msg();
msg.setContent("scripts Hell World! 996");
System.out.println("before filter , the content is : "+msg.getContent());
Filter html=new HTMLFilter();
Filter sensitive=new SensitiveFilter();
html.doFilter(msg);
sensitive.doFilter(msg);
System.out.println("after filter , the content is : "+msg.getContent());

不过,更为优雅的一种方式是设计一个FilterChain,我们把所有的Filter都加入到这个FilterChain里面,对于Msg直接去调用FilterChain的过滤方法即可把FilterChain中的所有Filter都执行(
而且还可以很灵活指定Filter顺序)

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
package cor;

import java.util.ArrayList;
import java.util.List;

/**
* @author Grey
* @date 2020/4/13
*/
public class FilterChain implements Filter {
private List<Filter> filters = new ArrayList<>();

public FilterChain addFilter(Filter filter) {
filters.add(filter);
return this;
}

@Override
public boolean doFilter(Msg msg) {
for (Filter filter : filters) {
if (!filter.doFilter(msg)) {
return false;
}
}
return true;
}
}

那么主方法在调用的时候,可以直接通过如下的方式:

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
FilterChain filterChain = new FilterChain();
filterChain.addFilter(new HTMLFilter()).addFilter(new SensitiveFilter());
Msg msg = new Msg();
msg.setContent("scripts Hell World! 996");
System.out.println("before filter , the content is : " + msg.getContent());
filterChain.doFilter(msg);
System.out.println("after filter , the content is : " + msg.getContent());
}
}

UML图如下:

image

应用

  • Servlet filter
  • Structs interceptor
  • SpringMVC interceptor
  • Dubbo Filter
  • Netty ChannelPipeline

状态模式

状态模式是一种行为型模式。

对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。

举个例子,Person有Cry, Smile, Say三种行为,但是在不同状态(SadState, HappyState)下,这三种行为不一样,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person {
private State state;

public Person(State state) {
this.state = state;
}

void cry() {
state.cry();
}

void smile() {
state.smile();
}

void say() {
state.say();
}
}

在Sad状态下,行为可能是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SadState implements State {
@Override
public void cry() {
System.out.println("Sad cry");
}

@Override
public void smile() {
System.out.println("Sad smile");
}

@Override
public void say() {
System.out.println("Sad say");
}
}

Happy状态下同理,那么主方法在调用的时候:

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
Person person = new Person(new SadState());
person.cry();
person.say();
person.smile();
person = new Person(new HappyState());
person.cry();
person.say();
person.smile();
}
}

Person就可以根据不同的状态来执行cry,say,smile的行为了

UML图如下:

image

迭代器模式

迭代器模式是一种行为型模式。

迭代器最典型的应用是容器遍历

image

模仿JDK的容器,我们自定义一个容器并实现iterator方法 我们先定义一个容器接口:Collection_.java

1
2
3
4
5
6
7
public interface Collection_<E> {
int size();

void add(E element);

Iterator_<E> iterator();
}

里面包括了一个iterator方法,所以每个实现这个容器接口的具体容器类型,都必须自定义iterator方法, 然后定义一个Iterator接口Iterator_.java, 具体容器中可以增加一个内部类来专门实现这个接口,
比如我们的具体容器类是ArrayList_.java

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
package Iterator;

import static java.lang.System.arraycopy;

/**
* @author Grey
* @date 2020/4/15
*/
public class ArrayList_<E> implements Collection_<E> {
private E[] objects = (E[]) new Object[10];
private int index = 0;

@Override
public int size() {
return index;
}

@Override
public void add(E element) {
if (objects.length == size()) {
// 满了就扩容为原来的两倍
E[] newObjects = (E[]) new Object[objects.length * 2];
arraycopy(objects, 0, newObjects, 0, objects.length);
objects = newObjects;
}
objects[index] = element;
index++;
}

@Override
public Iterator_<E> iterator() {
return new ArrayListIterator_<>();
}

private class ArrayListIterator_<E> implements Iterator_<E> {
private int currentIndex = 0;

@Override
public boolean hasNext() {
return currentIndex < index;
}

@Override
public E next() {
E o = (E) objects[currentIndex];
currentIndex++;
return o;
}
}

}

我们主要看 ArrayListIterator_这个内部类,里面其实是实现了 Iterator_ 这个接口,所以 ArrayList_ 的遍历操作会执行这个内部类中的操作规则来对其进行遍历。

如何实现一个快照迭代器

我们可以在容器中,为每个元素保存两个时间戳,一个是添加时间戳 addTimestamp,一个是删除时间戳 delTimestamp。当元素被加入到集合中的时候,我们将 addTimestamp 设置为当前时间,将 delTimestamp 设置成最大长整型值(Long.MAX_VALUE)。当元素被删除时,我们将 delTimestamp 更新为当前时间,表示已经被删除。注意,这里只是标记删除,而非真正将它从容器中删除。同时,每个迭代器也保存一个迭代器创建时间戳 snapshotTimestamp,也就是迭代器对应的快照的创建时间戳。当使用迭代器来遍历容器的时候,只有满足

1
addTimestamp < snapshotTimestamp < delTimestamp

的元素,才是属于这个迭代器的快照。如果元素的

1
addTimestamp > snapshotTimestamp

说明元素在创建了迭代器之后才加入的,不属于这个迭代器的快照;

如果元素的

1
delTimestamp<snapshotTimestamp

说明元素在创建迭代器之前就被删除掉了,也不属于这个迭代器的快照。这样就在不拷贝容器的情况下,在容器本身上借助时间戳实现了快照功能。

访问者模式

访问者模式是一种行为型模式。

访问者模式在结构不变的情况下动态改变对于内部元素的动作,举例说明:

假设我们需要构造一台电脑,有主板(Board),CPU,内存(Memory),但是针对企业用户和个人用户,电脑组件的价格是不一样的,我们需要根据不同客户获取一台电脑的总价格。

我们先抽象出电脑组件这个类

1
2
3
4
5
public abstract class ComputerPart {
abstract void accept(Visitor visitor);

abstract int getPrice();
}

每个具体组件会继承这个抽象类,以主板(Board)为例

1
2
3
4
5
6
7
8
9
10
11
public class Board extends ComputerPart {
@Override
void accept(Visitor visitor) {
visitor.visitBoard(this);
}

@Override
int getPrice() {
return 20;
}
}

抽象出一个访问者(Visitor)接口,

1
2
3
4
5
6
7
public interface Visitor {
void visitCPU(CPU cpu);

void visitBoard(Board board);

void visitMemory(Memory memory);
}

每个具体类型的访问者实现这个接口,然后定义其不同的价格策略,以公司访问者为例(CorpVisitor)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CorpVisitor implements Visitor {
private int totalPrice;

@Override
public void visitCPU(CPU cpu) {
totalPrice += cpu.getPrice() - 1;
}

@Override
public void visitBoard(Board board) {
totalPrice += board.getPrice() - 2;
}

@Override
public void visitMemory(Memory memory) {
totalPrice += memory.getPrice() - 3;
}

public int getTotalPrice() {
return totalPrice;
}
}

个人访问者(PersonalVisitor)类似

主方法调用

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
package visitor;

/**
* @author Grey
* @date 2020/4/16
*/
public class Main {
public static void main(String[] args) {
ComputerPart cpu = new CPU();
ComputerPart memory = new Memory();
ComputerPart board = new Board();
PersonalVisitor personalVisitor = new PersonalVisitor();
cpu.accept(personalVisitor);
memory.accept(personalVisitor);
board.accept(personalVisitor);
System.out.println(personalVisitor.getTotalPrice());

ComputerPart cpu2 = new CPU();
ComputerPart memory2 = new Memory();
ComputerPart board2 = new Board();
CorpVisitor corpVisitor = new CorpVisitor();
cpu2.accept(corpVisitor);
memory2.accept(corpVisitor);
board2.accept(corpVisitor);
System.out.println(corpVisitor.getTotalPrice());
}
}

UML图如下

image

应用

  • 做编译器的时候,需要生成AST,进行类型检查 根据抽象语法树,生成中间代码

  • XML文件解析

备忘录模式

备忘录模式是一种行为型模式。

用于记录对象的某个瞬间 类似快照 应用实例:

  1. 后悔药。
  2. 打游戏时的存档。
  3. Windows 里的 ctri + z。
  4. IE 中的后退。
  5. 数据库的事务管理。

一个简单的示例

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 Main {
public static void main(String[] args) {
Person person = new Person();
person.name = "zhangsan";
person.age = 12;
new Main().save(person);
new Main().load();
}

public void save(Person person) {
File c = new File("/tank.data");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(c));) {
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
}

public void load() {
File c = new File("/tank.data");
try (ObjectInputStream oos = new ObjectInputStream(new FileInputStream(c));) {
Person myTank = (Person) oos.readObject();
System.out.println(myTank);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

UML图:

image

命令模式

命令模式是一种行为型模式。

通过调用者调用接受者执行命令,顺序:调用者→命令→接受者,比如:CopyCommand中的doit方法,就是执行这个copy的命令,undo就是撤销上一次执行的命令,我们可以抽象出Command这个接口:

1
2
3
4
5
public interface Command {
void doit();

void undo();
}

CopyCommand实现这个接口,并实现doit和undo这两个方法,其他的命令也可以类似的实现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CopyCommand implements Command {
private Content content;

public CopyCommand(Content content) {
this.content = content;
}

@Override
public void doit() {
content.msg = content.msg + content.msg;
}

@Override
public void undo() {
content.msg = content.msg.substring(0, content.msg.length() / 2);
}
}

UML图如下

image

命令模式可以

  1. 结合责任链模式实现多次undo[TODO]
  2. 结合组合模式实现宏命令
  3. 结合记忆模式实现transaction回滚

解释器模式

解释器模式是一种行为型模式。

解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。

一般用于脚本语言解释器

示例:如何实现一个自定义接口告警规则功能?

一般来讲,监控系统支持开发者自定义告警规则,比如我们可以用下面这样一个表达式,来表示一个告警规则,它表达的意思是:每分钟 API 总出错数超过 100 或者每分钟 API 总调用数超过 10000 就触发告警。

1
api_error_per_minute > 100 || api_count_per_minute > 10000

在监控系统中,告警模块只负责根据统计数据和告警规则,判断是否触发告警。至于每分钟 API 接口出错数、每分钟接口调用数等统计数据的计算,是由其他模块来负责的。其他模块将统计数据放到一个 Map 中(数据的格式如下所示),发送给告警模块。接下来,我们只关注告警模块。

1
2
3
4

Map<String, Long> apiStat = new HashMap<>();
apiStat.put("api_error_per_minute", 103);
apiStat.put("api_count_per_minute", 987);

为了简化讲解和代码实现,我们假设自定义的告警规则只包含“||、&&、>、<、==”这五个运算符,其中,“>、<、==”运算符的优先级高于“||、&&”运算符,“&&”运算符优先级高于“||”。在表达式中,任意元素之间需要通过空格来分隔。除此之外,用户可以自定义要监控的 key,比如前面的 api_error_per_minute、api_count_per_minute。

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

public class AlertRuleInterpreter {

// key1 > 100 && key2 < 1000 || key3 == 200
public AlertRuleInterpreter(String ruleExpression) {
//TODO:由你来完善
}

//<String, Long> apiStat = new HashMap<>();
//apiStat.put("key1", 103);
//apiStat.put("key2", 987);
public boolean interpret(Map<String, Long> stats) {
//TODO:由你来完善
}

}

public class DemoTest {
public static void main(String[] args) {
String rule = "key1 > 100 && key2 < 30 || key3 < 100 || key4 == 88";
AlertRuleInterpreter interpreter = new AlertRuleInterpreter(rule);
Map<String, Long> stats = new HashMap<>();
stats.put("key1", 101l);
stats.put("key3", 121l);
stats.put("key4", 88l);
boolean alert = interpreter.interpret(stats);
System.out.println(alert);
}
}

实际上,我们可以把自定义的告警规则,看作一种特殊“语言”的语法规则。我们实现一个解释器,能够根据规则,针对用户输入的数据,判断是否触发告警。利用解释器模式,我们把解析表达式的逻辑拆分到各个小类中,避免大而复杂的大类的出现。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

public interface Expression {
boolean interpret(Map<String, Long> stats);
}

public class GreaterExpression implements Expression {
private String key;
private long value;

public GreaterExpression(String strExpression) {
String[] elements = strExpression.trim().split("\\s+");
if (elements.length != 3 || !elements[1].trim().equals(">")) {
throw new RuntimeException("Expression is invalid: " + strExpression);
}
this.key = elements[0].trim();
this.value = Long.parseLong(elements[2].trim());
}

public GreaterExpression(String key, long value) {
this.key = key;
this.value = value;
}

@Override
public boolean interpret(Map<String, Long> stats) {
if (!stats.containsKey(key)) {
return false;
}
long statValue = stats.get(key);
return statValue > value;
}
}

// LessExpression/EqualExpression跟GreaterExpression代码类似,这里就省略了

public class AndExpression implements Expression {
private List<Expression> expressions = new ArrayList<>();

public AndExpression(String strAndExpression) {
String[] strExpressions = strAndExpression.split("&&");
for (String strExpr : strExpressions) {
if (strExpr.contains(">")) {
expressions.add(new GreaterExpression(strExpr));
} else if (strExpr.contains("<")) {
expressions.add(new LessExpression(strExpr));
} else if (strExpr.contains("==")) {
expressions.add(new EqualExpression(strExpr));
} else {
throw new RuntimeException("Expression is invalid: " + strAndExpression);
}
}
}

public AndExpression(List<Expression> expressions) {
this.expressions.addAll(expressions);
}

@Override
public boolean interpret(Map<String, Long> stats) {
for (Expression expr : expressions) {
if (!expr.interpret(stats)) {
return false;
}
}
return true;
}

}

public class OrExpression implements Expression {
private List<Expression> expressions = new ArrayList<>();

public OrExpression(String strOrExpression) {
String[] andExpressions = strOrExpression.split("\\|\\|");
for (String andExpr : andExpressions) {
expressions.add(new AndExpression(andExpr));
}
}

public OrExpression(List<Expression> expressions) {
this.expressions.addAll(expressions);
}

@Override
public boolean interpret(Map<String, Long> stats) {
for (Expression expr : expressions) {
if (expr.interpret(stats)) {
return true;
}
}
return false;
}
}

public class AlertRuleInterpreter {
private Expression expression;

public AlertRuleInterpreter(String ruleExpression) {
this.expression = new OrExpression(ruleExpression);
}

public boolean interpret(Map<String, Long> stats) {
return expression.interpret(stats);
}
}

中介模式

中介模式是一种行为模式。

举个简单的例子,如果一个聊天室里面的用户1和用户2要聊天,聊天室就相当于中介的地位,用户1和用户2只管调用发消息方法,聊天室即可把消息给对方

1
2
3
4
5
public class ChatRoom {
public static void showMessage(User user, String content) {
System.out.println("user :" + user.getName() + " send a message, content is " + content);
}
}

以上代码表示,聊天室将user说的content展示出来

主方法只需要如下调用即可:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
User user = new User("Peter");
user.sendMessage("Hello ");
user = new User("Harry");
user.sendMessage("Hi");
}
}

User中的sendMessage方法

1
public void sendMessage(String content){ChatRoom.showMessage(this,content);}

image

Spring中使用到的设计模式

观察者模式

定义一个继承 ApplicationEvent 的事件;定义一个实现了 ApplicationListener 的监听器;定义一个发送者(DemoPublisher),发送者调用 ApplicationContext 来发送事件消息。

模板方法

image

适配器模式

Spring 定义了统一的接口 HandlerAdapter,并且对每种 Controller 定义了对应的适配器类。这些适配器类包括:AnnotationMethodHandlerAdapter、SimpleControllerHandlerAdapter、SimpleServletHandlerAdapter 等

策略模式

AopProxy 是策略接口,JdkDynamicAopProxy、CglibAopProxy 是两个实现了 AopProxy 接口的策略类。策略的创建一般通过工厂方法来实现。对应到 Spring 源码,AopProxyFactory 是一个工厂类接口,DefaultAopProxyFactory 是一个默认的工厂类,用来创建 AopProxy 对象。

组合模式

CacheManager 组合 Cache

装饰器模式

TransactionAwareCacheDecorator 增加了对事务的支持,在事务提交、回滚的时候分别对 Cache 的数据进行处理。TransactionAwareCacheDecorator 实现 Cache 接口,并且将所有的操作都委托给 targetCache 来实现,对其中的写操作添加了事务功能。这是典型的装饰器模式的应用场景和代码实现

工厂模式

BeanFactory 类和 ApplicationContext 相关类(AbstractApplicationContext、ClassPathXmlApplicationContext、FileSystemXmlApplicationContext…)

参考资料