[譯] Android 架構:Part 4 —— 實踐整潔架構

Android 架構系列的最後部分,咱們將 Clean Architecture 調整到 Android 平臺。咱們將 Android 和真實世界從業務邏輯中分離,令利益相關者滿意,使一切都容易測試。前端

理論很棒,可是當咱們建立一個新 Android 項目時,該從哪開始呢?讓咱們用整潔代碼弄髒咱們的手,把空白畫布變成一個架構。java

基礎

讓咱們先作一些基礎工做 —— 建立模塊並創建依賴關係,使其與依賴規則保持一致。android

這些將是咱們的模塊,從抽象到具體:git

1. domain

實體、用例、倉庫接口和設備接口放入 domain 模塊。github

理想狀況下,實體和業務邏輯應該是平臺無關的。爲了安全起見,爲了防止咱們在這裏放置一些 Android 的東西,咱們將使它成爲一個純 java 模塊。安全

2. data

data 模塊應當持有與數據持久化和操做相關的全部內容。在這裏咱們能夠找到 DAO、ORM、SharedPreferences、網絡相關的好比 Retrofit Service 或相似的東西。網絡

3. device

device 模塊應該擁有與 Android 相關的全部東西(除了數據持久化 和 UI)。例如 ConnectivityManager, NotificationManager 和 misc 傳感器的封裝類。架構

咱們將使 data 和 device 模塊成爲 Android 模塊,由於它們必須知道 Android,不能是純的 java。app

4. 最容易的部分,app 模塊(UI 模塊)

當你建立項目時,該模塊已經由 Android Studio 爲你建立好了。框架

在這裏,你能夠放置與 Android UI 相關的類,譬如 Presenter,Controller,ViewModel,Adapter 以及 View。

依賴

依賴規則定義了具體模塊依賴於抽象模塊。

你可能會記得本系列的第三部分,UI(app),DB-API(data)以及設備(device)的東西都在外層。這意味着它們在同一抽象級別。那麼咱們該如何將它們串在一塊兒呢?

理想的狀況下,這些模塊僅依賴於領域(domain)模塊。在這種狀況下,依賴關係看起來有點像一顆星:

可是這裏咱們涉及 Android 因此事情就不那麼完美了。由於咱們須要建立對象依賴圖以及初始化一些東西,模塊有時依賴另外一個模塊而不是領域模塊。

例如,咱們須要在 app 模塊建立依賴注入的對象依賴圖,這就迫使 app 模塊須要知道其他全部模塊。

咱們調整後的依賴關係圖:

磚,更多的磚

終於,是時候寫些代碼了。爲了容易演示,咱們將以 RSS 閱讀器 app 爲例。咱們的用戶應該可以管理他們的 RSS Feed 訂閱,從 Feed 中獲取文章並閱讀它們。

領域

讓咱們從領域層開始,建立咱們的核心業務模型和邏輯。

咱們的業務模型很是簡單:

  • Feed - 持有 RSS Feed 相關數據好比 url、縮略 URL、標題和描述
  • Article - 持有文章相關數據好比文章標題、url 和發表時間

至於咱們的邏輯,咱們將使用 UseCase 也就是交互器。它們將簡單類中的小部分業務邏輯封裝起來。它們都會實現通常的 UseCase 協議:

public interface UseCase<P> {

   interface Callback {

      void onSuccess();
      void onError(Throwable throwable);
    }

   void execute(P parameter, Callback callback);
 }
複製代碼

當用戶打開咱們的 app 要作的第一件事情是添加一個新的 RSS Feed 訂閱。因此從咱們的交互器開始,咱們建立 AddNewFeedUseCase,以及處理 feed 添加和驗證邏輯的輔助類。

AddNewFeedUseCase 使用 FeedValidator 來檢查 feed URL 的有效性,咱們還將建立 FeedRepository 協議,它爲咱們的業務邏輯提供基礎的增刪改查能力來管理 feed 數據:

public interface FeedRepository {

    int createNewFeed(String feedUrl);

    List<Feed> getUserFeeds();

    List<Article> getFeedArticles(int feedId);

    boolean deleteFeed(int feedId);
}
複製代碼

