标签: MVP模式

MVP模式是你的救命稻草吗

为什么要学习架构?

不管是MVC还是MVP,亦或则其他架构,它们的设计目的都是为了达到编码的*高境界,那就是:低藕合,高复用,易测试,好维护。

而要达到这个终*目标,首先要理解的是每个部分各自负责些什么,以及如何组合在一起。因此我个人认为,学习架构关键在两步:

  1. 如何把缠在一起的代码拆分
  2. 如何把拆开的代码再组合

很多新手在刚做项目时,都会把所有的代码,如数据的访问和处理,数据的展示,用户的输入都写在一起,代码和思维都呈现出一种线性的形式,一切都是直来直往的。这样代码量确实少,写的时候也觉得方便,但是带来了几个致命的问题:

  1. 当业务越来越复杂时,修改代码成了噩梦。同样的逻辑放在不同的地方,本来只用改一处的变成了需要改几百处。而又因为所有的逻辑都互相牵扯住,导致本来只想改一处的,结果却影响了几百处。
  2. 当时间越来越遥远时,理解代码成了噩梦。本来只需要阅读几行的时候,却因为所有的代码都杂在一起变成了要阅读几千行。时间一长,重新阅读时,别说别人,就是自己也很难一下就能掌握关键点。
  3. 当需要做一个功能时,却发现代码无法复用,明明是一样的逻辑也只能靠Ctrl+CCtrl+V。这又为今后修改代码时增加了工作量。
  4. 当需要测试时确发现很难进行测试,每次修改一处代码后都必须进行重复的人工测试,无法进行自动化测试,模块和模块也无法拆开来进行独立的单元测试,每次都要整体的测一遍才能确保一切完好如初。

要换的不是架构,而是思维方式

其实目前市面上的架构模式已经有很多种,各有不同,但模式终究只是一种设计理念的表现形式,学习再多的架构,你也只是多会用了几种工具而已,学习一种模式其实是在学一种思维方式:

如何在解决问题的时候把问题合理的拆分,又如何将拆分的零件合理的组装成解决问题的工具。

将这种思维方式深入到你的大脑里,血液里,直至骨髓,让它成为你思考问题的一种习惯和定式,逐渐成为你的一部分,那么这时你已达到无招胜有招的境界了。

闲话先不扯了,回正题。

实现架构没有唯一标准

这里首先需要说明的是无论MVP或MVC还是MVVM等任何一种架构和模式其实都没有谁优谁劣之分,而且就算是同一种架构,也可以根据不同的使用场景来做不同的实现方式,这里并没有宇宙*对的对错标准和概念定义。这和张三丰在教无忌太*拳以后让其先忘掉招式是一样的道理,在应用型领域,定式和概念只应是在学习的过程中才存在的,而真正学会以后应该马上忘掉定式,忘掉概念,将其用熟用活才是关键。

所以说我在此描述的概念也不是唯一的标准,而只是个人对其的理解。

什么是MVC

因为MVP是MVC的一个变种,因此我们先来看在MVC里代码是如何拆分和组合的,这里简要的描述常见的定义:

  1. 拆分:Model负责数据,View负责显示,Controller负责用户输入。
  2. 组合:View将用户操作和事件传递给Controller,Controller将用户命令翻译成消息来传递给Model,Model在处理完数据后,通知View来显示给用户,而View又从Model取出数据。

我认为在学习MVP之前,必须深刻理解什么叫做MVC,因为两者的区别其实没有你想象中的那么大,与其神话并盲目追崇新的架构,期望其能解脱你于苦海,还不如深刻的理解MVC的设计理念,这就和学习一门新语言一样,只有你真正深刻的理解了其思维方式,那么学习新的一门语言其实都是易如反掌的。因为它们其实都是在做一件事,只是做的过程上有些许差异而已。

更多MVC的理解,可以参考我之前写的一篇文章:在谈MVP之前,你真的懂MVC吗?

什么是MVP

MVP与MVC*大的区别就在与将Model和View通过Presenter隔开了,不再允许其互相直接通信,而所有的消息都是通过Presenter这个中间人来传递。

