Android 架構演化之路

devtf發表於2015-10-31

大家好! 過了一好陣子了(在此期間我收到了大量的讀者反饋) 我決定是時候回到手機程式架構這個話題上了(這裡用android程式碼舉例), 給大家另一個我認為好的解決方案.

在開始之前, 我這裡假設大家都讀過了我之前用簡潔的辦法架構Android程式一文. 如果你還沒有讀過, 現在應該去讀一下那篇文章, 讀過之後可以更好的理解我下面要講的內容.

Android架構演化之路

架構的演化

演化是指一個事物變化成為另一個不同的事物的一個平緩過程, 通常情況下會變得更加複雜或者變成更好.

軟體開發一直在進化和改變. 實際上, 一個好的程式碼結構必須幫助我們成長, 這意味著不用重新寫所有程式碼就可以擴充套件功能. (儘管有些情況下應該大量的重寫程式碼, 但那又是另一回事了, 這裡先不做探討).

這篇文章的重點是如何保持android程式碼的清晰直觀, 為了闡述這一問題, 我將會帶著大家看幾個我認為重要的關鍵點. 記住下面這個圖我們就可以開始了.

Android架構演化之路

反應式方法: RxJava

在這裡我就不講RxJava的好處了(我猜大家都已經自己體會過了) , 因為已經有很多文章壞蛋們都講過了, 而且講的還都不錯. 這裡我要講的是它是怎麼使得android開發變得非常有趣的, 還有它是如何幫助我完成搭建第一個乾淨簡潔的架構的.

首先, 我選擇一個反應式模式讓用例(在簡潔的架構命名規範中叫做interactor) 都返回Observables

      public abstract class UseCase {

        private final ThreadExecutor threadExecutor;
        private final PostExecutionThread postExecutionThread;

        private Subscription subscription = Subscriptions.empty();

        protected UseCase(ThreadExecutor threadExecutor,
            PostExecutionThread postExecutionThread) {
            this.threadExecutor = threadExecutor;
            this.postExecutionThread = postExecutionThread;
        }

        protected abstract Observable buildUseCaseObservable();

        public void execute(Subscriber UseCaseSubscriber) {
            this.subscription = this.buildUseCaseObservable()
            .subscribeOn(Schedulers.from(threadExecutor))
            .observeOn(postExecutionThread.getScheduler())
            .subscribe(UseCaseSubscriber);
        }

        public void unsubscribe() {
            if (!subscription.isUnsubscribed()) {
                subscription.unsubscribe();
                }
            }
        }

可以看出,所有的子用例都繼承自這個抽象類,並在buildUseCaseObservable()這個抽象方法中構造一個可以完成耗時操作並返回需要資料的Observable

需要注意的是execute()這個方法, 我們保證了Observable讓自己執行在一個單獨的執行緒中, 這樣的話就可以最小限度的減少在主執行緒耗時. 然後通過主執行緒的scheduler機制把Observable的執行結果返回給主執行緒.

到現在為止, 我們已經有了Observable , 但是它產生的資料得有人來處理 . 所以我這裡將presenter(MVP 三層架構中的presentation層的一部分)改為了 Subscribers ,當用例產生資料後可以及時更新UI.

也就是下面這樣的subscriber:

       private final class UserListSubscriber extends
            DefaultSubscriber<List<User>> {

          @Override public void onCompleted() {
              UserListPresenter.this.hideViewLoading();
          }

          @Override public void onError(Throwable e) {
                UserListPresenter.this.hideViewLoading();
                UserListPresenter.this.showErrorMessage(new DefaultErrorBundle((Exception) e));
                UserListPresenter.this.showViewRetry();
          }

          @Override public void onNext(List<User> users) {
                UserListPresenter.this.showUsersCollectionInView(users);
            }
        }

DefaultSubscriber只是簡單實現了對錯誤的處理, 每一個subscriber都是presenter中的一個繼承自 DefaultSubscriber 的內部類.

從下面這張圖中, 你可以得到一個比較完整的思路.

Android架構演化之路

我們來總結一下RxJava帶給我們的好處:

  • 實現了Observables 和 Subscribers的解耦: 保持了結構穩定並簡化了測試.
  • 使得非同步任務變得簡單: 多層非同步任務被執行時, java的thread和future的操作和同步會變得非常複雜, 使用 scheduler 可以讓我們在非同步執行緒和主執行緒之間跳轉變得非常簡單(省去了多餘的步驟), 特別是我們需要更新UI介面的時候. 同時也避免了使程式碼變成非常難以理解的”回撥地獄”.
  • 資料的傳遞/組合: 我們可以使多個Observables組合起來而不影響到client端, 這樣提高了整套解決方案的可擴充套件性.
  • 異常處理: 任何一個Observable.出現異常都會通知到consumer.

