如何从MVP模式进阶到Clean模式

Posted 文酱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何从MVP模式进阶到Clean模式相关的知识,希望对你有一定的参考价值。

    从类图上来看,MVP都是一个业务一个Presenter,每个Presenter都是一个接口,它还包含了View的接口,用于定于和View相关的行为,然后Activity等业务类实现View的接口,因为UI有关的操作只能在UI线程。

    采用MVP模式,和View相关的接口都要由业务类实现,自然,业务类本身就会有数量不小的方法,而逻辑相关的接口可以放在Presenter里面,然后由一个PresenterImpl去实现。

    说实话,虽然这样看上去确实将Activity变成各种View和Presenter组合的模块,但是控件的声明还是在Activity,所以Activity的职责就变成了View的方法实现和控件声明。

    如果我们只是单纯的想要在MVP模式下输出一句Hello World到TextView上,是一件不太容易的事情,当然,MVP本身就不是用来解决这种微小场景。

    是否有更加简单的模式能够应付所有场景?答案是几乎没有的,都是要结合实际场景,在大的框架模式不变的情况下,允许做小的调整,比如一个应用整体的框架是MVP模式,但一些实在是细微的场景,有必要也使用吗?

    MVP模式并不是插拔式的,虽然说真正在执行逻辑处理的是Presenter,而Activity只是提供对应的方法实现,但是假设要移除这块业务,要清理的东西也是很多的,但是它可以实现一定程度的复用,只要实现了View的接口,通过对应的Presenter,就可以拥有这些接口定义的行为协议,但是也仅仅只是行为协议,具体的实现还是要自己编写。

    这所谓的复用,就是将业务类作为一个系统运作的部件,只要能够满足部件的要求,系统就能马上运转起来。

    MVP为了保证业务类更好的成为部件,它就必须尽可能的解耦,如和UI相关的接口在View接口里,而逻辑相关的方法在Presenter里,并且还会多一层PresenterImpl,每个View的接口方法在Presenter哪个方法里面调用,都是在这里进行处理,从而将业务类打造成一个清晰的部件,它和其他部件之间没有任何关系。

    当然,我们也可以将业务类上升为PresenterImpl,这样就能减少很多类的编写,不过并不建议这样做,因为业务类如果成为PresenterImpl,那么如果出现类似功能的业务,就需要到这个业务类复制粘贴相关的代码,如果将这些逻辑锁在一个PresenterImpl里,只要声明这个PresenterImpl就能使用了。

    每个业务就算相似,也可能存在一定的差异,为了保证结构通用,是要尽量避免引入这些差异,差异就要隔离在对应的类里面,这就导致可能会有一些AbstractPresenter的产生,然后每个业务的Presenter在继承AbstractPresenter的通用行为基础上,再实现自己的行为。

    这是不可避免的,如果真的想要结构上清晰,提取通用业务是一定要的,而这部分工作往往会带来类数量的增加,因为它只是通用类,具体业务也会有自己对应的业务类。

    我们可以这么理解,MVP就是合理的规定,哪部分代码应该写在那里,而谁应该写哪部分代码的一个约定,这也是MVC,MVVM等框架模式都具备的能力。

    在我们以往认知中,依赖倒置原则要求我们面向抽象编程,每个部分都应该是和抽象打交道,而不是具体的实现,实现是会变的,但是抽象是几乎不会变的。

    只要好好遵循这个原则,都能写出很好的代码,结构清晰,并且具有良好的维护性,无论MVP,MVC,还是MVVM,本质都是这个原则的实现产物。

    MVP相比MVC来说,它有一个优势:它的Presenter是可以测试的。

    单纯的Presenter是没有和View有任何关联的,有关联的是PresenterImpl,所以我们如果只是测试Presenter,是并不牵扯到UI,而android的测试中,涉及到UI都是很麻烦的事情。

    对Presenter的测试,就和传统的接口测试是一样的。

    对于Android开发人员来说,由于xml布局文件的存在,View的绘制并不只是单纯的代码编写,我们希望布局也能够复用。

    Android为布局的复用提供了一定的支持,像是include的使用,就能把一个布局,切割成几个布局的组合。

    我们可以把每个布局按照本身一定的功能,划分为几个include,然后每个include都有自己的逻辑处理类,比如说,我们可以定义一个ViewController,这个ViewController负责View的初始化和对应功能实现,那么一个Activity本身就只是多个ViewController的组合类,而Activity的布局,也是多个include的组合。

    如果划分得足够清楚,可以通过组合不同的ViewController来实现不同的界面组合。

    这种模式在定义上,和MVC是一致的,ViewController就是Controller,Android的Activity本身在Android的机制里面其实也是Controller,只不过对于开发者来说,接触到的是Activity,所以就在Activity上做功夫了。

    在我们以往的依赖关系或者结构设计上,都是类似树状的结构,通过赋予每个类依赖,实现或者继承的关系,将他们连接到一起。

    这种结构就是依赖关系树。

    之所以是依赖关系树这种形状,很大关系是因为我们的设计,实质上只有两种:自顶而下或者自下而上,我们会先确定一个点,然后从这个点开始延伸出和其他点的关系,从而不断辐射出去。

    Clean模式在依赖关系上,是画一个同心圆。

    

    Clean模式的解释是,依赖应该是从外到内,因此在实现上,率先实现内层,再实现外层,而每一层都会把内部那一层完全包裹起来,形成一个类似洋葱的结构。

    我们在实现上,通常都会实现最核心的那一层,比如说,我们想实现一个功能,输出Hello World,那么首先要实现的就是输出Hello World的方法,不过我们这个方法后面可能不只是输出Hello World,因此就抽象成输出传入的String的方法,然后我们再编写外部传入String的方法,有可能是键盘输入,有可能是其他输入等等。。。这样一层一层写下去,一直写到触发这个需求的地方,需求的最初位置,也是依赖的起点。

    所以我们平常的做法和Clean模式对于依赖的理解是没啥区别的,当然,也不能说Clean模式就是画个圆就说是新的东西,它本身更加强调的是,干净。

    Clean,就是干净,而什么样的程度是干净,干净又能做到什么呢?

    所谓的干净,是因为Clean模式的依赖是从外到内,因此内部对外部是无感知的,就像我们剥洋葱,每剥掉一层,里面依然还是完整的,只不过变小的洋葱。

    Clean模式是如何做到这样的独立性呢?

    在Clean模式中,DB,Web,Devcices等数据来源是最外层,这个没毛病,数据输入都是任何一个系统的起点,但是它把UI也放在了最外层。然后再进一步的层级是Controllers,GateWays和Presenters等,按照依赖从外到内的设计思想,这些层级也是最先接触这些数据来源和UI,接着的层级就是Use Cases,也就是用户场景,最里面就是Entities,这个可以理解为用户场景中的实体。

    UI变成了一个独立的层级,和DB,Web等并列,而DB,Web这些层级在设计上,原本就具备自测的能力,并且它应该也是最先被测试的,因为它们是框架提供的能力。这样一层层下去,每个内层在测试的时候,只需要了解它的内层,不知道它的外层。

    我们好奇的是,UI怎么就变成了一个独立的层级?

    这里的UI应该是各种组件,前面有关MVP的讨论中也提到,PresenterImpl确实是需要从外部传入View接口的实现类,所以UI作为最外层的依赖,也是没问题的。

    在Android中,一般的层级并不会超过三层。实现层,也就是框架层,像是Web,DB等,就是Clean模式的外层,而中间层是接口适配层,负责连接实现层和内层的业务逻辑层。

    在Clean模式中,业务逻辑层,也就是内层,应该是对外层毫无感知的,所以我们测试业务逻辑层,完全可以跳过框架层。

    我们很常见的做法就是在Activity中调用网络接口来获取数据,然后在对应的回调中将该数据展示到对应的控件上,如果按照Clean模式,这时候不应该是直接就将数据和控件进行绑定,中间要有一个接口适配层,将这两者独立开来。

    任何时候,两个独立的部分都不应该直接交互,而是要通过一个抽象,依赖倒置原则在这里的产物就是接口适配层。

    对于Android,Clean模式的要求就是业务逻辑层完全不能持有外层的引用,也就是说,内层提供一个它需要的数据模型,而接口适配层负责将框架层传递过来的数据转化为业务逻辑层需要的数据模型,然后再传给业务逻辑层,这就是适配层的工作。

    因此,在Android中,Clean模式的内层必须暴露接口,以便外部传递需要的数据,而外层是知道这些数据模型,可以跳过内层,组装这些数据模型进行测试。

    无法应用到实际场景的模式都是假模式,因此我们现在赶紧开始试试Clean模式在代码上是如何表现的。

    我们可以简单点,假设一个用户场景:获取到某个来源的字符串,然后显示到TextView上。

    从最核心的内层开始编写。

    最核心的内层就是用户场景,并且它是与外层毫无关联的,但是它需要暴露一个回调,负责和上层打交道,所以它接受一个字符串,然后输出这个字符串:

