Clean Architecture - 清晰簡潔的Android 應用架構

我叫小鄧子發表於2018-11-30

Clean Architecture - 清晰簡潔的Android 應用架構

在我這幾年的學習和成長中,深刻的意識到搭建一個Android應用架構是件非常痛苦的事,它不僅要滿足不斷增長的業務需求,還要保證架構自身的整潔,這讓事情變得非常具有挑戰,但我們必須這樣做,因為健壯的Android架構是一款優秀APP的基礎。本文的程式碼示例可以從github中獲得,倉庫地址是*android-easy-cleanarchitecture

Why we need an architecture?

Android入門要求始終不高,因為Android Framework會幫我們做很多事,甚至不需要通過深入的學習就能寫出一個簡單的APP,比如說在ActivityFragment中擺放幾個View用來展示到螢幕上,後臺耗時任務放在Service中執行,元件之間使用Broadcast傳遞資料,由此看來“人人都能成為Android工程師”,真的是這樣嗎?

當然不是!!!

如果我們如此天真的開始程式設計,遲早會為此付出代價。那些依賴關係混亂,靈活性不夠高的程式碼將會成為我們最大的阻礙,任由發展的後果就是,導致專案一片狼藉,我們很難加入新的功能,只能對它進行重構甚至推翻重做。在開始程式設計前,我們不應該低估一個應用程式的複雜性,應該將你的APP看做一個擁有前端,後端和儲存特性的複雜系統。

另外,在軟體工程領域,始終都有一些值得我們學習和遵守的原則,比如:單一職責原則依賴倒置原則避免副作用等等。Android Framework不會強制我們遵守這些原則,或者說它對我們沒有任何限制,試想那些耦合緊密的實現類,處理大量業務邏輯的Activity或Fragment,隨處可見的EventBus,難以閱讀的資料流傳遞和混亂的回撥邏輯等等,它們雖然不會導致系統馬上崩潰,但隨著專案的發展,它們會變得難以維護,甚至很難新增新的程式碼,這無疑會成為業務增長的可怕障礙。

所以說,對於開發者們來講,一個好的架構指導規範,至關重要。

從事Android工作以來,我始終認為我們能將APP做的更好,我也遇到過很多好的壞的軟體設計,自己也做過很多不同的嘗試,我不斷地吸取教訓並做出改變,直到我遇到了Clean Architecture,我確定這就是我想要的,我決定使用它。本文的目標是分享我使用clean Architecture構建專案時所收穫的經驗,希望能夠為你的專案改進帶來靈感。

Avoid God Activity

可能是出於“快速迭代”,於是你整合了這個萬能的Activity,它無所不能:

  • 管理自身生命週期(在正確的生命週期中處理任務)
  • 維持UI狀態(配置變更時儲存/回覆檢視狀態)
  • 處理Intent(接收和傳送正確的Intent)
  • 資料更新(與遠端API同步資料,本地儲存)
  • 執行緒切換
  • 業務邏輯 ......

甚至突破了所有的約束壁壘:在Android世界裡面加入了業務程式碼;在BaseActivity中定義了所有子類可能用到的變數等等。它現在的確就是個“上帝”,方便且萬能的“上帝”!

隨著專案的發展,它已經龐大到無法再新增程式碼了,於是為它寫了很多幫助類,你想重構它:

god activity

不經意間,你已經埋下了黑色炸彈

看上去,業務邏輯被轉移到了幫助類中,Activity中的程式碼減少了,不再那麼臃腫,幫助類緩解了“萬能類”的壓力,但隨著專案的成長,業務的擴大,同時這些幫助類也變多,那個時候又要按照業務繼續拆分它們,APIHelperThisAPIHelperThat等等。原來的問題又出現了,測試成本還在,維護成本好像又增加了,那些混亂並且難以複用的程式又回來了,我們的努力好像都白費了。

然而你寫這個萬能類的初衷是什麼,想快捷、方便的使用一些功能函式嗎,尤其希望在子類中能夠很快的拿到。

當然,一部分人會根據不同的業務功能分離出不同的抽象類,但相對那種業務場景下,它們仍是萬能的。

無論什麼理由這種創造“上帝類”的做法都應該儘量避免,我們不應該把重點放在編寫那些大而全的類,而是投入精力去編寫那些易於維護和測試的低耦合類,如果可以的話,最好不要讓業務邏輯進入純淨的Android世界,這也是我一直努力的目標。

Clean architecture and The Clean rule

這種看起來像“洋蔥”的環形圖就是**Clean Architecture**,不同顏色的“環”代表了不同的系統結構,它們組成了整個系統,箭頭則代表了依賴關係,

Clean Architecture - 清晰簡潔的Android 應用架構

關於它的組成細節,在這裡就不做深入的介紹了,因為有太多的文章講的比我好,比我詳盡。另外值得一提的是architecture是面向軟體設計的,它不應該做語言差異,而本文將主要講述如何結合Clean Architecture構建你的Android應用程式。

