關於Android架構,你是否還在生搬硬套?

原文作者:Bezier

原文鏈接:https://juejin.cn/user/2629687546479742

前言

關於Android架構,可能在很多人心裏一直都是虛無縹緲的存在,似懂非懂、爲了用而用、處處生搬硬套,這種情況使用的意義真的很有限。本人有多個項目重構的經驗,恰好對設計領域較爲感興趣,今天我將毫無保留的將自己對架構、設計的理解分享給大家。

本文不會具體去講什麼是MVC、MVP、MVVM,但我描述的點應該都是這些模式的基石,從本質上講明白爲什麼這樣做,這樣做的好處是什麼,有了這些底層思想的支持再去看對應的架構模式,相信會讓你有一種煥然一新的感覺。

知識儲備:需掌握Java面向對象、六大設計原則,如果不理解也無妨,我儘量將用到的設計原則加以詳細描述

目錄

  • 1. 模塊化的意義何在?
    • 1.1 基本概念以及底層思想
    • 1.2 我們要基於哪些特性去做模塊化劃分?
    • 1.3 Android如何做分層處理?
    • 1.4 Data Mapper或許是解藥
    • 1.5 無處安放的業務邏輯
  • 2. 合理分層是給 數據驅動UI 做鋪墊
    • 2.1 什麼是 控制反轉?
    • 2.2 什麼是數據驅動UI?
    • 2.3 爲什麼說數據驅動UI底層思想是控制反轉?
    • 2.4 爲什麼引入Diff?
  • 3. 爲什麼我建議使用 函數式編程
    • 3.1 什麼是 函數式編程?
    • 3.2 Android視圖開發可以借鑑函數式編程思想

1. 模塊化的意義何在?

1.1 基本概念以及底層思想

所有的模塊化都是爲了滿足單一設計原則 (字面意思理解即可),一個函數或者一個類再或者一個模塊,職責越單一複用性就越強,同時能夠間接降低耦合性

在軟件工程的背景下,改動就會有出錯的可能,不要說"我注意一點就不會出錯"這種話,因爲人不是機器。我們能做的就是儘可能讓模塊更加單一,職責越單一影響到外層模塊的可能性就越小,這樣出錯的概率也就越低。

所以模塊化核心思想即:單一設計原則

1.2 我們要基於哪些特性去做模塊化劃分?

做模塊化處理的時候儘量基於兩種特性進行功能特性業務特性

功能特性

網絡、圖片加載等等都可稱之爲功能特性。比如網絡:我們可以將網絡框架的集成、封裝等等寫到同一個模塊(module、package等)當中,這樣可以增強可讀性(同一目錄一目瞭然)、降低誤操作概率,方便於維護也更加安全。同時也可將模塊託管至遠程如maven庫,可供多個項目使用,進一步提升複用性

業務特性

業務特性字面意思理解即可,就是我們常常編寫的業務,需要以業務的特性進行模塊劃分

爲什麼說業務特性優先級要高於功能特性

舉個例子如下圖:

image.png

相信很多人見過或者正在使用這種分包方式,在業務層把所有的AdapterPresenterActivity等等都放在對應的包中,這種方式合理嗎?先說答案不合理,首先這已經是在業務層,我們做的所有事情其實都在爲業務層服務,所以業務的優先級應該是最高的,我們應當優先根據業務特性將對應的類放入到同一個包中。

功能模塊核心是功能,應當以功能進行模塊劃分。業務模塊核心是業務,應當優先以業務進行模塊劃分,其次再以功能進行模塊劃分。

1.3 Android如何做分層處理?

前端開發其實就是做數據搬運,再展示到視圖中。數據視圖是兩個不同的概念,爲了提高複用性以及可維護性,我們應當根據單一設計原則我們應當將二者進行分層處理,所以無論是MVCMVP還是MVVM最核心的點都是將數據視圖進行分層。

絆腳石:

通常來講,我們通過網絡請求拿到數據結構都是後端定義的,這也就意味着視圖層不得不直接使用後端定義的字段,一旦後端進行業務調整會迫使我們前端從數據層-->視圖層都會進行對應的改動,如下僞代碼所示:

//原始邏輯
數據層
Model{
    title
}
UI層
View{
    textView = model.title
}

//後端調整後
數據層
Model{
    title
    prefix
}
UI層
View{
    textView = model.prefix + model.title
}

起初我們的textView顯示的是model中的title,但後端調整後我們需要在model中加一個prefix字段,同時textView顯示內容也要做一次字符串拼接。視圖層因爲數據層的改動而被動做了修改。既然做了分層我們想要的肯定是視圖、數據互不干擾,如何解決?往下看...

1.4 Data Mapper或許是解藥

Data Mapper是後端常用的一個概念,一般情況下他們是不會直接使用數據庫裏面的字段,而是加一個Data Mapper(數據映射)將數據庫錶轉按需換成Java Bean,這樣做的好處也很明顯,表結構甭管怎麼折騰都不會影響到業務層代碼。

