谈谈Java常用类库中的设计模式 - Part Ⅲ
概述
本文介绍的设计模式:
策略
观察者
代理
相关缩写:EJ - Effective Java
Here We Go
策略 (Stragety)
定义:定义算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。
场景:当不同的行为堆砌在一个类中,难以避免使用条件语句选择行为时,将这些行为封装在独立的策略类中,可以消除条件语句;一个系统需要动态地选择一种算法时。
类型:行为型
相比之前提到的设计模式,策略的使用更加广泛,它简单直观,作用强大,只要业务中存在同一场景会有不同的处理规则,就可以用到策略。
在Java类库中使用最频繁的策略当属Comparator。
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
Comparator的角色是抽象策略(abstract stragety),而实现了Comparator的具体排序规则就是具体策略(concrete strategy)。
在JDK 1.8的Comparator中还实现了诸多缺省方法,例如翻转排序、空值优先、排序规则链等,这里使用了模版方法的思想。
在客户端代码中实现好比较算法,剩下的排序工作,交给类库去做即可。
Collections.sort(studentList, new Comparator<Student>() {
@Override
public int compare(Student a, Student b) {
return a.getScore()-b.getScore();
}
});
当然在实际生产中使用更简便易读的comparing方法,岂不美哉?
Collections.sort(studentList,Comparator.comparing(Student::getScore));
除了Comparator,还有一个典型案例:线程池的饱和策略。以下是带有指定饱和策略的线程池构造方法(最后一个入参RejectedExecutionHandler handler
就是指定饱和策略)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
饱和策略将在有界队列被填满后触发,我们无需关心策略的具体调用,只需专注于如何设计策略使得线程池更加健壮。以下是ThreadPoolExecutor提供的四种预设策略中的抛弃最旧
策略:
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardOldestPolicy} for the given executor.
*/
public DiscardOldestPolicy() { }
/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
观察者 (Observer)
定义:定义一种一对多的依赖关系,让多个观察者对象同时监听某一主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
场景:当一个对象的改变需要同时改变其它对象的时候。
类型:行为型
观察者有一个更著名的名字:发布-订阅模型(Pub/Sub),在Redis、MQ中的使用非常广泛。实际上JDK在1.0版本就编写了观察者的支持类。
观察者需要实现Observer
接口,在主题通知时将调用updae方法更新观察者自己的状态。
public interface Observer {
void update(Observable o, Object arg);
}
主题对象已经被JDK实现,被观察者发生变化时调用notifyObservers
通知观察者,观察者队列由系统维护。(类库开发较早且没有维护,观察者集合仍然使用Vector。)
public class Observable {
private boolean changed = false;
private Vector<Observer> obs;
public Observable() {
obs = new Vector<>();
}
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
public void notifyObservers(Object arg) {
/*
* a temporary array buffer, used as a snapshot of the state of
* current Observers.
*/
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
...省略其它方法
}
值得一提的是,通知观察者的方法notifyObservers
在尽力缩小锁粒度:因为观察者执行变更的代码主题对象无法控制,此段时间不需要也不应该持有主题对象的锁,于是这里加锁复制了一个当前观察者队列的快照,在执行通知前释放锁。遵循了避免过渡同步[EJ Item 79]的原则。
代理 (Proxy)
定义:为其它对象提供一种代理以控制对这个对象的访问。
场景:为一个对象在不同的地址空间提供局部代表,隐藏一个对象存在于不同地址空间的事实;控制真实对象访问时的权限;调用真实对象时,代理可以附加其它逻辑。
类型:结构型
代理的实现与装饰器的实现:复合-转发 基本相同,需要代理类继承公共接口,并持有被代理类的实例进行转发。所以二者在结构和功能上非常相似,但比起追加功能,代理强调的是通信控制与屏蔽。
谈到Java类库中对代理的实践,那必然是动态代理了,实际上动态代理比代理更加先进:代理本身是结构型设计模式,但在反射的加持下,开发者可以将代理结构延迟到运行时动态构建,这就是动态代理。
List<Integer> trueList = Collections.emptyList();
//创建一个代理类,实现特定接口,并将调用分发到指定的调用处理器上
List proxyList = (List) Proxy.newProxyInstance(List.class.getClassLoader(),
new Class[]{List.class}, (proxy, method, args) -> {
System.out.println("enter invoke handler");
return method.invoke(trueList,args);
});
System.out.println(proxyList.size());
输出
----------------
enter invoke handler
0
通过Proxy.newProxyInstance
即可创建代理类,代理类实现了指定接口,并且任何方法调用将被分发到InvocationHandler
上,它持有真正被代理的对象,在执行完代理操作后,便可以将调用转发到真实对象上。
许多成熟框架都使用了动态代理来增强用户代码,例如Mybatis的MapperProxy,SpringAOP等等。
参考:
[1] Effective Java - 机械工业出版社 - Joshua Bloch (2017/11)
[2] 《大话设计模式》 - 清华大学出版社 - 陈杰 (2007/12)