注意咱們在領域層的命名是如何清晰地說明咱們的 app 是作什麼的。

把全部東西放在一塊兒,咱們的 AddNewFeedUseCase 看起來像這樣:

public final class AddNewFeedUseCase implements UseCase<String> {

   private final FeedValidator feedValidator;
   private final FeedRepository feedRepository;

   @Override
   public void execute(final String feedUrl, final Callback callback) {
       if (feedValidator.isValid(feedUrl)) {
           onValidFeedUrl(feedUrl, callback);
       } else {
           callback.onError(new InvalidFeedUrlException());
       }
   }

   private void onValidFeedUrl(final String feedUrl, final Callback callback) {
       try {
           feedRepository.createNewFeed(feedUrl);
           callback.onSuccess();
       } catch (final Throwable throwable) {
           callback.onError(throwable);
       }
   }
}

複製代碼

爲了簡潔起見,省略了構造函數。

如今,你可能會困惑,爲何咱們的用例和回調是一個接口?

爲了更好地演示咱們下一個問題,讓咱們來研究研究 GetFeedArticlesUseCase

得到 feedId -> 經過 FeedRepository 抓取 feed 文章 -> 返回 feed 文章列表

這是數據流問題,用例位於表現層和數據層之間,咱們怎樣創建起層和層之間的通訊?記得那些輸入和輸出端口嗎?

咱們的用例必須實現輸入端口(interface)。Presenter 調用用例的方法,數據流向用例(feedId)。用例將 feedId 轉換成 feed 文章列表,並但願將其返回給表現層。它擁有指向輸出端口(Callback)的引用,由於輸出端口是定義在同一層的,因此它調用了輸出端口的一個方法。所以數據將發送到輸出端口 —— Presenter。

咱們稍微調整一下 UseCase 協議:

public interface UseCase<P, R> {

   interface Callback<R> {
       void onSuccess(R return);
       void onError(Throwable throwable);
   }

   void execute(P parameter, Callback<R> callback);
}

public interface CompletableUseCase<P> {

   interface Callback {
       void onSuccess();
       void onError(Throwable throwable);
   }

   void execute(P parameter, Callback callback);
}
複製代碼

UseCase 接口是輸入端口,而 Callback 接口是輸出端口。

GetFeedArticlesUseCase 實現以下:

class GetFeedArticlesUseCase implements UseCase<Integer, List<Article>> {

   private final FeedRepository feedRepository;

   @Override
   public void execute(final Integer feedId, final Callback<List<Article>> callback) {
       try {
           callback.onSuccess(feedRepository.getFeedArticles(feedId));
       } catch (final Throwable throwable) {
           callback.onError(throwable);
       }
   }
 }

複製代碼

最後一件領域層須要注意的事情是,交互器只應該包含業務邏輯。在這樣作時,它們可使用 Repository,組合其它交互器,使用相似咱們例子中 FeedValidator 這樣的公共設施類。

UI

很好,咱們能夠抓取文章,如今讓咱們向用戶展現它們。

咱們的 View 有一個簡單的協議:

interface View {

   void showArticles(List<ArticleViewModel> feedArticles);
   
   void showErrorMessage();
   
   void showLoadingIndicator();
}
複製代碼

此 View 的 Presenter 的表現邏輯很是簡單。它抓取文章,轉換成視圖模型傳遞給 View,簡單吧,對嗎?

簡單的 Presenter 是 Clean Architecture 和 表現 —— 業務邏輯分離的另外一個偉大成就。

這是咱們的 FeedArticlesPresenter

class FeedArticlesPresenter implements UseCase.Callback<List<Article>> {

   private final GetFeedArticlesUseCase getFeedArticlesUseCase;
   private final ViewModeMapper viewModelMapper;

   public void fetchFeedItems(final int feedId) {
       getFeedArticlesUseCase.execute(feedId, this);
   }

   @Override
   public void onSuccess(final List<Article> articles) {
       getView().showArticles(viewModelMapper.mapArticlesToViewModels(articles));
   }

   @Override
   public void onError(final Throwable throwable) {
       getView().showErrorMessage();
   }
 }

複製代碼

