歡迎光臨
每天分享高質量文章

對 Android 開發的一點思考

作者:王英豪

連結:https://www.jianshu.com/p/5dfae715f2b3

17 年畢業開始工作到現在已快兩個年頭,在實際專案開發的過程中,我對 Android 開發有了一些自己的思考。本著碰撞才會有火花、討論才會進步的理念,我把對 Android 開發的一點思考分享出來,真誠的希望可以有不同的觀點,在糾結反駁之中得到最優解,共同進步。

最初的時候,你是否是一個完美主義者,不容忍任何一點 warning 與嘆號,if 必有 else,switch 必有 default,即使 else 和 default 中確實什麼也不用處理,你也會新增一個 //do nothing 註釋,表示這裡的邏輯是經過充分考慮的,下次閱讀程式時,告訴別人也告訴自己,這裡的確什麼也不用處理,可以快速跳過。

我想大多數開發者,都是經歷過這種心態的,然後在繁忙的版本迭代中、在趕著回家的加班時、在愈來愈發的對自己的薪水不滿時、在一次又一次看到團隊中別人得過且過的程式碼時,漸漸的,就可能對“生活”妥協,丟掉了完美主義。

然而如果你有更高的追求,就要勇敢的戰勝自己的感性。

使用 IntDef、StringDef

平時特常用的 View.setVisibility() 方法使用 IntDef 來規定引數的可選項,可以試想一下,假如沒用 IntDef 會怎麼樣?

對於初學者來說,可能要稍微閱讀一下原始碼或查下資料才能知道 setVisibility 有哪些引數可以設定。你可能會覺得沒什麼差,因為你很清楚 setVisibility 方法有哪些引數可以設定。

但若是程式中新增的一個方法呢?

比如你新接觸一個模組,某個介面有若干個跳轉 Action,你得先找到定義這些 Action 的地方,而若一不小心將這些 Action 分散寫在不同的地方,那對後面的維護和拓展可能就是一個災難。

建議凡是符合語意的邏輯,都必須用 IntDef、StringDef 來約束,它比列舉節省記憶體,效能更優,其 RetentionPolicy.SOURCE 表示此註解只在原始碼中存在,編譯時會剔除。你可以在 Android Studio 的 Live Templates 中新增 IntDef、StringDef 寫法:

 

使用精準表達的變數型別

比如你需要宣告一個變數來表示某個功能是否啟用,譬如控制你的 App 是否展示廣告,並且可以透過服務端線上下發開關來控制,如果沒有接收到下發的開關,就根據地區來決定是否展示。

這種情況下你會使用什麼型別的變數?

你可能會想到使用一個 int 型別變數來控制,然後需要給這個變數加上註釋:

 

// 0:展示; 1:不展示; 2:未接收到線上開關,需要根據地區決定是否展示
private int mShouldShowAd;

 

以後每當改動到這部分邏輯,都需要檢視一下這個變數數值對應的含義,隨著時間的推移和程式碼量的增多,在此邏輯之上可能堆積了很多程式碼,然後就會出現各種各樣的問題,別人可能在不存在的邏輯分支做了一些事:

 

if (mShouldShowAd == 0) {
    //do something
} else if (mShouldShowAd == 1) {
    //do something
} else if (mShouldShowAd == 2) {
    //do something
} else {
    //do something...
}

甚至可能對這個變數賦值 [0,2] 區間之外的數值! 你可能對這個變數的意義很瞭解也絕不會用錯,但你不能保證他人不會出現上面所說的荒唐的用法,因為這個變數型別並不能很精準的表達它的語意,也沒有任何約束性。

我們可以怎樣改善這種難維護、有風險的程式碼?

  • 可以使用 IntDef 規定這個變數的取值

  • 可以換成 Boolean 型別,用 null 表示未獲取到線上開關,恰好的表達語意並且易讀、易維護

使用盡可能少的變數

舉個例子:

 


mDebug = BuildConfig.DEBUG;

if (mDebug) {
    Log.d(TAG, "...");
}

 

你是否寫過這樣的邏輯?

明明已經存在了一個可以直接使用的變數條件,你仍然要重新定義。這個例子邏輯還十分簡單,此變數是 final 型別的,不會出錯。而如果是非 final 型別的變數,那就是強行增加了一個賦值聯動的邏輯,埋下了隱患,後續如果出了問題,白白的增加了定位問題的路徑與複雜度。

實際開發中我們可能自己都意識不到使用了不必要的變數,比如我們的服務端介面一般會有多個介面環境,那你的程式碼可能是這樣的:

 

//是否是測試環境
private static boolean sIsApiHostTest;
//是否是beta環境
private static boolean sIsApiHostBeta;
//正式環境host
private static String sApiHost = "http://api.com/";
//測試環境host
private static String sApiHostTest = "http://test.api.com/";
//beta環境host
private static String sApiHostBeta = "http://beta.api.com/";

