仍記得前年的這個時候我去一個新的公司報道,公司總共就十來個人做開發,剛好做 android 的那哥們離職,就丟了一個項目給我。后面的日子我算是飽受煎熬,今天我們以一個訪問網絡的部分,來給大家做一下講解。里面每次獲取接口數據是這樣的:
當時我看到這個的時候覺得沒什么,只是我想換一下網絡請求的框架,想把它換成 OKHttp ,我一直不喜歡用 xUtils。但后來想想還是算了,好幾十個地方那我不都得去改?得過且過吧。過了一段時間后臺突然說,我們需要加個平臺參數來區別到底是 android 端還是 iOS 端,每個接口都多帶個參數 platform = android。聽到這個的我眼淚掉下來,默默無聞的回家找了好幾個小時,所有訪問接口都統一加了個參數。第二天早上我其實就想到,我為什么不去改 xUtils 的源代碼呢?看來昨天那幾個小時白忙活了。這個時候我其實還沒有意識到我要去更換整個網絡架構,盡管我知道后面還是會有問題,得過且過吧。剛過幾天測試發現有些頁面有時候會崩掉有時候又不會,我一看后臺接口返回的數據接口尼瑪...
有數據的情況下:
無數據的情況下:
我記得我當時想去找后臺,讓他幫幫忙,但是問問 iOS 的那哥們他說他做了一些處理。我心里在想我不能就這么暴露我自己,因此下定決心要自己來封裝。所以就出現了我接下來要寫的這篇文章,我記得我在內涵段子項目部分寫過這個內容,但是那時是授人以魚,這次是要授人以漁。說明我不是亂寫還是有很扎實的理論依據,這是理論結合實戰的年代,當然我也不會寫得太完善很多問題會留作思考,授人以魚不如授人以漁。
1.一般寫法
上面這種寫法應該只有學校的老師才會教我們這樣寫吧,我以 OKHttp 為例,這種我就不寫了,寫一個稍微好一點的級別,請看初步版本:
調用部分
Map<String, Object> params = new HashMap<>();
// 特定參數
params.put("iid", 6152551759L);
params.put("aid", 7);
HttpUtils.get(this, ConstantValue.UrlConstant.HOME_DISCOVERY_URL,
params, true, new HttpCallBack<DiscoverListResult>() {
@Override
public void onSuccess(DiscoverListResult result) {
if (result.isOK()) {
// 沒有列表數據的情況, 打印 Toast 或者做一些其他處理
} else {
// 有數據列表的情況,顯示列表
showListData(result);
}
}
@Override
public void onFailure(Exception e) {
}
});
封裝部分
/**
* get 請求數據列表
* @param context 上下文
* @param url 訪問路徑
* @param params 訪問參數
* @param cache 是否緩存
* @param callback 數據解析回調
* @param <T> 解析類的泛型
*/
public static <T> void get(Context context, String url, Map<String, Object> params, final boolean cache, final HttpCallBack<T> callback) {
OkHttpClient mOkHttpClient = new OkHttpClient();
// 公共參數拼接
params.put("app_name", "joke_essay");
params.put("version_name", "5.7.0");
params.put("ac", "wifi");
params.put("device_id", "30036118478");
params.put("device_brand", "Xiaomi");
params.put("update_version_code", "5701");
params.put("manifest_version_code", "570");
params.put("longitude", "113.000366");
params.put("latitude", "28.171377");
params.put("device_platform", "android");
final String jointUrl = Utils.jointParams(url, params); //打印
// 緩存邏輯的一些處理
Request.Builder requestBuilder = new Request.Builder().url(jointUrl).tag(context);
//可以省略,默認是GET請求
Request request = requestBuilder.build();
// 異步請求數據
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
// 失敗
callback.onFailure(e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String resultJson = response.body().string();
// json 轉換,有無數據列表,緩存處理
}
});
}
這個看上去就稍微好一丁點了,至少也能解決掉一些問題,當然有些哥們估計是用的別人封裝好的那就另當別論了,其實前年網上對 OKHttp 的封裝還是比較少,關鍵不是不想而是很少。
2.鏈式調用
隨著項目越來越大功能越來越強,這明顯滿足不了功能,而且人員也越來越多的情況下,吐槽隨之而來,比如不支持 cookie , https 證書,超時斷線重連等等。好吧硬著頭皮去加了一下功能,這個時候問題就來了,就是一個方法要傳的參數一大堆,后面要跟十幾個參數,而有些參數我又不想要,只能全都傳 null 。第一,成員調用非常蛋疼需要對照你的參數一個一個去看,第二,可閱讀并不好并不美觀,所以又做了如下修改,最終調用就變成了如下方式:
HttpUtils.with(this)
.get() // get請求
.param("aid", 7)// 添加單個參數,params 可以添加多個參數
.param("iid", 6152551759L)
.url(ConstantValue.UrlConstant.HOME_DISCOVERY_URL)// 接口地址
.setCookie(true)//配置是否使用cookie
.cache(true)//設置是否使用緩存,如果使用緩存必須設置為true
.retryDelayMillis(1000)//配置請求失敗重試間隔時間,單位毫秒
.retryCount(3)//配置請求失敗重試次數
.execute(new HttpCallBack<DiscoverListResult>() {
@Override
public void onSuccess(DiscoverListResult result) {
if (result.isOK()) {
// 沒有列表數據的情況, 打印 Toast 或者做一些其他處理
} else {
// 有數據列表的情況,顯示列表
showListData(result);
}
}
@Override
public void onFailure(Exception e) {
}
});
3.單一職責原則
這時技術總監就上來問了,目前所有的功能都寫在一個類里怎么行呢,這樣隨著功能的增多,HttpUtils 類會越來越大,代碼也越來越復雜。這樣一看我的 HttpUtils 簡直就沒有設計可言,更不要說擴展性、靈活性了。網絡加載封裝就越來越脆弱…… 回家想了半天,那我多拆幾個類出來不就行了?到底怎么拆這個時候 單一職責就來了。
單一職責原則的英文名稱是Single Responsibility Principle,簡稱SRP。它的定義是:就一個類而言,應該僅有一個引起它變化的原因。簡單來說,一個類中應該是一組相關性很高的函數、數據的封裝。這個在 《Android 源碼設計模式》那本書中講得很清楚,學了就要用,到底是什么意思?請看我如何拆分,貼一些事例代碼。
/**
* Email 240336124@qq.com
* Created by Darren on 2017/3/12.
* Version 1.0
* Description:
*/
public class HttpCache {
/**
* 獲取數據
*/
public String getCache(String finalUrl) {
// 省略一些代碼 ...
}
/**
* 緩存數據
*/
public long saveCache(String finalUrl, String resultJson) {
// 省略一些代碼 ...
}
}
public class HttpUtils {
private OKHttpRequest mHttpRequest;
private HttpUtils(Context context) {
mHttpRequest = new OKHttpRequest();
mHttpRequest.with(context);
}
// 省略一些代碼 ...
}
3.開閉原則
代碼提交之后感覺思路是對的,后來一想最根本的問題還是沒有解決,我當初的第一感覺是想把 xutils 換成 OkHttp,但是我沒想過要去改每個接口,比如后來接著 Retrofit 又出來了,以后還指不定要出一些什么,我想能不能再寫得強大一些,可以切換,而不用改動原來的代碼。到底應該怎么辦?開閉原則就來了。
開閉原則的英文全稱是Open Close Principle,簡稱OCP,它是Java世界里最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是:軟件中的對象(類、模塊、函數等)應該對于擴展是開放的,但是,對于修改是封閉的。我的理解是對于原來寫好的代碼里面是不可修改,但是對于外部又是可擴展的。理解起來還是有點抽象,結合一下我的代碼就會好些,又要改了其實就是多了一個接口而已。
/**
* description:
* author: Darren on 2017/8/21 11:36
* email: 240336124@qq.com
* version: 1.0
*/
public class HttpUtils {
// 這個可以在 application 中去初始化
private static IHttpRequest mInitHttpRequest;
private IHttpRequest mHttpRequest;
public static void initHttpRequest(IHttpRequest httpRequest) {
mInitHttpRequest = httpRequest;
}
// 如果有兩種的情況下 比如 volley 下載文件并不是很屌 ,那么可以換成 OKHttp
public HttpUtils httpRequest(IHttpRequest httpRequest) {
this.mHttpRequest = httpRequest;
return this;
}
// 省略部分代碼 ......
public <T> void execute(HttpCallBack<T> callback) {
// 如果沒有指定,那么就用 application 中初始化的
if(mHttpRequest == null){
mHttpRequest = mInitHttpRequest;
}
mHttpRequest.get(mContext, mParams, mUrl, mCache, callback);
}
}
IHttpRequest 代碼
/**
* description:
* author: Darren on 2017/8/24 11:34
* email: 240336124@qq.com
* version: 1.0
*/
public interface IHttpRequest {
/**
* post 提交
*
* @param context
* @param params
* @param url
* @param cache
* @param callback
* @param <T>
*/
<T> void post(Context context, Map<String, Object> params, String url, final boolean cache, final HttpCallBack<T> callback);
/**
* get 提交
*
* @param context
* @param params
* @param url
* @param cache
* @param callback
* @param <T>
*/
<T> void get(Context context, Map<String, Object> params, String url, final boolean cache, final HttpCallBack<T> callback);
}
OKHttpRequest 代碼
/**
* description:
* author: Darren on 2017/8/24 10:32
* email: 240336124@qq.com
* version: 1.0
*/
public class OKHttpRequest implements IHttpRequest {
private HttpCache mHttpCache;
private OkHttpClient mOkHttpClient;
public OKHttpRequest() {
mHttpCache = new HttpCache();
mOkHttpClient = new OkHttpClient();
}
@Override
public <T> void post(Context context, Map<String, Object> params, String url, boolean cache, HttpCallBack<T> callback) {
// 省略部分代碼 ......
}
public <T> void get(Context context, Map<String, Object> params, String url, final boolean cache, final HttpCallBack<T> callback) {
// 省略部分代碼 ......
}
}
XUtilsRequest 代碼
/**
* description:
* author: Darren on 2017/8/24 10:32
* email: 240336124@qq.com
* version: 1.0
*/
public class XUtilsRequest implements IHttpRequest {
private HttpCache mHttpCache;
public XUtilsRequest() {
mHttpCache = new HttpCache();
}
@Override
public <T> void post(Context context, Map<String, Object> params, String url, boolean cache, HttpCallBack<T> callback) {
// 省略部分代碼 ......
}
public <T> void get(Context context, Map<String, Object> params, String url, final boolean cache, final HttpCallBack<T> callback) {
// 省略部分代碼 ......
}
}
Application 代碼
/**
* description:
* author: Darren on 2017/8/21 15:05
* email: 240336124@qq.com
* version: 1.0
*/
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
PreferencesUtil.getInstance().init(this);
x.Ext.init(this);
// 初始化指定網絡請求
HttpUtils.initHttpRequest(new XUtilsRequest());
}
}
開閉原則指導我們,當軟件需要變化時,應該盡量通過擴展的方式來實現變化,而不是通過修改已有的代碼來實現。我們盡量不要通過繼承等方式添加新的實現,這會導致類型的膨脹以及歷史遺留代碼的冗余。我們的開發過程中也沒有那么理想化的狀況,完全地不用修改原來的代碼,因此,在開發過程中需要自己結合具體情況進行考量,是通過修改舊代碼還是通過繼承使得軟件系統更穩定、更靈活,在保證去除“代碼腐化”的同時,也保證原有模塊的正確性。當然如果等我們了解了 Builder 設計模式或者加入其他的一些設計模式之后我們肯定可以寫得更強大和完善。我們接著往下面看。
4.里氏替換原則
里氏替換原則英文全稱是Liskov Substitution Principle,簡稱LSP。我們知道,面向對象的語言的三大特點是繼承、封裝、多態,里氏替換原則就是依賴于繼承、多態這兩大特性。里氏替換原則簡單來說就是,所有引用基類的地方必須能透明地使用其子類的對象。通俗點講,只要父類能出現的地方子類就可以出現。但是,反過來就不行了,有子類出現的地方,父類未必就能適應。 我們簡單的看幾個使用場景,在我們的開發過程中無所不在。
// 1. 今天所寫的初始化請求
HttpUtils.initHttpRequest(new XUtilsRequest());
HttpUtils.initHttpRequest(new OKHttpRequest());
// 2. RecyclerView 的 LayoutMananger
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setLayoutManager(new GridLayoutManager(this,3));
// 3. Retrofit 添加解析工廠 等等
使用的地方非常之多,setLayoutManager 的源碼大家可以自己去了解一下,上面的代碼就很好的反應了里氏替換原則,XUtilsRequest、OKHttpRequest 都可以替換 IHttpRequest 的工作。IHttpRequest 建立了 post 請求,get 請求,上傳,下載的接口規范,XUtilsRequest 等根據接口規范實現了相應的功能,用戶只需要在 Application 中指定具體的對象就可以動態地替換 IHttpRequest 中的請求。這就使得網絡請求具有了無線的可能性,也就是保證了可擴展性。
里氏替換原則和開閉原則有點相似,但仔細理解他們之間是不同的概念,只能夠說是有時候開閉原則和里氏替換原則往往生死相依、不棄不離,通過里氏替換來達到對擴展開放,對修改關閉的效果,這兩個原則其實就是面向對象思想中的抽象。
5.依賴倒置原則
依賴倒置原則英文全稱是Dependence Inversion Principle,簡稱DIP。依賴反轉原則指代了一種特定的解耦形式,高層模塊不依賴低層次模塊的細節,說白了高層次就是不依賴細節而是依賴抽象。那什么又是低層次什么是高層次?拿上面開閉原則那張圖來講,HttpUtils 是高層次,IHttpRequest、XUtilsRequest 和 OKHttpRequest 是低層次。剛開始 HttpUtils 是這么寫的:
public class HttpUtils {
private OKHttpRequest mHttpRequest;
private HttpUtils(Context context) {
mHttpRequest = new OKHttpRequest();
mHttpRequest.with(context);
}
// 省略一些代碼 ...
}
這個時候我們依賴的是具體的 OKHttpRequest,這種情況下很明顯我們依賴的是具體的細節, 在開閉原則過后,我們 HttpUtils 是這么寫的。
/**
* description:
* author: Darren on 2017/8/21 11:36
* email: 240336124@qq.com
* version: 1.0
*/
public class HttpUtils {
// 這個可以在 application 中去初始化
private static IHttpRequest mInitHttpRequest;
private IHttpRequest mHttpRequest;
public static void initHttpRequest(IHttpRequest httpRequest) {
mInitHttpRequest = httpRequest;
}
// 如果有兩種的情況下 比如 volley 下載文件并不是很屌 ,那么可以換成 OKHttp
public HttpUtils httpRequest(IHttpRequest httpRequest) {
this.mHttpRequest = httpRequest;
return this;
}
// 省略部分代碼 ......
public <T> void execute(HttpCallBack<T> callback) {
// 如果沒有指定,那么就用 application 中初始化的
if(mHttpRequest == null){
mHttpRequest = mInitHttpRequest;
}
mHttpRequest.get(mContext, mParams, mUrl, mCache, callback);
}
}
這個時候我們依賴的就已經不在是具體的細節了,而是抽象的 IHttpRequest ,具體的實現我們是在 Application 中配置的,可以配置 Okhttp 或者 xUtils 等等。從上面這幾個來看要讓整個系統更加靈活,似乎一直都是抽象的功勞。
6.接口隔離原則
接口隔離原則英文全稱是InterfaceSegregation Principles,簡稱ISP。它的定義是:客戶端不應該依賴它不需要的接口。另一種定義是:類間的依賴關系應該建立在最小的接口上。接口隔離原則將非常龐大、臃腫的接口拆分成為更小的和更具體的接口,這樣客戶將會只需要知道他們感興趣的方法。接口隔離原則的目的是系統解開耦合,從而容易重構、更改和重新部署,讓客戶端依賴的接口盡可能地小。
拿我最近在做的一個項目來說,我用手機去調用相機拍照,源碼我是沒有的,后面想了很多辦法把相機破解了,那么我獲取到的數據后需及時傳給后臺,這個時候建立了一個 Socket 通信機制,我在 Activity 的 onDestory 中斷開 Socket 連接我是這么寫的:
/**
* 中斷鏈接
*/
private void breakConnection() {
mReadFlag = false;
// 關閉輸入流
if (mInStream != null) {
try {
mInStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 關閉輸出流
if (mOutStream != null) {
try {
mOutStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 關閉Socket
if (mSocket != null) {
try {
mSocket.close();
mSocket = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
這段代碼可能我們平時都這么寫,各種try…catch嵌套,都是些簡單的代碼,但是會嚴重影響代碼的可讀性,并且多層級的大括號很容易將代碼寫到錯誤的層級中。大家應該對這類代碼也非常反感,那我們看看如何解決這類問題。我們看源碼發現他們都有一個 close 方法,而且這個方法是 Closeable 接口的,也就是說上面的這幾個類都是實現了 Closeable 接口,該接口標識了一個可關閉的對象。這意味著,在關閉這一百多個類型的對象時,都需要寫出上面這樣的這些代碼,而且并沒有什么實際的意義。這還了得!既然有共性,那么我們可以再寫一個方法:
/**
* 中斷鏈接
*/
private void breakConnection() {
mReadFlag = false;
// 關閉輸入流
close(mInStream);
// 關閉輸出流
close(mOutStream);
// 關閉Socket
close(mSocket);
}
/**
* 關閉 Closeable
* @param closeable
*/
private void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
代碼簡潔了很多!保證了代碼的重用性。close 方法的基本原理就是依賴于 Closeable 抽象而不是具體實現(這其實也是依賴倒置),并且建立在最小化依賴原則的基礎,它只需要知道這個對象是可關閉,其他的一概不關心,也就是這里的接口隔離原則。
Bob大叔(Robert C Martin)在21世紀早期將單一職責、開閉原則、里氏替換、接口隔離以及依賴倒置(也稱為依賴反轉)5個原則定義為SOLID原則,指代了面向對象編程的5個基本原則。當這些原則被一起應用時,它們使得一個軟件系統更清晰、簡單、最大程度地擁抱變化。SOLID被典型地應用在測試驅動開發上,并且是敏捷開發以及自適應軟件開發基本原則的重要組成部分。在經過這一系列的學習之后,我們發現這幾大原則最終就可以化為這幾個關鍵詞:抽象、單一職責、最小化。那么在實際開發過程中如何權衡、實踐這些原則,是大家需要在實踐中多思考與領悟,正所謂”學而不思則罔,思而不學則殆”,只有不斷地學習、實踐、思考,才能夠在積累的過程有一個質的飛越。
7.最少知識原則
最少知識原則又稱為迪米特原則英文全稱為Law of Demeter,簡稱LOD,雖然名字不同,但描述的是同一個原則:一個對象應該對其他對象有最少的了解。通俗地講,一個類應該對自己需要耦合或調用的類知道得最少,類的內部如何實現、如何復雜都與調用者或者依賴者沒關系,調用者或者依賴者只需要知道他需要的方法即可,其他的我一概不關心。類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。
迪米特法則還有一個英文解釋是:Only talk to your immedate friends,翻譯過來就是:只與直接的朋友通信。什么叫做直接的朋友呢?每個對象都必然會與其他對象有耦合關系,兩個對象之間的耦合就成為朋友關系,這種關系的類型有很多,例如組合、聚合、依賴等,這個我就不再做過多的講解了。
8.總結
末尾給大家推薦一本書《Android 源碼設計模式》,不是給大家打廣告,我也沒拿任何好處。很多人一上來就要源代碼,但是我的代碼一直以來都是不會公開的,就算公開也是帶著大家寫的代碼。我其實最想問的是代碼真的就那么重要嗎?OKhttp 自帶緩存,之所要自己寫緩存,是因為緩存其實也會分為很多種,比如我想把它先緩存到內存中,然后緩存到數據,接著緩存到磁盤,又或是我只想把他緩存到 SharedPreferences 我們可以借此機會練練手,當然這些我都不會在這里寫了,根據上面的一些思想,相信你一定有辦法把它寫好。目前還不兼容 Retrofit ,這個我們必須得等看過 Retrofit 的源碼過后才能做兼容。
最后提醒大家的是在應用開發過程中,最難的不是完成應用的開發工作,而是在后續的升級、維護過程中讓應用系統能夠擁抱變化。擁抱變化也就意味著在滿足需求且不破壞系統穩定性的前提下保持高可擴展性、高內聚、低耦合,在經歷了各版本的變更之后依然保持清晰、靈活、穩定的系統架構。當然,這是一個比較理想的情況,但我們必須要朝著這個方向去努力,那么遵循面向對象六大原則就是我們走向靈活軟件之路所邁出的第一步。
所有分享大綱:Android進階之旅 - 系統架構篇