[Android]使用MVP解决技术债务(翻译)
以下內容為原創,歡迎轉載,轉載請注明
來自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5892671.html
使用MVP解決技術債務
原文:https://medium.com/picnic-engineering/tackling-technical-debt-with-mvp-67e805ed5103#.couu0d5i0
免責申明:這篇博客并不是講關于怎么使用MVP的方式(上帝知道關于這些已經太多了)去寫Android代碼。而僅僅是我的個人經驗,關于怎么轉換我們的表現層到MVP架構來幫助我們解決一些累積的技術債務,而且在這個過程中也會幫助我們的app從一個原型轉變成一個更具維護性的產品。
任何從事Android工作足夠久、項目足夠大的開發者最有可能達到一個點,他們面對他們的代碼庫,覺得應該有更好的實現方案。我們在Picnic也是一樣,在Android app開發開始后大約八個月,我們到達了的那一刻,就在我們向公眾發布第一個版本的時候。
這一刻正好是在我們app推出的時候這也并不意外。直到那時,我們以一個非常快的速度在前進,不斷敲打我們的鍵盤,從零開始構建一個完整的產品,嘗試新的東西,結合用戶反饋到我們的app中,在每天的基礎上增加和丟棄特性。
為了跟上公司的速度我們砍掉了這里那里的邊邊角角。這樣的工作對我們來說很好,這也是我們能夠在這么短的時間內構建這個app的原因之一。但是正如預期那樣,最后這些決定的影響開始以技術債務的形式顯示出來。幸運的是這些技術債務是在數月之內建立的,在app的性能和穩定性上面并沒有任何真正的影響。反而我們是在其它領域開始注意到它:
- 新功能迭代時間的增加。
- 新入職的開發者遇到困難
- 它被證實難以實現自動化測試
- 整體功能的復雜性在增加
我們已經有了一個很好的想法和一個易于理解的架構,用于網絡層、錯誤處理和app內部模塊通信。但是像大多數Android開發者,我們會對把太多的邏輯放進Activity和Fragment中會產生內疚。
旁注:這是Android開發者的共同的問題,而作為開發者需要在黑暗中摸索,因為Google對這個話題保持沉默。我們從它們那里得到的第一個(算是)官方回復是來自Android團隊的一個開發者在 Google+ post,說明我們應該把核心的Android API作為一個‘系統框架’,意味著他們會帶我們手把手地到達Android核心的組件(Activity, BroadcastReceiver, Service 和 ContentProvider)。之后我們做什么都是看我們自己了。而且就在最近,Google終于提供了一系列的例子用來解決關于怎么構建一個Android app的共同問題,它著重于MVP。盡管只是beta,但是它可以在這里查看:Android Architecture Blueprints。
無論如何,這其實是一件好事,因為這意味著我們可以自由地去實驗任何我們喜歡的方式,而不是被強制在一個平臺遵循一個特定的模式。
現在講回我們的故事… 除非你處在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 buttonpublic void onResetPasswordClick() {String email = mEmailEditText.getText().toString();// Forward all logic to the PresentermPresenter.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 itif(isViewAttached()) {getView().showEmailNotValidError(); }} else {requestPasswordChangeAsync(email);}}private void requestPasswordChangeAsync(String email) {// Update the view's UI elementsif(isViewAttached()) {getView().hideKeyboard();getView().showLoading();// Call our API (results are posted back on an EventBus)api.forgotPassword(email);}}// Subscription to the event bus@Subscribepublic void onEvent(final Event event) {if (isViewAttached()) {// Update the view's UI elementsgetView().hideLoading();switch (event.getType()) {case FORGOT_PASSWORD_OK:getView().showPasswordRequestOk((String) event.getData());break;case FORGOT_PASSWORD_FAILED:getView().showPasswordRequestFail();break;}}} }轉載于:https://www.cnblogs.com/tiantianbyconan/p/5892671.html
總結
以上是生活随笔為你收集整理的[Android]使用MVP解决技术债务(翻译)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 边工作边刷题:70天一遍leetcode
- 下一篇: log4net保存到数据库系列二:独立配