從我的角度看這裡有一個小問題, 也是必須要付出的代價, 就是對這一概念不太熟悉的開發者的學習過程. 但是你會從中學到非常有價值的內容. 為了成功學習Reactive吧!

依賴注入: Dagger 2

我這裡不會講太多關於依賴注入的例子, 因為我之前寫過一篇專門說依賴注入的文章, 為了跟上我這裡的腳步, 強烈推薦大家讀一讀這篇文章.

值得強調的是, 像Dagger 2一樣的依賴注入框架可以帶給我們:

  • 元件的重用, 因為依賴可以被從外部配置和注入.
  • 最為合作者對抽象進行依賴注入時, 我們可以單單修改任何物件的實現, 而不用大量修改底層程式碼, 因為類的實現物件獨立而解耦的存在於另一個地方.
  • 依賴可以被注入到元件中去: 注入依賴的測試實現是有可能的, 這就使得測試變得更加容易.

Lambda 表示式: Retrolambda

沒有人會反對在我們的程式碼中使用Java 8 的 Lambdas, 使用Lambdas可以省去大量的樣板程式碼, 就像下面的程式碼塊:

    private final Action1<UserEntity> saveToCacheAction =
            userEntity -> {
                 if (userEntity != null) {
                     CloudUserDataStore.this.userCache.put(userEntity);
                 }
           };

但是這個問題我非常糾結. 在我們@SoundCloud, 曾經有過一次關於Retrolambda的討論, 主要的分歧是要不要用它 討論的結果是:

利:

  • Lambda 和方法的引用
  • 嘗試著用資源的方式.
  • Dev karma

弊:

  • java 8新特性的意外使用.
  • 第三方jar包很擾人.
  • 要在Android工程中使用它,必須引入第三方gradle外掛.

最終我們決定Retrolambda並不是一個能解決我們任何問題的庫: 使用了Retrolambda後程式碼的確好看易理解, 但這對我們來說並不是必須的, 因為現在大部分的IDE已經可以是實現這一功能, 至少是以可以接受的方式

老實說, 我在這裡提到Retrolambda的主要原因是想用一用它, 體驗一把在Android程式碼裡使用lambda是什麼感覺. 也許在我的業餘專案中可能會用到這個庫. 我只是把我的想法放在這裡, 用不用它的最終決定權在大家手裡. 當然了, 該作者創造了這麼偉大的一個庫也非常值得讚揚

測試途徑

說到測試, 和之前的例子並沒有什麼太大的不同.

  • Presentation層 : 用Espresso 2 和 Android Instrumentation 測試UI介面.
  • Domain 層: 因為只是正常的java模組,所以用JUnit + Mockito測試就好了.
  • Data層: 用 Robolectric 3 + JUnit + Mockito做遷移測試. 因為以前(案例第一個版本的時候)沒有內建單元測試支援,手動構造一個類似robolectric的框架非常複雜而且為了使它正常工作,還要一系列hack操作.

慶幸的是那都過去了, 現在所有的東西都可以直接用, 所以我重新把它們放在了資料模組中, 特別是可以放在預設的測試資料夾 src/test/java 下.

包結構組織

我認為程式碼/包的組織是一個良好架構的關鍵要素之一: 包結構是一個程式設計師看專案程式碼時最先注意到的 其他的一切要素都由它而來,也都取決於它.

下面是組織包結構常見的兩種方式:

  • 根據層級關係的不同: 單獨看每個包下面的程式碼通常情況下並沒有什麼聯絡, 這就降低了單個包裡的內聚性和模組性,而提高了包與包之間的耦合程度. 修改一個功能需要同時修改多個包下的多個檔案.而且, 要刪除一個功能也變得不是那麼簡單.
  • 根據功能的不同: 根據不同的包名可以找到對應的功能, 將功能(而且是隻有此功能)下的所有元件全都放在了一起. 這就提高了包裡的內舉性和模組性,而降低了包與包之間的耦合程度. 將協同工作的程式碼放在了一起,而不是將它們分佈在程式的各個地方.

我的建議是根據功能的不同來組織包結構, 可以帶來下面這些好處:

  • 更完善的模組化
  • 程式碼更加容易查閱
  • 最小化程式碼的作用域

有趣的是, 如果你在一個所謂的 功能性團隊 工作,(比如@SoundCloud), 程式碼結構的分配會變得更加容易更加模組化, 如果許多工程師在同樣的基礎程式碼上開發時這個優點就變得格外明顯.

Android架構演化之路

大家可以看出, 我的包結構看起來像是根據層級關係組織的: 這裡可能有舉例不太恰當的地方(比如將一切都放在’user’下面) 但是我會原諒自己這一次, 因為舉這個例子是為了供大家學習,為了表達我的觀點,主要目的是包含簡潔的架構思路. 照我說的做, 而不是照我做的做

附加彩蛋: 組織你的打包邏輯

