在 Android Architecture 系列的最后一部分, 我们将 Clean Architecture 稍微调整到了 Android 平台. 我们将 Android 和现实世界从业务逻辑中分离出来, 让满意的利益相关者满意, 并让所有事情都可以轻松测试.
这个理论很好, 但是当我们创建一个新的 Android 项目时, 我们从哪里开始? 让我们用干净的代码弄脏我们的手, 并将空白的画布变成一个架构.
基础
我们将首先奠定基础 - 创建模块并建立它们之间的依赖关系, 以便与依赖规则保持一致.
这些将是我们的模块, 从最抽象的一个到具体的实现:
1. domain
Entities, use cases, repositories interfaces, 和 device interfaces 进入 domain module.
理想情况下, 实体和业务逻辑应该是平台不可知的. 为了安全起见, 为了防止我们在这里放置一些 Android 的东西, 我们将使它成为一个纯粹的 Java 模块.
2. data
数据模块应包含与数据持久性和操作相关的所有内容. 在这里, 我们将找到 DAO,ORM,SharedPreferences, 网络相关的东西, 例如 Retrofit 服务和类似的东西.
3. device
设备模块应该包含与 Android 相关的所有内容, 而不是数据持久性和 UI. 例如, ConnectivityManager,NotificationManager 和 misc 传感器的包装类.
我们将使数据和设备模块都是 Android 模块, 因为他们必须了解 Android 并且不能是纯 Java.
4. The easiest part, app module (UI module)
创建项目时, Android 模块已经为您创建了该模块.
在这里, 您可以放置与 Android UI 相关的所有类, 例如 presenters,controllers,view models,adapters 和 views.
依赖
依赖规则定义具体模块依赖于更抽象的模块.
您可能还记得, 从本系列的第三部分可以看出, UI(应用程序),DB-API(数据)和 Device(设备)等东西都在外环中. 这意味着它们处于相同的抽象层次. 我们如何将它们连接在一起呢?
理想情况下, 这些模块仅取决于域模块. 在这种情况下, 依赖关系看起来有点像明星:
但是, 我们在这里与 Android 打交道, 事情并不完美. 因为我们需要创建对象图并初始化事物, 所以模块有时依赖于 domain 以外的其他模块.
例如, 我们正在 app 模块中创建用于依赖注入的对象图. 这迫使 APP 模块了解所有其他模块.
我们调整后的依赖关系图:
最后, 是时候编写一些代码. 为了更容易, 我们将以 RSS Reader APP 为例. 我们的用户应该能够管理他们的 RSS 提要订阅, 从提要中获取文章并阅读它们.
Domain
让我们从 domain 层开始, 创建我们的核心业务模型和逻辑.
我们的商业模式非常简单:
Feed - 持有 RSS 提要相关数据, 如网址, 缩略图网址, 标题和说明
Article - 保存文章相关数据, 如文章标题, 网址和发布日期
而对于我们的逻辑, 我们将使用 UseCases. 他们在简洁的类中封装了小部分业务逻辑. 他们都将实施通用的 UseCase 契约类:
- public interface UseCase<P> {
- interface Callback {
- void onSuccess();
- void onError(Throwable throwable);
- }
- void execute(P parameter, Callback callback);
- }
我们的用户在打开我们的应用时首先要做的就是添加一个新的 RSS 订阅. 因此, 要开始使用我们的 Use Case, 我们将创建 AddNewFeedUseCase 及其助手来处理 Feed 的添加和验证逻辑.
AddNewFeedUseCase 将使用 FeedValidator 来检查 Feed URL 的有效性, 并且我们还将创建 FeedRepository 契约类, 这将为我们的业务逻辑提供一些基本的 CRUD 功能来管理供稿数据:
- public interface FeedRepository {
- int createNewFeed(String feedUrl);
- List<Feed> getUserFeeds();
- List<Article> getFeedArticles(int feedId);
- boolean deleteFeed(int feedId);
- }
请注意我们在 Domain 层的命名是如何清楚地传递我们的 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);
- }
- }
- }
ps: 为简洁起见, 省略构造函数.
现在, 您可能想知道, 为什么我们的 use case 以及我们的回调是一个接口?
为了更好地展示我们的下一个问题, 让我们来研究 GetFeedArticlesUseCase.
它需要一个 feedId ->通过 FeedRespository 获取提要文章 ->返回提要文章
这里是数据流问题, 用例介于表示层和数据层之间. 我们如何建立层之间的沟通? 记住那些输入和输出端口?
我们的 Use Case 必须实现输入端口 (接口). Presenter 在 Use Case 上调用方法, 数据流向 Use Case(feedId). Use Case 映射 feedId 提供文章并希望将它们发送回表示层. 它有一个对输出端口(回调) 的引用, 因为输出端口是在同一层定义的, 因此它调用了一个方法. 因此, 数据发送到输出端口 - 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);
- }
- }
- }
在 Domain 层中要注意的最后一件事是 Interactors 应该只包含业务逻辑. 在这样做的时候, 他们可以使用存储库, 结合其他交互器, 并在我们的例子中使用一些实用工具对象, 如 FeedValidator.
UI
太棒了, 我们可以获取文章, 让我们现在将它们展示给用户.
我们的 View 有一个简单的契约类:
- interface View {
- void showArticles(List<ArticleViewModel> feedArticles);
- void showErrorMessage();
- void showLoadingIndicator();
- }
该视图的 Presenter 具有非常简单的显示逻辑. 它获取文章, 将它们映射到 view odels 并传递到 View, 很简单, 对吧?
简单的 presenters 是 Clean 架构和 presentation - business 逻辑分离的又一壮举.
这里是我们的 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 接口, 并将其自身传递给 use case, 它实际上是 use case 的输出端口, 并以这种方式关闭了数据流. 这是我们前面提到的数据流的具体示例, 我们可以在流程图上调整标签以匹配此示例:
我们的参数 P 是 feedId, 返回类型 R 是文章列表.
您不必使用 Presenter 来处理显示逻辑, 我们可以说 Clean 架构是 "前端" 不可知的 - 这意味着您可以使用 MVP,MVC,MVVM 或其他任何东西.
让我们在混合中抛出一些 Rx
现在, 如果你想知道为什么有这样的 RxJava, 我们将看看我们 UseCase 的反应式实现:
- public interface UseCase<P, R> {
- Single<R> execute(P parameter);
- }
- public interface CompletableUseCase<P> {
- Completable execute(P parameter);
- }
回调接口现在不见了, 我们使用 RxJava Single / Completable 接口作为我们的输出端口.
- Reactive GetFeedArticlesUseCase:
- class GetFeedArticlesUseCase implements UseCase<Integer, List<Article>> {
- private final FeedRepository feedRepository;
- @Override
- public Single<List<Article>> execute(final Integer feedId) {
- return feedRepository.getFeedArticles(feedId);
- }
- }
Reactive 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 Presenters 实现回调, 并且 RxJava 订阅者也包含在外层 - 在 Presenter 的某处.
Data 和 Device
Data 和 Device 包含业务逻辑不关心的所有实现细节. 它只关心契约类, 允许您轻松测试它并在不触及业务逻辑的情况下交换实施.
在这里, 您可以使用自己喜欢的 ORM 或 DAO 在本地存储数据, 并使用网络服务从网络获取数据. 我们将实现 FeedService 来获取文章, 并使用 FeedDao 将文章数据存储在设备上.
每个数据源 (网络和本地存储) 都将有自己的模型可供使用.
在我们的例子中, 它们是 ApiFeed - ApiArticle 和 DbFeed - DbArticle.
FeedRepository 的具体实现也可以在 Data 模块中找到.
Device 模块将持有作为 NotificationManager 类的包装的通知合同的实现. 我们也许可以使用业务逻辑中的通知来在用户可能感兴趣并推动参与的新文章发布时向用户显示通知.
Models, models everywhere.
您可能已经注意到我们提到的不仅仅是实体或业务模型, 还有更多的模型.
实际上, 我们也有 db 模型, API 模型, View 模型, 当然还有业务模型.
对于每个图层来说, 都有一个很好的实践, 可以使用它自己的模型, 因此具体的细节 (如 View) 不依赖于较低层实现的具体细节. 这样, 例如, 如果您决定从一个 ORM 更改为另一个, 则不必分解不相关的代码.
为了实现这一点, 有必要在每个图层中使用对象映射器. 在示例中, 我们使用 ViewModelMapper 将 Domain 里的 Article 模型映射到 ArticleViewModel.
总结
遵循这些准则, 我们创建了一个强大且多功能的架构. 起初, 它可能看起来像很多代码, 它有点像, 但请记住, 我们正在构建我们的架构以适应未来的变化和功能. 如果你做得对, 未来你会感恩.
在下一部分中, 我们将会介绍这个架构中最重要的部分, 可测试性以及如何测试它.
那么, 在此期间, 您最感兴趣的是架构实现的哪一部分?
这是 Android Architecture 系列的一部分. 检查我们的其他部分:
- Part 4: Applying Clean Architecture on Android (Hands-on) http://five.agency/android-architecture-part-4-applying-clean-architecture-on-android-hands-on/
- Part 3: Applying Clean Architecture on Android http://five.agency/android-architecture-part-3-applying-clean-architecture-android/
- Part 2: The Clean Architecture http://five.agency/android-architecture-part-2-clean-architecture/
- Part 1: every new beginning is hard http://five.agency/android-architecture-part-1-every-new-beginning-is-hard/
来源: https://juejin.im/entry/5af8078651882542682e4cc1