[Android]使用MVP解决技术债务(翻译)
以下內(nèi)容為原創(chuàng),歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明
來自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5892671.html
使用MVP解決技術(shù)債務(wù)
原文:https://medium.com/picnic-engineering/tackling-technical-debt-with-mvp-67e805ed5103#.couu0d5i0
免責(zé)申明:這篇博客并不是講關(guān)于怎么使用MVP的方式(上帝知道關(guān)于這些已經(jīng)太多了)去寫Android代碼。而僅僅是我的個(gè)人經(jīng)驗(yàn),關(guān)于怎么轉(zhuǎn)換我們的表現(xiàn)層到MVP架構(gòu)來幫助我們解決一些累積的技術(shù)債務(wù),而且在這個(gè)過程中也會(huì)幫助我們的app從一個(gè)原型轉(zhuǎn)變成一個(gè)更具維護(hù)性的產(chǎn)品。
任何從事Android工作足夠久、項(xiàng)目足夠大的開發(fā)者最有可能達(dá)到一個(gè)點(diǎn),他們面對他們的代碼庫,覺得應(yīng)該有更好的實(shí)現(xiàn)方案。我們在Picnic也是一樣,在Android app開發(fā)開始后大約八個(gè)月,我們到達(dá)了的那一刻,就在我們向公眾發(fā)布第一個(gè)版本的時(shí)候。
這一刻正好是在我們app推出的時(shí)候這也并不意外。直到那時(shí),我們以一個(gè)非常快的速度在前進(jìn),不斷敲打我們的鍵盤,從零開始構(gòu)建一個(gè)完整的產(chǎn)品,嘗試新的東西,結(jié)合用戶反饋到我們的app中,在每天的基礎(chǔ)上增加和丟棄特性。
為了跟上公司的速度我們砍掉了這里那里的邊邊角角。這樣的工作對我們來說很好,這也是我們能夠在這么短的時(shí)間內(nèi)構(gòu)建這個(gè)app的原因之一。但是正如預(yù)期那樣,最后這些決定的影響開始以技術(shù)債務(wù)的形式顯示出來。幸運(yùn)的是這些技術(shù)債務(wù)是在數(shù)月之內(nèi)建立的,在app的性能和穩(wěn)定性上面并沒有任何真正的影響。反而我們是在其它領(lǐng)域開始注意到它:
- 新功能迭代時(shí)間的增加。
- 新入職的開發(fā)者遇到困難
- 它被證實(shí)難以實(shí)現(xiàn)自動(dòng)化測試
- 整體功能的復(fù)雜性在增加
我們已經(jīng)有了一個(gè)很好的想法和一個(gè)易于理解的架構(gòu),用于網(wǎng)絡(luò)層、錯(cuò)誤處理和app內(nèi)部模塊通信。但是像大多數(shù)Android開發(fā)者,我們會(huì)對把太多的邏輯放進(jìn)Activity和Fragment中會(huì)產(chǎn)生內(nèi)疚。
旁注:這是Android開發(fā)者的共同的問題,而作為開發(fā)者需要在黑暗中摸索,因?yàn)镚oogle對這個(gè)話題保持沉默。我們從它們那里得到的第一個(gè)(算是)官方回復(fù)是來自Android團(tuán)隊(duì)的一個(gè)開發(fā)者在 Google+ post,說明我們應(yīng)該把核心的Android API作為一個(gè)‘系統(tǒng)框架’,意味著他們會(huì)帶我們手把手地到達(dá)Android核心的組件(Activity, BroadcastReceiver, Service 和 ContentProvider)。之后我們做什么都是看我們自己了。而且就在最近,Google終于提供了一系列的例子用來解決關(guān)于怎么構(gòu)建一個(gè)Android app的共同問題,它著重于MVP。盡管只是beta,但是它可以在這里查看:Android Architecture Blueprints。
無論如何,這其實(shí)是一件好事,因?yàn)檫@意味著我們可以自由地去實(shí)驗(yàn)任何我們喜歡的方式,而不是被強(qiáng)制在一個(gè)平臺(tái)遵循一個(gè)特定的模式。
現(xiàn)在講回我們的故事… 除非你處在Android開發(fā)世界的遠(yuǎn)古時(shí)期,你應(yīng)該會(huì)注意到表現(xiàn)層架構(gòu)是現(xiàn)在的熱門。關(guān)于最好的方式是什么,每個(gè)人甚至連他媽媽似乎都有自己的觀點(diǎn)。工作中標(biāo)準(zhǔn)的Android方式(類似MVC),到MVP,到通過data-binding的MVVM,所有的方式都沿用了 Uncle Bob 的 clean architecture。每一種方式圍繞贊成或者反對的意見都有一些有趣的討論,但是有一件事我們要明確知道,那就是我們應(yīng)該避免喝Kool-Aid(譯者注:這里是比喻,表示非常愚昧地接受信奉某種觀點(diǎn)或者思想)和期望其中一種是銀色子彈(譯者注:這里是比喻為具有極端有效性的解決方法)然后永遠(yuǎn)解決所有問題。
當(dāng)在考慮怎么去重構(gòu)我們的表現(xiàn)層時(shí),我們已經(jīng)有近一年的代碼庫的積累,我們很清楚我們的缺陷在哪里,然后我們需要使用一個(gè)新的實(shí)現(xiàn)(以上主要表示一些能夠解決我們的技術(shù)債務(wù)的點(diǎn))來達(dá)到我們的目標(biāo)。我們在虛擬的項(xiàng)目中試玩了一些,體驗(yàn)了各種方法的不同之處,然后最終決定使用MVP。從它的核心來說,MVP本身僅僅是一個(gè)概念,而Android框架,根據(jù)設(shè)計(jì),并不強(qiáng)制任何模式,我們可以自由地選擇實(shí)際的實(shí)現(xiàn)細(xì)節(jié)。
在Android團(tuán)隊(duì)中,首先我們是不過度工程的信徒,讓代碼隨著時(shí)間的推移自然地發(fā)展,而不是過早地在試圖為自己不可預(yù)知的未來做準(zhǔn)備的抽象之上增加抽象。正因?yàn)檫@個(gè)原因,我們選擇另一風(fēng)味的MVP,使得可以最低限度地保持我們的抽象層次。在代碼級(jí)別,這意味著有一個(gè)單獨(dú)的接口來表示View。所有其它的組件都是具體的類。你可能會(huì)問自己,怎么會(huì)只有View使用接口?考慮到我們迫切的需要,這是真正受益于這樣的接口的唯一的組件,因?yàn)槲覀儗?shí)際上有不同的具體的Views來共享相同的接口。所以在我們的案例中,這里的一個(gè)接口將被允許我們?nèi)ブ赜肞resenters。一些MVP實(shí)現(xiàn)建議給所有組件(M,V和P)設(shè)置接口。盡管這樣會(huì)工作得很完美,但是我們在較早的階段并不提倡,因?yàn)樘砑又蟮某杀臼谴a可讀性和維護(hù)性,尤其是當(dāng)我們考慮到新入職對MVP陌生的初級(jí)開發(fā)者的時(shí)候,好處超過面向接口編程的方式。
相比其他,MVP實(shí)現(xiàn)是非常標(biāo)準(zhǔn)的。View(Activity,Fragment或者一個(gè)自定義View)負(fù)責(zé)創(chuàng)造和維護(hù)Presenter,而Presenter處理各種業(yè)務(wù)相關(guān)的邏輯(數(shù)據(jù)獲取,存儲(chǔ),格式化等等),然后根據(jù)需要通過更新UI回調(diào)到View。在我們的案例中,數(shù)據(jù)層已經(jīng)是相當(dāng)模塊化了,構(gòu)造用于表示數(shù)據(jù)模型的POJOs,以及一個(gè)預(yù)先存在的控制層用于處理網(wǎng)絡(luò)通信。
這是一個(gè)非常標(biāo)準(zhǔn)的MVP設(shè)置,也因?yàn)樗芎唵?#xff0c;我們可以在幾周的時(shí)間內(nèi)替換幾乎我們的所有的UI代碼。因?yàn)槲覀円呀?jīng)存在獨(dú)立的數(shù)據(jù)層來處理所有與后端的API交互,所以真正需要重構(gòu)的只是Views和Presenters的交互。
在重構(gòu)的過程中,我們也學(xué)習(xí)了一些可能會(huì)派得上用場的東西:
生命周期:因?yàn)镻resenter是View創(chuàng)建的,我們需要確保完全地理解View的生命周期,特別是因?yàn)樗鼘⒆钣锌赡苋ヌ幚頎顟B(tài)更新和異步數(shù)據(jù)。舉個(gè)例子,每一個(gè)Presenter應(yīng)該在View destroyed的情況下有一個(gè)取消異步任務(wù)的方式,或者應(yīng)該在用戶暫停或者恢復(fù)視圖事件時(shí)重置到原始狀態(tài)等等。最后但同樣重要的是,當(dāng)View已經(jīng)被銷毀,試圖從Presenter去更新View元素,始終需要注意可怕的NPEs。
保持Views盡可能地愚蠢:我們的Views應(yīng)該不再包含任何業(yè)務(wù)相關(guān)的邏輯。它應(yīng)該只包含Android框架inflate和設(shè)置View的這些最低限度的東西。任何用戶交互應(yīng)該派發(fā)到Presenter。根據(jù)經(jīng)驗(yàn),如果你的views有任何其它方法去更新UI元素或者響應(yīng)用戶觸發(fā)的事件,那么你可能應(yīng)該去檢查它們的實(shí)現(xiàn)。
保持Presenter盡可能地純粹:這一點(diǎn),我們的意思時(shí)你應(yīng)該盡可能地避免有Android相關(guān)的代碼在你的presenters中。為這些組件編寫純粹的單元測試,而不需要使用其它如Robolectric等測試框架,這明顯地得到了簡化。這明顯說起來比做起來容易得多,因?yàn)槟憬K歸會(huì)在某些地方遇到這種情況,舉個(gè)例子,你將需要有一個(gè)Context的引用用來比如數(shù)據(jù)加載、訪問strings文件等等。
結(jié)論
那么,說了那么多,最終的結(jié)論是什么呢?總的來說,我很高興使用了MVP。它一定程度上幫我們解決了我們快速開發(fā)所累積的技術(shù)債務(wù),然后,我們準(zhǔn)備了更多來針對第二階段的開發(fā)。
一些值得一提的事情:
測試數(shù):在重構(gòu)之前,測試的數(shù)量用兩只手都可以數(shù)得過來。這是一個(gè)巨大的任務(wù)來針對包含了所有邏輯如執(zhí)行數(shù)據(jù)解析、格式化、網(wǎng)絡(luò)請求、錯(cuò)誤處理和管理自己的生命周期的Activity編寫測試。僅思考如果在這些條件下編寫測試就足以讓我們?nèi)ふ移渌姆绞搅恕R坏┺D(zhuǎn)換我們的第一份代碼到MVP,對此編寫測試就變得碎片化了。通過一個(gè)清晰的合同明確什么View能夠處理,我們可以把自己的代碼與Android UI框架隔離開,然后僅僅測試實(shí)際調(diào)用的是否是正確的方法,并給出每個(gè)測試場景。現(xiàn)在實(shí)際的業(yè)務(wù)相關(guān)邏輯被放置在Presenters中,因?yàn)樗鼈兘^大多數(shù)都不需要有Android OS相關(guān)的認(rèn)知(或者小部分相關(guān)的可以被mocked),我們也可以針對它們編寫非常有效率的單元測試,因此,在過去幾個(gè)月里,我們的測試用例從原來的10增加到900,而且還在增長中。
可預(yù)見性:這個(gè)是有一點(diǎn)軟度量,但是非常強(qiáng)大的一點(diǎn)。針對UI,我們選擇并堅(jiān)持一個(gè)通用的模式,我可以在代碼庫中獲得可預(yù)見的好處。這意味著,無論是哪種開發(fā)者眼里的UI元素(Activity,Dialog,Fragment等等),如果理解其中一個(gè)怎么工作,那也就能理解所有怎么工作。打開一個(gè)就算不是你寫的文件也不再會(huì)遇到讓你覺得驚喜的東西了。明確規(guī)定職責(zé),每一單個(gè)的UI組件都遵循相同的明確的模式。讓新入職的新開發(fā)者從第一天起就是高效的,這是非常寶貴的。
我們別忘記MVP并不只是用于表現(xiàn)層,但是作為前端開發(fā)人員,這里花費(fèi)了我們太多的時(shí)間。所以努力去尋找一個(gè)解決方案來給我們帶來更好的可預(yù)見性和在新的開發(fā)者加入我們的時(shí)候也能讓我們快速迭代是值得的。經(jīng)過全面的考慮,我們可以有把握地說MVP是可以幫助我們達(dá)到這個(gè)目標(biāo)的一個(gè)重要的里程碑。
P.S. 如果你仍然渴望看到一些源代碼,這里有一個(gè)我們MVP實(shí)現(xiàn)‘忘記密碼’用例的剝離下來的版本,展示MVP組件與用戶的交互,用戶點(diǎn)擊‘重置密碼’按鈕進(jìn)入他們的郵件地址(為保持代碼的簡潔,Android模版代碼已經(jīng)移除):
// 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;}}} }轉(zhuǎn)載于:https://www.cnblogs.com/tiantianbyconan/p/5892671.html
總結(jié)
以上是生活随笔為你收集整理的[Android]使用MVP解决技术债务(翻译)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 边工作边刷题:70天一遍leetcode
- 下一篇: Android 为应用添加默认加载页