Retrofit源码设计模式解析(下)

本文将接着《Retrofit源码设计模式解析(上)》,继续分享以下设计模式在Retrofit中的应用:

  1. 适配器模式
  2. 策略模式
  3. 观察者模式
  4. 单例模式
  5. 原型模式
  6. 享元模式

一、适配器模式

在上篇说明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网络请求,敬请关注……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值