Retrofit中的设计模式

4,472 阅读9分钟

前言--阅读本文你将得到什么

1.了解面向对象的基本原则,这也是设计模式的基础

2.了解Retrofit中使用的设计模式

3.了解装饰模式与代理模式的区别

Retrofit做的主要工作

Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装,网络请求的工作本质上是 OkHttp 完成,而 Retrofit 仅负责 网络请求接口的封装.

一般网络请求框架做的工作如上图所示

1.build request(API参数配置)

2.executor(这里可以有很多变体,比如有无队列,进出顺序,线程管理)

3.parse callback(解析数据,返回T给上层)

retrofit所做的主要分为以下3部分:

1.通过注解配置API参数,将resultful api拼接成真正的请求url

2.CallAdapter(你可以把它理解成executor),指定真正的网络请求

3.Converter(解析数据并转换成T)

面向对象的六大原则

  • 单一职责原则(Single Responsibility Principle)——SRP

单一职责原则很好理解,就是一个类尽量只做一件事。

  • 开闭原则(Open Close Principle)——OCP

开闭原则的定义是软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是关闭的。 即当需求发生改变的时候,我们需要对代码进行修改,这个时候我们应该尽量去扩展原来的代码,而不是去修改原来的代码,因为这样可能会引起更多的问题。

  • 里式替换原则(Liskov Substitution Principle)——LSP

里式替换原则简单说来就是:所有引用基类的地方必须能够透明地使用其子类的对象。 里氏替换原则通俗的去讲就是:子类可以去扩展父类的功能,但是不能改变父类原有的功能

里氏替换和开闭原则是比较相近的,通过里式替换来达到对扩展开放,对修改关闭的效果

  • 依赖倒置原则(Dependence Inversion Principle)——DIP

这个原则从名字根本看不出什么意思。高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。简单的说就是尽量面向接口编程.

依赖倒置原则在java语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。

  • 接口隔离原则(InterfacesSergregation Principle)——ISP

接口隔离原则的定义是:客户端不应该依赖它不需要的接口; 一个类对另一个类的依赖应该建立在最小的接口上。接口最小化,过于臃肿的接口依据功能,可以将其拆分为多个接口

以上这些设计思想用英文的第一个字母可以组成SOLID ,满足这个5个原则的程序也被称为满足了SOLID准则。

  • 迪米特原则(Law of Demeter)——LOD

迪米特原则也被称为最小知识原则,他的定义:一个对象应该对其他对象保持最小的了解。 因为类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大,所以这也是我们提倡的软件编程的总的原则:低耦合,高内聚。

六大原则与设计模式的关系

我们之所以使用设计模式,本质上是为了在后续开发中复用与扩展,这两个大概是最终目的

做个比喻的话,面向对象的六大原则就像是为了实现这个最终目的的心法,而二十三种设计模式则是根据这些心法演化出来的招式。

设计模式不必完全遵守六大原则,可根据实际情况变化。比如外观模式就违背了开闭原则。

Retrofit中用到的设计模式

构造者模式(通过builder构建)

Retrofit retrofit =new Retrofit.Builder()
            .baseUrl(server1.url("/"))
            .build();

优点:

1.良好的封装性,使用建造者模式可以使客户端不必知道产品内部细节,更加安全

2.链式调用 ,更加简洁,易懂

外观模式(门面模式)

要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。

Retrofit给我们暴露的方法和类不多。核心类就是Retrofit,我们只管配置Retrofit,然后做请求。剩下的事情就跟上层无关了,只需要等待回调。这样大大降低了系统的耦合度。对于这种写法,我们叫外观模式(门面模式)。

几乎所有优秀的开源library都有一个门面。比如Glide.with() ImageLoader.load() 。有个门面方便记忆,学习成本低,利于推广品牌。 Retrofit的门面就是retrofit.create()

优点:

1.对客户程序隐藏子系统细节,减少了客户对于子系统的藕合,能够拥抱变化

2.外观类对子系统的接口封装,使得系统更加易用。

缺点:

1.外观类接口膨胀。有时子系统过多,使得外观类API较多,一定程度上增加了用户使用成本

2.外观类没有遵循开闭原则,当业务变更时,可能需要直接修改外观类。

动态代理

代理模式即为其他对象提供一种代理以控制对这个对象的访问

再来说动态代理。以往的动态代理和静态代理使用的场景是类似的。都想在delegate调用方法前后做一些操作。如果我的代理类有很多方法,那我得额外写很多代码,所以这时候就引入了动态代理。

通过动态设置delegate,可以处理不同代理的不同方法

动态代理则利用反射机制在运行时创建代理类。

动态代理常被用于在真正的操作之前或之后做一些操作

public class ProxyHandler implements InvocationHandler{
    private Object object;
    public ProxyHandler(Object object){
        this.object = object;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoke "  + method.getName());
        method.invoke(object, args);
        System.out.println("After invoke " + method.getName());
        return null;
    }
}

Retrofit里的动态代理比较巧妙。实际上它根本就没有delegate。因为这个方法没有真正的实现。使用动态代理,只是单纯的为了拿到这个method上所有的注解。所有的工作都是由proxy做了

