前言
clean arch架构的提出也已经有一段时间了,最早的起源在uncle-bob的一篇博客,但是现在怕是已经访问不了了。但是没妨碍,如果有心搜索引擎上搜索一把,大概能找到不少的替代博文。
本文也将对clean arch的基本思想进行一些介绍,并且提供自己的实现方案。
clean arch
首先来看一张uncle bob所画的基础图
左侧
初看这张图也许会一脸懵逼,所以我们分不分来看。首先看左边的部分(大括号左侧部分),这幅图表示整个系统的层次架构,从下到上总共分成3层: data层,domain层 presentation层。
这三层属于单项依赖,domain层会依赖data层,presentation层会依赖domain层。这三层每一层都有各自的精细分工。
data层:负责数据的读取和存储,数据可以是本地数据,可以是远程数据,所以数据库的读取和写入,网络请求的最终发送大概都会在这里进行。
domain层:负责并且仅负责业务逻辑的处理,他并不关心data层到底是从数据库读取数据还是从网络获取数据,他只是向data层请求数据,并且等待返回。当返回数据后,根据自己的业务需求对数据进行处理。他也不会关心自己提供的业务如何被上层使用。
presentation层 : 这一层主要负责视图层的数据展示以及一些视图层的逻辑。使用domain层提供的业务,来完成最终的展示。
PS: 这里有个比较大的问题,就是对视图逻辑和业务逻辑的界定问题。当然不同的人对用一个逻辑存在不同的定义标准,所以这部分不同的人实现上可能也会不同。不过就个人而言,有一个设计习惯,就是保证domain层的平台无关性,比如我是开发android的,但是domain层不应该出现activity这种仅在android上能用的类。
理解分层逻辑是后续所有分析的基础,所以这一部分非常重要,跟之后所讲的例子相互印证会有比较深刻的理解。
中间
再来看中间大括号中的部分,如果你知道rxjava,那么或许会对observable和subscriber有比较深刻的了解。但是说实话,这里的observable和subscriber和rxjava的概念还是有些不同,并且,我们的clean arch也没有要求一定要使用rxjava这种第三方库。
所谓observable就是被订阅者,提供者。而subscriber就相当于订阅者,消费者。比如data层毫无疑问是提供者,他所做的事情就是提供数据。
对于domain层,首先是一个消费者,为什么这么说,因为他会从data层获取数据,也就是消费了数据。但是他不一定是消费者,部分业务逻辑如果不需要数据,那么就不必请求data层。不过domain层一定是提供者,他向上层提供了业务。
而presentation层当然是消费者,他需要从domain获取业务。如果你要较真,那么presentation当然也是提供者,像谁提供?向用户提供,暴露一个界面给用户,等待用户向其请求。
但是如果从整个流程来看的话,简化成图中的样子也是可以。
右侧
右侧内容就很简单,只是一个箭头,但是这个箭头表示的内容还是比较深刻的,那就是数据流向。我们的数据一定是单向流动的,从底层向上层传输,这倒是也符合flow的架构模式。
这个流向告诉我们我们不应该在presentation层中获取数据之后再向domain层请求业务处理数据。而是应该在domain层处理完数据之后再传给上层使用。如果你有类似上面这样的需求,请考虑新建一个业务,直接提供最终数据。
例子
可以在 https://github.com/zzxzzg/GuardZ/tree/master/CleanArch 下载到一个Clean Arch的demo.
打开demo,有如下三个模块
其中app是谷歌的官方demo,没有修改过代码。
笔者最早接触的clean arch就是这个demo了,所以从根本上来说还是比较具有指导意义的。但是我这里推荐的是剩下的两个module,这是笔者自己写的一个非常小的demo,深度集成了rxjava。
相比于google的demo而言,不管从理解难度上,编码复杂度上都有比较大的改进。所以接下去的内容会围绕这一套简单的代码进行。
假设我们有一个业务需求,是否需要显示开屏页,规则是当前版本第一次启动的时候需要显示开屏页。我们来分析这个业务,并且根据上面所说的三层来拆分这个业务。
首先是presentation层,界面展示,这个没啥好说的,就是展示开屏页或者直接展示内容。
还有data层,我们需要数据?是的,我们需要一个SharePreference来记录版本号。虽然这个数据很简单,但是他确确实实是一个需要记录保存的数据。
当然也有domain层,这个层次需要提供两个业务,保存版本号,还有比较版本号,根据规则确定是否需要显示开屏页。
data层
我们从data开始来看代码,首先我们定义了一个虚类。
public abstract class IAppinfoRepository extends IRepository {
public abstract Observable<Integer> getCurrentVersionObservable();
public abstract Flowable<Integer> getCurrentVersionFlowable();
public abstract Single<Integer> getCurrentVersionSingle();
public abstract Maybe<Integer> getCurrentVersionMaybe();
public abstract Completable setCurrentVersionLauncher(int currentVersion);
}
看上去提供了很多方法,还继承了一个IRepository。首先IRepository其实只是我用来做一些所有Repository都需要做的公共方法的,或者工具的,你也可以不继承,直接把IAppinfoRepository定义为一个接口。(Repository 后缀表示这是个数据提供者,这是我在clean arch中的命名习惯。)
发现方法很多,但是其实只是getCurrentVersion的多个rxjava类型的实现,实际上我们不需要这么多,只要其中一个就能完成任务。这里添加只是为了让代码看上去更洋气一些。实际写代码的时候我也不会全部去实现。你看关于set我就只实现了一个版本。
然后定义AppinfoRepository
public class AppinfoRepository extends IAppinfoRepository {
private IAppinfoRepository preferenceAppinfo;
public AppinfoRepository() {
preferenceAppinfo = new PreferenceAppinfo();
}
@Override
public Observable<Integer> getCurrentVersionObservable() {
return preferenceAppinfo.getCurrentVersionObservable();
}
@Override
public Flowable<Integer> getCurrentVersionFlowable() {
return preferenceAppinfo.getCurrentVersionFlowable();
}
@Override
public Single<Integer> getCurrentVersionSingle() {
return preferenceAppinfo.getCurrentVersionSingle();
}
@Override
public Maybe<Integer> getCurrentVersionMaybe() {
return preferenceAppinfo.getCurrentVersionMaybe();
}
@Override
public Completable setCurrentVersionLauncher(int currentVersion) {
return preferenceAppinfo.setCurrentVersionLauncher(currentVersion);
}
}
这个类相当于封装类,最终向domain层暴露,并且提供接口的类。至于具体其中的数据到底是从本地获取,还是从网络获取,或者你造假数据,这都和domain没关系。
最终的实现类是PerferenceRepository
public class PreferenceAppinfo extends IAppinfoRepository {
private SharedPreferences mPreferences;
public PreferenceAppinfo(){
mPreferences = SharedPreferencesUtils.getSharedPreferences(
SharedPreferencesUtils.FileName.NORMAL_PREFERENCE,
Context.MODE_PRIVATE
);
}
@Override
public Observable<Integer> getCurrentVersionObservable() {
Observable<Integer> integerObservable= getObservable(new RepositoryCallback<Integer>() {
@Override
public void subscribe(@NonNull ObservableEmitter<Integer> e) {
int version = mPreferences.getInt(SharedPreferencesUtils.Key.APP_VERSION,-1);
e.onNext(version);
e.onComplete();
}
});
return integerObservable;
}
@Override
public Flowable<Integer> getCurrentVersionFlowable() {
Flowable<Integer> integerFlowable = Flowable.create(new FlowableOnSubscribe<Integer>() {
@Override
public void subscribe(@NonNull FlowableEmitter<Integer> e) throws Exception {
int version = mPreferences.getInt(SharedPreferencesUtils.Key.APP_VERSION,-1);
e.onNext(version);
e.onComplete();
}
}, BackpressureStrategy.MISSING);
return integerFlowable;
}
@Override
public Single<Integer> getCurrentVersionSingle() {
Single<Integer> single = Single.create(new SingleOnSubscribe<Integer>() {
@Override
public void subscribe(@NonNull SingleEmitter<Integer> e) throws Exception {
int version = mPreferences.getInt(SharedPreferencesUtils.Key.APP_VERSION,-1);
e.onSuccess(version);
}
});
return single;
}
@Override
public Maybe<Integer> getCurrentVersionMaybe() {
Maybe<Integer> maybe = Maybe.create(new MaybeOnSubscribe<Integer>() {
@Override
public void subscribe(@NonNull MaybeEmitter<Integer> e) throws Exception {
int version = mPreferences.getInt(SharedPreferencesUtils.Key.APP_VERSION,-1);
e.onSuccess(version);
}
});
return maybe;
}
@Override
public Completable setCurrentVersionLauncher(final int currentVersion) {
return Completable.create(new CompletableOnSubscribe() {
@Override
public void subscribe(@NonNull CompletableEmitter e) throws Exception {
mPreferences.edit().putInt(SharedPreferencesUtils.Key.APP_VERSION,currentVersion).commit();
e.onComplete();
}
});
}
}
用于真正在本地sharepereference中存入数据和读取数据。
domain层
domain层的实现主要是两个case类。(Case前缀表示这个类是用来向上层提供一个业务功能的)
public class CaseSetCurrentVersion extends UseCase<CaseSetCurrentVersion.RequestValue,CaseSetCurrentVersion.ResponseValue> {
private AppinfoRepository mRepository;
@Override
@Deprecated
public Flowable<ResponseValue> asFlowable() {
throw useCrF();
}
@Override
@Deprecated
public Observable<ResponseValue> asObservable() {
throw useCrO();
}
@Override
public Completable asCompletable() {
mRepository = new AppinfoRepository();
return mRepository.setCurrentVersionLauncher(getRequestValues().mVersionCode);
}
public static final class ResponseValue implements UseCase.ResponseValue{
}
public static final class RequestValue implements UseCase.RequestValues{
public int mVersionCode;
public RequestValue(int versionCode){
mVersionCode = versionCode;
}
}
}
比如这个CaseSetCurrentVersion类,就向上层提供了一个业务功能,保存当前版本号。具体实现就是调用AppinfoRepository向上提供的接口。
还有一个CaseLoadPageSwitch ,用来判断当前版本是否大于保存的版本,这也是一个业务。实现原理同CaseSetCurrentVersion。
presentation层
关于最上层,我们可以继续使用其他的架构类型,比如,demo中,我们在presentation中继续使用了mvp的架构模式进行处理,所以来看一下和MainActivity对应的MainPresenter
public class MainPresenter {
CaseSetCurrentVersion mCaseSetCurrentVersion;
CaseLoadPageSwitch mLoadPageSwitch;
Context mContext;
public MainPresenter(Contracts.IMainActivity activity,Context context){
mCaseSetCurrentVersion = new CaseSetCurrentVersion();
mLoadPageSwitch = new CaseLoadPageSwitch();
mContext = context;
launcher();
}
public void setCurrentVersion(){
int versionCode = getVersionCode(mContext);
CaseSetCurrentVersion.RequestValue requestValue = new CaseSetCurrentVersion.RequestValue(versionCode);
mCaseSetCurrentVersion.setRequestValues(requestValue);
mCaseSetCurrentVersion.asCompletable().subscribeOn(Schedulers.io()).subscribe();
}
public void launcher(){
int versionCode = getVersionCode(mContext);
CaseLoadPageSwitch.RequestValue requestValue = new CaseLoadPageSwitch.RequestValue(versionCode);
mLoadPageSwitch.setRequestValues(requestValue);
mLoadPageSwitch.asObservable().subscribe(new Consumer<CaseLoadPageSwitch.ResponseValue>() {
@Override
public void accept(@NonNull CaseLoadPageSwitch.ResponseValue responseValue) throws Exception {
if(responseValue.isFirstLauncher){
setCurrentVersion();
//mLauncherView.loadIntroView();
}else{
//mLauncherView.loadMainView();
}
}
});
}
public static int getVersionCode(Context context) {
try {
PackageManager manager = context.getPackageManager();
PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
}
首先初始化了两个case,然后调用CaseLoadPageSwitch来判断当前版本情况,确定是否是当前版本第一次启动。
如果是第一次启动,继续调用保存当前版本的业务模块,然后调用activity的显示引导页的功能,否则直接显示主页内容。
可以看到presenter只负责和界面层通信,所有的业务处理都会委托给domain中提供的case进行处理。
补充
以上基本上就是这个架构的设计思想,但是还有一些补充的地方。
1. 关于两个case的互动,比如我们可以修改CaseLoadPageSwitch的业务可以修改,这里仅仅是用来判断版本,我们可以扩展到,如果第一次启动,直接存入当前版本号。所以我可以在CaseLoadPageSwitch之中直接调用CaseSetCurrentVersion进行操作。
2. 这里我们在presenter中获取当前版本号,而不是将这个功能放入case,这里有几个原因,首先获取版本号是和平台相关的,不希望进入到domain中(当然这条规则有时候还是打破好,因为毕竟你是在开发一个android应用,一味的追求平台无关性也许会让你的业务混乱,这里说这个规则主要还是当做一把权衡的尺来用)。另外,他需要参数Context的参数,该参数并不希望被到处传递,一旦他贯穿整个app之后,逻辑复杂度一定会呈现几何程度的上升。
使用感受
我尝试在实际项目中使用了该架构,在实际的操作过程中有如下感受
1.clean arch在app起步阶段并不算友好,需要比较多的前置工作,并且新业务代码会相对复杂和冗余不少。
2.在团队成员熟悉该架构,并且按照架构实施。项目代码积累一定量之后,优越性会逐渐展示,更清楚的思路,更多可复用的case(业务)类。
3.在项目维护阶段,定位问题直观。新业务的添加简单,几乎不会对原有代码造成影响。
4.clean arch配合模块化开发和rxjava才能发挥最大功力。
总结,clean arch是一个通过增加代码量来使系统逻辑清晰化的架构,如果你渴望写最少的代码,那么clean arch并不适合你,如果你渴望整洁的系统架构,那么clean arch就和他的名字一样干净。