谈谈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)

posted @ 2020-11-24 21:47  d1zzyboy  阅读(202)  评论(0编辑  收藏  举报