在使用clean架構搭建專案前,我閱讀了大量的文章,並付諸了很多實踐,我的收穫很大,經驗和教訓告訴我一個架構的清晰和整潔離不開這三個原則:

  • 分層原則
  • 依賴原則
  • 抽象原則

接下來我就分別闡述一下,我對這些原則的理解,以及背後的原因。

分層原則

首先,值得一提的是框架不會限制你對應用程式的具體分層,你可以擁有任意的層數,但是在Android中通常情況下我會劃分為3層:

  • 外層:實現層
  • 中間層:介面適配層
  • 內層:業務邏輯層

接下來,介紹下這三層所應包含的內容。

實現層

一句話:實現層就是Android框架層。這個地方應該是Android framework的具體實現,它應該包括所有Android的東西,也就是說這裡的程式碼應該是解決Android問題的,是與平臺特性相關的,是具體的實現細節,如,Activity的跳轉,建立並載入Fragment,處理Intent或者開啟Service等。

介面適配層

介面適配層的目的是連線業務邏輯與框架特定程式碼,擔任外層與內層之間的橋樑。

業務邏輯層

最重要的是業務邏輯層,我們在這裡解決所有業務邏輯,這一層不應該包含Android程式碼,應該能夠在沒有Android環境的情況下測試它,也就是說我們的業務邏輯能夠被獨立測試,開發和維護,這就是clean架構的主要好處。

依賴規則

依賴規則與箭頭方向保持一致,外層”依賴“內層,這裡所說的“依賴”並不是指你在gradle中編寫的那些dependency語句,應該將它理解成“看到”或者“知道”,外層知道內層,相反內層不知道外層,或者說外層知道內層是如何定義抽象的,而內層卻不知道外層是如何實現的。如前所述,內層包含業務邏輯,外層包含實現細節,結合依賴規則就是:業務邏輯既看不到也不知道實現細節

對於專案工程來講,具體的依賴方式完全取決於你。你可以將他們劃入不同的包,通過包結構來管理它們,需要注意的是不要在內部包中使用外部包的程式碼。使用包來進行管理十分的簡單,但同時也暴露了致命的問題,一旦有人不知道依賴規則,就可能寫出錯誤的程式碼,因為這種管理方式不能阻止人們對依賴規則的破壞,所以我更傾向將他們歸納到不同的Android module中,調整module間的依賴關係,使內層程式碼根本無法知道外層的存在。

另外值得一提的是,儘管沒人能夠阻止你跳過相鄰的層去訪問其它層的程式碼,但我還是強烈建議只與相鄰層進行資料訪問。

抽象原則

在依賴原則中,我已經暗示了抽象原則,順著箭頭方向由兩邊朝中間移動時,東西就越抽象,相對的,朝兩邊移動時,東西就越具體。這也是我一直反覆強調的,內圈包含業務邏輯,外圈包含實現細節

接下來我會用一個例子來解釋抽象原則:

在內層定一個抽象介面Notification,一方面,業務邏輯可以直接使用它來向使用者顯示通知,另一方面,我們也可以在外層實現該介面,使用Android framework提供的NotificationManager來顯示通知。業務邏輯使用的只是通知介面,它不瞭解實現細節,不知道通知是如何實現的,甚至不知道實現細節的存在。

這很好演示瞭如何使用抽象原則。當抽象與依賴結合後,就會發現使用抽象通知的業務邏輯看不到也不知道使用Android通知管理器的具體實現,這就是我們想要的:業務邏輯不會注意到具體的實現細節,更不知道它何時會改變。抽象原則很好的幫我們做到了這一點。

Apply on Android

按照上面提到的分層原則,我把專案分為了三層,也就是說它有三個Android module,如下圖所示:

Clean architecture modules

Domain中定義業務邏輯規則,在UI中實現介面互動,Model則是業務邏輯的具體實現方式(Android framework)。箭頭方向代表依賴關係,內層抽象,外層具體,外層知道內層,內層不瞭解外層。

具體到Android中的框架結構如下圖所示:

clean architecture structure

你可能有些困惑,為什麼Domain指向Data?既然Domain包含業務邏輯,它就應該是應用程式的中心,它不應該依賴Model,按照前面提到的原則,Domain是抽象的,Model是具體的,應該是Model依賴Domain,而不是Domain依賴Model

其實這很好理解,也是我始終強調的,這裡所說的“依賴”並不是指配置在gradle中的dependency,你應該將它理解為“知道”,“瞭解”,“意識”,圖中的箭頭代表了呼叫關係,而非模組間的依賴關係。我們應該能夠理解:抽象是理論,依賴是實踐,抽象是應用的邏輯佈局,依賴是應用的組合策略。對於框架結構的理解,我們應該跳出程式碼層面,不要侷限在慣性思維中,否則很快就會陷入邏輯混亂的怪圈。

與呼叫關係對應的就是資料流的走向:

clean architecture data stream

app中接受使用者的行為,根據domain中定義的業務規則,訪問model中的真實資料,然後依次返回,最終更新介面,這就是一個完整的資料流走向。

