设计模式与架构之美--Retrofit的那点事

前言

Retrofit 是一个 RESTful风格的 HTTP 网络请求封装框架,通过Retrofit使用,复杂的网络请求可以通过几行代码就实现,大大提高了开发人员的开发调试效率。除了使用方便外,Retrofit最值得称道的地方是集设计模式于一身的特点以及高可扩展性的设计架构思想,更值得我们去探究和学习。在看过很多Android源码库,Okhttp,EventBus,ButterKnift,LiteOrm,AndFix等一些常见库后,这些库更偏向于功能的实现,对设计架构和设计模式并不能很好的诠释(当然OK里面网络请求责任链的设计模式也很经典),Retrofit则除了实现基本的网络请求封装功能(不负责网络请求)外,更多的是网络封装框架的搭建,更多的是偏向于抽象编程,更适合设计模式与架构的探究。

那什么是设计模式?什么是架构呢?设计模式不是工具,它是软件开发的哲学,它能指导你如何去设计一个优秀的架构、编写一段健壮的代码、解决一个复杂的需求。架构则是软件开发设计的基本框架,设计模式之禅里面说的非常经典一句话,好的架构能持续地拥抱变化(也就是符合OCP原则,通过扩展去拥抱变化,而不是修改)。要知道变是永恒的主题,特别是在软件开发需求迭代时候,需求变化是高频率的事件,没有不变化的需求。所以我们在开发中更应该设计好软件的架构,应该去阅读更多优秀源码,掌握更多的设计模式和架构的设计。下面我们从基本使用一步一步剖析Retrofit是如何保持高扩展性,如果能持续拥抱变化…

使用对比与分析

1.请求方式一,在没有使用Retrofit时,OKHttp的请求

//1.创建RequestBody
RequestBody requestBody = new FormBody.Builder()
                .add("phoneNo", phoneNo)
                .add("pwd", pwd)
                .build();

//2.构造Request
Request req = new Request.Builder()
                .url("www.xxx.com")
                .post(requestBody)
                .build();
//3.构造一个HttpClient
  OkHttpClient client = new OkHttpClient();
//4. 发送请求
client.newCall(req)
                .enqueue(new Callback() {
                    //获得服务器返回的Response数据
                    @Override
                    public void onResponse(Call arg0, Response response) throws IOException {
                        //获取响应体
                        ResponseBody body = response.body();
                        //响应体获取到string
                        String requestBody = body.string();
                        //5.通过gson解析器解析成对应的对象实体
                        Object o = new Gson().fromJson(requestBody, Object.class);
                        //6. 处理 还会有线程切换 省略很多代码....

                    }

                    @Override
                    public void onFailure(Call arg0, IOException arg1) {
                        // TODO Auto-generated method stub

                    }
                });


2.请求方式二,使用Retrofit,OKHttp的请求


//1.创建请求接口
//##ApiService.java
//用户登录
@FormUrlEncoded
@POST("user/login.do")
Observable<String> login(@Field("phoneNo") String phoneNo,
                           @Field("pwd") String pwd);

//2.创建请求接口
//##xxxModel.java
ApiManager.getInstance().initRetrofit().create(ApiService.class).login(phoneNo, pwd)
              .subscribeOn(Schedulers.io())
              .observeOn(AndroidSchedulers.mainThread())
              .subscribe(new Consumer<String>() {
                  @Override
                  public void accept(String s) throws Exception {
                     //请求成功后的回调
                  }
              }, new Consumer<Throwable>() {
                  @Override
                  public void accept(Throwable throwable) throws Exception {
                       //请求异常后的回调
                  }
              });   
//注:Okhttp和Retrofit的初始化没有写上去,因为一般全局初始化一次就可以了,后面都不用q去写,不影响代码量的规模                       