注意 FeedArticlesPresenter 實現了 Callback 接口,並將自身傳遞給用例,它其實是用例的輸出端口,並以這種方式關閉數據流。這是咱們前面提到過的數據流的具體例子,咱們能夠在流程圖上調整標籤來匹配這個例子:

咱們的參數 P 是整數 feedId,返回類型 R 是文章列表。

你不必定必須使用 Presenter 來處理表現邏輯,咱們能夠說,Clean Architecture 是「前端」無關的 —— 這意味着你可使用 MVP,MVC,MVVM 或其餘任何東西。

咱們來加點 Rx

若是你想知道爲何會有這樣關於 RxJava 的炒做,那麼來看看咱們的用例的響應式實現:

public interface UseCase<P, R> {

   Single<R> execute(P parameter);         
}

public interface CompletableUseCase<P> {

   Completable execute(P parameter);
}
複製代碼

回調接口如今已經消失,咱們使用 RxJava Single / Completable 接口做爲輸出端口。

響應式 FeedArticlePresenter 的實現以下:

class FeedArticlesPresenter {
 
   private final GetFeedArticlesUseCase getFeedArticlesUseCase;
   private final ViewModeMapper viewModelMapper;
 
   public void fetchFeedItems(final int feedId) {
       getFeedItemsUseCase.execute(feedId)
                  .map(feedViewModeMapper::mapFeedItemsToViewModels)
                  .subscribeOn(Schedulers.io())
                  .observeOn(AndroidSchedulers.mainThread())
                  .subscribe(this::onSuccess, this::onError);
   }
 
   private void onSuccess(final List articleViewModels) {
      getView().showArticles(articleViewModels);
   }
 
   private void onError(final Throwable throwable) {
      getView().showErrorMessage();
   }
}
複製代碼

雖然有點隱蔽,相同的數據流反轉原則仍然存在,由於沒有 RxJava,Presenter 會實現回調,而使用 RxJava, 訂閱者也包含在外層 —— Presenter 的某個地方。

譯者注:若是你打算用 ViewModel 取代 Presenter,而且在項目中使用了 RxJava,那麼向你安利 使用 RxCommand 在 Android 上實現 MVVM

Data and Device

data 和 device 模塊包含全部業務邏輯不關心的實現細節。它只關係協議,使你容易測試,以及在不觸及業務邏輯的狀況下更換實現。

這裏,你可使用你喜歡的 ORM 或 DAO 來存儲本地數據,使用你喜歡的網絡服務來從網絡獲取數據。咱們將實現 FeedService 來拉取文章,使用 FeedDao 來存儲文章數據到設備。

每一個數據源(網絡和本地存儲)都有本身的模型來處理。

在咱們的例子中,它們是 ApiFeed - ApiArticle 和 DbFeed - DbArticle。

FeedRepository 的具體實現也能夠在 data 模塊中找到。

device 模塊持有 Notifications 協議的實現,就是對 NotificationManager 類的一個包裝。當有新的用戶可能感興趣並參與的文章發表時,咱們會在咱們的業務邏輯中使用 Notifications 來向用戶顯示一個通知。

模型,處處都是模型

你可能已經注意到,咱們說起的模型不只僅是實體或業務。

實際上,咱們還有 db 模型,API 模型,視圖模型,固然還有業務模型。

每一層都有本身的模型是個不錯的實踐,這樣你的具體細節,譬如視圖,就不須要依賴低層實現的具體細節。經過這種方式,舉個例子,當你決定更換 ORM 框架時,你就不須要破壞不相干的代碼。

爲了確保這點,有必要在每一個層中使用對象映射器。在咱們的例子中,咱們使用 ViewModelMapper 將 demain 模塊中的 Article 模型映射成 ArticleViewModel

總結

遵循這些準則,咱們建立了健壯且通用的架構。首先,看起要寫好多代碼,確實也是,可是記住,咱們是爲將來的變化和功能搭建咱們的架構。若是你正確地作了,將來你會感謝當初的決定。

在下一部分中,咱們會介紹多是這個架構中最重要的部分,它的可測試性以及如何測試它。那麼,在此期間,你對架構實現的哪部分最感興趣呢?

Part I

Part II

part III

源碼

原文

相關文章
相關標籤/搜索