Architecting Android…The evolution
更多 Android 博文請關(guān)注我的博客 http://xuyushi.github.io
Architecting Android…The evolution
本文為翻譯,在原文的基礎(chǔ)上略有改動
http://fernandocejas.com/2015/07/18/architecting-android-the-evolution/
在開始之前,你最好閱讀過這篇文章 ( http://xuyushi.github.io/2016/07/19/Android%20clean%20architecting/ )

Architecture evolution
進(jìn)化是一個(gè)循序漸進(jìn)的過程,其中的一部分變的不同通常是變得更復(fù)雜更好
可以這么說,軟件的發(fā)展很大程度上是軟件architecture的發(fā)展。事實(shí)上,一個(gè)好的軟件設(shè)計(jì)能幫助我們成長,同時(shí)能在不需要重寫的情況下,幫助我們更好的擴(kuò)展。
在這篇文章中,我將指出我認(rèn)為重要點(diǎn)和關(guān)鍵點(diǎn),讓你對 Android 的代碼結(jié)構(gòu)更清晰,請記住這張圖片,let's stared.

Reactive approach: RxJava
這里我不再過多說 Rxjava 的好處(你可以看看這個(gè) ),現(xiàn)在有很多關(guān)于 rxjava 的教程 ,我會指出它在 android 開發(fā)中的亮點(diǎn),已經(jīng)它是如何幫助我進(jìn)化 clean架構(gòu)的。
首先,我選擇了響應(yīng)式編程(也就是 rxjva,譯者注)來使得use cases(也被稱為 interactors)返回 Observables<T>,也就是說所有的層都會成一條鏈?zhǔn)?返回 Observables<T>
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();
}
}
}
可以看到,所有的 cases 都繼承自這個(gè)抽象類,并且實(shí)現(xiàn)了其中的抽象方法buildUseCaseObservable(),這個(gè)方法干著實(shí)際的業(yè)務(wù)活最后返回 Observable<T>
有一點(diǎn)需要強(qiáng)調(diào)一下,我們需要確保 execute()方法工作在獨(dú)立的線程中。 這樣能最小程度的阻塞 Android 的 主線程,
到目前為止,我們的 Observable<T> 已經(jīng)啟動并運(yùn)行,必須有人來觀測它發(fā)射的數(shù)據(jù),為了達(dá)成這個(gè)目標(biāo),我進(jìn)化了 presenters(MVP 的一部分),使得其包含了Subscribers,使其來響應(yīng)Observable 發(fā)射的數(shù)據(jù)
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);
}
}
每個(gè)subscriber都是presenter中的一個(gè)內(nèi)部類,并且繼承自DefaultSubscriber<T>。DefaultSubscriber<T>包含了一些默認(rèn)的錯(cuò)誤處理
當(dāng)了解所有的細(xì)節(jié)之后,可以通過下面這張圖了解整體的思路:

