本文将接着《Retrofit源码设计模式解析(上)》,继续分享以下设计模式在Retrofit中的应用:
- 适配器模式
- 策略模式
- 观察者模式
- 单例模式
- 原型模式
- 享元模式
一、适配器模式
在上篇说明CallAdapter.Factory使用工厂模式时,提到CallAdapter本身采用了适配器模式。适配器模式将一个接口转换成客户端希望的另一个接口,使接口本不兼容的类可以一起工作。
Call接口是Retrofit内置的发送请求给服务器并且返回响应体的调用接口,包括同步、异步请求,查询、取消、复制等功能。
public interface Call<T> extends Cloneable { // 同步执行请求 Response<T> execute() throws IOException; // 异步执行请求 void enqueue(Callback<T> callback); // 省略代码 // 取消请求 void cancel(); // 复制请求 Call<T> clone(); }
而客户端可能希望更适合业务逻辑的接口回调,比如响应式的接口回调。那么,就需要对Call进行转换,CallAdapter就上场了。CallAdapter包含两个方法:
public interface CallAdapter<T> { // 返回请求后,转换的参数Type类型 Type responseType(); // 接口适配 <R> T adapt(Call<R> call); }
如果客户端没有配置CallAdapter,Retrofit会采用默认的实现DefaultCallAdapterFactory直接返回Call对象,而如果配置了RxJava的RxJavaCallAdapterFactory实现,就会将Call<R>转换为Observable<R>,供客户端调用。
static final class SimpleCallAdapter implements CallAdapter<Observable<?>> { // 省略代码 @Override public <R> Observable<R> adapt(Call<R> call) { Observable<R> observable = Observable.create(new CallOnSubscribe<>(call)) .lift(OperatorMapResponseToBodyOrError.<R>instance()); if (scheduler != null) { return observable.subscribeOn(scheduler); } return observable; } }
总结下,适配器模式包含四种角色:
- Target:目标抽象类
- Adapter:适配器类
- Adaptee:适配者类
- Client:客户端类
CallAdapter对应Target,其adapt方法返回客户端类Client需要的对象;RxJavaCallAdapterFactory的get方法返回SimpleCallAdapter对象(或ResultCallAdapter对象)实现了CallAdapter<Observable<?>>,对应Adapter;Call<R>对应Adaptee适配者类,包含需要被适配的方法。
另外,适配器模式有对象适配器和类适配器两种实现。类适配器中的Adapter需要继承自Adaptee,对象适配则是采用复合的方式,Adapter持有Adaptee的引用。类适配器模式会使Adaptee的方法暴露给Adapter,根据“复合优先于继承”的思想,推荐使用对象适配器模式。
值得说明的是,这里SimpleCallAdapter并没有通过域的方式持有Call<R>,而是直接在CallAdapter的get方法中将Call<R>以入参形式传入。虽然并不是教科书式的对象适配器模式,但使用却更加灵活、方便。
二、策略模式
完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。针对这种情况,一种常规的做法是将多个策略写在一个类中,通过if…else或者switch等条件判断语句来选择具体的算法。这种方式实现简单、快捷,但维护成本很高,当添加新的策略时,需要修改源代码,这违背了开闭原则和单一原则。仍以CallAdapter为例,不同的CallAdapter代表着不同的策略,当我们调用这些不同的适配器的方法时,就能得到不同的结果,这就是策略模式。策略模式包含三种角色:
- Context上下文环境——区别于Android的Context,这里代表操作策略的上下文;
- Stragety抽象策略——即不同策略需要实现的方法;
- ConcreteStragety策略实现——实现Stragety抽象策略。
在Retrofit中,配置Retrofit.Builder时addCallAdapterFactory,配置的类就对应Context;不同的CallAdapter都需要提供adapt方法,CallAdapter<T>就对应Stragety抽象策略。RxJavaCallAdapterFactory的get方法返回SimpleCallAdapter对象(或ResultCallAdapter对象)就对应具体的策略实现。
这里可能会跟上篇中的工厂模式搞混,在说明工厂模式时,主要是强调的是:
public abstract CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit);
通过get方法返回不同的CallAdapter对象;策略模式强调的是这些不同CallAdapter对象的adapt方法的具体实现。
<R> T adapt(Call<R> call);
总结下:工厂模式强调的是生产不同的对象,策略模式强调的是这些不同对象的策略方法的具体实现,是在创建对象之后。
三、观察者模式
建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。在此,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展,这就是观察者模式的模式动机。
举个栗子:在Android编程中,常见的一种情况是界面上某个控件的状态对其它控件有约束关系,比如,需要根据某个EditText的输入值决定某个按钮是否可以点击,就需要此EditText是可观测的对象,而按钮是EditText的观测者,当EditText状态发生改变时,按钮进行相应的操作。
观察者模式包含四种角色:
- Subject抽象主题——也就是被观察对象,Observable是JDK中内置的类(java.util.Observable),当需要定义被观察对象时,继承自Observable即可;
- ConcreteSubject具体主题——具体被观察者,可以继承Observable实现,需要通知观察者时,调用notifyObservers;
- Observer抽象观察者——Observer也是JDK内置的,定义了update方法;
- ConcreteObserver具体观察者——实现Observer接口定义的update方法,以便在状态发生变化时更新自己。
public interface Observer { void update(Observable observable, Object data); }
public class Observable { List<Observer> observers = new ArrayList<Observer>(); // 省略代码 public void notifyObservers(Object data) { int size = 0; Observer[] arrays = null; synchronized (this) { if (hasChanged()) { clearChanged(); size = observers.size(); arrays = new Observer[size]; observers.toArray(arrays); } } if (arrays != null) { for (Observer observer : arrays) { observer.update(this, data); } } } }
所有与网络请求相关的库一定会支持请求的异步发送,通过在库内部维护一个队列,将请求添加到该队列,同时注册一个回调接口,以便执行引擎完成该请求后,将请求结果进行回调。Retrofit也不例外,Retrofit的网络请求执行引擎是OkHttp,请求类是OkHttpCall,其实现了Call接口,enqueue方法如下,入参为Callback对象。
void enqueue(Callback<T> callback);
在OkHttpCall的enqueue实现方法中,通过在okhttp3.Callback()的回调方法中调用上述入参Callback对象的方法,实现通知观察者。
@Override public void enqueue(final Callback<T> callback) { // 省略代码 call.enqueue(new okhttp3.Callback() { @Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) throws IOException { Response<T> response; try { response = parseResponse(rawResponse); } catch (Throwable e) { callFailure(e); return; } callSuccess(response); } @Override public void onFailure(okhttp3.Call call, IOException e) { try { callback.onFailure(OkHttpCall.this, e); } catch (Throwable t) { t.printStackTrace(); } }
private void callSuccess(Response<T> response) { try { callback.onResponse(OkHttpCall.this, response); } catch (Throwable t) { t.printStackTrace(); } }
总结下:Call接口对应Subject,定义被观察者的特性,包含enqueue等;OkHttpCall对应ConcreteSubject具体被观察者,Callback对应Observer抽象观察者,Callback的实现类对应ConcreteObserver具体观察者。
四、单例模式
单例模式可能是所有设计模式教程的第一个讲到的模式,也是应用最广泛的模式之一。Retrofit中也使用了大量的单例模式,比如BuiltInConverters的responseBodyConverter、requestBodyConverter等,并且使用了饿汉式的单例模式。由于这种单例模式应用最广,也是大家都清楚的,本节将扩展下单例模式的其它实现方式:
懒汉式单例模式:
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
懒汉单例模式的优点是单例只要有在使用是才被实例化,缺点是美的调用getInstance都进行同步,造成不必要的同步开销。
DCL(Double Check Lock):
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
DCL是对懒汉单例模式的升级,getInstance方法对instance进行了两次判空,第一层判断是为了避免不必要的同步,第二层判断是为了在null时创建实例,这里涉及到对象实例化过程的原子问题。在Java中,创建对象并非原子操作,而是包含分配内存、初始化成员字段、引用指向等一连串操作,而多线程环境下,由于指令重排序的存在,初始化指令和引用指令可能是颠倒,那么可能当线程执行第一个判断不为null返回的对象,却是未经初始化的(别的对象创建Singleton时,初始化指令和引用指令颠倒了)。
静态内部类:
public class Singleton { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.instance; } private static class SingletonHolder { private static final Singleton instance = new Singleton(); } }
上述DCL也是可能失效的,具体可参考《有关“双重检查锁定失效”的说明》。采用静态内部类,加载Singleton类时并不会初始化instance,同时也能保证线程安全,单例对象的唯一性。
枚举单例:
public enum Singleton { INSTANCE; }
枚举实例的创建默认是线程安全的,并且在任何情况下都只有一个实例。上述单例模式存在反序列化会重新创建对象的情况,而枚举不存在这个问题。但Android编程中,因为性能问题,不推荐使用枚举,所以,这种比较怪异的方式并不推荐。
使用容器实现单例模式:
public class Singleton { private static Map<String, Object> objectMap = new HashMap<>(); public static void addObject(String key, Object instance) { if (!objectMap.containsKey(key)) { objectMap.put(key, instance); } } public static Object getObject(String key) { return objectMap.get(key); } }
严格的讲,这并不是标准的单例模式,但确实实现了单例的效果。
单例的核心原理是将构造函数私有化,通过静态方法获取唯一实例。而怎么获取唯一实例?在Java中可能存在线程安全、反序列化等问题,因此衍生出上述这几个版本。在实际使用时需要根据并发环境、JDK版本以及资源消耗等因素综合考虑。
五、原型模式
原型模式是一种创建型模式,主要用于对象复制。使用原型模式创建对象比直接new一个对象在性能上要好的多,因为Object类的clone方法是一个本地方法,它直接操作内存中的二进制流。使用原型模式的另一个好处是简化对象的创建,使得创建对象就像在编辑文档时的复制粘贴。基于以上优点,在需要重复地创建相似对象时可以考虑使用原型模式。比如需要在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多。
原型模式有三种角色:
- Client客户端;
- Prototype原型——一般表现为抽象类或者接口,比如JDK中的Cloneable接口;
- ConcretePrototype具体原型类——实现了Prototype原型。
OkHttpCall实现了Call接口,Call接口继承自Cloneable,OkHttpCall的clone方法实现如下:
@Override public OkHttpCall<T> clone() { return new OkHttpCall<>(serviceMethod, args); }
clone的实现就是重新new了一个一样的对象,用于其他地方重用相同的Call,在ExecutorCallbackCall中有用到:
static final class ExecutorCallbackCall<T> implements Call<T> { // 省略代码 @SuppressWarnings("CloneDoesntCallSuperClone") // Performing deep clone. @Override public Call<T> clone() { return new ExecutorCallbackCall<>(callbackExecutor, delegate.clone()); } }
使用原型模式复制对象需要主要深拷贝与浅拷贝的问题。Object类的clone方法只会拷贝对象中的基本的数据类型,对于数组、容器对象、引用对象等都不会拷贝,这就是浅拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。
六、享元模式
享元模式是对象池的一种实现,运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式(Flyweight),它是一种对象结构型模式。
享元模式包含三种角色:
- Flyweight享元基类或接口;
- ConcreteFlyweight具体的享元对象;
- FlyweightFactory享元工厂——负责管理享元对象池和创建享元对象。
Retrofit中create方法创建ServiceMethod是通过loadServiceMethod方法实现。loadServiceMethod方法就实现了享元模式。
private final Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>(); ServiceMethod loadServiceMethod(Method method) { ServiceMethod result; synchronized (serviceMethodCache) { result = serviceMethodCache.get(method); if (result == null) { result = new ServiceMethod.Builder(this, method).build(); serviceMethodCache.put(method, result); } } return result; }
上篇讲到代理模式的时候,提到了这个方法的缓存使用了LinkedHashMap,系统中的Method接口数相对于请求次数是有数量级差距的,把这些接口的信息缓存起来是非常有必要的一个优化手段,这样的实现方式就是享元模式。
在享元模式中共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也称为细粒度对象。享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。在经典享元模式中,它的键是享元对象的内部状态,它的值就是享元对象本身。上述serviceMethodCache的key是method,value是ServiceMethod,method就是ServiceMethod的内部状态。
总结:Retrofit不愧是大师之作,设计模式的经典教程。其源码量并不大,但系统的可扩展性、可维护性极强,是客户端架构设计的典范,非常值得学习,五星推荐!
(后续笔者会分享,在Retrofit基础上封装更符合业务需求的Android网络请求,敬请关注……)