[Android]使用MVP解決技術債務(翻譯)


如下內容爲原創,歡迎轉載,轉載請註明
來自每天博客:http://www.cnblogs.com/tiantianbyconan/p/5892671.html
html

使用MVP解決技術債務

原文:https://medium.com/picnic-engineering/tackling-technical-debt-with-mvp-67e805ed5103#.couu0d5i0前端

免責申明:這篇博客並非講關於怎麼使用MVP的方式(上帝知道關於這些已經太多了)去寫Android代碼。而僅僅是個人我的經驗,關於怎麼轉換咱們的表現層到MVP架構來幫助咱們解決一些累積的技術債務,並且在這個過程當中也會幫助咱們的app從一個原型轉變成一個更具維護性的產品。java

任何從事Android工做足夠久、項目足夠大的開發者最有可能達到一個點,他們面對他們的代碼庫,以爲應該有更好的實現方案。咱們在Picnic也是同樣,在Android app開發開始後大約八個月,咱們到達了的那一刻,就在咱們向公衆發佈第一個版本的時候。android

這一刻正好是在咱們app推出的時候這也並不意外。直到那時,咱們以一個很是快的速度在前進,不斷敲打咱們的鍵盤,從零開始構建一個完整的產品,嘗試新的東西,結合用戶反饋到咱們的app中,在天天的基礎上增長和丟棄特性。git

爲了跟上公司的速度咱們砍掉了這裏那裏的邊邊角角。這樣的工做對咱們來講很好,這也是咱們可以在這麼短的時間內構建這個app的緣由之一。可是正如預期那樣,最後這些決定的影響開始以技術債務的形式顯示出來。幸運的是這些技術債務是在數月以內創建的,在app的性能和穩定性上面並無任何真正的影響。反而咱們是在其它領域開始注意到它:github

  • 新功能迭代時間的增長。
  • 新入職的開發者遇到困難
  • 它被證明難以實現自動化測試
  • 總體功能的複雜性在增長

咱們已經有了一個很好的想法和一個易於理解的架構,用於網絡層、錯誤處理和app內部模塊通訊。可是像大多數Android開發者,咱們會對把太多的邏輯放進Activity和Fragment中會產生內疚。編程

旁註:這是Android開發者的共同的問題,而做爲開發者須要在黑暗中摸索,由於Google對這個話題保持沉默。咱們從它們那裏獲得的第一個(算是)官方回覆是來自Android團隊的一個開發者在 Google+ post,說明咱們應該把核心的Android API做爲一個‘系統框架’,意味着他們會帶咱們手把手地到達Android核心的組件(Activity, BroadcastReceiver, Service 和 ContentProvider)。以後咱們作什麼都是看咱們本身了。並且就在最近,Google終於提供了一系列的例子用來解決關於怎麼構建一個Android app的共同問題,它着重於MVP。儘管只是beta,可是它能夠在這裏查看:Android Architecture Blueprints後端

不管如何,這實際上是一件好事,由於這意味着咱們能夠自由地去實驗任何咱們喜歡的方式,而不是被強制在一個平臺遵循一個特定的模式。api

如今講回咱們的故事… 除非你處在Android開發世界的遠古時期,你應該會注意到表現層架構是如今的熱門。關於最好的方式是什麼,每一個人甚至連他媽媽彷佛都有本身的觀點。工做中標準的Android方式(相似MVC),到MVP,到經過data-binding的MVVM,全部的方式都沿用了 Uncle Bob 的 clean architecture。每一種方式圍繞同意或者反對的意見都有一些有趣的討論,可是有一件事咱們要明確知道,那就是咱們應該避免喝Kool-Aid(譯者注:這裏是比喻,表示很是愚昧地接受信奉某種觀點或者思想)和指望其中一種是銀色子彈(譯者注:這裏是比喻爲具備極端有效性的解決方法)而後永遠解決全部問題。網絡

