leobert重構程式碼二三事--一.可怕的低階程式碼

weixin_33912246發表於2018-06-13

接下來會在簡書演繹一些程式碼重構以及架構設計上的小故事,毫不要臉的用作者自身故事改編

leobert是一位從事Android開發的IT engineer,機緣巧合之下來到了motorfans和一群有夢想的人一起奮鬥。

leobert不僅僅對Android客戶端開發有過硬的基礎,對專案管理,產品運營,架構設計都有一定的瞭解,在來公司之前,leobert對motorfans做了充分的分析,並對專案實現做了一定的預測。直到leobert真正接觸到程式碼資產。噩夢開始了。

無意於攻擊專案最初始的開發者Tony,Tony copy了一部分以前專案中的框架,並設計了一部分抽象體系作為基礎框架。

按照經驗來說,如果公司高薪聘請了一位架構師負責架構,並將他與實際生產隔離,那麼很有可能造就空中樓閣式的架構。而Tony是實際開發者,團隊leader,按理來說應該不會出現這樣的情況,設計的框架應該是接地氣的

然而真實情況是這樣的

  • Tony建立了一個龐大的助手類類,將各種可能(或者是他覺得可能)有用的程式碼全部放在了助手類中,並且沒有unit test。
  • Tony建立了一個無比複雜的activity基類,他具有這樣的特性:
    • 建立佈局
    • 支援自定義佈局
    • 繫結且必須繫結一個內容fragment

    該fragment具有以下特性:

    • 具有頁面重新整理功能和到底部載入功能
    • 通過模板呼叫重新整理和載入的webservice
    • 通過模板建立列表等集合型別檢視的子view
    • 以及一些其他零碎的UI互動

除此之外,就沒了!

這帶來的問題就比較明顯了,我們說OOP的三大基本特徵:“封裝”,“繼承”,“多型”;從這一點來看的話,Tony是在嘗試處理封裝,但是Tony對封裝的理解可能有點畸化,先不談MVP、MVC、MVVM等架構概念,Tony所定義的主要角色只有這些:

  • 各種自定義View控制元件、Dialog等
  • Activity頁面
  • Fragment頁面
  • 包含各種可能重複使用到的程式碼段的Utility

封裝的目的是:把客觀事物封裝成抽象的類,並且類可以把自己的資料、方法只讓可信的類或者類物件操作,對不可信的進行隱藏。
但是Tony所做的工作就是在描述一系列的客觀頁面事物,並且並沒有任何的物件“配合”概念,即根本沒有真正使用到“組合”,他的Activity、Fragment幾乎對一切都知曉、對一切特定領域的行為細節無所不知,他們知道DB是怎麼做CURD的,他們知道WebAPI請求中各種細節對View所帶來的影響,甚至可以說如果沒有其他基礎專案的支援,這些類還會知曉Http三次握手、斷開連線等等等等。

再說繼承,Tony的程式碼中,繼承體系非常的薄,只有兩代:BaseXXX 和 具體的ABCXXX
舉個實際例子:
Tony定義且只定義了兩個關於Fragment的抽象基類:

  • BasePtrFragment,顧名思義,這是一個抽象Fragment、具有下拉、上拉的行為操作。如果具體的頁面不允許存在上拉下拉操作就通過覆寫方法來禁止。實際情景中直接實現它的子類並沒幾個。
  • 繼承BasePTRFragment的BasePTRListFragment,更多的都是該類的子類。

那麼可以判斷:Tony認為在描述人類的時候,並不需要定義:Human、Male、Female三個類,只需要定義Human,並且存在成員方法boolean isMale()就可以了,當需要定義Boy這樣的類的時候,直接覆寫方法就行了。我並不能說這是錯的,但這是非常不合適的。當然這不是最致命的。

最致命的還在於封裝,我懷疑Tony在自覺繼承中不需要定義Male,Female並不能說是錯的這一點上進一步認為自己的封裝已經成功了。