/**
 * 是否是測試環境
 */
public static boolean isApiTest() {
    return sIsApiHostTest;
}

/**
 * 是否是beta環境
 */
public static boolean isApiBeta() {
    return sIsApiHostBeta;
}

/**
 * 獲取介面域名
 */
public static String getApiHost() {
    if (isApiTest()) {
        return sApiHostTest;
    } else if (isApiBeta()) {
        return sApiHostBeta;
    } else {
        return sApiHost;
    }
}

這樣看起來好像沒什麼問題,只要維護好 sIsApiHostTest、sIsApiHostBeta 這兩個變數就行了。

如果後面又添加了一個環境呢?

又添加了三四個環境呢?是不是還要維護多個變數?

這個邏輯可以透過減少變數來改善:

 

//當前環境host
private static String sCurApiHost;
//正式環境host
private static String sApiHost = "http://api.com/";
//測試環境host
private static String sApiHostTest = "http://test.api.com/";
//beta環境host
private static String sApiHostBeta = "http://beta.api.com/";

/**
 * 是否是測試環境
 */
public static boolean isApiTest() {
    return sApiHostTest.equals(sCurApiHost);
}

/**
 * 是否是beta環境
 */
public static boolean isApiBeta() {
    return sApiHostBeta.equals(sCurApiHost);
}

/**
 * 獲取介面域名
 */
public static String getApiHost() {
    return sCurApiHost;
}

再加上 StringDef 就完美了:

 

@StringDef({ApiHost.sApiHost, ApiHost.sApiHostTest, ApiHost.sApiHostBeta})
@Retention(RetentionPolicy.SOURCE)
public @interface ApiHost {
    //正式環境host
    String sApiHost = "http://api.com/";
    //測試環境host
    String sApiHostTest = "http://test.api.com/";
    //beta環境host
    String sApiHostBeta = "http://beta.api.com/";
}

//當前環境host
@ApiHost
private static String sCurApiHost = ApiHost.sApiHost;

/**
 * 是否是測試環境
 */
public static boolean isApiTest() {
    return ApiHost.sApiHostTest.equals(sCurApiHost);
}

/**
 * 是否是beta環境
 */
public static boolean isApiBeta() {
    return ApiHost.sApiHostBeta.equals(sCurApiHost);
}

/**
 * 獲取介面域名
 */
@ApiHost
public static String getApiHost() {
    return sCurApiHost;
}

/**
 * 設定介面域名
 */
@ApiHost
public static void setApiHost(@ApiHost String apiHost{
    sCurApiHost = apiHost;
}

不知道你有沒有感受到易讀性、可維護性、拓展性都蹭蹭蹭的往上漲呢?

回歸最初的完美主義

同時接受多個資料源資料的邏輯相比只接受一個資料源的資料需要考慮時序性等問題,要複雜很多。

打個比方,可以把資料源當作你的直接上級,上級會不定時的分配任務給你做,如果你有多個上級,一個讓你做任務 A,一個讓你做任務 B,且 A 需要在 B 之前完成,你要怎麼辦?

兩個上級都讓你做任務 A,但是隻用做一次,你要怎麼辦?

在安卓中較為典型的場景就是同時載入網路和本地快取資料到 UI 上,你的 UI 上展示的資料來自不同的地方,你需要考慮不同資料源之間如何協作。

谷歌推出的 Jetpack 開髮指南上推薦我們使用單一資料源,假如你的網路資料也需要快取的話,那你的實現邏輯應該是這樣:

  • 載入網路資料,傳回後插入到本地

  • 統一從本地取資料展示到 UI 上

這點和上面說的“使用盡可能少的變數”有相通之處,都是儘量規避使用多個條件變數對程式產生影響的邏輯。

職責分離

強烈建議什麼類裡就乾什麼事,別把邏輯都揉到一塊兒,這樣隨著程式碼量的增加,會愈發的難以維護,到最後就變成一顆存在重大隱患的地雷,看見就頭疼。

舉個例子,比如你要自定義一個 View,那就像系統控制元件一樣,只負責一個控制元件該負責的事,處理一下渲染、展示,把手勢互動透過介面開放出來,把資料的獲取寫在資料倉庫中。這樣如果資料展示出了問題,可以很快的定位到是資料獲取出了問題,還是渲染展示出了問題;如果這個控制元件的渲染展示是經過驗證的,之後就幾乎不用改動此控制元件,至少你有機會可以將你的自定義 View 寫的像系統的控制元件一樣穩定。

這裡再推薦一下谷歌的 Jetpack – MVVM 全家桶,MVC 真的是不易讀、難維護、問題多、很簡陋。

回歸最初的完美主義

希望你我可以戰勝感性,不向“生活”妥協,讓優秀成為準則和習慣,回歸最初的完美主義。

已同步到看一看
贊(0)

分享創造快樂