I am the STORM!!!

上一篇文章介绍了 MVP 模式,作为 Android 最朴实的架构,MVP 足以应对复杂度较低的业务场景,回想 2014~2015 年在 DP 做预订闪惠的日子,用的便是 MVP。

然而随着业务复杂度增加,Presenter 层的逻辑会随之变重,导致的结果便是 Presenter 类过大,动辄一两千行代码,实在是丑陋。在这种场景下,Clean 架构是一个很好的选择。

关于 Clean

什么是 Clean

CleanArchitecture
CleanArchitecture

Clean 架构最初并不是为了针对 Android 平台的问题而提出,而是作为软件系统的通用架构被设计出来,如上面的同心圆所示,它的核心思想是The Dependency Rule(单向依赖原则),即只能从外层向内层依赖,内层对外层一无所知,外层的变动不应当影响到内层。

另一个重要概念是Use Case(用例)用例是业务逻辑的最小抽象单元,它调用数据模块的接口,向上(Presenter)提供业务逻辑操作入口。

Android Clean

mvp-clean
mvp-clean

Android 平台在 Presenter 和 Model 之间定义了Domain Layer(域层),用于承载Use Case,如上图所示,Use Case 的背后是业务逻辑。Domain Layer 脱胎于 MVP 的 Presenter,这样可以避免相同的业务逻辑代码出现在两个 Presenter 中的问题。

Use Cases 异步性

鉴于 Domain Layer 是 UI 无关的,这里可以统一将 Use Case 放入工作线程运行,通过异步回调进行通信。至于更底层的数据库/网络操作,出于简化的目的,可以直接使用同步接口。

不同 Layer 是否需要定义各自的 Model

标准的做法是在视图层(View Layer)、域层(Domain Layer)和数据层(Data/Model Layer)分别使用不同的对象,如 VO、BO、DO 等。这样带来的问题是重复性增加。如果对象是不可变的,在各层之间具有相同的属性和方法,则可以只定义一个对象,共通使用。

如果视图层对象包含 Android 相关的方法或属性,则应当为其单独定义一个类,然后使用Mapper在不同层之间进行对象转换。

如何抽象业务场景

这里我们将 Clean 模式应用在 todo-app 上,想想还有点小激动。

Use Cases & Domain Layer

把 Use Case 的声明和运行机制拆开,是不是跟 Runnable + Executor 的机制很像?没错,这就是设计模式中的命令模式。设计模式不是彼此孤立的,一个项目里可以整体采用 Clean 架构,不同的子模块,应用不同的设计模式。设计模式是死的,人是活的。

抽象类UseCase即是命令,包含入参、出参、回调通知对象、运行的抽象过程(这里又出现了模版模式)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class UseCase<Q extends UseCase.RequestValues, P extends UseCase.ResponseValue> {
private Q mRequestValues;
private UseCaseCallback<P> mUseCaseCallback; // 任务完成后(成功or失败)的回调
// ...
// set and get
//..
void run() { // Executor 运行入口
executeUseCase(mRequestValues);
}
protected abstract voi8d executeUseCase(Q requestValues); // 子类必需实体化这个方法

public interface RequestValues {}
public interface ResponseValues {} // 接口约束

public interface UseCaseCallback<R> { // 任务完成后的回调
void onSuccess(R response);
void onError();
}
}

