使用MVP模式重構(gòu)代碼

之前寫了兩篇關(guān)于MVP模式的文章,主要講得都是一些概念,這里談?wù)勛约涸贏ndroid項(xiàng)目中使用MVP模式的真實(shí)感受,并以實(shí)例的形式一起嘗試來(lái)使用MVP模式去重構(gòu)我們現(xiàn)有的代碼。

有興趣的童鞋可以先去閱讀之前的文章,因?yàn)檫@里將不再重復(fù)概念的部分了,本文會(huì)假設(shè)你對(duì)MVP有一點(diǎn)了解了:

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

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

臃腫的Activity

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

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

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

因此如果完全按照這種定義的話,你應(yīng)該很難看到一個(gè)非常臃腫的Controller,因?yàn)镃ontroller在MVC模式中,本來(lái)就應(yīng)該是很輕的,而不是很重的部分,重的應(yīng)該是M層,甚至在前端交互復(fù)雜的時(shí)候,V層都應(yīng)該比C層要重。

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

那么我們?cè)賮?lái)看典型的Activity的代碼,處理的業(yè)務(wù)是常見的登錄頁(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();
    }
  }
  
}

其實(shí)這已經(jīng)是極度簡(jiǎn)化過(guò)的代碼了,真正的一個(gè)LoginActivity很容易就會(huì)超過(guò)幾千行,不信你去看自己項(xiàng)目里面的代碼就明白我在說(shuō)什么了。

而一對(duì)比概念,我相信大部分人一下子就會(huì)發(fā)現(xiàn)問(wèn)題,我們還是來(lái)看Activity作為一個(gè) Controller 到底都負(fù)責(zé)干什么了:

  1. 首先activity必須去操作View的控件,設(shè)置它們的回調(diào)函數(shù),有時(shí)也需要用代碼去控制它們?nèi)绾握故镜膶傩浴?/li>
  2. 然后activity一定需要去處理用戶的輸入,例如輸入的值,以及點(diǎn)擊事件等用戶行為。
  3. 而幾乎大部分異步網(wǎng)絡(luò)請(qǐng)求都從activity發(fā)起,以及服務(wù)器返回?cái)?shù)據(jù)的處理。
  4. activity一般還需要根據(jù)數(shù)據(jù)的操作結(jié)果,負(fù)責(zé)在頁(yè)面上將結(jié)果告之用戶,例如Toast或者其他View的操作。
  5. 除此之外,activity還需要管理其生命周期相關(guān)的所有事務(wù),例如在頁(yè)面退出的時(shí)候處理一下View控件和其他與生命期相關(guān)的邏輯。

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

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

當(dāng)然這是一個(gè)壞的例子,其實(shí)很多代碼是可以封裝到獨(dú)立的層去的,例如網(wǎng)絡(luò)請(qǐng)求,數(shù)據(jù)解析等。但就算你怎么封裝和重構(gòu),你最多能做的事情也就是把本來(lái)就不應(yīng)該放在Controller里的Model層分離出去,這是你原本就應(yīng)該做的事情。但你很難在activity將controller和view分離開來(lái),怎么寫activity作為Controller,都和View的關(guān)系太緊密,必須多多少少去控制如何展示數(shù)據(jù)這個(gè)View的責(zé)任。

Presenter是來(lái)給activity減負(fù)的嗎?

很多人會(huì)認(rèn)為MVP中引入Presenter的概念,是為了給日益臃腫的activity來(lái)減負(fù)的,而我不這樣認(rèn)為,我認(rèn)為Presenter和Controller的責(zé)任是差不多的,它們后期承擔(dān)的目的都其實(shí)很簡(jiǎn)單,就是用來(lái)隔離Model和View的,也就是常說(shuō)的展示層和業(yè)務(wù)層的解藕。

那么該如何解決activity的問(wèn)題呢?目前常見的MVP在Android里的實(shí)踐有兩種解決方案:

  1. 直接將Activity看作View,讓它只承擔(dān)View的責(zé)任。
  2. 將Activity看作一個(gè)MVP三者以外的一個(gè)Controller,只控制生命周期。