對於前端我覺得可以適當引入Data Mapper,將後端數據轉換成本地模型,本地模型只與設計圖對應,將後端業務視圖完全隔離。這也就解決了 1.3 面臨的問題,具體方式如下:

數據層
Model{
    title
    prefix
}
本地模型(與設計圖一一對應)
LocalModel{
    //將後端模型轉換爲本地模型
    title = model.prefix + model.title
}
UI層
View{
    textView = localModel.title
}

LocalModel相當於一箇中間層,通過適配器模式將數據層與視圖層做隔離。

前端引入Data Mapper後可以脫離後端進行開發,只要需求明確就可以做視圖層的開發,完全不需要擔心後端返回什麼結構字段。並且這種做法是一勞永逸的,比如後端需要對某些字段做調整,我們可以不暇思索直奔數據層,涉及到的調整100%不會影響到視圖層

注意點:

當下有一部分公司爲了將前後端分離更徹底,由前端開發人員提供Java Bean(相當於LocalModel)的結構,好處也很明顯,更多的業務內聚到後端,很大程度提升了業務的靈活性,畢竟App發一次版成本還是比較大的。面對這種情況我們其實沒必要再編寫Data Mapper。所以任何架構設計都要結合實際情況,適合自己的纔是最好的。

1.5 無處安放的業務邏輯

關於業務邏輯其實是一個很籠統的概念,甚至可以將任意一行代碼稱之爲業務邏輯,如此寬泛的概念我們該如何去理解?我先大致將它分爲兩個方面:

  • 界面交互邏輯:視圖層的交互邏輯,比如手勢控制、吸頂懸浮等等都是根據業務需要實現的,所以嚴格來說這部分也屬於業務邏輯。但這部分 業務邏輯一般在視圖層實現。
  • 數據邏輯:這部分是大家常說的業務邏輯,屬於強業務邏輯,比如根據不同用戶類型獲取不同數據、展示不同界面,加上Data Mapper一系列操作其實就是給後端兜底,幫他們補全剩餘邏輯而已。爲了方便大家理解下文我將 數據邏輯統稱爲 業務邏輯

前面我們說到,Android開發應該具備數據層視圖層,那業務邏輯放在哪一層比較合適呢?比如MVVM模式下大家都說將業務邏輯放到ViewModel處理,這麼說也沒有太大的問題,但如果一個界面足夠複雜那對應的ViewModel代碼可能會有成百上千行,看起來會很臃腫可讀性也非常差。最重要的一點這些業務很難編寫單元測試用例

關於業務邏輯我建議單獨寫一個use case處理。

use case通常放在ViewModeler數據層之間,業務邏輯以及Data Mapper都應該放在use case中,每一個行爲對應一個use case。這樣就解決了ViewModeler臃腫的問題,同時更方便編寫測試用例。

注意點:

好的設計都是特定場景解決特定問題,過度設計不僅解決不了任何問題反而會增加開發成本。以我目前經驗來看Android開發至少一半的場景都很簡單:請求-->拿數據-->渲染視圖最多再加個Data Mapper,流程很單一併且後期改動的可能也不太大,這種情況就沒必要寫一個use case,Data Mapper扔到數據層即可。

2. 合理分層是給 數據驅動UI 做鋪墊

先說結論:數據驅動UI的本質是控制反轉

2.1 什麼是 控制反轉?

控制即對程序流程的控制,一般由我們開發者承擔,此過程爲控制。但開發者是人所以不可避免出現錯誤,此時可以將角色做一個反轉由成熟的框架負責整個流程,程序員只需要在框架預留的擴展點上,添加跟自己的業務代碼,就可以利用框架來驅動整個程序流程的執行,此過程爲反轉

控制反轉概念和設計原則中的依賴倒置很相似,只是少了一個依賴抽象

打個比方:

現有一個HTTP請求的需求,如果想自己維護HTTT鏈接、自己管理TCP Socket、自己處理HTTP緩存.....就是整個HTTP協議全部自己封裝,先不說這個工程能不能靠個人實現,就算實現也是漏洞百出,此時可以換個思路:通過OkHttp去實現,OkHttp是一個成熟的框架用它基本上不會出錯。個人封裝HTTP協議到使用OkHttp框架,這個過程在控制HTTP的角色上發生了一個反轉個人--->成熟的框架OkHttp即控制反轉,好處也很明顯,框架出錯的概率遠低於個人。

2.2 什麼是數據驅動UI?

通俗一點說就是當數據改變時對應的UI也要跟着變,反過來說當需要改變UI只需要改變對應的數據即可。現在比較流行的UI框架如FlutterComposeVue其本質都是基於函數式編程實現數據驅動UI,它們共同的目的都是爲了解決數據,UI一致性問題。

在當前的Android中可以使用DataBinding實現同樣的效果,以Jetpack MVVM爲例:ViewModelRepository拿到數據暫存到ViewModel對應的ObservableFiled即可實現數據驅動UI,但前提是從Repository拿到的數據可以直接用,如果在Activity或者Adapter做數據二次處理再notify UI,已經違背數據驅動UI核心思想。所以想實現數據驅動UI必須要有合理的分層(UI層拿到的數據無需處理,可以直接用)Data Mapper恰好解決這一問題,同時也可規避大量編寫BindAdapter的現狀。

