使用MVP模式重構代碼

之前寫了兩篇關于MVP模式的文章,主要講得都是一些概念,這里談談自己在Android項目中使用MVP模式的真實感受,并以實例的形式一起嘗試來使用MVP模式去重構我們現有的代碼。

有興趣的童鞋可以先去閱讀之前的文章,因為這里將不再重復概念的部分了,本文會假設你對MVP有一點了解了:

1. 在談MVP之前,你真的懂MVC嗎?

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

臃腫的Activity

大部分談Android架構的時候,都基本會提到Activity越來越臃腫的問題,這幾乎是一個普遍現象,而包括我本人在內的,都會首先將這個罪責推到MVC架構上,但如果你真的花時間去重構activity的時候,你會發(fā)現問題其實往往出在自己身上。

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

  1. 負責獲取和處理用戶的輸入。
  2. 負責將輸入傳給負責業(yè)務邏輯層去做數據上的操作(如增刪改查)。
  3. 負責將業(yè)務邏輯層對于數據操作的結果,傳給View層去做展示。

因此如果完全按照這種定義的話,你應該很難看到一個非常臃腫的Controller,因為Controller在MVC模式中,本來就應該是很輕的,而不是很重的部分,重的應該是M層,甚至在前端交互復雜的時候,V層都應該比C層要重。

我認為對于Controller的理解,就是一個站在M和V兩者之間的一個翻譯家,M來自地球,V來自火星。而如果站在中間的這個翻譯者,話比他兩的話還多,老是搶話,自言自語,這樣顯然是不合適的。

那么我們再來看典型的Activity的代碼,處理的業(yè)務是常見的登錄頁面:

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很容易就會超過幾千行,不信你去看自己項目里面的代碼就明白我在說什么了。

而一對比概念,我相信大部分人一下子就會發(fā)現問題,我們還是來看Activity作為一個 Controller 到底都負責干什么了:

  1. 首先activity必須去操作View的控件,設置它們的回調函數,有時也需要用代碼去控制它們如何展示的屬性。
  2. 然后activity一定需要去處理用戶的輸入,例如輸入的值,以及點擊事件等用戶行為。
  3. 而幾乎大部分異步網絡請求都從activity發(fā)起,以及服務器返回數據的處理。
  4. activity一般還需要根據數據的操作結果,負責在頁面上將結果告之用戶,例如Toast或者其他View的操作。
  5. 除此之外,activity還需要管理其生命周期相關的所有事務,例如在頁面退出的時候處理一下View控件和其他與生命期相關的邏輯。

你會發(fā)現Activity天生的責任太重,其中確實覆蓋了 Controller 的原本的責任,例如處理用戶輸入,將用戶操作轉換成傳遞給業(yè)務邏輯層的命令等職責。

但如果你仔細的分析,你會發(fā)現activity不僅僅需要承擔Controller的責任,還需要處理大量View的邏輯,例如控件的監(jiān)聽的屬性,如何展示數據的職責也往往落到了它的肩上。更何況你很容易在activity寫操作數據和網絡請求的代碼,也就是讓它又承擔了Model的責任,那么請問這樣的Activity能不臃腫嗎?

當然這是一個壞的例子,其實很多代碼是可以封裝到獨立的層去的,例如網絡請求,數據解析等。但就算你怎么封裝和重構,你最多能做的事情也就是把本來就不應該放在Controller里的Model層分離出去,這是你原本就應該做的事情。但你很難在activity將controller和view分離開來,怎么寫activity作為Controller,都和View的關系太緊密,必須多多少少去控制如何展示數據這個View的責任。

Presenter是來給activity減負的嗎?

很多人會認為MVP中引入Presenter的概念,是為了給日益臃腫的activity來減負的,而我不這樣認為,我認為Presenter和Controller的責任是差不多的,它們后期承擔的目的都其實很簡單,就是用來隔離Model和View的,也就是常說的展示層和業(yè)務層的解藕。

那么該如何解決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的一部分,永遠只讓它處理展示的邏輯,不允許它去處理數據,和擁有業(yè)務邏輯。但是這樣也有一個缺點,就是V和P的依賴關系不太規(guī)范了,理論上你是不應該在View里面去實例化Presenter和Model的,這其實是不合理的,正確的依賴關系,確實是應該在一個獨立的更上層去實例化Model,View,Presenter的,這樣依賴才是較為合理的關系,這點來看Google的架構模式確實更合理,但實操上也會麻煩一點,必須讓每個activity擁有一個獨立的fragment,這個我是覺得可以自由取舍,你是要概念上的合理,還是現實中的方便,其實都可以。

因為重點還是在于如何分離展示層和業(yè)務層,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負責數據的處理和業(yè)務邏輯
 */
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區(qū)處理
            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負責數據的處理和業(yè)務邏輯
 */
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個類變?yōu)?個類

在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的時候,很容易代碼很清晰,但大腦卻很混亂,甚至有暈車的感覺。

