从 JDK 源码中学习设计模式

发布于 2024-02-29
一、创建型模式 这类模式提供创建对象的机制, 能够提升已有代码的灵活性和可复用性。 1. 工厂模式 Factory Pattern 用来创建对象的设计模式,可以屏蔽对象的创建过程,只需要传入参数即可获取对象。 JDK Calendar

一、创建型模式

这类模式提供创建对象的机制, 能够提升已有代码的灵活性和可复用性。

1. 工厂模式 Factory Pattern

用来创建对象的设计模式,可以屏蔽对象的创建过程,只需要传入参数即可获取对象。

JDK Calendar

import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

Calendar calendar1 = Calendar.getInstance();
Calendar calendar2 = Calendar.getInstance(TimeZone.getTimeZone("GMT+8"));
Calendar calendar3 = Calendar.getInstance(TimeZone.getTimeZone("GMT+8"), Locale.CHINA);

可以根据不同的参数来获取不同的 Calendar 子类,如 GregorianCalendarJapaneseImperialCalendarBuddhistCalendar

2. 构造/建造者模式 Builder Pattern

用来创建对象的设计模式,可以通过链式调用来设置属性。

JDK Calendar

import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

Calendar calendar = new Calendar.Builder()
        .setTimeZone(TimeZone.getTimeZone("GMT+8"))
        .setLocale(Locale.CHINA)
        .setDate(2020, 0, 1)
        .build();

通过内部类的 Builder 来构建对象,可以通过链式调用来设置属性。

3. 单例模式 Singleton Pattern

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

常见做法是将构造函数私有化,然后提供一个静态方法来获取实例,在这个方法中判断实例是否已经创建,如果没有则创建,如果有则直接返回。不过还有通过枚举,静态内部类等方式来实现,篇幅问题这里不再展开,可以参考 从JDK中学习设计模式——单例模式

JDK Runtime

Runtime runtime = Runtime.getRuntime();

看下 Runtime 的源码:

public class Runtime {
    private static final Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

}

4. 原型模式 Prototype Pattern

使你能够复制已有对象,而又无需使代码依赖它们所属的类。

你可能对上面的 "无需使代码依赖它们所属的类" 感到困惑,其实他是指,复制对象时,无需手动创建一个属于相同类的对象,再遍历原始对象的所有成员变量依次设置到新对象中。

JDK Cloneable

JDK 中的 java.lang.Cloneable 接口就是原型模式的一种实现,通过实现这个接口,然后重写 clone 方法来实现原型模式。

public class User implements Cloneable {

    private final String name;

    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

}

测试方法

public class PrototypePatternDemo {

    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User("张三", 18);
        User userClone = (User) user.clone();
        System.out.println(user);
        System.out.println(userClone);
        System.out.println(user == userClone);
    }

}

结果输出

User{name='张三', age=18}
User{name='张三', age=18}
false

从结果可以看出,useruserClone 是两个不同的对象,但是他们的属性是一样的,这就是原型模式的作用。当然你也可以在 clone 方法中手动设置属性,这样就可以实现深拷贝。


二、结构型模式

1. 装饰器模式 Decorator Pattern

不改变原有类的情况下扩展其功能,使用组合代替继承的方式来实现。

JDK Collections.unmodifiableList

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

List<String> list = new ArrayList<>();
List<String> unmodifiableList = Collections.unmodifiableList(list);

对原 List 进行装饰,使其不可修改。原理是底层会创建一个 UnmodifiableList 对象,将原 List 传入,调用所以获取型方法时,会调用原 List 的方法,但是修改方法会抛出异常避免修改。

JDK InputStream

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;

InputStream inputStream = new FileInputStream("file.txt");
InputStream bufferedInputStream = new BufferedInputStream(inputStream);

这里对原 FileInputStream 进行装饰,使其具有缓冲功能。同理也可以自己创建具有加密,限流,监控等功能的装饰器。

2. 适配器模式 Adapter Pattern

当一个类不能直接被使用时,可以通过适配器来转换为另一个类。

JDK FutureTask

FutureTask 构造函数一:传入 Callable

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

FutureTask 构造函数二:传入 Runnable

