从 JDK 源码中学习设计模式
一、创建型模式
这类模式提供创建对象的机制, 能够提升已有代码的灵活性和可复用性。
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 子类,如 GregorianCalendar,JapaneseImperialCalendar,BuddhistCalendar
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从结果可以看出,user 和 userClone 是两个不同的对象,但是他们的属性是一样的,这就是原型模式的作用。当然你也可以在 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 提供了一系列工厂方法来创建线程池,如 newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor 等。
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 方法,实际上是调用了 ImageProxy 的 invoke 方法,然后在这个方法中调用了 RealImage 的 display 方法。
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();
}
}如果不这样做,那么每个数据库都需要实现一套 DataSource 和 JdbcOperations,如有 MySQL,Oracle,PostgreSQL,就需要这样:
- DataSource
- MySQLDataSource
- OracleDataSource
- PostgreSQLDataSource首先每个数据库都需要实现 DataSource 接口,然后每个数据源都需要实现 JdbcOperations 接口:
- DataSource
- MySQLDataSource implements JdbcOperations
- OracleDataSource implements JdbcOperations
- PostgreSQLDataSource implements JdbcOperations这样会有很多重复的代码,而且如果有新的数据库,还需要新增一套 DataSource 和 JdbcOperations。
但是现在有了 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 中已经提供了 Observer 和 Observable 接口,分别表示观察者和被观察者。
首先被观察者需要实现 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 方法中可以调用下一个 Filter 的 doFilter 方法,也可以决定是否调用下一个 Filter 的 doFilter 方法。
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 图:

命令模式主要是为了解耦请求的发送者和接收者,请求的发送者不需要知道接收者是谁,只需要知道命令是什么,然后发送给调用者,调用者再调用命令的 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 "台灯打开"
}
}看完了这个示例,你可能会觉得这个示例有点简单,甚至有点多此一举,本来可以直接创建一个台灯对象存储起来,然后直接调用台灯的 on 和 off 方法,为什么要多此一举呢?
public class CommandPatternDemo {
public static void main(String[] args) {
Light light = new Light();
light.on(); // Should print "台灯打开"
light.off(); // Should print "台灯关闭"
}
}不过我们再深入想想,如果你有很多地方需要控制台灯,那么你就需要在每个地方都创建一个台灯对象,然后调用台灯的 on 和 off 方法。 但如果我们需要增加个定时关闭的功能,如果直接调用台灯的 off 方法,那么你就需要在每个地方都修改代码增加个定时器来实现。或者需要增加个调用日志,定时开启等功能,同理也需要在每个地方都修改代码。
JDK 中本来想找个
Runnable来举例,但是发现Runnable并不严格符合命令模式,他没有接收者,只有命令和调用者.
7. 迭代器模式 Iterator Pattern
提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示 (列表、栈、数组)。
JDK Iterator
java.util.Iterator 就是一个很好的例子,它提供了一种顺序访问集合的方法,而不暴露集合的内部表示。
public interface Iterator<E> {
boolean hasNext();
E next();
}Collection 接口就实现了 Iterator 接口,我们平时使用的 ArrayList,LinkedList 等都实现了 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());
}
}
}其实迭代器模式就是把迭代这个操作抽象出来,然后让集合类实现这个接口。不然的话,每个集合类都自己有一个迭代方法的话,可能方法名都不一样,调用时会很麻烦,也会有很多重复的代码。 而且根据需要我们还可以在迭代器中增加一些方法,如 previous,first,last 等方法,这样就可以实现双向迭代。
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 来调用,这也是一种中介者模式的应用。 但是中介者模式也有缺点,因为所有的交互都集中在了中介者中,中介者会变得很复杂。