在Google推出的官方MVP實(shí)例里,使用的就是第2種思路,它讓每一個(gè)Activity都擁有一個(gè)Fragment來(lái)作為View,然后每個(gè)Activity也對(duì)應(yīng)一個(gè)Presenter,在Activity里只處理與生命周期有關(guān)的內(nèi)容,并跳出MVP之外,負(fù)責(zé)實(shí)例化Model,View,Presenter,并負(fù)責(zé)將三者合理的建立聯(lián)系,承擔(dān)的就是一個(gè)上帝視角。

在實(shí)踐中,也有很多觀點(diǎn)會(huì)簡(jiǎn)化掉Fragment,直接將Activity視為View,這個(gè)也是我比較贊同的,更簡(jiǎn)便一些,而且這樣觀念上也容易理解一些,你就把a(bǔ)ctivity看作View的一部分,永遠(yuǎn)只讓它處理展示的邏輯,不允許它去處理數(shù)據(jù),和擁有業(yè)務(wù)邏輯。但是這樣也有一個(gè)缺點(diǎn),就是V和P的依賴關(guān)系不太規(guī)范了,理論上你是不應(yīng)該在View里面去實(shí)例化Presenter和Model的,這其實(shí)是不合理的,正確的依賴關(guān)系,確實(shí)是應(yīng)該在一個(gè)獨(dú)立的更上層去實(shí)例化Model,View,Presenter的,這樣依賴才是較為合理的關(guān)系,這點(diǎn)來(lái)看Google的架構(gòu)模式確實(shí)更合理,但實(shí)操上也會(huì)麻煩一點(diǎn),必須讓每個(gè)activity擁有一個(gè)獨(dú)立的fragment,這個(gè)我是覺得可以自由取舍,你是要概念上的合理,還是現(xiàn)實(shí)中的方便,其實(shí)都可以。

因?yàn)橹攸c(diǎn)還是在于如何分離展示層和業(yè)務(wù)層,activity具體承擔(dān)什么責(zé)任都可以,但只能承擔(dān)一個(gè)責(zé)任。

例如之前的代碼可以被重構(gòu)成如下結(jié)構(gòu):

/**
 * View負(fù)責(zé)展示數(shù)據(jù)
 */
public interface UserView {
 
  void showLoginSuccessMsg(User loginedUser);
  void showLoginFailMsg(String errorMsg); 
  
}
/**
 * Presenter負(fù)責(zé)做View和Model的中間人
 */
public interface UserPresenter {
 
  void login(String username, String password);
  
}
/**
 * Model負(fù)責(zé)數(shù)據(jù)的處理和業(yè)務(wù)邏輯
 */
public interface UserModel {

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

這里將Activity被視為View, 僅負(fù)責(zé)數(shù)據(jù)的展示,并且將用戶的操作事件路由給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將用戶的點(diǎn)擊事件直接路由給Presenter區(qū)處理
            mPresenter.login(username, password); 
        } 
    });
  }
  
  @Override
  public void showLoginSuccessMsg(User loginedUser) {
    // Presenter在處理完畢后, 會(huì)通知View更新UI來(lái)通知用戶數(shù)據(jù)操作的結(jié)果
    Toast.makeToast(getApplicationContext(), "Login Success", Toast.LENGTH_SHORT).show();
  }
  
  @Override
  public void showLoginFailMsg(String errorMsg) {
    // Presenter在處理完畢后, 會(huì)通知View更新UI來(lái)通知用戶數(shù)據(jù)操作的結(jié)果
    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層則負(fù)責(zé)將在View和Model做中間人:

/**
 * Model負(fù)責(zé)數(shù)據(jù)的處理和業(yè)務(wù)邏輯
 */
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路由過(guò)來(lái)的用戶操作,
    // 將其轉(zhuǎn)換成相對(duì)的命令,傳遞給Model來(lái)做數(shù)據(jù)操作
    mUserModel.login(username, password, new Callback(){
      public void onSuccess(User user) {
        // Model層對(duì)數(shù)據(jù)操作后,將結(jié)果返回給Presenter,
        // 再由Presenter來(lái)通知View去更新UI來(lái)通知用戶數(shù)據(jù)操作的結(jié)果
        mView.showLoginSuccessMsg(user);
      }
      public void onFail(String errorMsg) {
        mView.showLoginFailMsg(user);
      }
    });
  }
  
}