public FutureTask(Runnable runnable, V result) { // 因为 `FutureTask` 有个 `get` 方法,用来获取结果,但 Runnable 的 run 方法没有返回值,所以需要手动指定一个。
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

主要看这里的 Executors.callable 返回了一个 Callable 类的实例:

public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    // 这里返回的是一个实现了 Callable 的适配器
    return new RunnableAdapter<T>(task, result);
}

再来看看 RunnableAdapter:

// 实现了 Callable
static final class RunnableAdapter<T> implements Callable<T> {
    final Runnable task;
    final T result;
    // 传入 Runnable 接口的实现类和返回值
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        // 在 call 方法中调用 Runnable 的 run 方法,变相实现了 Runnable 转 Callable 的过程.
        task.run();
        return result;
    }
}

3. 外观模式/门面模式 Facade Pattern

创建一个统一的接口,用来屏蔽内部复杂的底层或结构。 这个模式有点笼统,很可能你在开发中已经用到了,但是没有意识到,比如我们使用 Spring MVC 开发的 restful 接口,对外只提供了一个统一的接口,但是内部可能调用了很多服务,数据库,缓存等。

JDK Executors

Executors 提供了一系列工厂方法来创建线程池,如 newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutor 等。

public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>()));
    }
}

Executors 内部其实根据不同的参数创建了不同的线程池,对外提供了统一的接口。其实这个类也算一个工厂模式(静态工厂方法模式)。

4. 代理模式 Proxy Pattern

为对象创建一个代理,你可以操作代理而不是源对象,代理可以对源对象进行访问,并且可以提供额外的功能,如在访问源对象前后进行一些操作。

JDK Proxy

java.lang.reflect.Proxy 提供了创建动态代理的方法,可以根据传入的接口和处理器来创建代理对象。

JDK 动态代理是基于接口的,所以你需要为被代理对象创建一个接口:

interface Image {
    void display();
}

然后有一个实现类,也就是我们的被代理者:

class RealImage implements Image {
    private final String filename;

    public RealImage(String filename) {
        this.filename = filename;
    }

    public void display() {
        System.out.println("Displaying " + filename);
    }
}

再创建一个代理类,实现 InvocationHandler 接口:

class ImageProxy implements InvocationHandler {
    private final Image realImage;

    public ImageProxy(Image realImage) {
        this.realImage = realImage;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("display")) {
            System.out.println("Proxying before displaying");
            realImage.display();
            System.out.println("Proxying after displaying");
        }
        return null;
    }
}

然后演示一下:

public class ProxyPatternDemo {
    public static void main(String[] args) {
        Image realImage = new RealImage("test.jpg");
        Image proxyImage = (Image) Proxy.newProxyInstance(
                realImage.getClass().getClassLoader(),
                realImage.getClass().getInterfaces(),
                new ImageProxy(realImage)
        );

        proxyImage.display();
    }
}

上面我们先创建了一个 RealImage 对象,然后通过 Proxy.newProxyInstance 方法创建了一个代理对象,这个代理对象实现了 Image 接口,然后调用代理对象的 display 方法,实际上是调用了 ImageProxyinvoke 方法,然后在这个方法中调用了 RealImagedisplay 方法。

5. 桥接模式 Bridge Pattern

将抽象部分与它的实现部分分离,使它们都可以独立的变化。

这个模式很抽象,不易理解,其实本质上是为了解决多维度的变化,比如一个类有多个维度的变化,如颜色,形状等,如果使用继承的方式来实现,会导致类的爆炸式增长,所以可以使用桥接模式来解决。

JDK JDBC

这个有人说是桥接模式,但我自己感觉又不太像,有点生搬硬套的感觉,这里也有个博主写了一篇文章大家可以参考下 JDBC和桥接模式

Spring JdbcTemplate

Spring 的 JdbcTemplate 就是一个很好的例子,他将 DataSource 这个接口和 JdbcOperations 这个接口分离开来,DataSource 是用来获取数据库连接的,JdbcOperations 是用来执行 SQL 语句的。

import javax.sql.DataSource;

