細細品讀Retrofit的設計之美二

JerryloveEmily發表於2017-12-22

本篇文章已授權為微信公眾號 code小生  釋出轉載請註明出處:www.jianshu.com/p/dab7f5720…

1.細細品讀Retrofit的設計之美一 2. 細細品讀Retrofit的設計之美二


引言

在上一篇 品讀Retrofit設計之美後,我們瞭解了Builder構建者模式和(動態)代理模式在Retrofit中的做用,以及它們的使用套路。今天繼續品讀Retrofit框架值得我們好好思考的設計:抽象工廠模式

抽象工廠模式

在看Retrofit的抽象工廠模式的應用前,先來了解下,抽象工廠模式的套路,不扯虛的直接舉一個實用的例子:

我們都知道作為app開發者,通常的app應用都會有使用者系統,一個使用者系統往往都包含了以下模組:1. 登入模組。 2. 註冊模組。 3. 找回密碼模組。 4. 使用者個人資訊模組。 這幾個模組代表工廠需要生產的不同型別的產品,使用者系統帳號,我們可能是app自身的帳號、密碼、或者手機簡訊驗證碼的登入方式,也可能是第三方平臺帳號登入:微信、QQ、新浪微博等等。對於不同的平臺的使用者帳號我們可以看做不同的品牌工廠,比如:app自身的使用者帳號工廠、微信的使用者帳號工廠、QQ的使用者帳號工廠、新浪微博的使用者帳號工廠。

這樣來設計一個使用者系統是不是更清晰點,而且不同的品牌的工廠便於替換,也就是替換登入的平臺,不同的產品模組類的功能職責也變的比較單一符合設計模式的單一原則。

案例實現

  1. 首先抽象出各個產品介面出來,每種模組產品都有各自的功能
// ******************IBaseUser.java,抽象使用者實體
/**
 * 抽象使用者實體介面,便於泛型化設計
 */
public interface IBaseUser {
}

// 1. ******************ILoginer.java,登入模組
/**
 * 登入抽象介面
 * @param <U>   使用者資訊
 */
public interface ILoginer<U extends IBaseUser> {

    // 登入
    void login(U user);

    // 登出、退出帳號
    void logout(U user);
}

// 2. ******************IRegister.java,註冊模組
/**
 * 註冊帳號介面
 * @param <U> 使用者資訊
 */
public interface IRegister<U extends IBaseUser> {
    // 註冊帳號
    void registerAccount(U user);
}

//  3. ******************IFindPwder.java,找回密碼模組
/**
 * 找回密碼介面
 * @param <U>   使用者資訊
 */
public interface IFindPwder<U extends IBaseUser>  {
    // 找回密碼
    void findPwd(U user);
}

//  4. ******************IUserInfoer.java,使用者資訊模組
/**
 * 使用者資訊相關介面
 * @param <U>   使用者資訊
 */
public interface IUserInfoer<U extends IBaseUser> {

    // 獲取使用者資訊
    U getUserInfo();

    // 儲存使用者資訊
    void saveUserInfo(U userInfo);
}
複製程式碼

這些產品模組的介面規範功能抽象,對於app的使用者系統來說基本夠用了。當然上面的這些介面,也可以統一用一個介面檔案來寫,這些模組就作為子介面巢狀在裡面,這是為了方便管理。

  1. 然後是工廠的抽象介面,用於生產不同品牌的不同產品
//  ******************IUserSystemFactory .java,抽象的工廠介面
/**
 * 使用者系統抽象工廠:登入、註冊、找回密碼、使用者資訊等模組
 */
public interface IUserSystemFactory {

    // 獲取登入模組,登入器
    ILoginer getLoginer();

    // 獲取註冊模組,註冊器
    IRegister getRegister();

    // 找回密碼模組
    IFindPwder getFindPwder();

    // 使用者資訊模組
    IUserInfoer getUserInfoer();
}
複製程式碼

主要就是獲取不同模組的產品抽象介面物件,便於客戶端使用工廠的模組物件的時候多型性。

  1. 實現不同登入方式的工廠和具體的使用者系統模組