而Model層大家則已經(jīng)可以腦補(bǔ)出來(lái)了,只負(fù)責(zé)對(duì)于數(shù)據(jù)的操作而已了。例如請(qǐng)求服務(wù)器獲取數(shù)據(jù),獲取查詢本地?cái)?shù)據(jù)庫(kù)都可以。

從1個(gè)類變?yōu)?個(gè)類

在MVP的實(shí)踐中,很明顯的結(jié)構(gòu)變化就是很多頁(yè)面從1個(gè)類變成了3個(gè)甚至更多的類。

例如,原來(lái)只有一個(gè) LoginActivity ,而現(xiàn)在會(huì)變成至少3個(gè)類:

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

而你以為這些就夠了,就太天真的,在MVP里,為了解藕三者之間的關(guān)系,還需要通過(guò)接口來(lái)通信,P層是通過(guò)接口來(lái)和M層通信的,P層和V層之間也是通過(guò)接口來(lái)互相通信的(但V層對(duì)P層的通信被視為被動(dòng)通信,而非主動(dòng)通信)

接口列表:

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

這里插一句題外話,在Google官方的MVP實(shí)例里的,有一個(gè)契約類的概念,這個(gè)契約類的概念引入我覺得真的很贊,其實(shí)它只是將View和Presenter的接口寫到了一個(gè)類里面,但這樣寫則會(huì)使得讀代碼的人一目了然就可以了解這個(gè)頁(yè)面需要展示些什么,有什么操作。

如果你以為MVP各一個(gè)接口這樣就應(yīng)該夠了,我只能說(shuō)你還是太年輕太天真。要知道很多消息在MVP三者之間傳遞,不僅僅是同步消息,還有很多異步消息,例如用戶點(diǎn)擊了一個(gè)按鈕,View將該事件傳遞給Presenter,Presenter異步的向Model請(qǐng)求數(shù)據(jù),Model異步的返回?cái)?shù)據(jù)給Presenter,Presenter再將Model處理的結(jié)果異步的傳遞給View,讓其向用戶作出回應(yīng)。

可想而之,這樣異步操作,自然少不了一些Callback的接口類,雖然可以用內(nèi)部類來(lái)解決,但如果不用范型的話,這些Callback的接口類數(shù)目還是很多的。

這里也插一句提外話,我個(gè)人推薦使用rxJava來(lái)解決回調(diào)惡魔的問(wèn)題,不過(guò)這僅僅是個(gè)人偏好而已。

從直來(lái)直去變成跳來(lái)跳去

上文說(shuō)了,從1個(gè)類的代碼,分離到了N個(gè)文件,三個(gè)層面以后,原本直來(lái)直去的代碼結(jié)構(gòu),就會(huì)變成跳來(lái)跳去,例如之前1000行代碼是寫在一起的,現(xiàn)在把其中View的部分代碼獨(dú)立到了View的文件里,把其中Model的部分獨(dú)立到Model的文件中,然后用Presenter放在它們中間,做一個(gè)中間人。

而且再加上很多消息的傳遞是異步的,因?yàn)樵诳创a的時(shí)候,或者在調(diào)試的時(shí)候,你必須從過(guò)去線性的思維變成跳躍式的,很多代碼過(guò)去你開一個(gè)文件,順著看下來(lái)就明白的,DEBUG模式下,一順運(yùn)行下來(lái)的,現(xiàn)在變成了你需要開N個(gè)文件,DEBUG模式下就看著從View的一個(gè)方法,跳到Presenter的一個(gè)方法,然后再跳到Model的一個(gè)方法,然后再原路跳回來(lái),友情提示,剛開始用MVP的時(shí)候,很容易代碼很清晰,但大腦卻很混亂,甚至有暈車的感覺。

