本文將接着《Retrofit源碼設計模式解析(上)》,繼續分享以下設計模式在Retrofit中的應用:
- 適配器模式
- 策略模式
- 觀察者模式
- 單例模式
- 原型模式
- 享元模式
一、適配器模式
在上篇說明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不愧是大師之作,設計模式的經典教程。其源碼量並不大,但系統的可擴展性、可維護性極強,是客戶端架構設計的典範,非常值得學習,五星推薦!