而这样做的目的主要是为了将数据和展示划出更明确的界限。

首先我们来看MVP到底是在解决什么问题的:

  • 数据管理
    1. 什么是数据?(Model)
    2. 如何筛选数据?(Selections)
    3. 如何改变数据?(Commands)
  • 用户界面
    1. 如何展示数据?(View)
    2. 如何通过事件来改变数据?(Interactor)
    3. 如何将这些放在一起?(Presenter)

可以看出其实一个GUI程序的核心,还是在于用户和数据之间的关系。而架构的引入也是为了解决这几个核心的问题。

那么MVP又是如何解决这几个问题的,Model,View,Presenter三者又各自负责什么呢?谁来负责请求网络数据,访问和存储本地数据,谁来负责处理数据,谁来负责显示数据,谁又来负责和用户交互呢?

具体表现在代码上,也可以说:网络请求应该放在哪,数据库访问应该放在哪,业务逻辑应该放在哪里,处理用户输入应该放在哪,谁又来控制显示或隐藏View的等具体的细节问题。

带着这些具体问题,我们一起来学习。

如何拆分

首先来看MVP各自负责什么:

  1. Model,负责定义数据(解决什么是数据)
  2. Presenter, 负责在Model和View之间,从model里取出数据,格式化后在View上展示(解决如何把数据和用户界面放在一起)。
  3. View,负责担任一个被动界面,用于展示数据。(解决如何展示数据)

和MVC比较而已,这里出现一个*大的疑问就是:那么谁来负责和用户的操作交互呢?答案是,用户在View上的所有操作(事件)都由View路由给Presenter,由Presenter来与其交互。

而和MVC*大的区别在于Model和View完全被Presenter隔开了,Presenter作为它们之间的中间人去传递所有数据。

如何组合

三者又是如何组合起来的呢?

很显然Presenter作为中间者,它是同时拥有View和Model的引用的,为了在它们之间起到桥梁作用,即Presenter会主动和View和Model进行通信

Model和View必须是完全隔离的,不允许两者之间互相通信,保持对彼此的不感知,这样的好处是你彻底将数据和展示分离来开,并且可以独立的为Model去做测试。

Model在三者中是独立性*高的,Model不应该拥有对View的引用,而且Model也不需要保存对Presenter的引用,对于Presenter而已,Model只需要提供接口,等着Presenter来调用时返回相应数据即可,这和经典MVC模式中是非常不同的,在MVC中Model在数据发送变化后,是需要发送广播来告之View去更新用户界面的,而在MVP中,Model是不应该去通知View,而是通知Presenter来间接的更新View的。

Presenter和Model的关系也应该是基于接口来通信,这样才能把Model和Presenter的耦合度也降到*低,那么在需要改变Model内部实现,甚至彻底替换Model的时候,Presenter则是无需随之改变的。这样做带来的另一个好处就是你可以通过Mock一个Model来对Presenter以及View做模拟测试了,从而提高了可测试性。

那么View和Presenter的关系呢?View是需要拥有对Presenter的引用,但仅仅是为了将用户的操作和事件立即传递给Presenter,为了让View和Presenter耦合较低,View也只应该通过接口与Presenter通信,从而保证View是完全被动的,一方面它由用户的操作触发来和Presenter通信,另一方面它完全受Presenter控制,唯一需要做的事情就是如何展示数据。

简要总结三者之间的关系是:View和Model之间没有联系,View通过接口向Presenter来传递用户操作,Model不主动和Presenter联系,被动的等着Presenter来调用其接口,Presenter通过接口和View/Model来联系。

View <- 接口 <- Presenter ->接口 -> Model

View -> 接口 -> Presenter <- 接口 <- Model

Talk is cheap, show me the code

为了便于理解,这里提供一些伪代码加注释演示一下如何实现MVP模式:

View

interface IUserView {

  void setPresenter(presenter);
  void showUsers(users);
  void showDeleteUserComplete();
  void showDeleteUserError();

}

class UserView implements IUserView {

  UserPresenter presenter;