咋一看,在使用和无使用retrofit的两种请求方式区别不大,代码量没有明显减少和简洁。其实不然,每次新加一个请求接口,在没有使用retrofit时候,基本上会重复上面的所有步骤;而在使用retrofit后,只需要定义新的接口,然后进行事件订阅异步请求即可。在一个比较大规模的app,比如咱们丰巢管家app,有100多个接口,可以减少上万行代码量,而且方式二的写法更加便于维护,我们对比两种请求方式,Retrofit的工作只是将方式二的代码写法转换成了方式一的写法,其实Retrofit是不进行网络请求逻辑,只是对请求接口进行了封装而已,只是在Okhttp上加了一层协议封装层,可以说是基于网络应用层之上对网络请求参数等进行再次封装。从多个请求接口分析,方式一需要重复写大量的代码,但是仔细分析,代码基本一样,只是一些参数和返回结果不同罢了,那Retrofit是怎么实现接口封装的,减少重复代码的呢?除了减少重复代码,我们在初始化Retrofit时候,设置不同的ConvertAdapter和CallAdapt,得到不同的转换结果,基本上能满足所有常见的网络协议格式的解析,那么它又是怎么实现扩展性的呢?这里就多亏于它的设计模式与设计架构了。

Retrofit 设计模式分析

接下来我们根据Retrofit使用流程的源码一点点分析Retrofit所用到的设计模式

门面模式

Retrofit retrofit = new Retrofit.Builder()
			.baseUrl(BASE_URL)//设置基地址
			.addCallAdapterFactory(RxJava2CallAdapterFactory.create())//设置CallAdapter适配器
			.client(client)//设置请求客户端
			.addConverterFactory(GsonConverterFactory.create())//设置转换适配器
			.build();

Retrofit所有的调用都通过Retrofit这个类实现,这里就是一个典型的门面设计模式,把各种子系统的功能都放到Retrofit里面统一管理和调用,隐蔽子系统的接口,对变化进行隔离。

建造者模式

//build方法
public Retrofit build() {
      。。。

      return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
          callbackExecutor, validateEagerly);
    }

在创建Retrofit实例时候,设置各种参数,然后通过build来创建一个实例,这里采用了构建者的设计模式,把复杂对象的构建和表示分离,通过Builder把所有的参数设置上去后,然后调用build进行Retrofit的实例化。

动态代理模式

//网络请求使用
retrofit.create(ApiService.class).login(phoneNo, pwd)
               .subscribeOn(Schedulers.io())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe(new Consumer<String>() {
                ...
               });

//Create方法
public <T> T create(final Class<T> service) {
   ...
   return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
       new InvocationHandler() {
         private final Platform platform = Platform.get();

         @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
             throws Throwable {
           ...
           ServiceMethod<Object, Object> serviceMethod =
               (ServiceMethod<Object, Object>) loadServiceMethod(method);
           OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
           return serviceMethod.callAdapter.adapt(okHttpCall);
         }
       });
 }

将请求接口传入create方法里,通过动态代理的方式,把所有的接口统一进行注解的参数值获取进行网络请求接口的封装。

享元模式