并且我認為這樣的代碼結構,甚至加大了調試的困難,過去直來直去,你很容易判斷出數據是斷在哪里,而現在你很難判斷出數據斷在哪一個層面,例如用戶點擊了刷新,需要從服務器拉回數據刷新到列表。但當頁面沒有正常展示數據的時候,你必須知道在哪個環(huán)節(jié)出錯了,而我告訴你,因為分成了三個層,并且消息和數據在三個層之間傳遞,那么出錯的可能性也變多了:

  1. 可能是View層沒有把用戶的事件傳遞給Presenter層。
  2. Presenter層可能接受到View層事件,但沒有將操作傳遞給Model層。
  3. Model層可能接受到Presenter的請求,但沒有將數據傳回給Presenter層。
  4. Presenter層可能接受到Model的返回值了,但沒有正確的將數據傳回給View層。
  5. View層可能接受到了Presenter返回值,只是沒有正確的將數據顯示到頁面而已。

在調試的時候,你會發(fā)現,跟蹤一個問題變復雜了,消息在MVP之間傳來傳去的,你很難一下定位到問題出在哪個層面。

那么為什么要用MVP?

說了這么MVP帶來的麻煩,例如多寫了很多類,思維跳來跳去,消息傳來傳去,層層回調把人轉暈,那回頭去思考:我們?yōu)槭裁匆肕VP,為什么要這樣拆分代碼,不是說這樣代碼更清晰,更容易理解了嗎,為什么我看不懂我的代碼了,為什么調試起來如何麻煩?

其實,這樣我要反復說的,如果你只是學會怎么使用MVP,那么你只是換了一個架構而已,這就和你換了一個IDE寫代碼,卻期望換了IDE就可以讓代碼突然變的更好一樣。而你真正需要做的,依然是我之前說過的:

你需要換的是腦子,而不是架構。

如果你還在每次修改一行代碼,就整體去測試你的系統(tǒng),那么你把代碼寫在一個文件里,還是拆分到幾個文件里,其實是沒有區(qū)別的。你只是把代碼拆開在放,而這樣的拆注定只是形式的,最終我相信寫著寫著,你會在View里面寫Model的邏輯,在Model里面寫View的邏輯,并且和過去一樣,Presenter越來越臃腫。

為什么要把架構里的各個層次分得清清楚楚,每個層面負責什么,不應該負責負責,如何組合起來都需要嚴格的定義起來,你要知道,每一種架構都不是編碼規(guī)范,也不是組織代碼的規(guī)范,它們都是一種思維方式。

之前說過,良好的架構都是在解決幾個問題:低藕合,高復用,易測試,好維護。

如果你還在你的類和類之間new來new去,你引用我,我引用你,互相依賴,層層依賴,那么你把它們寫在一個文件里,和把它們幾個文件里有區(qū)別嗎?

如果你的一個類還承擔多個職責,明明這是個叫 Car 的類,卻又在承擔輪子,又在承擔引擎的責任,那么你抽象和不抽像,封裝不封裝真的有區(qū)別嗎?

如果你的一個方法還在做兩件甚至三件事情,甚至把一整套事情都做完了,動輒超過幾屏的函數,那么你真的覺得用不用架構真的有區(qū)別嗎?

單元測試&MVP

為什么要把代碼拆分成不同的文件,為什么要把架構拆分成不同的層面,其實思想都是在將一個復雜的整體拆分成一個個獨立的模塊,然后再用合理的接口將這些模塊組裝到一起,成為一個完整而穩(wěn)定的系統(tǒng)。

很多文章都會提到“易測試”的概念,在編碼里面,易測試絕對不是易于測試人員去測試的意思,而只有一個意思,那就是易于單元測試,易于將整體拆分成獨立的單元進行測試。

但是很多時候,我們都會認為寫單元測試是一種浪費時間的事情,但其實這是非常錯誤的一種觀點,單元測試反倒是在節(jié)省時間。

就像上文提到的,如果你還是修改了一處代碼,然后就跑一遍系統(tǒng),整體的測試一遍,那么不使用MVP反而比使用MVP調試要輕松。但你反過來想,你如果還是每次都是整體的測試,那么你把代碼分開的意義又何在呢?將代碼拆分成獨立的層次,獨立的模塊,一來是為了更好的復用,二來就是為了能夠獨立的測試。

可以說使用MVP,如果只是按照Google的實例去拆分代碼,這只做到了第一步,而第二步就是去看Google實例中是如何寫單元測試的,如何獨立的對Model層去做測試,對View層去做測試,以及Presenter層如何測試。你就會發(fā)現之所以拆分,帶來的最大好處就是測試友好了。你可以獨立的去做測試,因為拆分了,所以互相藕合低了,互相藕合低了,所以各自更獨立了,各個更獨立了就使得單元測試成為了可能性,你可以獨立的對MVP里的每一個層面,每一個模塊,每一個公開函數進行獨立的測試,當你確保了每一個獨立的函數,每一個類,每一個包都能都獨立的完成自己的邏輯,那么通過接口把它們組合在一起后,整體測試反而變成依然輕松了,你不需要關心代碼跳來跳去,消息傳來傳去,只要每個模塊,每個層次的邏輯是正確的,是經過單元測試的,那么整體系統(tǒng)就不會出現太大的問題。

所以說,最終我認為MVP的關鍵還是在于 單元測試 ,不管你是用MVC,還是MVP,如果你的代碼是能夠進行良好的單元測試,那么說明你的架構就不可能有太大問題,而使用什么架構只是表象,真正起區(qū)別代碼高低境界的還是思考問題的方式。

下一篇預告將繼續(xù)對MVP模式進行展開,并將重點放在如何在Android上對MVP各個模塊進行單元測試。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容