public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
    public JdbcTemplate(DataSource dataSource) {
        setDataSource(dataSource);
        afterPropertiesSet();
    }
}

如果不这样做,那么每个数据库都需要实现一套 DataSourceJdbcOperations,如有 MySQL,Oracle,PostgreSQL,就需要这样:

- DataSource
  - MySQLDataSource
  - OracleDataSource
  - PostgreSQLDataSource

首先每个数据库都需要实现 DataSource 接口,然后每个数据源都需要实现 JdbcOperations 接口:

- DataSource
  - MySQLDataSource implements JdbcOperations
  - OracleDataSource implements JdbcOperations
  - PostgreSQLDataSource implements JdbcOperations

这样会有很多重复的代码,而且如果有新的数据库,还需要新增一套 DataSourceJdbcOperations

但是现在有了 JdbcTemplate,只需要实现 DataSource 接口,传递给 JDBCTemplate,然后在 JdbcTemplate 中实现 JdbcOperations 接口,这样增加新的数据库时,只需要实现 DataSource 接口即可。

这就对应了 将抽象部分与它的实现部分分离,使它们都可以独立的变化。,现在 DataSource 和 JdbcOperations 都可以独立的变化,功能也是分离的,由 JdbcTemplate 来桥接。

6. 组合模式 Composite Pattern

将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

JDK FileSystem

java.nio.file 包下的文件系统就是一个很好的例子,它将文件和目录都抽象成了 Path 对象,然后可以通过 Files 类来操作这些对象。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileSystemDemo {
    public static void main(String[] args) {
        Path path = Paths.get("file.txt");
        Files.exists(path);
        Files.isDirectory(path);
        Files.isRegularFile(path);
    }
}

JDK File

java.io.File 也是类似的,它将文件和目录都抽象成了 File 对象,然后可以通过 File 类来操作这些对象。

import java.io.File;

public class FileDemo {
    public static void main(String[] args) {
        File file = new File("file.txt");
        file.exists();
        file.isDirectory();
        file.isFile();
    }
}

其实就是对于有共性的两种不同类型的事物,抽象成一个接口,而不是创建两个类,在成员变量中可以存放自己所属的类型,这样就可以实现对于两种不同类型的事物的统一处理。

7. 享元模式 Flyweight Pattern

主要为了减少对象的创建,以用来减少内存占用和提高性能,在不影响程序的情况下可以共享对象来减少对象的创建。

JDK Integer

JDK 的 Integer 类缓存了 -128 到 127 之间的整数,因为这些整数在程序中使用的频率比较高,所以缓存起来可以减少对象的创建。

public final class Integer extends Number implements Comparable<Integer> {
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
}

IntegerCache 类是 Integer 的一个静态内部类,会在 Integer 类加载的时候初始化:

public final class Integer extends Number implements Comparable<Integer> {
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                    sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) - 1);
                } catch (NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for (int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {
        }
    }
}

还有池化技术:线程池连接池 等,这些都是享元模式的应用,它们都是为了减少对象的创建,提高性能。


三、行为模式

1. 模板模式 Template Pattern / 模板方法模式 Template Method Pattern

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。其实就是父类定义一些抽象方法,子类实现这些方法。

JDK Comparable

public interface Comparable<T> {
    int compareTo(T o);
}

Comparable 接口中只有一个抽象方法 compareTo,这个方法的具体实现是由子类来实现的。如 Integer,String 等类都实现了这个接口,以 Integer 为例:

public final class Integer extends Number implements Comparable<Integer> {
    public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }
}

2. 观察者模式 Observer Pattern / 发布订阅模式 Publish-Subscribe Pattern

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。有时也叫做发布-订阅模式。

JDK Observer & Observable

JDK 中已经提供了 ObserverObservable 接口,分别表示观察者和被观察者。

首先被观察者需要实现 Observable 接口:

import java.util.Observable;

class User extends Observable {

    private String name;

    public void setName(String name) {
        this.name = name;
        super.setChanged();           // 调用 Observable 提供的方法,标记状态已经改变
        super.notifyObservers(name);  // 调用 Observable 提供的方法,通知所有观察者
    }
    
}