因為使用者系統大部分情況下都需要和UI互動,所以封裝了一層基類把Context上下文統一起來,減少子類的不必要的重複。

// *************BaseLoginer.java
/**
 * 登入模組的基類
 * @param <U>   使用者資訊
 */
public abstract class BaseLoginer<U extends IBaseUser> implements ILoginer<U> {

    private Context mContext;

    public BaseLoginer(Context context) {
        this.mContext = context;
    }
}

// *************BaseUserSystemFactory.java
/**
 * 使用者系統工廠基類
 */
public abstract class BaseUserSystemFactory implements IUserSystemFactory {

    private Context mContext;

    public BaseUserSystemFactory(Context context) {
        this.mContext = context;
    }

    // 工廠物件可以獲取上下文
    public Context getContext(){
        return mContext;
    }
}
複製程式碼

比如,當我們使用app自己的使用者帳號登入的時候的實現

// ******************SystemAccountLoginer.java
/**
 * 使用應用帳號登入
 */
public class SystemAccountLoginer extends BaseLoginer<User> {

    public SystemAccountLoginer(Context context) {
        super(context);
    }

    @Override
    public void login(User user) {
        // 登入app
    }

    @Override
    public void logout(User user) {
        // 登出退出帳號
    }
}

// ******************SystemAccountFactory.java
/**
 * 系統帳號登入時的使用者系統工廠
 */
public class SystemAccountFactory extends BaseUserSystemFactory {
    private SystemAccountFactory(Context context) {
        super(context);
    }

    public static IUserSystemFactory create(Context context){
        return new SystemAccountFactory(context);
    }

    @Override
    public ILoginer getLoginer() {
        // 返回對應的登入產品(app自己的帳號平臺登入物件)
        return new SystemAccountLoginer(getContext());
    }

    @Override
    public IRegister getRegister() {
        // 返回對應的註冊產品(app自己的帳號平臺註冊物件)
        return null;
    }

    @Override
    public IFindPwder getFindPwder() {
        // 返回對應的找回密碼產品(app自己的帳號平臺找回密碼物件)
        return null;
    }

    @Override
    public IUserInfoer getUserInfoer() {
        // 返回對應的使用者資訊產品(app自己的帳號平臺使用者資訊物件)
        return null;
    }
}
複製程式碼

再比如,用微信來登入應用

// ******************WeixinLoginer.java
/**
 * 使用微信登入
 */
public class WeixinLoginer extends BaseLoginer<User> {
    public WeixinLoginer(Context context) {
        super(context);
    }

    @Override
    public void login(User user) {
        // 使用微信登入
    }

    @Override
    public void logout(User user) {
        // 退出登入
    }
}

// ******************WeixinFactory.java
/**
 * 系統帳號登入時的使用者系統工廠
 */
public class WeixinFactory extends BaseUserSystemFactory {
    private WeixinFactory(Context context) {
        super(context);
    }

    public static IUserSystemFactory create(Context context){
        return new WeixinFactory(context);
    }

    @Override
    public ILoginer getLoginer() {
        return new WeixinLoginer(getContext());
    }

    @Override
    public IRegister getRegister() {
        return null;
    }

    @Override
    public IFindPwder getFindPwder() {
        return null;
    }

    @Override
    public IUserInfoer getUserInfoer() {
        return null;
    }
}
複製程式碼

這裡我實現了登入產品模組的,其它的模組也是一樣的。對於呼叫者的使用也很簡單:

// 客戶端呼叫
// 使用自己的帳號平臺
IUserSystemFactory factory = SystemAccountFactory.create(this);
// 使用微信平臺帳號
//        IUserSystemFactory weixinFactory = WeixinFactory.create(this);
User user = new User();
user.setUserId("1256339899879");
user.setPhone("13888888888");
// 使用自己的帳號登入app
factory.getLoginer().login(user);
// 使用自己的帳號註冊
factory.getRegister().registerAccount(user);
// 使用找回自己帳號的密碼
factory.getFindPwder().findPwd(user);
// 獲取使用者資訊
factory.getUserInfoer().getUserInfo();
複製程式碼