//### loadServiceMethod
private final Map<Method, ServiceMethod<?, ?>> serviceMethodCache = new ConcurrentHashMap<>();
...
ServiceMethod<?, ?> loadServiceMethod(Method method) {
    ServiceMethod<?, ?> result = serviceMethodCache.get(method);
    if (result != null) return result;

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = new ServiceMethod.Builder<>(this, method).build();
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

在获取ServiceMethod对象时候,通过loadServiceMethod方法,在serviceMethodCache里面去查找,如果没有,那么创建一个新的ServiceMethod实例,然后放入serviceMethodCache缓存中,通过享元设计模式,避免创建过多创建对象影响性能。

适配器模式

在获取到ServiceMethod实例后,调用serviceMethod.callAdapter.adapt(okHttpCall)进行参数的组装,请求方法的创建。下面具体分析callAdapter实例

ServiceMethod(Builder<R, T> builder) {
    ...
   this.callAdapter = builder.callAdapter;
   //builder.callAdapter = createCallAdapter()
    ...
 }

private CallAdapter<T, R> createCallAdapter() {
    ...
    return (CallAdapter<T, R>) retrofit.callAdapter(returnType, annotations);
    ...
}

ServiceMethod里面的callAdapter实例实际上是获取的builder里面的callAdapter实例,最终也是调用的retrofit实例里面的callAdapter。这里的callAdapter可以通过retrofit的addCallAdapterFactory添加工厂类获取到callAdapter,这里看一个默认的ExecutorCallAdapterFactory,通过get获取到CallAdapter,我们注意到里面的adapt方法,通过传入Call call参数,返回Call 结果,这里典型的适配器设计方案,通过adapt将参数转化成相同的Call类型结果,以便后面统一去处理Call类型的结果。

final class ExecutorCallAdapterFactory extends CallAdapter.Factory {
  final Executor callbackExecutor;


  @Override
  public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    ...
    return new CallAdapter<Object, Call<?>>() {
      @Override public Type responseType() {
        return responseType;
      }

      @Override public Call<Object> adapt(Call<Object> call) {
        return new ExecutorCallbackCall<>(callbackExecutor, call);
      }
    };
  }

单例模式

final class DefaultCallAdapterFactory extends Factory {
    static final Factory INSTANCE = new DefaultCallAdapterFactory();

    DefaultCallAdapterFactory() {
    }

}
...
DefaultCallAdapterFactory.INSTANCE
...

一个子系统模块中,只需要也仅一个实例存在,那么就可以采用单例设计模式,这里DefaultCallAdapterFactory的实例的获取就是通过单例模式进行创建的。Retrofit里面的转换器的创建方式运用了很多单例设计模式方式,减少开销而且方便使用。

原型模式

...

@SuppressWarnings("CloneDoesntCallSuperClone") // Performing deep clone.
@Override public Call<T> clone() {
  return new ExecutorCallbackCall<>(callbackExecutor, delegate.clone());
}

OkHttpCall实现了Call接口,Call接口继承自Cloneable.clone的实现就是重新new了一个一样的对象,用于其他地方重用相同的Call.这样简化Call对象的创建,使得创建对象就像在编辑文档时的复制粘贴。

观察者模式

static final class ExecutorCallbackCall implements Call {
  final Executor callbackExecutor;
  final Call delegate;

  ExecutorCallbackCall(Executor callbackExecutor, Call delegate) {
      this.callbackExecutor = callbackExecutor;
      this.delegate = delegate;
    }

  @Override public void enqueue(final Callback callback) {
    checkNotNull(callback, "callback == null");

    delegate.enqueue(new Callback() {
      @Override public void onResponse(Call call, final Response response) {
        callbackExecutor.execute(new Runnable() {
          @Override public void run() {
            if (delegate.isCanceled()) {
              // Emulate OkHttp's behavior of throwing/delivering an IOException on cancellation.
              callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled"));
            } else {
              callback.onResponse(ExecutorCallbackCall.this, response);
            }
          }
        });
      }

      @Override public void onFailure(Call call, final Throwable t) {
        callbackExecutor.execute(new Runnable() {
          @Override public void run() {
            callback.onFailure(ExecutorCallbackCall.this, t);
          }
        });
      }
    });
  }
}
}

所有与网络请求相关的库一定会支持请求的异步发送,通过在库内部维护一个队列,将请求添加到该队列,同时注册一个回调接口,以便执行引擎完成该请求后,将请求结果进行回调。Retrofit也不例外,Retrofit的网络请求执行引擎是OkHttp,请求类是OkHttpCall,其实现了Call接口,enqueue方法如下,入参为Callback对象。在OkHttpCall的enqueue实现方法中,通过在okhttp3.Callback()的回调方法中调用上述入参Callback对象的方法,实现通知观察者。

包裹模式

如上ExecutorCallbackCall类的分析,ExecutorCallbackCall实现了Call接口,同事在构造方法里面传入了Call参数,然后对call实例的Call接口对应的方法进行更加具体的实现,这是典型的包裹设计模式,在不改变原有类的情况下,通过包裹一层加入一层具体的细节。

简单工厂模式

class Platform {
  ...
  //通过简单工厂的方式创建不同的Platform实例
  private static Platform findPlatform() {
    try {
      Class.forName("android.os.Build");
      if (Build.VERSION.SDK_INT != 0) {
        return new Android();
      }
    } catch (ClassNotFoundException ignored) {
    }
    try {
      Class.forName("java.util.Optional");
      return new Java8();
    } catch (ClassNotFoundException ignored) {
    }
    return new Platform();
  }

  static class Java8 extends Platform {
    ...
  }

  static class Android extends Platform {
    ...
  }

}

Platform实例的创建,通过简单工厂的设计模式,定义一套抽象产品类Platform,具体的产品Platform继承抽象Platform实现不同的产品,然后在不同的条件下,通过静态调用方式得到不同的产品实例。

策略模式