為了更方便理解,我對專案進行了簡單的拆解,並在圖中加上了類的用例描述,它看起來就像這樣:

clean architecture UML

對上圖所表示內容做一下總結:

首先,專案被分為三層:

  • app:UI,Presenter ...
  • domain:Entity,Use case,Repository ...
  • model:DB,API ...

其次,更細節的子模組劃分:

UI

檢視,包含所有的Android控制元件,負責UI展示。

Presenter

處理使用者互動,呼叫適當的業務邏輯,並將資料結果傳送到UI進行渲染。也就是說Presenter將擔任著介面適配層的責任,連線Android實現和業務邏輯,負責資料的傳遞和回撥。

Entity

實體,也就是業務物件,是應用的核心,它代表了應用的主要功能,你應該能夠通過檢視這些應用來判斷這款應用的功能,例如,如果你有一個新聞應用,這些實體將是體育、汽車或者財經等實體類。

Use case

用例,即interactor,也就是業務服務,是實體的擴充套件,同時也是業務邏輯的擴充套件。它們包含的邏輯並不僅針對於一個實體,而是能處理更多的實體。一個好的用例,應該可以用通俗的語言來描述所做的事情,例如,轉賬可以叫做TransferMoneyUseCase。

Repository

抽象的核心,它們應該被定義為介面,為UseCase提供相應的輸入和輸出,能夠直接對實體進行CRUD等操作。或者它們可以暴露一些更復雜的操作行為,如過濾,聚合等,具體的實現細節可以由外層來實現。

DB&API

資料庫和API的實現都應該放在這裡,比如上面示例中,可以將DAO,Retrofit,json解析等放在這裡。它們應該能夠實現在Repository中定義的介面,是具體的實現細節,能夠對實體類進行直接操作。

Show code

你可以像前面UML圖中演示的那樣,組合你的MVPViewMVPPresenter,讓它們更容易被管理和維護。

首先定義BaseViewBasePresenter,在BaseView中我是用了RxJavaObservable作為結果型別。:

public interface BaseView<T> {

  void showData(Observable<T> data);

  void showError(String errorMessage);
}
複製程式碼
public interface BasePresenter<V> {

  void attachView(V view);

  void detachView();
}
複製程式碼

假設你有一個根據城市ID獲取該城市已上映電影的需求,那麼你可以這樣組合你的MovieViewMoviePresenter介面:


interface MovieContract {

  interface Presenter<Request, Result> extends BasePresenter<View<Result>> {
    void loadData(Request request);
  }

  interface View<Result> extends BaseView<Result> {
    void showProgress();
  }
}
複製程式碼

泛型的加入,有效保證了資料的型別安全

接下來實現你自己的XXXPresenterXXXView介面的實現類,就像這樣:

class MoviePresenterImp implements MovieContract.Presenter<MovieUseCase.Request, List<MovieEntity>> {

  @Override public void attachView(UserContract.View<List<MovieEntity>> view) {
     /*subscribe MovieUseCase and do some initialization*/
  }

  @Override public void detachView() {
    /*unsubscribe MovieUseCase and release resources*/
  }

  @Override public void loadData(MovieUseCase.Request request) {
     /*load data from MovieUseCase*/
  }

}


class MovieActivity extends AppCompatActivity implements MovieContract.View<List<MovieEntity>> {

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    /*also initialize the corresponding presenter*/
  }

  @Override public void showData(Observable<List<MovieEntity>> data) {
    /*show data and hide progress*/
  }

  @Override public void showError(String errorMessage) {
    /*show error message and hide progress*/
  }

  @Override public void showProgress() {
    /*show progress*/
  }
}

複製程式碼

關於示例中的UseCase.Request來自於Clean Architecture: Dynamic Parameters in Use Cases:在XXXUseCase中建立靜態內部類Request作為動態請求引數的容器。其實這很好理解,而且也完全正確,因為UseCase就是你定義業務規則的地方,把業務(請求)條件業務規則定義組合在一起不僅容易理解也更方便管理。不過我會在下篇文章中介紹另一種動態引數方式,也是我一直在使用的。

總結:

我相信你和我一樣,在搭建框架的過程中遭遇著各式各樣的挑戰,從錯誤中吸取教訓,不斷優化程式碼,調整依賴關係,甚至重新組織模組結構,這些你做出的改變都是想讓架構變得更健壯,我們一直希望應用程式能夠變得易開發易維護,這才是真正意義上的團隊受益。

不得不說,搭建應用架構的方式多種多樣,而且我認為,沒有萬能的,一勞永逸的架構,它應該是不斷迭代更新,適應業務的。所以說,你可以按照文中提供的思路,嘗試著結合業務來構建你的應用程式。

另外值得一提的是,如果你想做的更好,可以為你的專案加入模板化,元件化等策略,因為並沒有說一個專案只能使用一種框架結構。: )

最後,希望這篇文章能夠對你有所幫助,如果你有其他更好的架構思路,歡迎分享或與我交流。

相關文章