當在考慮怎麼去重構咱們的表現層時,咱們已經有近一年的代碼庫的積累,咱們很清楚咱們的缺陷在哪裏,而後咱們須要使用一個新的實現(以上主要表示一些可以解決咱們的技術債務的點)來達到咱們的目標。咱們在虛擬的項目中試玩了一些,體驗了各類方法的不一樣之處,而後最終決定使用MVP。從它的核心來講,MVP自己僅僅是一個概念,而Android框架,根據設計,並不強制任何模式,咱們能夠自由地選擇實際的實現細節。

在Android團隊中,首先咱們是不過分工程的信徒,讓代碼隨着時間的推移天然地發展,而不是過早地在試圖爲本身不可預知的將來作準備的抽象之上增長抽象。正由於這個緣由,咱們選擇另外一風味的MVP,使得能夠最低限度地保持咱們的抽象層次。在代碼級別,這意味着有一個單獨的接口來表示View。全部其它的組件都是具體的類。你可能會問本身,怎麼會只有View使用接口?考慮到咱們迫切的須要,這是真正受益於這樣的接口的惟一的組件,由於咱們實際上有不一樣的具體的Views來共享相同的接口。因此在咱們的案例中,這裏的一個接口將被容許咱們去重用Presenters。一些MVP實現建議給全部組件(M,V和P)設置接口。儘管這樣會工做得很完美,可是咱們在較早的階段並不提倡,由於添加以後的成本是代碼可讀性和維護性,尤爲是當咱們考慮到新入職對MVP陌生的初級開發者的時候,好處超過面向接口編程的方式。

相比其餘,MVP實現是很是標準的。View(Activity,Fragment或者一個自定義View)負責創造和維護Presenter,而Presenter處理各類業務相關的邏輯(數據獲取,存儲,格式化等等),而後根據須要經過更新UI回調到View。在咱們的案例中,數據層已是至關模塊化了,構造用於表示數據模型的POJOs,以及一個預先存在的控制層用於處理網絡通訊。

這是一個很是標準的MVP設置,也由於它很簡單,咱們能夠在幾周的時間內替換幾乎咱們的全部的UI代碼。由於咱們已經存在獨立的數據層來處理全部與後端的API交互,因此真正須要重構的只是Views和Presenters的交互。

在重構的過程當中,咱們也學習了一些可能會派得上用場的東西:

  • 生命週期:由於Presenter是View建立的,咱們須要確保徹底地理解View的生命週期,特別是由於它將最有可能去處理狀態更新和異步數據。舉個例子,每個Presenter應該在View destroyed的狀況下有一個取消異步任務的方式,或者應該在用戶暫停或者恢復視圖事件時重置到原始狀態等等。最後但一樣重要的是,當View已經被銷燬,試圖從Presenter去更新View元素,始終須要注意可怕的NPEs。

  • 保持Views儘量地愚蠢:咱們的Views應該再也不包含任何業務相關的邏輯。它應該只包含Android框架inflate和設置View的這些最低限度的東西。任何用戶交互應該派發到Presenter。根據經驗,若是你的views有任何其它方法去更新UI元素或者響應用戶觸發的事件,那麼你可能應該去檢查它們的實現。

  • 保持Presenter儘量地純粹:這一點,咱們的意思時你應該儘量地避免有Android相關的代碼在你的presenters中。爲這些組件編寫純粹的單元測試,而不須要使用其它如Robolectric等測試框架,這明顯地獲得了簡化。這明顯提及來比作起來容易得多,由於你終歸會在某些地方遇到這種狀況,舉個例子,你將須要有一個Context的引用用來好比數據加載、訪問strings文件等等。

結論

那麼,說了那麼多,最終的結論是什麼呢?總的來講,我很高興使用了MVP。它必定程度上幫咱們解決了咱們快速開發所累積的技術債務,而後,咱們準備了更多來針對第二階段的開發。

