我的clean architecture

前言    

    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就和他的名字一样干净。

猜你喜欢

转载自my.oschina.net/zzxzzg/blog/1647895