  // 保持对Presenter的引用,用于路由用户操作
  void setPresenter(presenter) {
      this.presenter = presenter;
  }

  // 将Presenter传递来的数据展示出来
  void showUsers(users) {
      draw(users);
  }

  // Model操作数据成功后,通过Presenter来告之View需要更新用户界面
  void showDeleteUserComplete() {
      alert("Delete User Complete");
  }

  // Model操作数据失败后,也是通过Presenter来告之View需要更新用户界面
  void showDeleteUserError() {
      alert("Delete User Fail");
  }

  // 当用户点击某个按钮时,将用户操作路由给presenter,由presenter去处理
  void onDeleteButtonClicked(event) {
      presenter.deleteUser(event);
  }

}

Model

interface IUserModel {

  List<User> getUsers();
  boolean deleteUserById();

}

class UserModel implements IUserModel {

  // 在数据库里查找数据,并将数据返回给presenter
  List<User> getUsers() {
       return getUsersInDatabase(id);
  }

  // 在数据库里删除数据,并将结果返回给presenter
  User deleteUserById(id) {
      return deleteUserByIdInDatabase(id);
  }

}

Presenter

interface IUserUserPresenter {

  void deleteUser(event);

}

class UserUserPresenter implements IUserPresenter {

  // 保持对View的引用
  IUserView view;
  // 保持对Model的引用
  IUserModel model;

  UserUserPresenter(IUserView view, IUserModel model) {
    this.view = view;    
    this.model = model;

    this.view.setPresenter(this);   
  }

  void start() {
    // 从Model中取出数据
    List<User> users = model.getUsers();
    // 将数据发送给View,让其展示到用户界面
    view.showUsers(users);
  }

  void deleteUser(event) {
    // View将用户操作路由过来,由Presenter来处理
    long uid = whichUserNeedToDeleteBy(event);
    // 将用户操作翻译成命令或消息传递给model,以改变数据
    boolean success = model.deleteUserById(uid);
    // 将Model操作数据后的结果通知View来改变用户界面
    if (success) {
          view.onDeleteUserSuccess();
    } else {
        view.onDeleteUserFail();  
    }
  }
}

OK,到此一个*简单的MVP模式就实现了,可以看到整个架构的轮廓已经很清晰了,而且面向接口编程也带来了很多的好处,让代码之间藕和较少,对测试支持也更为友好,理解和维护起来也更加方便了。

以后有时间再为大家描述如何在android里面实现MVP模式,以便更具体的理解。有疑问欢迎参与讨论,大家一起学习进步。

使用MVP模式重构代码

之前写了两篇关于MVP模式的文章,主要讲得都是一些概念,这里谈谈自己在Android项目中使用MVP模式的真实感受,并以实例的形式一起尝试来使用MVP模式去重构我们现有的代码。

有兴趣的童鞋可以先去阅读之前的文章,因为这里将不再重复概念的部分了,本文会假设你对MVP有一点了解了:

1. 在谈MVP之前,你真的懂MVC吗?

2. MVP模式是你的救命稻草吗?

臃肿的Activity

大部分谈Android架构的时候,都基本会提到Activity越来越臃肿的问题,这几乎是一个普遍现象,而包括我本人在内的,都会首先将这个罪责推到MVC架构上,但如果你真的花时间去重构activity的时候,你会发现问题其实往往出在自己身上。

一般的MVC里的 Controller 需要做的事情:

  1. 负责获取和处理用户的输入。
  2. 负责将输入传给负责业务逻辑层去做数据上的操作(如增删改查)。
  3. 负责将业务逻辑层对于数据操作的结果,传给View层去做展示。

因此如果完全按照这种定义的话,你应该很难看到一个非常臃肿的Controller,因为Controller在MVC模式中,本来就应该是很轻的,而不是很重的部分,重的应该是M层,甚至在前端交互复杂的时候,V层都应该比C层要重。

我认为对于Controller的理解,就是一个站在M和V两者之间的一个翻译家,M来自地球,V来自火星。而如果站在中间的这个翻译者,话比他两的话还多,老是抢话,自言自语,这样显然是不合适的。

