Android架構演化之路
- 原文鏈接 : Architecting Android…The evolution
- 原文作者 : Tuenti
- 譯文出自 : 開發技術前線 www.devtf.cn。未經允許,不得轉載!
- 譯者 : dustookk
- 校對者: chaossss
- 狀態 : 完成
</ul> </blockquote>大家好! 過了一好陣子了(在此期間我收到了大量的讀者反饋) 我決定是時候回到手機程序架構這個話題上了(這里用android代碼舉例), 給大家另一個我認為好的解決方案.
在開始之前, 我這里假設大家都讀過了我之前用簡潔的辦法架構Android程序一文. 如果你還沒有讀過, 現在應該去讀一下那篇文章, 讀過之后可以更好的理解我下面要講的內容.
![]()
架構的演化
演化是指一個事物變化成為另一個不同的事物的一個平緩過程, 通常情況下會變得更加復雜或者變成更好.
軟件開發一直在進化和改變. 實際上, 一個好的代碼結構必須幫助我們成長, 這意味著不用重新寫所有代碼就可以擴展功能. (盡管有些情況下應該大量的重寫代碼, 但那又是另一回事了, 這里先不做探討).
這篇文章的重點是如何保持android代碼的清晰直觀, 為了闡述這一問題, 我將會帶著大家看幾個我認為重要的關鍵點. 記住下面這個圖我們就可以開始了.
![]()
反應式方法: RxJava
在這里我就不講RxJava的好處了(我猜大家都已經自己體會過了) , 因為已經有很多文章和壞蛋們都講過了, 而且講的還都不錯. 這里我要講的是它是怎么使得android開發變得非常有趣的, 還有它是如何幫助我完成搭建第一個干凈簡潔的架構的.
首先, 我選擇一個反應式模式讓用例(在簡潔的架構命名規范中叫做interactor) 都返回Observablespublic 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(); } } }
</pre>
可以看出,所有的子用例都繼承自這個抽象類,并在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); } }</pre><br />
DefaultSubscriber只是簡單實現了對錯誤的處理, 每一個subscriber都是presenter中的一個繼承自 DefaultSubscriber 的內部類.
從下面這張圖中, 你可以得到一個比較完整的思路.
![]()
我們來總結一下RxJava帶給我們的好處:
實現了Observables 和 Subscribers的解耦: 保持了結構穩定并簡化了測試.
</li>使得異步任務變得簡單: 多層異步任務被執行時, java的thread和future的操作和同步會變得非常復雜, 使用 scheduler 可以讓我們在異步線程和主線程之間跳轉變得非常簡單(省去了多余的步驟), 特別是我們需要更新UI界面的時候. 同時也避免了使代碼變成非常難以理解的”回調地獄”.
</li>數據的傳遞/組合: 我們可以使多個Observables組合起來而不影響到client端, 這樣提高了整套解決方案的可擴展性.
</li>異常處理: 任何一個Observable.出現異常都會通知到consumer.
</li> </ul>從我的角度看這里有一個小問題, 也是必須要付出的代價, 就是對這一概念不太熟悉的開發者的學習過程. 但是你會從中學到非常有價值的內容. 為了成功學習Reactive吧!
依賴注入: Dagger 2
我這里不會講太多關于依賴注入的例子, 因為我之前寫過一篇專門說依賴注入的文章, 為了跟上我這里的腳步, 強烈推薦大家讀一讀這篇文章.
值得強調的是, 像Dagger 2一樣的依賴注入框架可以帶給我們:
組件的重用, 因為依賴可以被從外部配置和注入.
</li>最為合作者對抽象進行依賴注入時, 我們可以單單修改任何對象的實現, 而不用大量修改底層代碼, 因為類的實現對象獨立而解耦的存在于另一個地方.
</li>依賴可以被注入到組件中去: 注入依賴的測試實現是有可能的, 這就使得測試變得更加容易.
</li> </ul>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), 代碼結構的分配會變得更加容易更加模塊化, 如果許多工程師在同樣的基礎代碼上開發時這個優點就變得格外明顯.
![]()
大家可以看出, 我的包結構看起來像是根據層級關系組織的: 這里可能有舉例不太恰當的地方(比如將一切都放在’user’下面) 但是我會原諒自己這一次, 因為舉這個例子是為了供大家學習,為了表達我的觀點,主要目的是包含簡潔的架構思路. 照我說的做, 而不是照我做的做
![]()
附加彩蛋: 組織你的打包邏輯
我們都知道房子都是從地基開始修筑的. 軟件開發也是一個道理, 我這里要強調的是, 代碼架構中, 打包系統(及其組織架構)是非常重要的一部分
在Android開發中, 我們使用一個叫做gradle的非常強大的打包系統. 這里有 一系列竅門 幫助大家在組織打包腳本時變得格外輕松:
- 根據功能的不同, 將打包系統分成多個腳本文件.
![]()
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 框架的依賴
源代碼
更多閱讀:
- Architecting Android..the clean way
- Tasting Dagger 2 on Android
- The Mayans Lost Guide to RxJava on Android
- It is about philosophy: Culture of a good programmer
引用