//### RxJava2CallAdapter
@Override public Object adapt(Call<R> call) {
    Observable<Response<R>> responseObservable = isAsync
        ? new CallEnqueueObservable<>(call)
        : new CallExecuteObservable<>(call);

    Observable<?> observable;
    if (isResult) {
      observable = new ResultObservable<>(responseObservable);
    } else if (isBody) {
      observable = new BodyObservable<>(responseObservable);
    } else {
      observable = responseObservable;
    }

在调用RxJava2CallAdapter adapt方法适配器时,配置Retrofit.Builder时addCallAdapterFactory,不同的CallAdapter都需要提供adapt方法,CallAdapter就对应Stragety抽象策略。如果是Result,那么通过ResultObservable来进行实例化方案,如果是body,那么通过实例化BodyObservable进行处理,如果是响应,对应的直接获取到responseObservable来处理。这里就是一种策略的选择方法,通过定义不同的算法策略簇,通过不同的条件进行不同的选择。
除了这里RxJava2CallAdapter adapt外,其实在Retrofit策略模式很多,典型的CallAdapter的选择,ConvertAdapter的选择都是采用策略设计模式,这样才能使Retrofit适应场景更广,更便于扩展。

Retrofit 设计架构分析

在分析完设计模式后,大家可能会问,retrofit里面采用了大量的设计模式去构建这个网络请求封装框架,有什么用?不用设计模式也可以去实现这些功能,其实不然,设计模式的使用是为了更好地构建设计架构,是优美架构设计的基础,怎样去打造一个高可用,高扩展的模块,符合OCP设计原则,设计模式是构建符合设计原则系统的基石。
设计模式与架构之美--Retrofit的那点事_第1张图片
设计模式与架构之美--Retrofit的那点事_第2张图片

由于对接系统的差异性等等问题,网络请求参数,请求和响应数据格式,请求写法等等都可能有很大差异,Retrofit通过适配器、策略设计模式以及抽象工程设计模式等,在build构建Retrofit对象时候,设置不同的callAdapter和ConvertAdapter对象来进行不同的网络请求方式和处理不同的网络请求数据,实现retrofit的任意可扩展的目的。
比如,小型项目中不需要引入Rxjava 库,可以将CallAdapter设置成DefaultCallAdapterFactory,进行call的方式请求,如果觉得系统的GsonConvert性能不高,我们可以引入 Retrofit2-FastJson-Converter 转换器进行json解析,如果响应的是xml数据,我们可以引入com.squareup.retrofit2:converter-simplexml库设置到ConverterFactory进行xml解析,这样使得我们网络请求能适应各种变化,可扩展性特别高。既然Retrofit用起来简单方便,适合很多场景,它真的就完全符合OCP设计原则么?

大家考虑过这种场景没有,比如某一天,突然有新得更强大的网络请求client库出现或者Http2.0全名普及,但是okhttp却没有了维护,没有去支持Http2.0,为了追求更好的网络性能,我们项目需要更换成最新的其他库的方式来进行网络请求获取数据,那么这个时候怎么办?我们可以很容易发现,Retrofit的client设置方式直接是强耦合OKHttp的,也就是说只能基于OKHttp之上使用retrofit,没有对网络client进行解耦,导致没法切换成其他更优秀的client,在需要更换更强大的client时,只能同时移除okhttp和retrofit,换成新的网络请求方式的写法,这样更换成本巨大,而且极其容易出现问题,据说最新的Retrofit库都直接引入了OKHttp。这里就是Retrofit设计的最大败笔,client的设置直接耦合了具体的实现,而不是抽象,这样设计也不符合DIP迪米特设计原则,同时也违背了OCP开闭设计原则,导致整体扩展性大幅下降。

总结

Retrofit在不到四千行的代码量中却使用了将近十三种的设计模式,是一个android版很好的设计模式范本,通过大量的设计模式,是整个模块变得扩展性,适应性极强,基本上能满足所有的网络请求,包括接口参数,请求返回写法,请求与响应报文格式等等。Retrofit最值得学习的它里面采用大量的设计模式,设计基本也符合设计原则,虽然client的设计并不满足迪米特原则和开闭原则,但是整体来说,是一款极其优秀的三方库了。如果需要从战略风险层面考虑系统的稳定性,可以在Retrofit的源码仓库fork一个新的分支,对Retrofit的client的设置过程进行重新改造,抽象一个通用的client抽象接口模板,client的设置只对抽象接口进行耦合,不对事对具体的网络client实例类进行强耦合。这样改造之后,及时后面不用ok了,也可以在使用Retrofit而网络请求方法不需要大的变化,这样增加了稳定性大大减小了维护成本。

你可能感兴趣的:(Android应用层)