Architecting Android…The evolution

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 我們能獲得的好處

  • ObservablesSubscribers解耦。使得維護(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é)果是:

  1. 優(yōu)點(diǎn):
    • lambda表達(dá)式和方法的引用。
    • Try with resources.
    • Dev karma.(???)
  2. 缺點(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 instrumentatioespresso 做集成 和功能測試
  • Domain Layer: 使用JUnitmockito做單元測試
  • 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ā)人員在同一代碼庫工作。

-w458

可以看到,我的項(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í)的一些技巧。

-w214
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/從零開始/ **

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,319評論 25 708
  • afinalAfinal是一個(gè)android的ioc,orm框架 https://github.com/yangf...
    passiontim閱讀 15,896評論 2 45
  • 本周任務(wù) 了解JavaScript基礎(chǔ) 掌握Bootstrap布局 常用組件的使用 時(shí)間要求 本次學(xué)習(xí)任務(wù)截止4月...
    NPU李天閱讀 360評論 2 2
  • 作者|油炸橘子皮[古風(fēng)] 夜行人 簡介&目錄上一章:[古風(fēng)] 夜行人 第九章:枝節(jié) “噠噠”的馬蹄聲在直隸去往滁州...
    油炸橘子皮閱讀 433評論 0 0
  • 昨天晚上夢到你了,你在部隊(duì)很辛苦,很累,瘦了很多,但是你很開心 。畢竟,你選擇了你的方向,我只能默默地祝福你。
    寂靜里安然閱讀 156評論 0 0

友情鏈接更多精彩內(nèi)容