然后观察者需要实现 Observer 接口:


import java.util.Observer;

class UserObserver implements Observer {

    @Override
    public void update(Observable observable, Object arg) {
        System.out.println("观察到用户的名字已经改变为 {}, {}", observable, arg);
    }
}

最后在需要的地方进行注册和通知:

public static void main(String[] args) {
    User user = new User();
    UserObserver userObserver = new UserObserver();
    user.addObserver(userObserver);
    user.setName("张三");
}

3. 策略模式 Strategy Pattern

定义一系列算法/策略的接口,每个策略都实现这个接口,这样可以很方便的替换算法/策略。

JDK Comparator

jdk 中的 java.util.Comparator 接口就是一种排序策略接口,你可以根据不同的需求实现不同的排序策略,然后调用 Collections.sort 方法时可以传入 Comparator 接口的实现类。

先来看看 Comparator 接口的源码:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

这个接口只有一个抽象方法 compare,传入两个参数,返回一个整数,表示两个对象的大小关系。

4. 责任链模式 Chain of Responsibility Pattern

定义一个处理链,每个处理器都可以处理请求,或交给下一个处理器处理。

JDK Filter

javax.servlet.Filter 就是一种责任链模式,它的 doFilter 方法中可以调用下一个 FilterdoFilter 方法,也可以决定是否调用下一个 FilterdoFilter 方法。

public interface Filter {
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
}

FilterChain 接口:

public interface FilterChain {
    void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException;
}

这样可以定义多个 Filter,使用 FilterChain 来组织成一个责任链并控制调用的顺序,每个 Filter 都可以决定是否调用下一个 Filter

责任链模式在开发中很常用,且有很多变种或增强的版本,如跳链,更加灵活的控制是否调用下一个处理器等。 但是在实际开发中,需要注意责任链模式的性能问题,因为责任链模式的处理是递归的,如果责任链过长,可能会导致栈溢出。 责任链的顺序可以通过给责任链增加注解,增加方法,配置文件等方式来实现,这样可以更加灵活的控制责任链的顺序。

5. 状态模式 State Pattern

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

这里在 JDK 中没有找到合适的例子,我来用一个示例来说明一下:

现在有一个播放器,有 初始化 INITIALIZED待播放 PREPARED播放 PLAYING暂停 PAUSED 四种状态,按钮有 播放暂停,当播放器处于不同的状态时,对应的按钮的行为也是不同的。

  • 当播放器处于 INITIALIZED 状态时,所有按钮都是不可用的,如果点击则提示 "正在初始化,请稍后..."
  • 当播放器处于 PREPARED 状态时,播放 按钮可用,暂停 按钮不可用,如果点击 播放 按钮,则播放器进入 PLAYING 状态,如果点击 暂停 按钮,则提示 "播放器还没有开始播放"
  • 当播放器处于 PLAYING 状态时,播放 按钮不可用,暂停 按钮可用,如果点击 播放 按钮,则提示 "播放器正在播放中",如果点击 暂停 按钮,则播放器进入 PAUSED 状态
  • 当播放器处于 PAUSED 状态时,播放 按钮可用,暂停 按钮不可用,如果点击 播放 按钮,则播放器进入 PLAYING 状态,如果点击 暂停 按钮,则提示 "播放器已经暂停"

先来使用普通的 if-else 来实现:

public class VideoPlayer {

    private String state = INITIALIZED;

    public static final String INITIALIZED = "INITIALIZED";
    public static final String PREPARED = "PREPARED";
    public static final String PLAYING = "PLAYING";
    public static final String PAUSED = "PAUSED";

    public void init() {
        System.out.println("开始初始化");
        state = INITIALIZED;

        // 假如初始化需要一秒钟
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("初始化完成");
        state = PREPARED;
    }

    public void play() {
        if (Objects.equals(state, PREPARED) || Objects.equals(state, PAUSED)) {
            System.out.println("播放");
            state = PLAYING;
        } else if (Objects.equals(state, INITIALIZED)) {
            System.out.println("正在初始化,请稍后...");
        } else if (Objects.equals(state, PLAYING)) {
            System.out.println("播放器正在播放中");
        }
    }