并且我認(rèn)為這樣的代碼結(jié)構(gòu),甚至加大了調(diào)試的困難,過(guò)去直來(lái)直去,你很容易判斷出數(shù)據(jù)是斷在哪里,而現(xiàn)在你很難判斷出數(shù)據(jù)斷在哪一個(gè)層面,例如用戶點(diǎn)擊了刷新,需要從服務(wù)器拉回?cái)?shù)據(jù)刷新到列表。但當(dāng)頁(yè)面沒有正常展示數(shù)據(jù)的時(shí)候,你必須知道在哪個(gè)環(huán)節(jié)出錯(cuò)了,而我告訴你,因?yàn)榉殖闪巳齻€(gè)層,并且消息和數(shù)據(jù)在三個(gè)層之間傳遞,那么出錯(cuò)的可能性也變多了:

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

在調(diào)試的時(shí)候,你會(huì)發(fā)現(xiàn),跟蹤一個(gè)問(wèn)題變復(fù)雜了,消息在MVP之間傳來(lái)傳去的,你很難一下定位到問(wèn)題出在哪個(gè)層面。

那么為什么要用MVP?

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

其實(shí),這樣我要反復(fù)說(shuō)的,如果你只是學(xué)會(huì)怎么使用MVP,那么你只是換了一個(gè)架構(gòu)而已,這就和你換了一個(gè)IDE寫代碼,卻期望換了IDE就可以讓代碼突然變的更好一樣。而你真正需要做的,依然是我之前說(shuō)過(guò)的:

你需要換的是腦子,而不是架構(gòu)。

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

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

之前說(shuō)過(guò),良好的架構(gòu)都是在解決幾個(gè)問(wèn)題:低藕合,高復(fù)用,易測(cè)試,好維護(hù)。

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

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

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

單元測(cè)試&MVP

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

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

但是很多時(shí)候,我們都會(huì)認(rèn)為寫單元測(cè)試是一種浪費(fèi)時(shí)間的事情,但其實(shí)這是非常錯(cuò)誤的一種觀點(diǎn),單元測(cè)試反倒是在節(jié)省時(shí)間。

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

可以說(shuō)使用MVP,如果只是按照Google的實(shí)例去拆分代碼,這只做到了第一步,而第二步就是去看Google實(shí)例中是如何寫單元測(cè)試的,如何獨(dú)立的對(duì)Model層去做測(cè)試,對(duì)View層去做測(cè)試,以及Presenter層如何測(cè)試。你就會(huì)發(fā)現(xiàn)之所以拆分,帶來(lái)的最大好處就是測(cè)試友好了。你可以獨(dú)立的去做測(cè)試,因?yàn)椴鸱至耍曰ハ嗯汉系土耍ハ嗯汉系土耍愿髯愿?dú)立了,各個(gè)更獨(dú)立了就使得單元測(cè)試成為了可能性,你可以獨(dú)立的對(duì)MVP里的每一個(gè)層面,每一個(gè)模塊,每一個(gè)公開函數(shù)進(jìn)行獨(dú)立的測(cè)試,當(dāng)你確保了每一個(gè)獨(dú)立的函數(shù),每一個(gè)類,每一個(gè)包都能都獨(dú)立的完成自己的邏輯,那么通過(guò)接口把它們組合在一起后,整體測(cè)試反而變成依然輕松了,你不需要關(guān)心代碼跳來(lái)跳去,消息傳來(lái)傳去,只要每個(gè)模塊,每個(gè)層次的邏輯是正確的,是經(jīng)過(guò)單元測(cè)試的,那么整體系統(tǒng)就不會(huì)出現(xiàn)太大的問(wèn)題。

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

下一篇預(yù)告將繼續(xù)對(duì)MVP模式進(jìn)行展開,并將重點(diǎn)放在如何在Android上對(duì)MVP各個(gè)模塊進(jìn)行單元測(cè)試。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容