MVP框架的演化

作者: 蓝灰_q | 来源:发表于2017-07-29 11:48 被阅读104次

    MVP这种架构在Android界已经基本成为标配,MVP本身也有很多写法和变种,当然,没有最好的架构,只有最合适的架构,具体架构要怎么写,还是要看实际项目的需要。
    我们在这里简单梳理一下MVP的一些演化版本,希望为具体的项目实现提供一点参考。
    MVP本身的概念,就是把Model、View和Presenter相互解耦,大概可以这样理解:


    MVP的基本概念

    各自分工如下:

    • View负责外部界面交互,不直接处理业务逻辑
    • Presenter负责内部业务逻辑,不直接处理业务数据
    • Model负责核心业务数据,与数据库和网络进行数据交互

    原始MVP

    如果仅从分工的角度实现MVP,只需要发生两次引用:

    • 向View里引用Presenter,(因为View都是Fragment或Activity,有特定的构造函数,所以一般采用set方式引用),以处理具体的内部业务逻辑,代码形如:
    public class TasksFragment extends Fragment{
      ...
      private Presenter mPresenter;
      public void setPresenter(Presenter presenter) {
            mPresenter = presenter;
        }
      ...
    }
    
    • 向Presenter里引用Model和View,其中View需要通过接口封装一下再引用(一般在构造函数中引用),引用Model为业务逻辑提供核心的业务数据,引用View操作与界面相关的业务逻辑,代码形如:
    public class Presenter{
      ...
      private Repository mRepository;//model的实现这里不再展开
      private TasksFragment mView;
      public Presenter(Repository tasksRepository, TasksFragment tasksView) {//presenter里引用model
            mRepository = tasksRepository;
            mTView = tasksView;
            mTasksView.setPresenter(this);//view里引用presenter
        }
      ...
    }
    

    这就是一个最原始的MVP的实现,这个版本有一个严重的问题,就是可维护性太差!
    这版MVP虽然实现了各司其职,但其实质只不过是把代码拆到了不同的文件里,在实现中,M、V和P都引用了实体类的实例,形成了非常紧密的耦合,它其实只是实现了这样的效果:


    实际效果

    很显然,难以复用,难以扩展,未来的维护简直是个灾难。
    为了解耦合,很自然地,要使用接口去解耦合。

    演化1-Google Architecture

    Google在github上开源的architecture是个教科书般的MVP框架,它是这样做的:

    • V和P的接口化和注入
      View和Presenter不再引用实体类,而是引用抽象接口,View里引用的Presenter的接口,Presenter里引用的也是View的接口,这样的View和Presenter的代码形如:
    public class TasksFragment extends Fragment implements IView{//实现接口以便注入到Presenter
      ...
      private IPresenter mPresenter;
      public void setPresenter(IPresenter presenter) {//view已有特定的构造函数,以set方式注入为宜
            mPresenter = presenter;
        }
      ...
    }
    

    public class Presenter implements IPresenter{//实现接口以便注入到view
      ...
      private Repository mRepository;//model的实现这里不再展开
      private IView mView;
      public Presenter(Repository tasksRepository, IView tasksView) {//presenter里注入model和view
            mRepository = tasksRepository;
            mTView = tasksView;
            mTasksView.setPresenter(this);//view里注入presenter
        }
      ...
    }
    

    如果愿意的话,model也可以采用接口注入的形式(google architecture并没有做model的接口注入,是为了确保引用的实例是一个全局唯一的数据层单例,这样容易避免污染数据),这样就能实现一个完好解耦的MVP:


    可维护性良好MVP
    • 集中管理V和P的接口
      其实就是把V和P的接口放在同一个接口文件下了,代码形如:
    public interface ITasksContract {
        interface IView{...}
        interface IPresenter{...}
    }
    

    这样做有两个好处:
    1.从一组业务来讲,业务逻辑和界面逻辑在同一个文件中定义,极具连贯性,极大地方便了阅读、理解和维护(这也会引导你先从接口开始写代码)
    2.从多组业务来讲(App一般有多组业务),便于管理好多个V和P的接口,这些接口天然按照业务分别写在不同文件里,扩展和引用更加清晰,不易出错

    google architecture还做了一项改进,为V和P的接口定义了更基础的接口,在基础接口中统一定义了View注入Presenter的行为和Presenter开启业务工作的行为,代码形如:

    public interface BaseView<T> {//用泛型定义Presenter
        void setPresenter(T presenter);//用set注入Presenter
    }
    

    public interface BasePresenter {
        void start();//开启业务工作
    }
    

    你自己实现的V和P的接口,只要继承基础接口,就能保证MVP基础行为的一致性,这样你的V和P就可以更加专注于业务
    (除了教科书般的MVP,google architecture还提供了教科书般的数据Model层实现,不过这里就不做展开了)

    演化2-泄露的问题

    上面的这种做法,有一个潜在的问题,就是内存泄露
    我们知道,Presenter为了实现业务逻辑,一手持有数据Model,一手持有View,这里面有一个隐含的bug:
    数据Model在处理数据时,无论是处理本地数据还是网络数据,都是耗时操作,是不能在主线程运行的;而View,是必须在主线程运行的。这就容易产生一个问题,当View关闭退出时,Presenter可能还在异步线程里工作,而且Presenter还持有着View的实例——标准的内存泄露场景
    要避免持有型的内存泄露,一个很有效的办法就是把强引用的持有变成弱引用,就是说,在Presenter里,要用WeakReference的方式去持有View,实现代码形如:

        protected WeakReference<T> mViewRef; // view 的弱引用
        public void attachView(T view){//持有View
            mViewRef = new WeakReference<T>(view);
        }
        public void detachView(){
            if (mViewRef != null){
                mViewRef.clear();
                mViewRef = null;
            }
        }
        public T getView() {//获取view的实例
            return mViewRef.get();
        }
    

    这段代码其实是通用代码,根据聚焦业务的原则,应该抽象为基础行为,而接口是不能实现任何方法的,所以,这段代码只能通过抽象类实现通用化,整个类的代码形如:

    public abstract class MVPBasePresenter<T> {
        protected WeakReference<T> mViewRef; // view 的弱引用
        public void attachView(T view){//持有View
            mViewRef = new WeakReference<T>(view);
        }
        public void detachView(){
            if (mViewRef != null){
                mViewRef.clear();
                mViewRef = null;
            }
        }
        public T getView() {//获取view的实例
            return mViewRef.get();
        }
    }
    

    其中,attachView和detachView要在View的相应的生命周期中调用,这样的话,我们又需要为View实现相关的抽象类,Fragment和Activity都需要

    //需要两个泛型类型,一个用来继承Presenter的抽象类,而这个Presenter抽象类又需要一个View的泛型
    public abstract class MVPBaseFragment<V,T extends MVPBasePresenter<V>> extends Fragment {
        protected T mPresenter;
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mPresenter = createPresenter();
            mPresenter.attachView((V)this);
        }
        @Override
        public void onDestroy() {
            super.onDestroy();
            if(mPresenter!=null)
            mPresenter.detachView();
        }
        protected abstract T createPresenter();
    }
    

    这样一个业务Fragment在实例化时,代码形如:

    //ICategoryContract.View是IView接口
    //我们还定义了ICategoryContract.Presenter作为IPresenter接口
    //CategoryPresenter继承了MVPBasePresenter抽象了和IPresenter接口
    public class MainFragment 
    extends MVPBaseFragment<ICategoryContract.View,CategoryPresenter> 
    implements ICategoryContract.View {...}
    

    我们实际上把Presenter抽象类和IPresenter业务接口做了分离,把View抽象类和IView业务接口做了分离,基础行为和业务逻辑互不干扰。
    Activity的代码内容类似,这里不再重复。
    到了实际项目中,V和P分别继承对应的抽象类,因为抽象类里已经实现了弱引用和相关的管理,所以我们可以专注于业务逻辑的实现。
    不过,这样做带来两个问题:

    • 如果在View的构造函数中自动处理Presenter的实例化,实际上会束缚了我们自己的写作方式,比如我们的Presenter需要注入Model,就不能用构造方式注入;更严重的是,如果我们在Presenter初始化时需要设置某些UI控件,因为抽象类的oncreate需要先于业务类的oncreate去执行(业务类里需要先执行super.oncreate),会遇到UI控件不能及时初始化的问题。
    • Android的View其实是在不断扩张的,以Activity为例,常见的就包括Activity、AppCompatActivity、FragmentActivity、RxAppCompatActivity等,如果使用这种抽象类的模式,每遇到一种Activity,就得去做一个对应的抽象类,可扩展性很差。

    参照Google的做法,我们应该再多做一点接口的文章

    演化3-View的剥离

    我们回头再看一遍Presenter抽象类

    //Presenter抽象类
    public abstract class MVPBasePresenter<T>{...}
    

    其实在Presenter抽象类里,用来处理View的泛型是与业务无关的,我们此前是做了一个View的抽象类来配合Presenter做弱引用处理,其实细想起来,这个View的角色没必要使用抽象类,我们用一个IView基础接口就可以满足需要:

    //基础接口,不需要定义任何方法
    public interface IMVPBaseView {}
    

    我们的业务接口里,IView业务接口继承这个基础接口:

    public interface CategoryContract {
        ...
        interface View implements IMVPBaseView{
        ...
        }
    }
    

    我们的Fragement可以恢复Google教科书那样的简洁:

    public class TasksFragment extends Fragment implements IView{//实现的接口中包含基础行为和业务逻辑
      ...
    }
    

    最终,我们的MVP结构是这样的:
    Model:接口注入(更灵活)或引用一个全局单例(更干净)
    View:IMVPBaseView(基础行为)-> IContract.View(业务逻辑)-> XXFragment(V的具体实现)
    Presenter:(MVPBasePresenter(基础行为) + IContract.Presenter)-> XXPresenter(P的具体实现)
    当然,在这种方式下,Presenter的创建、初始化、销毁等行为,也还给了最终的业务Fragment(或Activity)。

    演化4-Dagger

    MVP里面其实有大量的依赖关系和注入行为,代码会显得比较复杂,而Dagger是一个专门处理依赖注入的框架,可以用配置的方式实现复杂的依赖关系,所以我们完全可以用Dagger来实现MVP
    在Dagger(Dagger2)里,核心要素就是Module、Inject和Component,它们分别起这样的作用:

    • Module:提供依赖,其实就是把我们此前用set或构造参数注入的依赖实例,改用module配置出来,由Dagger负责传给要注入的类,比如把IView和数据Model注入到Presenter里,代码形如:
    //为presenter提供IView参数实例
    @Module
    public class TasksPresenterModule {
        private final TasksContract.View mView;
        public TasksPresenterModule(TasksContract.View view) {
            mView = view;
        }
        @Provides//提供参数的函数方法
        TasksContract.View provideTasksContractView() {
            return mView;
        }
    }
    

    //为presenter提供数据Model参数实例(google官方示例里又嵌套了几层component)
    @Singleton//要求dagger实现单例
    @Component(modules = {TasksRepositoryModule.class, ApplicationModule.class})
    public interface TasksRepositoryComponent {
        TasksRepository getTasksRepository();
    }
    
    • Inject:指定依赖,就是说明某个属性对象是需要用Module注入进来的,比如在Presenter里说明某个modle对象和某个view对象是需要dagger注入进来的,代码形如:
    //presenter类的参数改用Dagger注入
    class TasksPresenter implements TasksContract.Presenter {
        private final TasksRepository mTasksRepository;
        private final TasksContract.View mTasksView;
        /**
         * Dagger strictly enforces that arguments not marked with {@code @Nullable} are not injected
         * with {@code @Nullable} values.
         */
        @Inject   //参数是需要注入的
        TasksPresenter(TasksRepository tasksRepository, TasksContract.View tasksView) {
            mTasksRepository = tasksRepository;
            mTasksView = tasksView;
        }
        ...
    }
    

    同样,在Activity里也要用dagger注入persenter,代码形如:

    public class TasksActivity extends AppCompatActivity {
        @Inject TasksPresenter mTasksPresenter;//内部对象是需要注入的
        ...
    }
    
    • Component:组装器,做两件事:1-把做好的Module对象作为参数提供给要注入的类,比如把Modle对象和IView对象实例化,作为Presenter的参数,完成Presenter的实例化;2-把完成注入和实例化的类,注入到当前类里,比如把完成实例化的Presenter注入到Activity里,代码形如:
    public class TasksActivity extends AppCompatActivity {
        @Inject TasksPresenter mTasksPresenter;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            ...
            // Create the presenter
            DaggerTasksComponent.builder()
                    //component会做出presenter需要的两个参数
                    .tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
                    .tasksPresenterModule(new TasksPresenterModule(tasksFragment))
                    .build()//构造出Presenter的实例
                    .inject(this);//把Presenter注入到当前Activity中
            ...
        }
        ...
    }
    

    Dagger只是用注解来配置依赖关系,编译时还是用工厂类和传参等形式实现的依赖注入,例如,针对上述代码,Dagger的apt插件会在编译时把它转成形如这样的代码:

    ...
    //生成的Component类里,Module工厂类实现Module的实例化
    this.provideTasksViewProvider = MainModule_ProvideTasksViewFactory.create(builder.mainModule);
    ...
    //生成的Component类里,Presenter工厂类实现Presenter的实例化
    mainPresenterProvider = MainPresenter_Factory.create(provideTasksRepositoryProvider,provideTasksViewProvider);
    ...
    //生成的Activity的Injector类里,用构造参数实现依赖注入
    this.mainPresenterProvider = mainPresenterProvider;
    

    这样用Dagger实现的MVP,最开始会有点别扭,因为类之间的注入关系好像不像直接代码实现那样熟悉,但习惯之后,你会发现这么几个好处:
    1.基于JSR330的稳定和标准的依赖注入方法
    2.依赖关系是配置化的,代码可读性更强,也容易聚焦业务
    3.可以通过注解实现全局单例

    演化5-Kotlin的引入

    作为基础通用框架,我们必须有一个Kotlin的版本,当然,不同的演化版本,会有不同的写法,如果参照演化3的版本,对应的Kotlin版本形如:

    //基础IMVPView接口
    interface IMvpView {
    }
    
    //基础MvpPresenter抽象类
    abstract class MvpPresenter<T:IMvpView> {
        protected var mViewRef:WeakReIference<T>?=null
    
        fun attachView(view:T){
            mViewRef= WeakReference(view)
        }
        fun detachView(){
            if(mViewRef!= null){
                mViewRef!!.clear()
                mViewRef=null
            }
        }
        val view:T? get() = mViewRef!!.get()
    }
    //业务逻辑接口
    interface ICatContract {
        interface View<Presenter>{
            fun refreshUI()
        }
        interface Presenter{
            fun doInitPage()
        }
    }
    //业务Presenter
    class CatPresenter : MvpPresenter<CatActivity>(),ICatContract.Presenter {
        override fun doInitPage() {
        }
    }
    //业务Activity
    class CatActivity : AppCompatActivity(),MvpView,ICatContract.View<CatPresenter> {
        val TAG: String = "CatActivity"
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_test)
        }
        override fun refreshUI() {
        }
    }
    

    总结

    MVP作为一个基础型的结构,核心作用在于辅助我们实行良好的可读性和可维护性,我们可以为一个Presenter提供多种View的实现(例如,一个业务可以同时有全屏Activity和对话框Activity两种形式,分别提供给不同的业务环节,背后却使用同一个Presenter),也可以为一个Presenter提供不同的数据Model(例如,在两个根据后台数据动态绘制界面的Activity实例中,业务逻辑一致,可以使用同一种Presenter,但数据内容不同,就可以使用两个分别注入了不同Model的Presenter实例)
    MVP里有Passive View(Presenter通过View的接口操作View,并作为主要的业务驱动方)和Supervisor Controller(Presenter负责大量复杂的View逻辑)两种衍生,
    MVP还是一个开放性的结构,你可以根据自己的需要,去规避某些缺陷,或取得某些优势,如何去演化一个适合自己需求的MVP框架,一方面满足需求,一方面保持灵活,完全看自己的发挥了

    关于MVVM

    MVP的结构比较通透明了,不过其中的View总是要写一些业务逻辑相关的代码,比如操纵Presenter,处理生命周期,实例化Model对象等,如果需要更进一步,把View的角色限定为纯粹的UI,不做任何业务逻辑,不涉及任何数据,就需要用到MVVM模式了。
    在MVVM模式里,不再有Presenter,用ViewModel来处理业务逻辑,ViewModel不处理UI,而View只负责UI,与ViewModel建立数据绑定关系,通过databinding自动实现UI和Model之间的数据操作。
    技术细节推荐阅读如何构建Android MVVM应用程序

    引用

    Github Google android-architecture
    Android App的设计架构:MVC,MVP,MVVM与架构经验谈

    相关文章

      网友评论

        本文标题:MVP框架的演化

        本文链接:https://www.haomeiwen.com/subject/smhzkxtx.html