讓我們來列舉一下通過 Rxjava 我們能獲得的好處
-
將
Observables和Subscribers解耦。使得維護(hù)和測試更方便 - 簡化了異步的任務(wù)。java切換線程的操作還是比較復(fù)雜的。而且在 Android 中,我們需要在非主線程處理事務(wù),在主線程更新UI,很容易出門"callbackhell",使得代碼難以閱讀
-
數(shù)據(jù)的傳輸和組織。我們在不影響客戶端的情況下,可以輕易合并多個(gè)
Observables<T>。這使得我們的解決方案十分靈活 - 錯(cuò)誤處理。所有的錯(cuò)誤都可以在 subscribe 中處理。
在我看來,還是有缺點(diǎn)的。就是對于不了解這些概念的開發(fā)者需要一定的學(xué)習(xí)曲線。但是這是非常值得的。
Dependency Injection: Dagger 2
我不會過多的介紹依賴注入,因?yàn)樵?a target="_blank" rel="nofollow">這篇文章已經(jīng)介紹過了( http://xuyushi.github.io/2016/07/16/Android%20Application%20從零開始%202%20——DI/ ),這篇文章我強(qiáng)烈建議你讀一讀。我一下就簡單的介紹一下
值得一提的是,通過引入像 dagger2 這樣的依賴注入工具我們可以:
- 代碼的復(fù)用,因?yàn)橐蕾囮P(guān)系可以注入和外部配置。
- 當(dāng)注入對象時(shí),我們可以改變對象的實(shí)現(xiàn)即可,不需要再調(diào)用的代碼處做修改。因?yàn)?strong>對象的構(gòu)造和使用已經(jīng)分離解耦
- 依賴可注入一個(gè)組件,它可能注入這些依賴它使測試更容易的mock實(shí)現(xiàn)。
Lambda expressions: Retrolambda
在我們的代碼中使用 java8 的 lambdas ,相信沒人會抱怨。他簡化了我們的代碼,比如
private final Action1<UserEntity> saveToCacheAction =
userEntity -> {
if (userEntity != null) {
CloudUserDataStore.this.userCache.put(userEntity);
}
};
不過,我在這里百感交集,我來解釋下原因。事實(shí)證明,在@SoundCloud我們有大約Retrolambda的討論,主要是是否要使用它,結(jié)果是:
- 優(yōu)點(diǎn):
- lambda表達(dá)式和方法的引用。
- Try with resources.
- Dev karma.(???)
- 缺點(diǎn):
- Accidental use of Java 8 APIs.
- 3rd part lib, quite intrusive.
- 3rd part gradle plugin to make it work with Android.
最后的決定取決于是否能為我們解決問題:你的代碼看起來更好,更具可讀性,但這些都是可有可無的。因?yàn)楝F(xiàn)在大部分的牛逼的 IDEs 都包含了這些功能。自動了折疊了這些代碼
Testing approach
在測試中,在這個(gè)例子的第一個(gè)版本沒有關(guān)系大變動:
- Presentation Layer: 使用
android instrumentatio和espresso做集成 和功能測試 - Domain Layer: 使用
JUnit和mockito做單元測試 - Data Layer: 使用
Robolectric(因?yàn)檫@層有 Android 的依賴)和 junit、mockito做集成和單元測試。
Package organization
我認(rèn)為代碼/包的組織良好的架構(gòu)的關(guān)鍵因素之一:封裝結(jié)構(gòu)是瀏覽源代碼時(shí)由程序員遇到的第一件事情。一切都從它流。一切都依賴于它。
我們可以把你的應(yīng)用程序劃分成包2路徑區(qū)分:
- 按層分包,每包中的內(nèi)容通常不是密切相關(guān),這導(dǎo)致低內(nèi)聚 和 低模塊化,包與包之間的高耦合。這將導(dǎo)致很多問題。例如,修改會發(fā)生在不同包的文件中,刪除一個(gè)功能也會涉及到很多文件
- 按功能分包,試圖將功能相關(guān)的文件放在同一封裝中,這導(dǎo)致在高凝聚力和高度模塊化,包和包帶之間的最小耦合。每個(gè)文件緊密的放在一起,而不是分散在項(xiàng)目的個(gè)個(gè)角落
我的建議是按照功能分包,這有以下幾個(gè)優(yōu)點(diǎn)
- 模塊化程度高
- 更簡單的代碼導(dǎo)航
- 范圍最小
當(dāng)你在團(tuán)隊(duì)合作時(shí),代碼所有權(quán)會更容易組織,更模塊化,這是一個(gè)不斷發(fā)展的組織的一場勝利,許多開發(fā)人員在同一代碼庫工作。

可以看到,我的項(xiàng)目結(jié)構(gòu)看起來就像按層分包一樣,我可能已經(jīng)知道我錯(cuò)在哪里了。但在這種情況下我會原諒自己,因?yàn)檫@個(gè)例子的主旨是學(xué)習(xí),是介紹 clean架構(gòu)方法的主要概念。照我說的做,不要像我一樣:) (貌似作者對其項(xiàng)目的分包不是很滿意,不過我到覺得沒啥不好,挺清晰的)
Extra ball: organizing your build logic
我們都知道,萬丈高樓平地起,房子的地基很重要。軟件開發(fā)也一樣。構(gòu)建系統(tǒng)(及其組織)是一個(gè)軟件體系結(jié)構(gòu)的一個(gè)重要的一步
在 Android 中 我們使用 gradle 來構(gòu)建我們的項(xiàng)目。gradle 是一個(gè)與平臺無關(guān)很牛逼的一個(gè)構(gòu)建工具。這里說一下使用 gradle構(gòu)建 應(yīng)用時(shí)的一些技巧。

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
}
}
}
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來設(shè)置你的 gradle 文件了。不要把所有的配置都寫在一個(gè)build.gradle文件中,不然你的項(xiàng)目會變得難以維護(hù)
創(chuàng)建依賴
...
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}",
]
}
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
}
如果你希望在其他 module 中也使用同一的 version,或者使用同樣的依賴,這樣做很有效。另一個(gè)好處就是你可以在一個(gè)地方控制所有的依賴
Wrapping up
記住,沒有銀彈,但是每個(gè)好的軟件架構(gòu)會幫我們的代碼結(jié)構(gòu)保持整潔和健康,方便擴(kuò)展便于維護(hù)
**我現(xiàn)在根據(jù)這個(gè)架構(gòu)為模板做一個(gè)開源 APP,完成以后會開源,詳見請見 http://xuyushi.github.io/tags/從零開始/ **