DataBinding並非函數式編程,它只是通過AbstractProcessor生成中間代碼,將數據映射到XML中

2.3 爲什麼說數據驅動UI底層思想是控制反轉?

當前Android生態能實現數據綁定UI的框架只有兩個:DataBinding、Compose(暫不討論)

在引入DataBinding之前渲染一條數據通常需要兩步,如下:

var title = "iOS"
fun setTitle(){
     //第一步更改數據源
     title = "Android"
     //第二個更改UI
     textView = title
}

共需要兩步更改數據源、更改UI,數據源UI有一個忘記修改便會出現BUG,千萬不要說:“兩個我都不會忘記修改”,當面臨複雜的邏輯以及十幾個甚至幾十個的數據源很難保證不出錯。這種問題可以通過DataBinding解決,只需更改對應的ObservableFiledUI便會同步修改,控制UI狀態也從個人反轉到的DataBinding,個人疏忽的事情DataBinding可不會。

所以說數據驅動UI底層思想是控制反轉

2.4 爲什麼引入Diff?

引入diff之前:

RecyclerView想要實現動態刪除、添加、更新需要分別手動更新數據和UI,這樣在中間插了一道並且分別更新數據和UI已經違背了前面所說的數據驅動UI,而我們想要的是不管刪除、添加或者更新只有一個入口,只要改變數據源就會驅動UI做更新,想要滿足這一原則只能改變數據源後對RecyclerView做全部刷新,但這樣會造成性能問題,複雜的界面會感到明顯的卡頓。

引入diff之後:

Diff算法通過對oldItemnewItem做差異化比對,會自動更新改變的item,同時支持刪除、添加的動畫效果,這一特性解決了RecyclerView需要實現數據驅動UI的性能問題

3 爲什麼我建議使用 函數式編程

3.1 什麼是 函數式編程?

  • 一個入口,一個出口。
  • 不在函數鏈內部執行與運算本身無關的操作
  • 不在函數鏈內部使用外部變量(實際上這一條很難遵守,可以適當突破)

說的通俗點就是給定一個初始值,經過函數鏈的運行會得到一個目標值,運算的過程中外部沒有插手的權限,同時不做與本身無關的操作,從根本上解決了不可預期錯誤的產生。

舉個例子:

//Kotlin代碼

listOf(10, 20).map {
   it + 1
}.forEach {
   Log.i("list""$it")
}

上面這種鏈式編程就是標準的函數式編程,輸入到輸出之間開發者根本沒有插手的機會(即Log.i(..)之前開發者沒有權限處理list),所以整個流程是100%安全的,RxJavaFlow鏈式高階函數都是標準的函數式編程,它們從規範層面解決數據安全問題。所以我建議在Kotlin中 碰到數據處理儘量使用鏈式高階函數(RxJava、Kotlin Flow亦然)

3.2 Android視圖開發可以借鑑函數式編程思想

Android視圖開發大都遵循如下流程:請求-->處理數據-->渲染UI,這一流程可以借鑑函數式編程,將請求作爲入口,渲染做爲出口,在這個流程中儘量不做與當前行爲無關的事(這也要求ViewModel,Repository中的函數要符合單一原則)。這樣說有點籠統,下面舉個反例:

    View{
        //刷新
        fun refresh(){
            ViewModel.load(true)
        }
        //加載更多
        fun loadMore(){
            ViewModel.load(false)
        }
    }

    ViewModel{
        //加載數據
        load(isRefresh){
            if (isRefresh){
                //刷新
            }else{
                //加載更多
            }
        }
    }

View層有刷新、加載更多兩種行爲,load(isRefresh)一個入口,兩個出口。面臨的問題很明顯,修改刷新加載更多都會對對方產生影響,違反開閉原則中的閉(對修改關閉:行爲沒變不準修改源代碼),導致存在不可預期的問題產生。可以借鑑函數式編程思想對其進行改進,將ViewModelload函數拆分成refreshloadMore,這樣刷新加載更多兩種行爲、兩個入口、兩個出口互不干涉,通過函數的銜接形成兩條獨立的業務鏈條。

函數式編程可以約束我們寫出規範的代碼,面對不能使用函數式編程的場景,我們可以嘗試自我約束往函數式編程方向靠攏,大致也能實現相同的效果。

綜上所述

  • 合理的分層可以提升複用性、降低模塊間耦合性
  • Data Mapper 可以讓視圖層脫離於後端進行開發
  • 複雜的業務邏輯應該寫到use case中
  • 數據驅動UI的本質是控制反轉
  • 通過函數式編程可以寫出更加安全的代碼

如果大家對Jetpack MVVM感興趣歡迎留言,下篇文章我可以寫一下自己的看法..

參考文章:KunMinX 之 MVVM系列


本文分享自微信公衆號 - 秉心說TM(gh_c6504b1af5ae)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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