那么我们再来看典型的Activity的代码,处理的业务是常见的登录页面:

public class UserActivity extends Activity {

  private RequestQueue mQueue = Volley.newRequestQueue(this);

  private TextView mUsernameTextView;
  private TextView mPasswordTextView;
  private Button mLoginBtn;

  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
    setContentView(R.layout.login);

    mUsernameTextView = (TextView) findViewById(R.id.username);
    mPasswordTextView = (TextView) findViewById(R.id.password);

    mLoginBtn = (Button) findViewById(R.id.login_btn);
    mLoginBtn.setOnClickListener(new View.OnClickListener() {
          public void onClick(View v)  {
              String username = mUsernameTextView.getText().toString();
               String password = mPasswordTextView.getText().toString();

               String loginUrl = "http://somesite.com/login.php";

               JSONObjectRequest request = new JSONObjectRequest(loginUrl, Method.POST,
                 new Response.Listener<JSONObject>() {
                      public void onResponse(JSONObject json) {
                          if (json != null && json.get("isOk") == true) {
                              Toast.makeToast(getApplicationContext(), "Login Success", Toast.LENGTH_SHORT).show();
                                startActivity(new Intent(LoginActivity.this, MainActivity.class));
                        } else {
                              Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
                        }
                    }
               },
                new Response.ErrorListener(){
                      public void onError(VolleyError error) {
                          Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
                    }
               });
              mQueue.add(request);
        } 
    });
  }

  @Override
  protected void onStop() {
      super.onStop();
    if (mQueue != null) {
          mQueue.cancelAll();
    }
  }

}

其实这已经是*度简化过的代码了,真正的一个LoginActivity很容易就会超过几千行,不信你去看自己项目里面的代码就明白我在说什么了。

而一对比概念,我相信大部分人一下子就会发现问题,我们还是来看Activity作为一个 Controller 到底都负责干什么了:

  1. 首先activity必须去操作View的控件,设置它们的回调函数,有时也需要用代码去控制它们如何展示的属性。
  2. 然后activity一定需要去处理用户的输入,例如输入的值,以及点击事件等用户行为。
  3. 而几乎大部分异步网络请求都从activity发起,以及服务器返回数据的处理。
  4. activity一般还需要根据数据的操作结果,负责在页面上将结果告之用户,例如Toast或者其他View的操作。
  5. 除此之外,activity还需要管理其生命周期相关的所有事务,例如在页面退出的时候处理一下View控件和其他与生命期相关的逻辑。

你会发现Activity天生的责任太重,其中确实覆盖了 Controller 的原本的责任,例如处理用户输入,将用户操作转换成传递给业务逻辑层的命令等职责。

但如果你仔细的分析,你会发现activity不仅仅需要承担Controller的责任,还需要处理大量View的逻辑,例如控件的监听的属性,如何展示数据的职责也往往落到了它的肩上。更何况你很容易在activity写操作数据和网络请求的代码,也就是让它又承担了Model的责任,那么请问这样的Activity能不臃肿吗?

当然这是一个坏的例子,其实很多代码是可以封装到独立的层去的,例如网络请求,数据解析等。但就算你怎么封装和重构,你*多能做的事情也就是把本来就不应该放在Controller里的Model层分离出去,这是你原本就应该做的事情。但你很难在activity将controller和view分离开来,怎么写activity作为Controller,都和View的关系太紧密,必须多多少少去控制如何展示数据这个View的责任。

Presenter是来给activity减负的吗?

很多人会认为MVP中引入Presenter的概念,是为了给日益臃肿的activity来减负的,而我不这样认为,我认为Presenter和Controller的责任是差不多的,它们后期承担的目的都其实很简单,就是用来隔离Model和View的,也就是常说的展示层和业务层的解藕。

那么该如何解决activity的问题呢?目前常见的MVP在Android里的实践有两种解决方案:

  1. 直接将Activity看作View,让它只承担View的责任。
  2. 将Activity看作一个MVP三者以外的一个Controller,只控制生命周期。

