[译] Android 架构:Part 4 —— 实践 Clean Architecture(含源码)

main_image.png

Android 架构系列的最后部分,我们将 Clean Architecture 调整到 Android 平台。我们将 Android 和真实世界从业务逻辑中分离,令利益相关者满意,使一切都容易测试。

理论很棒,但是当我们创建一个新 Android 项目时,该从哪开始呢?让我们用整洁代码弄脏我们的手,把空白画布变成一个架构。

基础

让我们先做一些基础工作 —— 创建模块并建立依赖关系,使其与依赖规则保持一致。

这些将是我们的模块,从抽象到具体:

1. domain

实体、用例、仓库接口和设备接口放入 domain 模块。

理想情况下,实体和业务逻辑应该是平台无关的。为了安全起见,为了防止我们在这里放置一些 Android 的东西,我们将使它成为一个纯 java 模块。

2. data

data 模块应当持有与数据持久化和操作相关的所有内容。在这里我们可以找到 DAO、ORM、SharedPreferences、网络相关的比如 Retrofit Service 或类似的东西。

3. device

device 模块应该拥有与 Android 相关的所有东西(除了数据持久化 和 UI)。例如 ConnectivityManager, NotificationManager 和 misc 传感器的封装类。

我们将使 data 和 device 模块成为 Android 模块,因为它们必须知道 Android,不能是纯的 java。

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

源码

原文

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容