然后选取一个“加载全部任务”的场景为例,看它是如何实现抽象类的,以下是GetTask.java的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class GetTasks extends UseCase<GetTasks.RequestValues, GetTasks.ResponseValues> {
private final TasksRepository mTasksRepository; // 更底层的数据源对象
private final FilterFactory mFilterFactory; // 过滤器工厂类

public GetTasks(@NonNull TaskRepository tasksRepository, @NonNull FilterFactory filterFactory) { // 构造时传入数据源对象和过滤器工厂
mTasksRepository = checkNotNull(tasksRepository, "tasksRepository cannot be null!");
mFilterFactory = checkNotNull(filterFactory, "filterFactory cannot be null!");
}

@Override
protected void executeUseCase(final RequestValues values) { // 利用泛型约束,在不同UseCase实现类里使用不同参数
if (values.isForceUpdate()) {
mTasksRepository.refreshTasks();
}
mTasksRepository.getTasks(new TasksDataSource.LoadTasksCallback() { // 调用数据源异步方法
@Override
public void onTasksLoaded(List<Task> tasks) {
TasksFilterType currentFiltering = values.getCurrentFiltering();
TaskFilter taskfilter = mFilterFactory.create(currentFiltering);

List<Task> tasksFiltered = taskFilter.filter(tasks);
ResponseValue responseValue = new ResponseValue(tasksFiltered);
getUseCaseCallback().onSuccess(responseValue);
}

@Override
public void onDataNotAvailable() {
getUseCallback().onError();
}
});
}

public static final class RequestValues implements UseCase.RequestValues {
private final TasksFilterType mCurrentFiltering;
private final boolean mForceUpdate;
// ...
// constructor, get and set
// ...
}

public static final class ResponseValues implements UseCase.ResponseValues {
private final List<Task> mTasks;
// ...
}
}

我在上述代码的注释里增加了一些说明,此外,有几处需要额外的注意:

  1. Use Case 构造函数里需要传入底层数据源对象,这意味着不可以在 Presenter 层直接构造 Use Case,因为 Presenter 不应当跨过 Domain Layer 直接访问数据源。—— 想想如何实现?IOC,DI,Bingo!
  2. 在 Use Case 的类内部,以静态类的方式声明RequestValuesResponseValues,代码集中,易于管理。UseCase.java基类中提供的空接口也利于不同的子类声明各自不同的参数类型。在模版方法executeUseCase里分别进行调用,化元归一。

Executor:任务执行机制

命令模式由命令与执行机制两部分组成,Java 中的Executor框架就是一种典型应用。虽然Executor只是一个简单的接口,但它却为灵活而强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程和执行过程解耦开来。

1
2
3
4
// Java 中的 Executor,不负责运行结果通知
public interface Executor {
void execute(Runnable command);
}

项目里使用同样的机制作为UseCase的运行框架。

UseCaseScheduler.java

1
2
3
4
5
6
7
public interface UseCaseScheduler {
// todo-mvp-clean 中的 Executor
void execute(Runnable runnable);
// 增加结果通知回调
<V extends UseCase.ResponseValue> void notifyResponse(final V response, final UseCase.UseCaseCallback<V> useCaseCallback);
<V extends UseCase.ResponseValue> void onError(final UseCase.UseCaseCallback<V> useCaseCallback);
}

它的实现类采用线程池方式实现:

UseCaseThreadPoolScheduler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class UseCaseThreadPoolScheduler implements UseCaseScheduler {
private final Handler mHandler = new Handler(); // 将运算结果通知主线程
public static final int POOL_SIZE = 2;
public static final int MAX_POOL_SIZE = 4;
public static final int TIMEOUT = 30;
ThreadPoolExecutor mThreadPoolExecutor; // 使用线程池提供复用
public UseCaseThreadPoolScheduler() {
mThreadPoolExecutor = new ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE, TIMEOUT, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(POOL_SIZE));
}

@Override
public void execute(Runnable runnable) {
mThreadPoolExecutor.execute(runnable); // 任务提交给线程池处理
}

@Override
public <V extends UseCase.ResponseValue> void notifyResponse(final V response, final UseCase.UseCaseCallback<V> useCaseCallback) {
mHandler.post(new Runnable() {
@Override
public void run() {
useCaseCallback.onSuccess(response); // 主线程处理结果
}
});
}

@Override
public <V extends UseCase.ResponseValue> void onError(final UseCase.UseCaseCallback<V> useCaseCallback) {
mHandler.post(new Runnable() {
@Override
public void run() {
useCaseCallback.onError(); // 主线程处理结果
}
});
}
}

UseCaseHandler:任务执行机制包装