在Google推出的官方MVP实例里,使用的就是第2种思路,它让每一个Activity都拥有一个Fragment来作为View,然后每个Activity也对应一个Presenter,在Activity里只处理与生命周期有关的内容,并跳出MVP之外,负责实例化Model,View,Presenter,并负责将三者合理的建立联系,承担的就是一个上帝视角。

在实践中,也有很多观点会简化掉Fragment,直接将Activity视为View,这个也是我比较赞同的,更简便一些,而且这样观念上也容易理解一些,你就把activity看作View的一部分,永远只让它处理展示的逻辑,不允许它去处理数据,和拥有业务逻辑。但是这样也有一个缺点,就是V和P的依赖关系不太规范了,理论上你是不应该在View里面去实例化Presenter和Model的,这其实是不合理的,正确的依赖关系,确实是应该在一个独立的更上层去实例化Model,View,Presenter的,这样依赖才是较为合理的关系,这点来看Google的架构模式确实更合理,但实操上也会麻烦一点,必须让每个activity拥有一个独立的fragment,这个我是觉得可以自由取舍,你是要概念上的合理,还是现实中的方便,其实都可以。

因为重点还是在于如何分离展示层和业务层,activity具体承担什么责任都可以,但只能承担一个责任。

例如之前的代码可以被重构成如下结构:

/**
 * View负责展示数据
 */
public interface UserView {

  void showLoginSuccessMsg(User loginedUser);
  void showLoginFailMsg(String errorMsg); 

}
/**
 * Presenter负责做View和Model的中间人
 */
public interface UserPresenter {

  void login(String username, String password);

}
/**
 * Model负责数据的处理和业务逻辑
 */
public interface UserModel {

  void login(String username, String password, Callback callback);

}

这里将Activity被视为View, 仅负责数据的展示,并且将用户的操作事件路由给P去做处理。

public class UserActivity extends Activity implements UserView {

  private UserContract.Presenter mPresenter;

  private TextView mUsernameTextView;
  private TextView mPasswordTextView;
  private Button mLoginBtn;

  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
    setContentView(R.layout.login);

    mPresenter = new AddressListPresenter(this, new UserModelImpl());

    mUsernameTextView = (TextView) findViewById(R.id.username);
    mPasswordTextView = (TextView) findViewById(R.id.password);

    mLoginBtn = (Button) findViewById(R.id.login_btn);
    mLoginBtn.setOnClickListener(new View.OnClickListener() {
          public void onClick(View v)  {
              String username = mUsernameTextView.getText().toString();
              String password = mPasswordTextView.getText().toString();
            // View将用户的点击事件直接路由给Presenter区处理
            mPresenter.login(username, password); 
        } 
    });
  }

  @Override
  public void showLoginSuccessMsg(User loginedUser) {
    // Presenter在处理完毕后, 会通知View更新UI来通知用户数据操作的结果
    Toast.makeToast(getApplicationContext(), "Login Success", Toast.LENGTH_SHORT).show();
  }

  @Override
  public void showLoginFailMsg(String errorMsg) {
    // Presenter在处理完毕后, 会通知View更新UI来通知用户数据操作的结果
    Toast.makeToast(getApplicationContext(), "Login Fail", Toast.LENGTH_SHORT).show();
  }

  @Override
  protected void onResume() {
      super.onResume();
      mPresenter.subscribe();
  }

  @Override
  protected void onPause() {
      super.onPause();
      mPresenter.unSubscribe();
  }

}

Presenter层则负责将在View和Model做中间人:

/**
 * Model负责数据的处理和业务逻辑
 */
public class UserPresenterImpl implements UserPresenter {

  private UserView mUserView;
  private UserModel mUserModel;

  public UserPresenterImpl(UserView view, UserModel model) {
    mUserView = view;
    mUserModel = model;
  }

  public void login(String username, String password) {
    // Presenter处理View路由过来的用户操作,
    // 将其转换成相对的命令,传递给Model来做数据操作
    mUserModel.login(username, password, new Callback(){
      public void onSuccess(User user) {
        // Model层对数据操作后,将结果返回给Presenter,
        // 再由Presenter来通知View去更新UI来通知用户数据操作的结果
        mView.showLoginSuccessMsg(user);
      }
      public void onFail(String errorMsg) {
        mView.showLoginFailMsg(user);
      }
    });
  }

}

