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不愧是大師之作,設計模式的經典教程。其源碼量並不大,但系統的可擴展性、可維護性極強,是客戶端架構設計的典範,非常值得學習,五星推薦!


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