一些值得一提的事情:

  • 測試數:在重構以前,測試的數量用兩隻手均可以數得過來。這是一個巨大的任務來針對包含了全部邏輯如執行數據解析、格式化、網絡請求、錯誤處理和管理本身的生命週期的Activity編寫測試。僅思考若是在這些條件下編寫測試就足以讓咱們去尋找其它的方式了。一旦轉換咱們的第一份代碼到MVP,對此編寫測試就變得碎片化了。經過一個清晰的合同明確什麼View可以處理,咱們能夠把本身的代碼與Android UI框架隔離開,而後僅僅測試實際調用的是不是正確的方法,並給出每一個測試場景。如今實際的業務相關邏輯被放置在Presenters中,由於它們絕大多數都不須要有Android OS相關的認知(或者小部分相關的能夠被mocked),咱們也能夠針對它們編寫很是有效率的單元測試,所以,在過去幾個月裏,咱們的測試用例從原來的10增長到900,並且還在增加中。

  • 可預見性:這個是有一點軟度量,可是很是強大的一點。針對UI,咱們選擇並堅持一個通用的模式,我能夠在代碼庫中得到可預見的好處。這意味着,不管是哪一種開發者眼裏的UI元素(Activity,Dialog,Fragment等等),若是理解其中一個怎麼工做,那也就能理解全部怎麼工做。打開一個就算不是你寫的文件也再也不會遇到讓你以爲驚喜的東西了。明確規定職責,每一單個的UI組件都遵循相同的明確的模式。讓新入職的新開發者從第一天起就是高效的,這是很是寶貴的。

咱們別忘記MVP並不僅是用於表現層,可是做爲前端開發人員,這裏花費了咱們太多的時間。因此努力去尋找一個解決方案來給咱們帶來更好的可預見性和在新的開發者加入咱們的時候也能讓咱們快速迭代是值得的。通過全面的考慮,咱們能夠有把握地說MVP是能夠幫助咱們達到這個目標的一個重要的里程碑。

P.S. 若是你仍然渴望看到一些源代碼,這裏有一個咱們MVP實現‘忘記密碼’用例的剝離下來的版本,展現MVP組件與用戶的交互,用戶點擊‘重置密碼’按鈕進入他們的郵件地址(爲保持代碼的簡潔,Android模版代碼已經移除):

// BasePresenter.java (Base class for all our Presenters)
public abstract class BasePresenter<V> {

  private WeakReference<V> mView;

  public void bindView(@NonNull V view) {
    mView = new WeakReference<>(view);
  }

  public void unbindView() {
    mView = null;
  }

  public V getView() {
    if (mView == null) {
      return null;
    } else {
      return mView.get();
    }
  }

  protected final boolean isViewAttached() {
    return mView != null && mView.get() != null;
  }
}

// IForgotPasswordView.java (view interface)
public interface IForgotPasswordView {
  void showLoading();
  void hideLoading();
  void setEmailText(String email);
  void showEmailNotValidError();
  void showPasswordRequestOk(String message);
  void showPasswordRequestFail();
}

// ForgotPasswordFragment.java (view implementation)
public class ForgotPasswordFragment implements IForgotPasswordView,
        View.OnClickListener {

  // Triggered by the user clicking a button
  public void onResetPasswordClick() {
    String email = mEmailEditText.getText().toString();
    
    // Forward all logic to the Presenter
    mPresenter.requestPasswordChange(email);
  }
  
}

// ForgotPasswordPresenter.java
public class ForgotPasswordPresenter extends BasePresenter<IForgotPasswordView> {
  
  public void requestPasswordChange(String email) {
    if (!Utils.isEmailValid(email)) {
      
      // Make sure the view is still alive before trying to access it
      if(isViewAttached()) {
        getView().showEmailNotValidError();    
      }
    } else {
      requestPasswordChangeAsync(email);
    }
  }
  
  private void requestPasswordChangeAsync(String email) {
    
    // Update the view's UI elements
    if(isViewAttached()) {
      getView().hideKeyboard();
      getView().showLoading();
      
      // Call our API (results are posted back on an EventBus)
      api.forgotPassword(email);
    }
    
  }

  // Subscription to the event bus
  @Subscribe
  public void onEvent(final Event event) {

    if (isViewAttached()) {
    
      // Update the view's UI elements
      getView().hideLoading();
          
      switch (event.getType()) {
        case FORGOT_PASSWORD_OK:
          getView().showPasswordRequestOk((String) event.getData());
          break;
        case FORGOT_PASSWORD_FAILED:
          getView().showPasswordRequestFail();
          break;
        }
      }
  }
}
相關文章
相關標籤/搜索