public class Interactor {
    private Callback mCallback;

    public Interactor(Callback callback) {
        mCallback = callback;
    }

    public void run(String info) {
        if (mCallback != null) {
            mCallback.showInput(info);
        }
    }

    public interface Callback {
        void showInput(String info);
    }
}

    对于MVP模式来说,最核心的部分就是Interactor,它负责将外部的Model转换成ViewModel。

    在MVP中,Presenter是不直接操作View和Model的,它要做的工作就是把ViewModel传给View。Model并不等于ViewModel,Model是原始的数据,而ViewModel是视图数据,比如说一个登陆页面上的密码和用户名,这些数据的集合就是一个ViewModel。

    Interactor既然作为最内层,它就不应该直接和最外层拿Model,所以我们需要一个中间层Converter。

    我们假设这里的场景是从User模型中获取name字段,这就是Interactor的数据来源,那么Converter的职责就是从接受到的User模型中取出name传给Interactor。

public class Converter {

public void converterNameToInteractor(Callback callback, User user) {
new Interactor(callback).run(user.getName());
}
}

    依赖是从外到内的,所以Interactor不能直接从外层设置Callback,它不能和外层有任何交集,这部分工作都在中间层Converter。

    Clean模式向我们做出了保证,Interactor是可以测试的,而且应该是可以独立于Android系统,在任何JVM机器上测试的代码,因为它已经隔离了外层的依赖,而这部分隔离的标准就是Interactor不依赖于任何框架的库和包,它依赖的是Java的库和包。

    按照Clean模式的原则,我们这里的Converter有个问题:它知道了User这个Model。

    这个有什么问题呢?User是外层的数据模型,而Converter从这个Model提取出Interactor需要的name这个String,然后Interactor通过Callback将name这个ViewModle传递给外层UI显示。

    这个过程完全没有问题,但是Clean模式要求内层不能知道外层的东西,中间层的Converter现在知道了外层的User,这个是要剥离的。

    我们只要将User这个参数修改为String就可以了。

    这样就会产生一个疑问:Converter不是将外层的数据模型转换为内层的数据模型吗?那么User.getName这个操作放在Converter不是应该的吗?

    道理上是这样的没错,但是为了保证依赖上的干净,比如说,我们现在要测试Converter,但是有了User这个依赖,就要知道外层了,就不能独立测试了,而且我们这里只是一个简单的参数,假设我们的Interactor需要的是不同类型的多个字段,它自己本身可能会为此提供一个Model,那么Converter的职责就是接受需要的字段,然后组装这个Model。

    为了独立,牺牲了便利,这在代码设计上是很常见的考虑,但是我们也要权衡一下,这牺牲的便利和换来的独立,哪个更加重要。

    接下来我们只要让Activity实现Callback接口就可以了。