對於呼叫者來說很簡單,只要關心當前用的是什麼平臺的帳號系統,而不需要關心具體的實現方式。也把不同平臺的登入、註冊、獲取使用者資訊等分離開來。當然往往不同的平臺可能退出當前帳號的方式是一樣,這個時候,其實可以把BaseLoginer當做代理物件,目標介面就是ILoginer,目標物件另外新建一個類實現目標介面,利用代理模式。

Retrofit抽象工廠的應用

我們都知道網路請求通訊,當服務端返回資料後,都需要進行解析轉換為可以直接使用的實體物件,便於設定顯示到UI介面上,我們在構建Retrofit物件的時候往往會給構建器注入一個解析轉換器工廠物件。

new Retrofit.Builder()
                .baseUrl(AppConst.BASE_URL)
                .client(buildHttpClient())
                .addConverterFactory(FastJsonConverterFactory.create())
                .build();
複製程式碼

其中FastJsonConverterFactory.create()建立的就是一個Factory抽象工廠物件。

// 資料轉換器抽象產品類
// F是入參,T是出參(轉換後的資料型別)
public interface Converter<F, T> {
  // 產品的轉換操作
  T convert(F value) throws IOException;

  // 抽象工廠類
  abstract class Factory {
    // 工廠生產的請求響應的轉換器產品
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
        Retrofit retrofit) {
      return null;
    }

    // 工廠生產的請求發起的轉換器產品
    public Converter<?, RequestBody> requestBodyConverter(Type type,
        Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
      return null;
    }

   // 工廠生產的用於轉換字串資料型別的轉換器產品
    public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
        Retrofit retrofit) {
      return null;
    }
  }
}
複製程式碼

接下來看看使用FastJson作為轉換器的工廠實現類:

public class FastJsonConverterFactory extends Converter.Factory {
  // 建立工廠物件
  public static FastJsonConverterFactory create() {
    return new FastJsonConverterFactory();
  }

  private FastJsonConverterFactory() {
  }

  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
                                                          Retrofit retrofit) {
    return new FastJsonResponseBodyConverter<>(type, mParserConfig, featureValues, features);
  }

  @Override
  public Converter<?, RequestBody> requestBodyConverter(Type type,
         Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
    return new FastJsonRequestBodyConverter<>(serializeConfig, serializerFeatures);
  }
}
複製程式碼

通過封裝一個create方法,來建立工廠物件,外部呼叫者就不需要關係工廠物件是如何建立的。這點和我上面舉的例子是一樣的。再一個通過responseBodyConverter、requestBodyConverter方法分別建立了請求響應和請求發起這兩種產品的物件。

再來看看FastJsonRequestBodyConverter請求發起轉換產品的實現:

// 實現了轉換器這抽象產品類,入參是RequestBody,返回的結果是泛型T
final class FastJsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
  private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8");
  private SerializeConfig serializeConfig;
  private SerializerFeature[] serializerFeatures;

  FastJsonRequestBodyConverter(SerializeConfig config, SerializerFeature... features) {
    serializeConfig = config;
    serializerFeatures = features;
  }

  @Override
  public RequestBody convert(T value) throws IOException {
    byte[] content;
    if (serializeConfig != null) {
      if (serializerFeatures != null) {
        content = JSON.toJSONBytes(value, serializeConfig, serializerFeatures);
      } else {
        content = JSON.toJSONBytes(value, serializeConfig);
      }
    } else {
      if (serializerFeatures != null) {
        content = JSON.toJSONBytes(value, serializerFeatures);
      } else {
        content = JSON.toJSONBytes(value);
      }
    }
    return RequestBody.create(MEDIA_TYPE, content);
  }
}
複製程式碼

