I am the STORM!!!
上一篇文章介绍了 MVP 模式,作为 Android 最朴实的架构,MVP 足以应对复杂度较低的业务场景,回想 2014~2015 年在 DP 做预订闪惠的日子,用的便是 MVP。
然而随着业务复杂度增加,Presenter 层的逻辑会随之变重,导致的结果便是 Presenter 类过大,动辄一两千行代码,实在是丑陋。在这种场景下,Clean 架构是一个很好的选择。
关于 Clean
什么是 Clean
Clean 架构最初并不是为了针对 Android 平台的问题而提出,而是作为软件系统的通用架构被设计出来,如上面的同心圆所示,它的核心思想是The Dependency Rule(单向依赖原则),即只能从外层向内层依赖,内层对外层一无所知,外层的变动不应当影响到内层。
另一个重要概念是Use Case(用例),用例是业务逻辑的最小抽象单元,它调用数据模块的接口,向上(Presenter)提供业务逻辑操作入口。
Android 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; void run() { 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) { 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; }
public static final class ResponseValues implements UseCase.ResponseValues { private final List<Task> mTasks; } }
|
我在上述代码的注释里增加了一些说明,此外,有几处需要额外的注意:
- Use Case 构造函数里需要传入底层数据源对象,这意味着不可以在 Presenter 层直接构造 Use Case,因为 Presenter 不应当跨过 Domain Layer 直接访问数据源。—— 想想如何实现?IOC,DI,Bingo!
- 在 Use Case 的类内部,以静态类的方式声明
RequestValues
和ResponseValues
,代码集中,易于管理。UseCase.java
基类中提供的空接口也利于不同的子类声明各自不同的参数类型。在模版方法executeUseCase
里分别进行调用,化元归一。
Executor:任务执行机制
命令模式由命令与执行机制两部分组成,Java 中的Executor
框架就是一种典型应用。虽然Executor
只是一个简单的接口,但它却为灵活而强大的异步任务执行框架提供了基础,该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程和执行过程解耦开来。
1 2 3 4
| public interface Executor { void execute(Runnable command); }
|
项目里使用同样的机制作为UseCase
的运行框架。
UseCaseScheduler.java
1 2 3 4 5 6 7
| public interface UseCaseScheduler { 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(); }); }
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); TasksFragment tasksFragment = (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame); if (tasksFragment == null) { tasksFragment = TasksFragment.newInstance(); ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), tasksFragment, R.id.contentFrame); }
mTasksPresenter = new TasksPresenter( Injection.provideUseCaseHandler(), tasksFragment, Injection.provideGetTasks(getApplicationContext()), Injection.provideCompleteTasks(getApplicationContext()), Injection.provideActivateTask(getApplicationContext()), Injection.provideClearCompleteTasks(getApplicationContext()) ); 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 的任务列表功能绘制成类图,加深理解。
最后更新时间:
本文系作者原创,如转载请注明出处。欢迎留言讨论,或通过邮件进行沟通~