設計模式與架構之美--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<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) {
    checkNotNull(callback, "callback == null");

    delegate.enqueue(new Callback<T>() {
      @Override public void onResponse(Call<T> call, final Response<T> 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<T> 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通過適配器、策略設計模式以及抽象工程設計模式等,在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而網絡請求方法不需要大的變化,這樣增加了穩定性大大減小了維護成本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章