public <T> T create(final Class<T> service) {
    validateServiceInterface(service);
    return (T)
        Proxy.newProxyInstance(
            service.getClassLoader(),
            new Class<?>[] {service},
            new InvocationHandler() {
              private final Platform platform = Platform.get();
              private final Object[] emptyArgs = new Object[0];

              @Override
              public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                  throws Throwable {
                // If the method is a method from Object then defer to normal invocation.
                if (method.getDeclaringClass() == Object.class) {
                  return method.invoke(this, args);
                }
                args = args != null ? args : emptyArgs;
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);
              }
            });
  }

装饰模式

装饰模式又称包装模式,即动态地给一个对象添加一些额外的职责。

装饰模式与代理模式的共同点在于增强功能,但是代理的特点是添加逻辑控制,而装饰则是动态地添加功能

static final class ExecutorCallbackCall<T> implements Call<T> {
    final Executor callbackExecutor;
    final Call<T> delegate;

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

    @Override public void enqueue(final Callback<T> callback) {
      Objects.requireNonNull(callback, "callback == null");

      delegate.enqueue(new Callback<T>() {...}
      }
   }

你可以将ExecutorCallbackCall当作是Wrapper,而真正去执行请求的源Source是OkHttpCall。之所以要有个Wrapper类,是希望在源Source操作时去做一些额外操作。这里的操作就是线程转换,将子线程切换到主线程上去。

enqueue()方法是异步的,也就是说,当你调用OkHttpCall的enqueue方法,回调的callback是在子线程中的,如果你希望在主线程接受回调,那需要通过Handler转换到主线程上去。ExecutorCallbackCall就是用来干这个事。当然以上是原生retrofit使用的切换线程方式。如果你用rxjava,那就不会用到这个ExecutorCallbackCall而是RxJava的Call了

适配器模式

适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起的两个类能够在一起工作(类似转接头)

再回来看看Retrofit,为什么我们需要转接头呢。那个被转换的是谁?我们看看CallAdapter的定义。Adapts a {@link Call} into the type of {@code T}. 这个Call是OkHttpCall,它不能被我们直接使用吗?被转换后要去实现什么特殊的功能吗?

我们假设下。一开始,retrofit只打算在android上使用,那就通过静态代理ExecutorCallbackCall来切换线程。但是后来发现rxjava挺好用啊,这样就不需要Handler来切换线程了嘛。想要实现,那得转换一下。将OkHttpCall转换成rxjava(Scheduler)的写法。再后来又支持了java8(CompletableFuture)。大概就是这样一个套路。

适配器模式就是,已经存在的OkHttpCall,要被不同的标准,平台来调用。设计了一个接口CallAdapter,让其他平台都是做不同的实现来转换,这样不花很大的代价就能再兼容一个平台

策略模式

策略模式定义了一系列算法,并将每一个算法封装起来,而且使他们还可以互相替换。策略模式让算法独立于使用它的客户而独立变化。

使用场景:

针对同一类型问题的多种处理方式,仅仅是具体行为有差别时

- private CallAdapter<Observable<?>> getCallAdapter(Type returnType, Scheduler scheduler) {  
  Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);  
  Class<?> rawObservableType = getRawType(observableType);
  if (rawObservableType == Response.class) {    
    if (!(observableType instanceof ParameterizedType)) {  
      throw new IllegalStateException("Response must be parameterized"     
           + " as Response<Foo> or Response<? extends Foo>"); 
    }    

Type responseType=getParameterUpperBound(0,(ParameterizedType)observableType);   
     return new ResponseCallAdapter(responseType, scheduler);  
} 
 if (rawObservableType == Result.class) {
    if (!(observableType instanceof ParameterizedType))
   {      throw new IllegalStateException("Result must be parameterized"     
    + " as Result<Foo> or Result<? extends Foo>");    
   }    
      Type responseType = getParameterUpperBound(0, (ParameterizedType)observableType); 
    return new ResultCallAdapter(responseType, scheduler);  
}  
return new SimpleCallAdapter(observableType, scheduler);
}

看看retrofit中的CallAdapter是如何确立的?

它是根据api方法声明的returnType来创建具体的CallAdapter实例的

根据不同的策略使用不同的算法,不同的returnType声明就是set不同的Strategy

装饰模式与代理模式的区别

在Retrofit中既用到了动态代理也用到了装饰模式

装饰模式与静态代理很像,很容易造成混淆

代理模式更偏向于逻辑的控制,装饰模式更偏向于功能的增强

有一个比较大的区别是客户知不知道代理委托了另一个对象

在代理模式中,目标类对于客户端是透明的,而装饰模式中,客户端是对特殊目标的对象进行增强

伪代码如下:

//代理模式
public class Proxy implements Subject{
       private Subject subject;
       public Proxy(){
             //关系在编译时确定
            subject = new RealSubject();
       }
       public void doAction(){
             ….
             subject.doAction();
             ….
       }
}

//装饰模式
//装饰器模式
public class Decorator implements Component{
        private Component component;
        public Decorator(Component component){
            this.component = component
        }
       public void operation(){
            ….
            component.operation();
            ….
       }
}

总结