public class MainActivity extends AppCompatActivity implements Interactor.Callback{
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

textView = (TextView) findViewById(R.id.text);
Converter converter = new Converter();
User user = new User("张三");
converter.converterNameToInteractor(this, user.getName());
}

@Override
public void showInput(String info) {
textView.setText(info);
}
}

     Activity就是外层,它这里有数据的来源,UI的展示。

     Clean模式大概就是这样的结构,我们可以看到依赖确实是一步一步传递过去的,每一层都可以独立于外层进行测试。

     这一路下来,Presenter去了哪里?

     当然,这里我们还是可以抽取出Presenter:

public class Presenter implements Interactor.Callback{
    private View mView;

    public Presenter(View view){
        this.mView = view;
    }

    @Override
    public void showInput(String info) {
        if(mView != null) {
            mView.showText(info);
        }
    }

    public interface View{
        void showText(String text);
    }
}

     然后Activity再修改成这样:

public class MainActivity extends AppCompatActivity implements Presenter.View{
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView) findViewById(R.id.text);
        Presenter presenter = new Presenter(this);
        presenter.showInput(new User("张三").getName());
    }

    @Override
    public void showText(String text) {
        textView.setText(text);
    }
}

    实际上,Converter和Presenter是并列的,前面的例子中,Converter实际上是把Presenter的工作也做了,不是一个单纯的转换,是有一个分配数据的操作,所以我们如果要加入Converter这个层级,必须保证它就仅仅只是一个数据转换的类。

    现在我们修改Converter:

public class Converter {

    public String converterName(String name) {
        return name;
    }
}

   然后Activity再这样修改:

public class MainActivity extends AppCompatActivity implements Presenter.View{
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = (TextView) findViewById(R.id.text);
        Presenter presenter = new Presenter(this);
        Converter converter = new Converter();
        User user = new User("张三");
        presenter.showInput(converter.converterName(user.getName()));
    }

    @Override
    public void showText(String text) {
        textView.setText(text);
    }
}

    现在只是因为Converter要转换的类型只是String的单一字段,如果是自定义类型,就不会显得这么小题大做了。

    Clean模式的依赖是从外到内,那么外层能否跳过中间层,直接接触内层呢?比如说,我们这里的Activity干脆就实现Callback这个接口?

    最好不要这样做,我们要保证干净,就要彻底的隔离,保证每剥掉一层,内部还是完整的。

    不过基于Java的语言特性,内层提供的接口,谁都可以实现,然后只要由中间层传过来就行了,也是能够保证依赖从外到内的原则,如果真的想要严格隔离,我们这里利用访问权限来控制,将中间层和内层放在同一个包里,内层的接口都只有包访问权限,不同包的外层自然就无法访问到了。

    命名空间在隔离上是能够发挥特别好的作用,因此我们要做好包的管理。

    在很多实际的编码工作,如果严格按照外层-->中间层-->内层,内层-->中间层-->外层这种访问顺序,可以预计,中间层的类的数量可能会爆炸,比如内层不能直接访问数据库这样的外层,那么它就会通过一个中间层来获取数据,有可能只是简单的查询。

    当然,我们可以优化中间层,比如对某个表的操作可以合并到一个类里面,这样就会减少很多中间层。

    严格并不是坏事,规矩还是要遵守的,我们能做的灵活,就是中间层的管理,但是外层和内层,是必须保证一定是隔离开来的。       

    

以上是关于如何从MVP模式进阶到Clean模式的主要内容,如果未能解决你的问题,请参考以下文章

关于Android MVP模式的思考

Android进阶之路-详解MVP

Kotlin之框架模式MVP总结和测试代码

作为过来人,对于Android MVP模式的一些详解

从Google的todo-mvp源码中学习MVP模式

从Google的todo-mvp源码中学习MVP模式