上文介绍了项目里采用Executor作为命令的运行框架,Executor接收的是Runnable类型的任务,我们还需要一层UseCase处理器,用于将UseCase封装成Runnable,并且处理任务的返回值。UseCaseHandler就是负责处理UseCase的组件。

UseCaseHandler.java,省略部分代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class UseCaseHandler {
private static UseCaseHandler INSTANCE; // 单例模式,饿汉
public <T extends UseCase.RequestValues, R extends UseCase.ResponseValue> void execute(final UseCase<T, R> useCase, T values, UseCase.UseCaseCallback<R> callback) {
useCase.setRequestValues(values);
useCase.setUseCaseCallback(new UiCallbackWrapper(callback, this));
mUseCaseScheduler.execute(new Runnable() {
@Override
public void run() {
useCase.run();
});
}

// UI回调包装器类,将成功/失败事件交给UI线程处理
private static final class UiCallbackWrapper<V extends UseCase.ResponseValue> implements UseCase.UseCaseCallback<V> {
private final UseCase.UseCaseCallback<V> mCallback;
private final UseCaseHandler mUseCaseHandler;
public UiCallbackWrapper(UseCase.UseCaseCallback<V> callback, UseCaseHandler useCaseHandler) {
mCallback = callback;
mUseCaseHandler = useCaseHandler;
}

@Override
public void onSuccess(V response) {
mUseCaseHandler.notifyResponse(response, mCallback);
}

@Override
public void onError() {
mUseCaseHandler.notifyError(mCallback);
}
}

UseCaseHandler隐藏了任务的执行机制,对外暴露出通过UseCase提交命令的接口,此时 Presenter 就可以借助UseCaseHandler来提交各项异步任务,并且获取回调。注意到回调是在 UI 线程发生的,意味着 Presenter 可以在回调中直接操作 UI 元素。

Activity:穿针引线的组织者

与 MVP 架构一样,在 MVP clean 架构中,Activity 同样担任初始化 Fragment、Presenter 的职责。Activity 类是十分简单的,80% 的逻辑都写在onCreate当中。

TaskActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tasks_act); // Set up the toolbar.
// 初始化 ActionBar
// ...
// 初始化 DrawerLayout
// ...
// FragmentManager与Activity是一一对应的,所以这里通过R.id.contentFrame获取Fragment,在Activity B中也可以用R.id.contentFrame获取B当中的Fragment,不用担心重复
TasksFragment tasksFragment = (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
if (tasksFragment == null) {
// Create the fragment
tasksFragment = TasksFragment.newInstance();
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
}

// 将使用到的Task用作构造参数,创建Presenter
mTasksPresenter = new TasksPresenter(
Injection.provideUseCaseHandler(),
tasksFragment,
Injection.provideGetTasks(getApplicationContext()),
Injection.provideCompleteTasks(getApplicationContext()),
Injection.provideActivateTask(getApplicationContext()),
Injection.provideClearCompleteTasks(getApplicationContext())
);
// Load previously saved state, if available.
if (savedInstanceState != null) {
TasksFilterType currentFiltering = (TasksFilterType) savedInstanceState.getSerializable(CURRENT_FILTERING_KEY);
mTasksPresenter.setFiltering(currentFiltering);
}
}

随后在View.onResume中会调用Presenter.start来首次加载数据。

1
2
3
4
5
@Override
public void onResume() {
super.onResume();
mPresenter.start();
}

至此,MVP-clean 架构的设计思路就被我们分析透彻了,可以看出,相比于 MVP,clean 架构抽象出来 Domain 层,使具体的业务操作独立可复用,且减轻了 Presenter 类的负担。

还有一个好处是每一个 Use Case 都是可测试的,单元测试粒度被细化,更容易定位问题,从而针对性解决。关于测试我讲的不多,可以直接参考源码进行理解。

任务列表功能类图

最后,我们将 todo app 的任务列表功能绘制成类图,加深理解。

mvp-clean-类图
mvp-clean-类图