而Model层大家则已经可以脑补出来了,只负责对于数据的操作而已了。例如请求服务器获取数据,获取查询本地数据库都可以。

从1个类变为3个类

在MVP的实践中,很明显的结构变化就是很多页面从1个类变成了3个甚至更多的类。

例如,原来只有一个 LoginActivity ,而现在会变成至少3个类:

  1. LoginActivity(View)
  2. LoginPresenterImpl (Presenter)
  3. LoginModelImpl (Model)

而你以为这些就够了,就太天真的,在MVP里,为了解藕三者之间的关系,还需要通过接口来通信,P层是通过接口来和M层通信的,P层和V层之间也是通过接口来互相通信的(但V层对P层的通信被视为被动通信,而非主动通信)

接口列表:

  1. LoginView (interface for View)
  2. LoginPresenter (interface for Presenter)
  3. LoginMode (interface for Model)

这里插一句题外话,在Google官方的MVP实例里的,有一个契约类的概念,这个契约类的概念引入我觉得真的很赞,其实它只是将View和Presenter的接口写到了一个类里面,但这样写则会使得读代码的人一目了然就可以了解这个页面需要展示些什么,有什么操作。

如果你以为MVP各一个接口这样就应该够了,我只能说你还是太年轻太天真。要知道很多消息在MVP三者之间传递,不仅仅是同步消息,还有很多异步消息,例如用户点击了一个按钮,View将该事件传递给Presenter,Presenter异步的向Model请求数据,Model异步的返回数据给Presenter,Presenter再将Model处理的结果异步的传递给View,让其向用户作出回应。

可想而之,这样异步操作,自然少不了一些Callback的接口类,虽然可以用内部类来解决,但如果不用范型的话,这些Callback的接口类数目还是很多的。

这里也插一句提外话,我个人推荐使用rxJava来解决回调恶魔的问题,不过这仅仅是个人偏好而已。

从直来直去变成跳来跳去

上文说了,从1个类的代码,分离到了N个文件,三个层面以后,原本直来直去的代码结构,就会变成跳来跳去,例如之前1000行代码是写在一起的,现在把其中View的部分代码独立到了View的文件里,把其中Model的部分独立到Model的文件中,然后用Presenter放在它们中间,做一个中间人。

而且再加上很多消息的传递是异步的,因为在看代码的时候,或者在调试的时候,你必须从过去线性的思维变成跳跃式的,很多代码过去你开一个文件,顺着看下来就明白的,DEBUG模式下,一顺运行下来的,现在变成了你需要开N个文件,DEBUG模式下就看着从View的一个方法,跳到Presenter的一个方法,然后再跳到Model的一个方法,然后再原路跳回来,友情提示,刚开始用MVP的时候,很容易代码很清晰,但大脑却很混乱,甚至有晕车的感觉。

并且我认为这样的代码结构,甚至加大了调试的困难,过去直来直去,你很容易判断出数据是断在哪里,而现在你很难判断出数据断在哪一个层面,例如用户点击了刷新,需要从服务器拉回数据刷新到列表。但当页面没有正常展示数据的时候,你必须知道在哪个环节出错了,而我告诉你,因为分成了三个层,并且消息和数据在三个层之间传递,那么出错的可能性也变多了:

  1. 可能是View层没有把用户的事件传递给Presenter层。
  2. Presenter层可能接受到View层事件,但没有将操作传递给Model层。
  3. Model层可能接受到Presenter的请求,但没有将数据传回给Presenter层。
  4. Presenter层可能接受到Model的返回值了,但没有正确的将数据传回给View层。
  5. View层可能接受到了Presenter返回值,只是没有正确的将数据显示到页面而已。

在调试的时候,你会发现,跟踪一个问题变复杂了,消息在MVP之间传来传去的,你很难一下定位到问题出在哪个层面。

那么为什么要用MVP?