我們還以Human為例子,我們忽略掉醫學、生物學的嚴謹知識系統,可能有這樣一層基類XXXXAnimal,它依賴了消化系統,傳入一些食物(肉類、植物類)得到脂肪、蛋白質等產物(忽略掉一系列不知道、忘掉的知識),在Human中,我們注入的依賴(具體的消化系統)和牛類中注入的會有所區別(牛不止一個胃);哪怕忽略掉此處的物種差異,畢竟Tony認為一個Fragment都是有上拉下拉的、如果哪個類不具備該屬性覆寫成員方法就行。OK進一步往下思考人的消化系統也是有所差別的,成人能夠接受的食物和一個月的baby是不一致的。那麼是在Human中依賴一個HumanDigestiveSystem呢還是直接描述一下消化系統的具體細節呢?

我猜這時候諸位一定毫不猶豫的選擇依賴一個HumanDigestiveSystem,畢竟已經拆解的很赤裸裸了,而且消化系統的細節太複雜了,想脫離物件直接寫也太TMD開玩笑了。

而Tony呢?他在實際生產中選擇的是在Human中直接寫消化系統的細節,以BasePTRListFragment為例,他在其中描述了一些內容,我抽取重點:

  • 模板方法-獲取頁面重新整理介面地址,http方法行為
  • 模板方法-序列化介面返回的資料、返回一個List資料集
  • 模板方法-獲取列表控制元件adapter
  • webapi呼叫的callback實現類,解析資料見2,解析到的資料填充到adapter
  • 下拉的事件回撥中呼叫一個類似如下虛擬碼的方法
protected void refreshPage() {
String method = getApiInvokeMethod();
String url = getApiUrl();
Map<String,Object> params = getApiParams();
WebApi.request(method,url,new Callback() {
      void onSuccess(byte[] res) {
           boolean succsee= 解析res取出基本json結構中的返回碼判斷成功失敗;
            if(success){
                List<?> data =  parseResponse(res)
                adapter.resetData(data);
                .....
            } else {
                提示錯誤資訊;
                顯示空檢視
            }
      };
      void onFailure(int httpCode,byte[] res) {
              ToastUtils.httpFailure(httpCode);
              onRefreshFailure();//錯誤檢視
      };
})
}

等等。

看起來這個抽象類描述了下拉重新整理式列表頁面的資料重新整理流程。但如果他就此收手,那麼一切都還是可接受的。實際情況中絕大多數頁面下拉不僅僅是牽涉一個介面。

於是Tony通過覆寫refreshPage方法,除了callSuper,還將額外要請求的介面細節全部寫一遍。我的天、我已經不想再回憶那種程式碼了。

那是一個描述不了實際情況的抽象Fragment,具有3k行程式碼;
那是一系列重寫父類的具體行為的子類,重寫的內容是模板方法過程;
簡而言之就是拿一個並不合適的BasePTRListFragment強行做基類

哪怕退一步說,就算是那種只有一個web介面的頁面,我們真的特別需要這個抽象類嗎?或許吧,有一個大約100行的抽象類描述一下過程細節也就足夠了。

實在是太晚了,我也不想再去描述哪些令人懊惱的程式碼了,文末給出Leobert的一些經驗:

  • 系統最初的設計、不一定能夠直接面面俱到,也不一定要面面俱到,但是重點一定要明確;----定義好消化系統類(先忽略掉食道胃小腸大腸等),表現出Human可以通過消化系統進食、消化的特徵即可(同理還有運動系統、迴圈系統等),不要直接在Human中嘗試寫消化的過程細節(模板方法),這不是描述Human的重點。
  • 組合優於繼承,這是對上面一條的一個補充,如果一個類中的一些細節具有差異化,值得考慮是否有封裝的必要;----定義健康的消化系統和有病灶的消化系統(前提是已經定義消化系統)要比:忽略消化系統類、定義消化系統健康的人和消化系統有病灶的人要好;在消化系統中依賴胃、腸等類例項,通過他們是否有病灶要比:繼承消化系統類、重寫類特徵要好
  • 不停的利用里氏代換原則思考整合體系是否合理。

考慮到本篇所介紹的專案背景,其他內容不強行往上靠。

持續更新,但有空再說

相關文章