    public void pause() {
        if (Objects.equals(state, PLAYING)) {
            System.out.println("暂停");
            state = PAUSED;
        } else if (Objects.equals(state, INITIALIZED)) {
            System.out.println("正在初始化,请稍后...");
        } else if (Objects.equals(state, PREPARED)) {
            System.out.println("播放器还没有开始播放");
        } else if (Objects.equals(state, PAUSED)) {
            System.out.println("播放器已经暂停");
        }
    }

}

测试类:

public class VideoPlayerTest {

    public static void main(String[] args) {
        VideoPlayer videoPlayer = new VideoPlayer();
        videoPlayer.play();     // Should print "正在初始化,请稍后..."
        videoPlayer.pause();    // Should print "正在初始化,请稍后..."
        videoPlayer.init();     // Should print "开始初始化" and then "初始化完成"

        videoPlayer.pause();    // Should print "播放器还没有开始播放"
        videoPlayer.play();     // Should print "播放"
        videoPlayer.play();     // Should print "播放器正在播放中"
        videoPlayer.pause();    // Should print "暂停"
        videoPlayer.pause();    // Should print "播放器已经暂停"
    }

}

看着晕么(我写的都晕),这么多判断,代码冗长,而且容易出错,这时候就可以使用状态模式来解决这个问题。

首先需要抽象出状态类:

public abstract class State {

    protected Context context;

    public void setContext(Context context){
        this.context = context;
    }

    public abstract void play();

    public abstract void pause();

}

然后每个状态都需要实现这个抽象类:

public class PrearedState extends State {

    @Override
    public void play() {
        context.setState(Context.playingState);
        System.out.println("播放");
    }

    @Override
    public void pause() {
        System.out.println("播放器还没有开始播放");
    }
}
public class InitializedState extends State {

    @Override
    public void play() {
        System.out.println("正在初始化,请稍后...");
    }

    @Override
    public void pause() {
        System.out.println("正在初始化,请稍后...");
    }

}
public class PlayingState extends State {

    @Override
    public void play() {
        System.out.println("播放器正在播放中");
    }

    @Override
    public void pause() {
        context.setState(Context.pausedState);
        System.out.println("暂停");
    }

}
public class PausedState extends State {

    @Override
    public void play() {
        context.setState(Context.playingState);
        System.out.println("播放");
    }

    @Override
    public void pause() {
        System.out.println("播放器已经暂停");
    }
    
}

然后有个上下文对象,来存储当前的状态:

public class Context {

    private State state;

    public final static InitializedState initializedState = new InitializedState();
    public final static PausedState pausedState = new PausedState();
    public final static PlayingState playingState = new PlayingState();
    public final static PrearedState prearedState = new PrearedState();

    public void setState(State state) {
        this.state = state;
        this.state.setContext(this);
    }

    public State getState() {
        return state;
    }

}

然后播放器中有个上下文对象,再根据当前状态类调用对应的方法。

public class VideoPlayer {

    private Context context = new Context();

    public VideoPlayer() {
        context.setState(Context.initializedState);
    }

    public void init() {
        System.out.println("开始初始化");

        // 假如初始化需要一秒钟
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("初始化完成");
        context.setState(Context.prearedState);
    }

    public void play() {
        this.context.getState().play();
    }

    public void pause() {
        this.context.getState().pause();
    }

}

测试类不变:

public class VideoPlayerTest {

    public static void main(String[] args) {
        VideoPlayer videoPlayer = new VideoPlayer();
        videoPlayer.play();     // Should print "正在初始化,请稍后..."
        videoPlayer.pause();    // Should print "正在初始化,请稍后..."
        videoPlayer.init();     // Should print "开始初始化" and then "初始化完成"

        videoPlayer.pause();    // Should print "播放器还没有开始播放"
        videoPlayer.play();     // Should print "播放"
        videoPlayer.play();     // Should print "播放器正在播放中"
        videoPlayer.pause();    // Should print "暂停"
        videoPlayer.pause();    // Should print "播放器已经暂停"
    }

}

6. 命令模式 Command Pattern

这里在 JDK 中没有找到合适的例子,我来用一个示例来说明一下:

先看下命令模式的 UML 图:

命令模式 UML 图

命令模式主要是为了解耦请求的发送者和接收者,请求的发送者不需要知道接收者是谁,只需要知道命令是什么,然后发送给调用者,调用者再调用命令的 execute 方法,然后命令再调用接收者的方法。

比如我们有一个开关,一个台灯,开关不一定只能控制台灯,还可以控制其他的设备。在这里开关是命令的发送者,台灯是命令的接收者,开关不需要知道台灯是谁,只需要知道命令是什么,台灯也不需要知道开关是谁,只需要执行开关操作。

首先定义一个命令接口:

public interface Command {
    void execute();
}

然后定义一个接收者 (Receiver):

public class Light {

    public void on() {
        System.out.println("台灯打开");
    }

    public void off() {
        System.out.println("台灯关闭");
    }

}

然后定义一个开关命令的实现类 (ConcreteCommand):

public class LightOnCommand implements Command {

    private final Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }

}
public class LightOffCommand implements Command {

    private final Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.off();
    }

}

然后定义一个开关 (Invoker):

public class Switch {

    private Command command;

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

    public void execute() {
        command.execute();
    }

}

测试类 (Client):

public class CommandPatternDemo {

    public static void main(String[] args) {
        Light light = new Light();
        Command lightOnCommand = new LightOnCommand(light);
        Command lightOffCommand = new LightOffCommand(light);

        Switch s = new Switch();

        s.setCommand(lightOnCommand);
        s.execute();    // Should print "台灯打开"

        s.setCommand(lightOffCommand);
        s.execute();    // Should print "台灯打开"
    }

}

看完了这个示例,你可能会觉得这个示例有点简单,甚至有点多此一举,本来可以直接创建一个台灯对象存储起来,然后直接调用台灯的 onoff 方法,为什么要多此一举呢?

public class CommandPatternDemo {

    public static void main(String[] args) {
        Light light = new Light();

        light.on();    // Should print "台灯打开"
        light.off();   // Should print "台灯关闭"
    }

}

不过我们再深入想想,如果你有很多地方需要控制台灯,那么你就需要在每个地方都创建一个台灯对象,然后调用台灯的 onoff 方法。 但如果我们需要增加个定时关闭的功能,如果直接调用台灯的 off 方法,那么你就需要在每个地方都修改代码增加个定时器来实现。或者需要增加个调用日志,定时开启等功能,同理也需要在每个地方都修改代码。

JDK 中本来想找个 Runnable 来举例,但是发现 Runnable 并不严格符合命令模式,他没有接收者,只有命令和调用者.

参考: 详解命令模式本质及其在高复杂调用中的实践案例-阿里技术

7. 迭代器模式 Iterator Pattern

提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示 (列表、栈、数组)。

JDK Iterator

java.util.Iterator 就是一个很好的例子,它提供了一种顺序访问集合的方法,而不暴露集合的内部表示。

public interface Iterator<E> {
    
    boolean hasNext();
    
    E next();
}

Collection 接口就实现了 Iterator 接口,我们平时使用的 ArrayListLinkedList 等都实现了 Collection 接口,所以我们可以使用 Iterator 来遍历这些集合。

public class IteratorPatternDemo {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张三");
        list.add("李四");
        list.add("王五");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

}

其实迭代器模式就是把迭代这个操作抽象出来,然后让集合类实现这个接口。不然的话,每个集合类都自己有一个迭代方法的话,可能方法名都不一样,调用时会很麻烦,也会有很多重复的代码。 而且根据需要我们还可以在迭代器中增加一些方法,如 previousfirstlast 等方法,这样就可以实现双向迭代。

8. 备忘录模式 Memento Pattern

允许不暴露对象实现细节的情况下保存和恢复对象之前的状态。

这个模式在 JDK 中没有找到合适的例子,我来用一个示例来说明一下:

比如我们要保存一个对象的历史状态/快照,可以随时恢复。首先如何给对象创建一个快照呢?可能你会遍历对象的成员变量并将其复制到一个新的对象中,但对象中有些成员变量可能是 private 的,你无法直接访问到它们。而且当你想恢复到某个状态时,就要修改原始对象的成员变量为快照时的状态,这样也会碰到私有成员变量的问题。如果设置成 public 的话,又会暴露对象的实现细节,无法保证对象的封装性。

所以备忘录模式其实就是为了解决这个两个问题,解决的方式也很简单:首先原始对象,也就是需要保存快照的对象称之为原发器 (Originator) 对象,创建快照和恢复快照的方法都放到这个对象中,这样其他对象就不用从该对象内部复制成员变量了,他可以自己读取自己的状态并保存到一个快照对象中。

public class Originator {

    private String state;
    
    public void setState(String state) {
        this.state = state;
    }
    
    public String getState() {
        return state;
    }

    // 生成一个快照对象(里面包含了当前对象需要记录的状态)
    public Memento save() {
        return new Memento(state);
    }

    // 从快照对象中恢复状态
    public void restore(Memento memento) {
        state = memento.getState();
    }

}

其中 Memento 就是备忘录对象,它保存了原发器对象的状态,但是不暴露原发器对象的实现细节。

public class Memento {

    private final String state;

    public Memento(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

}

这些 Mememto 还需要保存到一个容器中,这里称之为管理者 (Caretaker) 对象,它保存了备忘录对象,但是不会修改备忘录对象。

public class Caretaker {

    private final List<Memento> mementos = new ArrayList<>();

    public void add(Memento memento) {
        mementos.add(memento);
    }

    public Memento get(int index) {
        return mementos.get(index);
    }

}

测试类:

public class MementoPatternDemo {

    public static void main(String[] args) {
        Originator originator = new Originator();
        Caretaker caretaker = new Caretaker();

        originator.setState("State #1");
        caretaker.add(originator.save());
        System.out.println("current State: " + originator.getState());
        
        originator.setState("State #2");
        caretaker.add(originator.save());
        System.out.println("current State: " + originator.getState());

        originator.setState("State #3");
        caretaker.add(originator.save());
        System.out.println("current State: " + originator.getState());
 
        originator.restore(caretaker.get(0));
        System.out.println("First saved State: " + originator.getState());
        
        originator.restore(caretaker.get(1));
        System.out.println("Second saved State: " + originator.getState());
    }

}

这个模式其实核心就是为了不暴露对象实现细节,将一些职责放到了原发器对象中,这样就可以保证对象的封装性。

9. 访问者模式 Visitor Pattern

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新操作。

10. 中介者模式 Mediator Pattern

用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显示的相互引用,从而使其耦合松散,而且可以独立的改变他们之间的交互。

Spring ApplicationEvent

Spring 中 ApplicationEvent 也可以认为是一个中介者,用来封装一系列的对象交互,当一个对象发生变化时,可以发布一个事件,然后其他对象可以监听这个事件,从而实现对象之间的交互。

具体可以参考 Spring 使用事件监听来解耦代码,这里不再赘述。

自实现中介者模式

如有一个数据同步的功能,有三个数据源,分别是 MySQL,Redis,ES,同步策略如下:

  • MySQL 数据源发生变化时,需要同步到 Redis 和 ES
  • Redis 数据源发生变化时,无需同步
  • ES 数据源发生变化时,需要同步到 MySQL

使用中介模式来实现:

首先定义一个数据库抽象类:

public abstract class AbstractDatabase {

    protected AbstractMediator mediator;    // 中介者

    public AbstractDatabase(AbstractMediator mediator) {
        this.mediator = mediator;
    }

    // 仅添加数据
    public abstract void addData(String data);

    // 添加数据并按策略同步
    public abstract void addDataAndSync(String data);

}

然后定义三个数据库的实现类:

public class EsDatabase extends AbstractDatabase {

    private final List<String> dataset = new ArrayList<>();

    public EsDatabase(AbstractMediator mediator) {
        super(mediator);
    }

    @Override
    public void addData(String data) {
        System.out.println("ES 添加数据:" + data);
        this.dataset.add(data);
    }

    @Override
    public void addDataAndSync(String data) {
        addData(data);
        this.mediator.sync(this, data);    // 数据同步作业交给中介者管理
    }