我們都知道房子都是從地基開始修築的. 軟體開發也是一個道理, 我這裡要強調的是, 程式碼架構中, 打包系統(及其組織架構)是非常重要的一部分

在Android開發中, 我們使用一個叫做gradle的非常強大的打包系統. 這裡有 一系列竅門 幫助大家在組織打包指令碼時變得格外輕鬆:

根據功能的不同, 將打包系統分成多個指令碼檔案.

Android架構演化之路

ci.gradle:

     def ciServer = 'TRAVIS'
        def executingOnCI = "true".equals(System.getenv(ciServer))

        // Since for CI we always do full clean builds, we don't want to pre-dex
        // See http://tools.android.com/tech-docs/new-build-system/tips
        subprojects {
          project.plugins.whenPluginAdded { plugin ->
            if ('com.android.build.gradle.AppPlugin'.equals(plugin.class.name) ||
                'com.android.build.gradle.LibraryPlugin'.equals(plugin.class.name)) {
              project.android.dexOptions.preDexLibraries = !executingOnCI
            }
          }
        }

build.gradle:

      apply from: 'buildsystem/ci.gradle'
        apply from: 'buildsystem/dependencies.gradle'

        buildscript {
          repositories {
            jcenter()
          }
          dependencies {
            classpath 'com.android.tools.build:gradle:1.2.3'
            classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
          }
        }

        allprojects {
          ext {
            ...
          }
        }
        ...

如上圖, 你可以利用 “apply from: ‘buildsystem/ci.gradle’” 將配置檔案匯入任意build指令碼中. 不要將所有打包指令碼寫在一個build.gradle檔案中, 否則你會慢慢製造出一個怪物. 我已經受過教訓了.

將依賴整合到map中

dependencies.gradle:

       ...

        ext {
          //Libraries
          daggerVersion = '2.0'
          butterKnifeVersion = '7.0.1'
          recyclerViewVersion = '21.0.3'
          rxJavaVersion = '1.0.12'

          //Testing
          robolectricVersion = '3.0'
          jUnitVersion = '4.12'
          assertJVersion = '1.7.1'
          mockitoVersion = '1.9.5'
          dexmakerVersion = '1.0'
          espressoVersion = '2.0'
          testingSupportLibVersion = '0.1'

          ...

          domainDependencies = [
              daggerCompiler:     "com.google.dagger:dagger-compiler:${daggerVersion}",
              dagger:             "com.google.dagger:dagger:${daggerVersion}",
              javaxAnnotation:    "org.glassfish:javax.annotation:${javaxAnnotationVersion}",
              rxJava:             "io.reactivex:rxjava:${rxJavaVersion}",
          ]

          domainTestDependencies = [
              junit:              "junit:junit:${jUnitVersion}",
              mockito:            "org.mockito:mockito-core:${mockitoVersion}",
          ]

          ...

          dataTestDependencies = [
              junit:              "junit:junit:${jUnitVersion}",
              assertj:            "org.assertj:assertj-core:${assertJVersion}",
              mockito:            "org.mockito:mockito-core:${mockitoVersion}",
              robolectric:        "org.robolectric:robolectric:${robolectricVersion}",
          ]
        }

build.gradle:

       apply plugin: 'java'

        sourceCompatibility = 1.7
        targetCompatibility = 1.7

        ...

        dependencies {
          def domainDependencies = rootProject.ext.domainDependencies
          def domainTestDependencies = rootProject.ext.domainTestDependencies

          provided domainDependencies.daggerCompiler
          provided domainDependencies.javaxAnnotation

          compile domainDependencies.dagger
          compile domainDependencies.rxJava

          testCompile domainTestDependencies.junit
          testCompile domainTestDependencies.mockito
        }

如果你希望在不同的模組中重複利用相同的依賴的版本,上面的建議就會變得非常有用 或者你想將不同的依賴版本放到不同的模組中去也是一樣的. 另一個好處是 可以在一個地方控制所有的依賴

總結

這差不多就是我要說的了, 大家要記住 並沒有包治百病的藥, 但是一個好的程式架構可以幫助我們保持程式碼的整潔和健康, 同時也保證整了靈活性和可維護性

下面是一些我想指出的當你遇到程式問題時應有的態度:

  • 遵守 SOLID 原則
  • 不要想的太多(不要過度開發)
  • 要實際
  • 最大限度的在工程中減少對 android 框架的依賴

原始碼

  1. Clean architecture github repository – master branch
  2. Clean architecture github repository – releases

更多閱讀:

  1. Architecting Android..the clean way
  2. Tasting Dagger 2 on Android
  3. The Mayans Lost Guide to RxJava on Android
  4. It is about philosophy: Culture of a good programmer

引用

  1. RxJava wiki by Netflix
  2. Framework bound by Uncle Bob
  3. Gradle user guide
  4. Package by feature, not layer

相關文章