標籤:android
Android MVPR 架構模式-Part1
- 原文連結 : MVPR: A FLEXIBLE, TESTABLE ARCHITECTURE FOR ANDROID (PT. 1)
- 原文作者 :Matthew Dupree
- 譯文出自 : 開發技術前線 www.devtf.cn
- 譯者 : chaossss
- 校對者: Mr.Simple
- 狀態 : 完成
全面的單元測試能提高內部系統的代碼品質,因為系統的每一個組件都需要被測試,因此每個單元都需要在系統外被構建,在測試環境中進行測試。對對象進行單元測試需要建立該對象,提供該對象需要的依賴,並與它進行互動,最終實驗室檢驗環境的輸出是否與預期一致。因此,為了讓一個類易於進行單元測試,類的依賴必須明確,而且能夠輕易地被替代和明確被調用和驗證的責任。在軟體工程領域中,這就意味著代碼必須松耦合、高內聚,也就是說:設計優秀的。
Steve Freeman 和 Nat Pryce,也就是測試驅動的物件導向開發。
最近我在嘗試讓 Google 的 IO App 變得可單元測試,我這樣做的其中一個原因是驗證 Freeman 和 Pryce 在引用中對單元測試的總結。即使現在我還是沒有把 IOSched 中的任何一個 Activity 重構,但我已經在重構代碼的過程中感受到他們所說的東西了。
我現在在重構的 Activity 是 SessionDetailActivity,如果你一直有在關注我的話就會知道我說的是哪個 Activity,但如果你只是第一次看我的博文,你可以看看下面這張圖瞭解下 SessionDetailActivity 的介面是咋樣的。
就像我在這個系列博文的序中所說,要讓 SessionDetailActivity 可被單元測試,有幾個麻煩必須解決。我在這個系列的上一篇博文中說過,對它動態構建的 View 進行單元測試是一個挑戰,但在那篇博文中,我提到我解決這個問題的辦法並不能治本,因為在 View 和 Presenter 之間存在著循環相依性。
循環相依性是 Android 應用架構存在大問題的徵兆:Activity 和 Presenter 都違反了單一職責原則,它們至少需要完成兩件事:為 View 綁定資料並對使用者的輸入作出相應。這也是為什麼 SessionDetailActivity 這個類會作為 Android 開發的 Model 被使用,使得類的代碼數超過1000行。
我堅信有更好的辦法架構我們的應用,在接下來的博文裡,我會提出一種擁有以下特性的新架構:
將通常由 Presenter 和 Activity 負責的多重職責打破
打破一般存在於 View 間或 Activity 和 Presenter 之間的循環相依性
允許我們用構造方法對所有為使用者展示資料以及相應使用者輸入的對象進行依賴注入
讓 UI 相關的商務邏輯易於進行單元測試,而且不可能在沒有必要的依賴時被構建以履行他們的職責,而且通過利用彙總和多態性修改對象的行為。
在這片博文中,我會嘗試總結開發新的 Android 應用架構的原因。
為什麼需要新的架構?Activity/Fragment/Presenter 會變得臃腫
Activity 和 Fragment(接下來我會統稱為 Activities,但我說的也適用於 Fragment)是違反單一職責原則的典型:
處理 View 的事件
更新資料 Model
調用其他 View
與系統組件互動
處理系統事件
基於系統事件更新 View
正如 Richa 所說,這些職責大部分從 Activities 中剝離,但即使我們這樣做了,Activities 還是違反了單一職責原則。即使是最簡單的 Activities 還是需要將 Model 的資料和 View 綁定,並對使用者輸入作出相應,例如:
public class SessionDetailActivity extends BaseActivity implements LoaderManager.LoaderCallbacks<Cursor>, ObservableScrollView.Callbacks { //... @Override protected void onCreate(Bundle savedInstanceState) { //Responsibility 1: Responding to user‘s action (in this case, a click) mAddScheduleButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { boolean starred = !mStarred; SessionsHelper helper = new SessionsHelper(SessionDetailActivity.this); showStarred(starred, true); helper.setSessionStarred(mSessionUri, starred, mTitleString); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mAddScheduleButton.announceForAccessibility(starred ? getString(R.string.session_details_a11y_session_added) : getString(R.string.session_details_a11y_session_removed)); } /* [ANALYTICS:EVENT] * TRIGGER: Add or remove a session from My Schedule. * CATEGORY: ‘Session‘ * ACTION: ‘Starred‘ or ‘Unstarred‘ * LABEL: Session title/subtitle. * [/ANALYTICS] */ AnalyticsManager.sendEvent( "Session", starred ? "Starred" : "Unstarred", mTitleString, 0L); } }); //... //Responsibility 2: Fetching and binding data to the view LoaderManager manager = getLoaderManager(); manager.initLoader(SessionsQuery._TOKEN, null, this); manager.initLoader(SpeakersQuery._TOKEN, null, this); manager.initLoader(TAG_METADATA_TOKEN, null, this); }
Google IOSched 應用中的 SessionDetailActivity 就是 Activity 即使只負責綁定資料到 View 中和響應使用者輸入也會變得臃腫的絕佳範例。即使我們把這部分代碼從 SessionDetailActivity 中剝離,還是有一個類有700多行代碼。不信我?你大可以去看看源碼,Presenter 也會因為 Activity 那樣的原因變得臃腫:Presenter 通常負責綁定資料以及響應使用者輸入,所以 Presenter 也需要像 Activity 那樣通過剝離額外的職責被瘦身。
Activities/Fragment/Presenter 通常在 View 間存在循環相依性
Activities 通常通過它們和 View 之間的循環相依性履行綁定資料到 View 和響應使用者輸入的職責(例如:作為 setContentView() 方法參數的 View)。下面是範例:
mAddScheduleButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { boolean starred = !mStarred; SessionsHelper helper = new SessionsHelper(SessionDetailActivity.this); showStarred(starred, true); helper.setSessionStarred(mSessionUri, starred, mTitleString); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { mAddScheduleButton.announceForAccessibility(starred ? getString(R.string.session_details_a11y_session_added) : getString(R.string.session_details_a11y_session_removed)); } /* [ANALYTICS:EVENT] * TRIGGER: Add or remove a session from My Schedule. * CATEGORY: ‘Session‘ * ACTION: ‘Starred‘ or ‘Unstarred‘ * LABEL: Session title/subtitle. * [/ANALYTICS] */ AnalyticsManager.sendEvent( "Session", starred ? "Starred" : "Unstarred", mTitleString, 0L); } });
SessionDetailActivity 持有對 mAddScheduleButton 的引用,而且 mAddScheduleButton 也持有對 SessionDetailActivity 的引用。我等會會說,這樣的循環相依性限制我們通常用於 Activities 中實現 UI 相關的商務邏輯的方法。
MVP 的 Presenter 有著和它們和 View 相同的循環相依性,在我能詳細解釋之前,我必須簡單地介紹傳統 Android 應用架構中 View 和 MVP 模式中 View 的區別。
MVP 模式中的 View 就像我定義的,只是 MVP 模式三巨頭其中之一,通常被定義為一個介面,而且一般會在 Activity,Fragment 或 Android 傳統架構中的 View 中實現。Android 傳統架構中的 View 就像它的名字,是一個 View 的子類。
使用 MVP 模式中的 View 和 Presenter 僅僅是在它們之間無形中重新建立了和 Android 傳統架構中 View 和 Activities 之間相同的循環相依性。
Presenter 需要 MVP 模式中的 View 使得它們能綁定資料到 MVP 模式中的 View,MVP 模式 中的 View 需要對 Presenter 的引用,使得它能傳遞點擊和其他 UI 相關的事件給 Presenter。Square 的博文就有存在著循環相依性的 MVP 模式的實現。
循環相依性在你想要為單元測試構建對象(或通常情況下)都會產生問題。然而,通常情況下,我們都不會把 MVP 模式的 View 和 Presenter 或 Activities 和 View 間的循環相依性當作問題,因為 Activities 和 Fragment 被系統初始化,而且因為我們並沒有用依賴注入去注入 Activity 和/或 Fragment 的依賴。相反的是,我們只是初始化了 Activity 在 onCreate() 方法中需要的任何依賴:
public class MyActivity extends Activity implements MVPView { View mButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_browse_sessions); //... final Presenter presenter = new Presenter(this); mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { presenter.onButtonClicked(); } }); }}
初始化在 onCreate() 方法中依賴的混合類,然而,限制我們使用組合和多態性去實現 UI 相關的商務邏輯。下面是一個你應該使用多態性實現 UI 相關的商務邏輯的例子:假設你開發了一個被使用者使用的應用,而且使用者在不同的等級時有不同的特權,那麼他們需要通過郵件驗證或回答其他使用者提的問題以提高等級。我們可以想象有許多按鈕用於完成依賴等級完成的不同功能,或 View 由使用者等級決定的初始狀態。多態性為我們提供整潔,可拓展的方式去實現這樣的邏輯:我們建立一個 Presenter 用於為使用者綁定不同的等級,不管使用者在什麼等級中,我們都能把 MVP 模式中的 View 傳到特定的 Presenter 子類中,並讓該子類處理相應的點擊事件或者基於使用者的等級呈現 UI。當然了,還有許多架構 Android 應用的方式,使得我們能夠在存在 Presenter 和 MVP 模式中的 View 間循環相依性的情況下利用多態性,但這些方法都不夠優雅,或者說他們為了完成單元測試作出了極大的貢獻。
這篇博文剩下的篇幅已經不足以讓我一一細述我記得的那些解決方案,但我能簡要的說說為什麼解決 MVP 模式中的 View 和 Presenter 間循環相依性的方法不理想。你可以想象我們可以只建立一個 MVP 模式的 View 或 Presenter,而沒有它們履行職責所需的任何依賴。換句話說,我們可以像下面這樣:
public class MyActivity extends Activity implements MVPView { View mButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_browse_sessions); //... final Presenter presenter = new Presenter(); //**** presenter.setView(this); //**** mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { presenter.onButtonClicked(); } }); }}
這樣我們就能通過多態性解決上面提到的問題,但這並沒有打破循環相依性。它能做的是允許我們在無效狀態建立一個對象。這並不是最簡潔的解決辦法,把這放在 Freeman 和 Pryce 話裡:
“建立或不建立,不需要嘗試”
我們想要確保總是建立有效對象,部分地建立對象然後通過設定它的屬性完成它是脆弱的……
結論
Presenter 和 Activities 違反了單一職責原則,他們常常負責綁定資料到 View 中和響應使用者的輸入,這些都會使 Activities 和 Presenter 變得臃腫。
Presenter 和 Activities 常常會因為他們和 View 間的循環相依性擁有多重職責,即使這樣的循環參考不會帶來什麼問題,但這會更難以對 View 和/或 Presenter 進行單元測試,而且會限制我們使用多態性實現 UI 相關的商務邏輯。
就像我之前說的,我認為會有一種架構應用的辦法不會有上面這些烈士,在下一篇博文中,我會提出可供選擇的架構。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Android MVPR 架構模式-Part1