實現了轉換器這抽象產品介面類,入參是RequestBody,返回的結果是泛型T(因為請求的引數是針對具體業務的作為框架無法確定,於是用泛型來代替),這個FastJsonRequestBodyConverter產品的功能就是convert轉換功能,這裡使用了阿里巴巴的json解析庫fastJson來轉換,具體的實現就是通過JSON.toJSONBytes方法轉換出json的位元組陣列,然後交由給OkHttp的RequestBody.create來構建一個請求體,並且請求的多媒體型別是json格式的。OkHttp中的實現:

public static RequestBody create(final MediaType contentType, final byte[] content,
      final int offset, final int byteCount) {
    if (content == null) throw new NullPointerException("content == null");
    Util.checkOffsetAndCount(content.length, offset, byteCount);
    return new RequestBody() {
      @Override public MediaType contentType() {
        return contentType;
      }

      @Override public long contentLength() {
        return byteCount;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        // content請求的引數內容都通過Okio的BufferedSink來寫入了
        sink.write(content, offset, byteCount);
      }
    };
  }
複製程式碼

你會發現RequestBody是個抽象類,writeTo是個抽象方法,那麼必定就有呼叫此方法的地方。也不能盲目的看原始碼找,一個請求的構建最好的地方就是發起請求的時候,call.enqueue(callback),通過enqueue發起一個非同步的請求,但Call是介面,也不曉得實現類。還有個辦法就是倒退的方式,將游標放置上門的writeTo方法上,按組合鍵(有使用到writeTo的地方):ctrl + alt + F7:

有使用到writeTo的地方

很明顯是最後一個ReqeustBuilder,請求構建類,跟進去是ContentTypeOverridingRequestBody,它是個代理類,目標物件是其內部的RequestBody物件這個物件我們猜測就是上文FastJsonRequestBodyConverter的converter轉換建立的RequestBody。再來看看ContentTypeOverridingRequestBody在RequestBuild的build()構建方法中有使用:

// 很明顯因為請求物件初始化比較複雜,就通過構建者模式構建了一個OkHttp的Request物件
class RequestBuild{
  Request build() {
    // 很明顯我們在構建Retrofit的時候有傳入FastJson的請求發起產品的生成工廠物件,因此姑且任務body是有值的不等於null
    RequestBody body = this.body;
    if (body == null) {
      // Try to pull from one of the builders.
      if (formBuilder != null) {
        body = formBuilder.build();
      } else if (multipartBuilder != null) {
        body = multipartBuilder.build();
      } else if (hasBody) {
        // Body is absent, make an empty body.
        body = RequestBody.create(null, new byte[0]);
      }
    }
    
    // 這裡給body做了一層代理,實際的目標介面還是之前FastJsonRequestBodyConverter建立的body目標物件自己來呼叫的
    // 而後把代理物件body給了Request進行構建請求發起物件。
    MediaType contentType = this.contentType;
    if (contentType != null) {
      if (body != null) {
        body = new ContentTypeOverridingRequestBody(body, contentType);
      } else {
        requestBuilder.addHeader("Content-Type", contentType.toString());
      }
    }

    // 這裡又通過OkHttp的Request類自身的構建者最終建立了Request物件
    return requestBuilder
        .url(url)
        .method(method, body)
        .build();
  }
}
複製程式碼

繼續看RequestBuild的build()的呼叫者是ServiceMethod的toRequest()方法:

Request toRequest(Object... args) throws IOException {
    RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,
        contentType, hasBody, isFormEncoded, isMultipart);
    // ....省略程式碼
    for (int p = 0; p < argumentCount; p++) {
      handlers[p].apply(requestBuilder, args[p]);
    }
    return requestBuilder.build();
  }
複製程式碼

先看看apply方法,它是ParameterHandler的抽象方法,裡面有很多引數的建立的實現:

image.png

@Override void apply(RequestBuilder builder, T value) {
      if (value == null) {
        throw new IllegalArgumentException("Body parameter value must not be null.");
      }
      RequestBody body;
      try {
        // 呼叫的這個convert這個方法就是上面fastjson工廠轉換建立請求發起RequestBody物件的呼叫處
        body = converter.convert(value);
      } catch (IOException e) {
        throw new RuntimeException("Unable to convert " + value + " to RequestBody", e);
      }
      // 這裡把建立的RequestBody物件設定給了RequestBuild構建者。這就是構建者的好處(初始化一個Request物件不容易,屬性的初始化時機和位置有各種情況)
      builder.setBody(body);
    }