    public void count() {
        int count = this.dataset.size();
        System.out.println("Elasticsearch 统计,目前有 " + count + " 条数据,数据:" + this.dataset.toString());
    }

}
public class MysqlDatabase extends AbstractDatabase {
    private List<String> dataset = new ArrayList<>();

    public MysqlDatabase(AbstractMediator mediator) {
        super(mediator);
    }

    @Override
    public void addData(String data) {
        System.out.println("Mysql 添加数据:" + data);
        this.dataset.add(data);
    }

    @Override
    public void addDataAndSync(String data) {
        addData(data);
        this.mediator.sync(this, data); // 数据同步作业交给中介者管理
    }

    public void select() {
        System.out.println("Mysql 查询,数据:" + this.dataset.toString());
    }
}
public class RedisDatabase extends AbstractDatabase {
    private List<String> dataset = new ArrayList<>();

    public RedisDatabase(AbstractMediator mediator) {
        super(mediator);
    }

    @Override
    public void addData(String data) {
        System.out.println("Redis 添加数据:" + data);
        this.dataset.add(data);
    }

    @Override
    public void addDataAndSync(String data) {
        addData(data);
        this.mediator.sync(this, data);    // 数据同步作业交给中介者管理
    }

    public void cache() {
        System.out.println("Redis 缓存的数据:" + this.dataset.toString());
    }
}

然后定义一个中介者接口:

@Data
public abstract class AbstractMediator {

    protected MysqlDatabase mysqlDatabase;

    protected RedisDatabase redisDatabase;

    protected EsDatabase esDatabase;

    public abstract void sync(AbstractDatabase sender, String data);

}

然后定义一个中介者的实现类:

public class SyncMediator extends AbstractMediator {
    @Override
    public void sync(AbstractDatabase sender, String data) {
        if (sender instanceof MysqlDatabase) {
            // mysql 同步到 redis 和 Elasticsearch
            this.redisDatabase.addData(data);
            this.esDatabase.addData(data);
        } else if (sender instanceof RedisDatabase) {
            // redis 缓存同步,不需要同步到其他数据库
        } else if (sender instanceof EsDatabase) {
            // Elasticsearch 同步到 Mysql
            this.mysqlDatabase.addData(data);
        }
    }
}

测试类:

public class Client {
    public static void main(String[] args) {
        AbstractMediator syncMediator = new SyncMediator();

        MysqlDatabase mysqlDatabase = new MysqlDatabase(syncMediator);
        RedisDatabase redisDatabase = new RedisDatabase(syncMediator);
        EsDatabase esDatabase = new EsDatabase(syncMediator);

        syncMediator.setMysqlDatabase(mysqlDatabase);
        syncMediator.setRedisDatabase(redisDatabase);
        syncMediator.setEsDatabase(esDatabase);

        mysqlDatabase.addDataAndSync("1");  // mysql 添加数据 1,将同步到Redis和ES中
        printCurrentData(mysqlDatabase, redisDatabase, esDatabase);

        redisDatabase.addDataAndSync("2");  // Redis添加数据 2,将不同步到其它数据库
        printCurrentData(mysqlDatabase, redisDatabase, esDatabase);

        esDatabase.addDataAndSync("3");     // ES 添加数据 3,只同步到 Mysql
        printCurrentData(mysqlDatabase, redisDatabase, esDatabase);
    }

    private static void printCurrentData(MysqlDatabase mysqlDatabase, RedisDatabase redisDatabase, EsDatabase esDatabase) {
        mysqlDatabase.select();
        redisDatabase.cache();
        esDatabase.count();
    }
}

从更抽象的层面看,其实就是 N 个组件需要相互调用,但是这样直接调用会导致耦合度过高,调用关系复杂,所以可以使用中介者模式来解决这个问题。 扩展:之前参加某个项目,其中有个组件叫 EBS (Event Bus Service),各个系统之间的接口不允许互相调用,只能通过 EBS 来调用,这也是一种中介者模式的应用。 但是中介者模式也有缺点,因为所有的交互都集中在了中介者中,中介者会变得很复杂。

四、参考