说了这么MVP带来的麻烦,例如多写了很多类,思维跳来跳去,消息传来传去,层层回调把人转晕,那回头去思考:我们为什么要用MVP,为什么要这样拆分代码,不是说这样代码更清晰,更容易理解了吗,为什么我看不懂我的代码了,为什么调试起来如何麻烦?

其实,这样我要反复说的,如果你只是学会怎么使用MVP,那么你只是换了一个架构而已,这就和你换了一个IDE写代码,却期望换了IDE就可以让代码突然变的更好一样。而你真正需要做的,依然是我之前说过的:

你需要换的是脑子,而不是架构。

如果你还在每次修改一行代码,就整体去测试你的系统,那么你把代码写在一个文件里,还是拆分到几个文件里,其实是没有区别的。你只是把代码拆开在放,而这样的拆注定只是形式的,*终我相信写着写着,你会在View里面写Model的逻辑,在Model里面写View的逻辑,并且和过去一样,Presenter越来越臃肿。

为什么要把架构里的各个层次分得清清楚楚,每个层面负责什么,不应该负责负责,如何组合起来都需要严格的定义起来,你要知道,每一种架构都不是编码规范,也不是组织代码的规范,它们都是一种思维方式。

之前说过,良好的架构都是在解决几个问题:低藕合,高复用,易测试,好维护。

如果你还在你的类和类之间new来new去,你引用我,我引用你,互相依赖,层层依赖,那么你把它们写在一个文件里,和把它们几个文件里有区别吗?

如果你的一个类还承担多个职责,明明这是个叫 Car 的类,却又在承担轮子,又在承担引擎的责任,那么你抽象和不抽像,封装不封装真的有区别吗?

如果你的一个方法还在做两件甚至三件事情,甚至把一整套事情都做完了,动辄超过几屏的函数,那么你真的觉得用不用架构真的有区别吗?

单元测试&MVP

为什么要把代码拆分成不同的文件,为什么要把架构拆分成不同的层面,其实思想都是在将一个复杂的整体拆分成一个个独立的模块,然后再用合理的接口将这些模块组装到一起,成为一个完整而稳定的系统。

很多文章都会提到“易测试”的概念,在编码里面,易测试*对不是易于测试人员去测试的意思,而只有一个意思,那就是易于单元测试,易于将整体拆分成独立的单元进行测试。

但是很多时候,我们都会认为写单元测试是一种浪费时间的事情,但其实这是非常错误的一种观点,单元测试反倒是在节省时间。

就像上文提到的,如果你还是修改了一处代码,然后就跑一遍系统,整体的测试一遍,那么不使用MVP反而比使用MVP调试要轻松。但你反过来想,你如果还是每次都是整体的测试,那么你把代码分开的意义又何在呢?将代码拆分成独立的层次,独立的模块,一来是为了更好的复用,二来就是为了能够独立的测试。

可以说使用MVP,如果只是按照Google的实例去拆分代码,这只做到了*步,而第二步就是去看Google实例中是如何写单元测试的,如何独立的对Model层去做测试,对View层去做测试,以及Presenter层如何测试。你就会发现之所以拆分,带来的*大好处就是测试友好了。你可以独立的去做测试,因为拆分了,所以互相藕合低了,互相藕合低了,所以各自更独立了,各个更独立了就使得单元测试成为了可能性,你可以独立的对MVP里的每一个层面,每一个模块,每一个公开函数进行独立的测试,当你确保了每一个独立的函数,每一个类,每一个包都能都独立的完成自己的逻辑,那么通过接口把它们组合在一起后,整体测试反而变成依然轻松了,你不需要关心代码跳来跳去,消息传来传去,只要每个模块,每个层次的逻辑是正确的,是经过单元测试的,那么整体系统就不会出现太大的问题。

所以说,*终我认为MVP的关键还是在于 单元测试 ,不管你是用MVC,还是MVP,如果你的代码是能够进行良好的单元测试,那么说明你的架构就不可能有太大问题,而使用什么架构只是表象,真正起区别代码高低境界的还是思考问题的方式。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速