複製程式碼

ServiceMethod的toRequest()方法呼叫者是OkHttpCall的createRawCall()

private okhttp3.Call createRawCall() throws IOException {
    Request request = serviceMethod.toRequest(args);
    okhttp3.Call call = serviceMethod.callFactory.newCall(request);
    if (call == null) {
      throw new NullPointerException("Call.Factory returned null.");
    }
    return call;
  }
複製程式碼

上面的程式碼意思是,通過一些引數建立了一個請求發起物件,然後再通過一個工廠物件建立了一個用於發起請求的okhttp3的call物件,再來看看createRawCall()方法的呼叫,它有三個地方呼叫了:

// 一個同步的請求方法
public synchronized Request request() {}

// 非同步的請求方法,但是沒有請求回撥
public Response<T> execute() throws IOException {}

// 非同步的請求方法,有請求回撥介面物件處理
public void enqueue(final Callback<T> callback) {}
複製程式碼

很明顯我們在發起一個網路業務請求的時候,使用的就是enqueue(callback)方法,大概來看看具體的實現:

@Override public void enqueue(final Callback<T> callback) {
    // 這裡請求回撥如果是null,直接就報空指標異常,這點在開發的時候需要做好非空判斷處理
    if (callback == null) throw new NullPointerException("callback == null");

    okhttp3.Call call;
    Throwable failure;

    synchronized (this) {
      if (executed) throw new IllegalStateException("Already executed.");
      executed = true;

      call = rawCall;
      failure = creationFailure;    // 建立的時候的有可能有錯
      if (call == null && failure == null) {
        try {
          // 初次構建使用的時候,會去建立一個call
          call = rawCall = createRawCall();
        } catch (Throwable t) {
          failure = creationFailure = t;
        }
      }
    }

    if (failure != null) {
      // 出錯則回撥請求失敗
      callback.onFailure(this, failure);
      return;
    }

    if (canceled) {
     // 請求如有取消,則取消
      call.cancel();
    }

    // 此處才是,真正發起請求的地方,把請求交由給底層OkHttp來做。
    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 callFailure(Throwable 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();
        }
      }
    });
  }
複製程式碼

這樣倒過來分析,不知有沒有更清晰點,梳理下:

  1. Retrofit構建的時候,為其設定了FastJson的工廠物件。

  2. 上面可知call.enqueue(callback),call就是OkHttpCall物件。

  3. enqueue建立的時候會先調createRawCall

  4. createRawCall會先呼叫serviceMethod的toRequest方法

  5. 在toRequest方法中,建立RequestBuild物件,並且把設定的業務請求的api裡的引數物件請求體Body使用FastJson工廠建立的FastJsonRequestConverter來convert出一個RequestBody設定給RequestBuild物件,並最終通過構建者模式建立Request物件。

  6. 再通過callFactory工廠建立一個用於請求的call,最終交由okhttp的enqueue方法來發起真正的網路請求。


總結

今天的篇幅也比較長,主要說明了抽象工廠設計模式的使用,具體舉了個在開發中比較實用的多平臺登入的使用者系統模組的問題,當然這只是個例子實際專案中需要完善的還很多。通用的例子還有很多比如:多種支付方式的切換、多種地圖SDK的切換、多種網路框架的切換、多種持久化資料儲存方式的切換、多種資料處理方式的切換、多種圖片載入器的切換等等。

後面主要介紹了Retrofit中抽象工廠的應用,以及簡單分析了,Retrofit是如何構建請求和發起請求的。


我是JerryloveEmily,感謝您的閱讀,

喜歡就點個讚唄,“❤喜歡”,

鼓勵又不花錢,您在看,我就繼續寫~

非簡書使用者,可以點右上角的三個“...”,然後"在Safari中開啟”,就可以點贊咯~

相關文章