談到架構,想到的一定是MVC、MVP、MVVM這幾個詞了,然后對比一下它們的優缺點,接下來就是站隊的時間了。常常寫MVC,偶然見到了MVP,“嗯,真香~”。寫久了MVP,又聽說了MVVM,“嗯,真香~”。“真香”定律真是被用得淋漓盡致,此外還要喜新厭舊一番,使用MVVM的鄙視使用MVP的,使用MVP的又鄙視使用MVC的。架構,就在這樣的鄙視鏈下,“螺旋”發展。讓我們跟隨歷史的進程,看一看架構是如何進化進化再進化的吧!
MVC——時代的創造者
從我接觸Android起,MVC所扮演的唯一角色就是告訴你不要這么做,這導致長久以來總認為講MVC是沒有意義的事情,知道這個概念還不如不知道呢。但實際上MVC何其優秀,在java web開發中大名鼎鼎的SSM框架中,第二個S就叫Spring MVC,可以說是黃金法則了。為什么同樣的架構到了Android中就成了反派,整天被diss呢?這事我們要從MVC的名字說起。
MVC是由三個詞組成的,M(Model)主要用來處理數據,例如從網絡或者數據庫獲取業務相關的數據;V(View)是界面,用來響應用戶的行為以及展示數據給用戶;C(Controller)用來接收到View的請求傳遞給Model。整體流程就是V接收到了用戶的操作,通過C將請求傳遞給M,由M處理后通過回調或者觀察者等方式告知V,從而完成整個過程。在傳統的web開發中(傳統的,也就是過去的~),V對應的是jsp,它不僅包含了前端的一切,還可以寫一點java代碼。M和C則是抽象出來的概念,也就是隨我們怎么寫。在其他的平臺上事件流向可能稍有出入,但總體結構是一致的。
這個流程早被我們記得滾瓜爛熟,但現在讓我們持懷疑態度審視一下C的存在,V通過它最終向M獲取數據,M又可以直接通知到V,那我直接讓V向M請求數據,架構改名為MV架構算了(C聽起來如此雞肋,干掉它有什么不可以的呢?)。不過一個使用如此廣泛的架構怎么會犯這種錯誤,C一定有它獨特的意義,只不過還得深入思考一下。
有時候正著看理解不了的事情,反著看也許就有思路了。我們假設架構真的只有MV,頁面V1向M1獲取了數據,頁面V2也要向M1獲取數據。看起來很合理對吧?但是這合理建立在一個基本假設上,那就是V1、V2乃至Vn向M1獲取的數據都是一致的。這個假設只能應對一些簡單的場景,大部分情況是V需要的數據和M給出的并不一致,需要進行一定的邏輯操作,有的只是簡單的變換下數據格式,有的可能要和來自其他M的數據一并處理得到一個全新的數據,而M本身是不具備這種能力的。當然V本身具備這個能力,但話又說回來了,我們談架構的目的,不就是為了解決V繁重的問題么。這樣C就有了立身之處,它可以協助V對M返回的數據進行改造,使得V可以專注在用戶的交互和展示上。
若僅是通過加入Controller分離了View既要處理數據又要顯示數據還要響應用戶操作的行為,MVC也不至經久不衰,Controller的加入還帶來了許多有建設性的好處。首先Model成為一個相對獨立的單元,除了通知回調外它不需要關心外界的一切,不用管誰在使用它,怎么使用它,這一特點也使得Model可以復用。Controller本身不綁定View,因此Controller也可以復用,而沒有Controller時這段處理邏輯放在V層,顯然是無法復用的。單元測試成為可能,M和C都不依賴于V,都屬于純業務代碼,對它們進行單元測試,書寫難度小于UI測試,產生的效果又明顯高于UI測試。
有了以上優點,缺點也呼之而出了。M要主動通知V,使得M、V之間“藕斷絲連”,總有一些業務會在V層實現,從而削弱了C的作用。V什么都不想做,全部交給C處理,因此模塊越大,V和C之間聯系就越緊密,C會越來越像某個V的附屬品,它們“狼狽為奸”,根本不給其他V任何機會。當然還有放之四海而皆準的一點,使用MVC會增加代碼整體結構的復雜性。
整體而言,MVC是開創性的,它的優點明顯比缺點更重要,因此能夠備受關注。現在我們繼續探索,當把這個優秀的架構引入Android時會發生什么問題。Android開發和web開發最大的區別在于Controller,在java web開發中,Controller是一個抽象概念,整個類都是由我們定義和書寫的。但是在Android中,有一個可以稱為“上帝”的類,是Controller的天然選擇,這就是Activity(也可以是Fragment,這里僅用來說明問題)。這就是問題的關鍵了,jsp本身有能力處理View的一切,但是xml卻實在弱小的可憐,它的大部分功能都是依賴Activity完成的,這樣在MVC架構下Activity就要兼任Controller和View的職責,這使得本就分的不明不白的C和V又緊緊地貼合在一起,只剩下一個M是獨立了,而它和V也有微弱的關聯,MVC在這里的表現實在是差強人意。
當一個理論在新問題面前失效,我們要么改進它,要么完全推倒它,建立一個全新的理論。但這實在是太小的一個問題了,唯一的阻礙就是Activity的存在而已,而且這個類主要作用是管理View的,所以我們把Activity+xml文件一起當做View處理,再抽象出一個完全由我們控制的Controller,就可以完全再現MVC。但我們終究是不滿意的,Activity明明就可以當作Controller,現在我們讓了一步,新增了一個真正的Controller,卻沒有撈到任何好處,在java中只要jsp+controller+model就可以實現MVC了,而我們要Activity+xml+controller+model才能完成同樣的事,還要忍受同樣的缺點。要么各退一步,我增加一個controller,你搞定一個缺點,要么就決一死戰,退出Android舞臺吧,雖然暫時沒有更好的架構,但我就是想寧缺毋濫。
現實當然不會如此血雨腥風,Android沒有MVC問題只會更多,會更快失去控制,MVC如果不能開疆拓土也會更快淹沒在歷史長河中。所以Android和MVC就各退一步達成了和解,MVC在此要做的就是徹底斬斷M和V的聯系。Activity+xml和jsp的不同之處在于Activity是純粹的java語言,它可以像其他類一樣實現接口,使用設計模式。所以我們可以利用抽象統一管理Activity和xml,然后稍微用下策略,悄悄地把M通知V的事情轉移到C中來,實現一次“偷梁換柱”。具體做法就是定義抽象的View,讓Activity和xml來實現,再把View交到Controller手里,讓Controller做中間人,處理任何M和V不想做的事。如此V的能力被徹底削減,它不再有能力處理一丁點業務,而C被委以重任,其地位水漲船高,M這下終于獲得自由之身,V不能對它產生任何念頭了。將M、V、C三層都進行抽象的這個改動,就是廣為人知的MVP架構。
現在我們進入了自己熟悉的領域,MVP在Android應用架構中起著重要的作用,現在依然是無數人心中的不二之選(MVVM的確很有特色,但要完全取代MVP還差一些火候)。既然是熟悉的領域,我們就要對它抽絲剝繭,看看它在方方面面的表現是優秀還是平庸。
后繼有力,MVP力挽狂瀾
將M、V、C三層全部抽象而來的MVP,明顯有更強的控制欲。V被剝削成了“花架子”,當用戶執行了某操作,只要這個操作涉及到一點數據,它就無法獨自完成,需要向隔壁的P求援(P就是被委以重任的C),P處理完成后高冷地把結果一甩,V只能被動地接受;M和從前一樣,它始終是獨立的,不染塵埃;P真正地揚眉吐氣一番,死死地拿捏著M和V,一切盡在掌握中。
我們先來挖掘一下它的優勢,M和V隔離后,可以互不干涉,一個專注業務,一個專注頁面,分工更明確了。V的抽象可以讓單元測試覆蓋更廣的范圍,在MVC中單元測試對V是束手無策的,但現在我們可以在抽象層對V進行測試。現在讓我們把目光轉向P,它是在C的基礎上加強的,自然也繼承了C的缺點,模塊越大就和V聯系越緊密,而且因為它全權操縱V,這個缺點只會比C更惡劣。
說來說去,MVP就是MVC的一次“因地制宜”,它的理論基礎就這些,實際使用時因為場景多變和Android獨特的平臺特性還會遇到各種各樣的問題,接下來我們就通過代碼看下可能發生的情況和應對之法。
1. MVP的基本結構
MVP里強調的是M、V、P三層都是抽象的概念,因此如果嚴格按照抽象定義,一個完整的MVP應該至少包含三個接口與三個實現類,而按照谷歌的推薦,這三個接口會通過一個Contract的概念放在一起,以更直觀地了解到MVP的全部面目(不管是誰的推薦,使代碼變得直觀就是一個很好的 idea),所以一個最完善的MVP大致和下面的示例一致,以簡單使用用戶名和密碼進行登錄為例。
public interface LoginContract{
public interface LoginModel{
User login(String userName, String userPwd);
}
public interface LoginPresenter{
void login(String userName, String userPwd);
}
public interface LoginView{
void loginSuccess();
void loginFailed(String cause);
void invalidUserName();
void invalidUserPwd();
}
}
以上就是抽象層的全部代碼,使用Contract可以讓我們在一個文件中了解到MVP的全部行為,這不是架構本身的意圖,但它對我們是有利的。然后分別定義LoginModelImpl、LoginPresenterImpl,以及實現了LoginView的Activity或Fragment,一個和登錄行為相關的MVP就實現好了。
顯而易見,未使用任何架構時我們只要寫一個Activity就能搞定,使用MVP后類的數量飆升至六個,如果Contract也算在內的話就有七個類了,這樣的話我們使用MVP的代價是不是過于沉重?即使它有數不清的優點,但就這一點就足夠勸退一大批人。可是我們沒有注意的是,抽象是一種思想,而不是一種格式。Activity由系統控制,所以我們不得不借助抽象來實現V的行為,但是M和P完全是我們自己造就的,抽象與否我們有絕對的掌控權。拿P來說,定義接口有兩個好處:一是可以支持多實現,二是可以更輕易看到它的所有方法(類肯定比接口長很多),不定義接口也沒什么壞處,畢竟很少會需要多個實現類的。因此在極簡情況下,只定義V的抽象,四個類就可以實現MVP了,而在最壞情況下才是六個或七個類。所以最好的方式是,根據需求,合理選擇是否定義M和P的接口。
為了說明后續的問題,貼一下現有的Presenter實現類。
public class LoginPresenterImpl implements LoginContract.LoginPresenter{
private LoginContract.LoginView mLoginView;
private LoginContract.LoginModel mLoginModel;
public LoginPresenterImpl(){
mLoginModel = new LoginModelImpl();
}
public void attach(@NonNull LoginContract.LoginView loginView){
mLoginView = loginView;
}
public void detach(){
mLoginView = null;
}
@Override
public void login(String userName, String userPwd){
if(TextUtils.isEmpty(userName)){
if(mLoginView != null){
mLoginView.invalidUserName();
}
return;
}
if(TextUtils.isEmpty(userPwd)){
if(mLoginView != null){
mLoginView.invalidUserPwd();
}
return;
}
User user = mLoginModel.login(userName, userPwd);
if(user == null){
if(mLoginView != null){
mLoginView.loginFailed("some error");
}
return;
}
if(mLoginView != null){
mLoginView.loginSuccess();
}
}
}
2. P的體積暴漲和可重用性問題
以上示例雖然寫了好幾個類,但整體而言結構清晰,看起來很簡潔舒爽,但是實際項目中大多頁面的功能都是極其復雜的,從模塊化的角度去看也是多個模塊耦合在一起,如此一來P的整潔就很難保持了,很可能隨著業務的堆積體積暴漲,在一個P中實現了多個模塊的功能也使得P的可重用性大大降低。讓我們憑空捏造一個復雜場景,在某個頁面除了本身的功能之外,我們希望根據用戶會員身份決定是否推薦一些促銷信息,會以彈窗的方式展示給用戶,同時在Toolbar上想要輪播當前的熱搜詞,誘導用戶進入搜索以購買商品,然后還希望在頁面上加一個類似廣告的小浮窗,對不同的用戶給予不同的折扣活動推薦。P的結構可能會像這樣:
public interface GoodsPresenter{
// 頁面本身的核心內容
void getGoodsByPage(int page);
void getBanner();
// ...
// 用戶會員相關
void getUserVip(String userId);
void getVipSales(String vipId);
// ...
// 搜索的誘導
void getHotSearchKeywords();
// ...
// 給予不同用戶不同的折扣活動推薦
void getGoodsSaleForUser(String userId);
// ...
// 其他更多模塊更多的功能
}
隨著業務變繁重,P的暴漲是必然的,但也是可以接受的,因此P的接口變長并沒有問題,問題在于P的實現類。可以肯定的是,在很多頁面都會有會員相關的卡點,搜索的熱搜詞也不一定只在當前頁面展示,APP里也可能到處在推廣某個活動…,這時候P的重用性問題就暴露出來了,一模一樣的邏輯要在無數個Presenter里寫無數遍,我們會很快掉入可怕的重復“地獄”。
解決問題的辦法似乎只有一個,那就是按照模塊拆分成多個P,當前的GoodsPresenter只處理當前頁面的內容,VipPresenter處理會員的內容,SearchPresenter處理搜索的內容…,同樣的M和V也做此拆分,MVP框架就轉成 MM..VV..PP.. 框架,對每個MVP單元來說都是獨立的、可模塊化的。
但是MVP單元化了,并不代表復雜性真正降低了,在原有的結構下每個Activity只需要實現一個V,持有一個P就可以完成工作,只是這個P格外大,而現在每個Activity要實現多個V,持有多個P,還要了解在什么場景下使用什么P來工作,這樣一來只是把問題從P轉移到了Activity而已。
P的拆分勢在必行,不然無法重用,Activity也應該僅實現一個V,持有一個P才能保持單純,這看起來是矛盾的,難道真的魚與熊掌不可兼得了嗎?別忘了我們MVP的全部概念都是抽象的,對應于Java就是一個個接口,而接口是可以多繼承的,利用這一點就可以讓矛盾化為無形。我們可以再建一個P,它同時繼承GoodsPresenter、VipPresenter、SearchPresenter…,對V也進行同樣的處理,然后在實現層組合多個P和V一起實現功能。改進后的P如下所示:
public interface GoodsPresenter{
// 頁面本身的核心內容
void getGoodsByPage(int page);
void getBanner();
// ...
}
public interface VipPresenter{
// 用戶會員相關
void getUserVip(String userId);
void getVipSales(String vipId);
// ...
}
public interface SearchPresenter{
// 搜索的誘導
void getHotSearchKeywords();
// ...
}
public interface GoodsProxyPresenter extends GoodsPresenter, VipPresenter, SearchPresenter{
}
public class GoodsProxyPresenterImpl implements GoodsProxyPresenter{
private GoodsPresenter mGoodsPresenter;
private VipPresenter mVipPresenter;
private SearchPresenter mSearchPresenter;
public GoodsProxyPresenterImpl(){
mGoodsPresenter = new GoodsPresenterImpl();
mVipPresenter = new VipPresenterImpl();
mSearchPresenter = new SearchPresenterImpl();
}
@Override
public void attach(GoodsProxyView view){
mGoodsPresenter.attach(view);
mVipPresenter.attach(view);
mSearchPresenter.attach(view);
}
@Override
public void getGoodsByPage(int page){
mGoodsPresenter.getGoodsByPage(page);
}
@Override
public void getUserVip(String userId){
mVipPresenter.getUserVip(userId);
}
// ...
}
我們解決了以上的問題,又增加了一個類,當然這也不算增加類,因為本身這些Presenter是必然存在的,把它們組合起來使用也是必然的。但我們還是引入了一個問題,或者說是觸犯了一條規則:最少知道原則,簡單說就是只知道需要知道的事情。按照這樣的設計,模塊與模塊之間的P、V是可以互相調用的,它們不得不暴露在模塊之外,把自己的全部細節展示出來。在當前的模塊下我們只需要知道用戶的會員狀態,也就是VipPresenter的一個功能,但卻一股腦的擁有了VipPresenter的全部。而且V的繼承也會使Activity里增加很多冗余的實現,例如VipView中有一個upgradeVip的功能,我們的Activity不需要這個能力,但不得不增加一個空實現。
所以在這種復雜的場景下,MVP還要進一步改造,才能更合理、更有效。接下來我們繼續研究如何利用最少知道原則來優化MVP的能力。
3. 最少知道原則
現在我們既希望能夠跨模塊調用,又不想暴露全部細節,即滿足最少知道原則,就只能另辟蹊徑找到更好的辦法。我們先從V的組合說起,一個V繼承了多個V會變得極其龐大,而且大部分都是冗余的,那我們不讓它冗余不就好了嗎?從這一點出發可以有兩種方案,一是不繼承,把需要的方法重新寫一遍,二是只繼承需要的部分,這就需要被繼承的V本身也是多繼承的。方案一沒什么可說的,它和其他任何模塊都不關聯,這是優點也是缺點,好在夠獨立,壞在不能復用,而且重復定義一樣的接口不利于后期管理,當然這都不是什么大事。方案二會好一些,它的做法是將V分成可共享與不可共享兩部分,通過繼承來連接,例如VipView中有兩個方法會被其他模塊引用,其他方法不會被引用,就可以定義成這樣:
public interface SharedVipView{
void getUserVip(String vipId);
void getVipSales(VipSales sales);
}
public interface VipView extends SharedVipView{
void upgradeVip(boolean isSuccessful);
}
這樣做的好處是只需要暴露SharedVipView即可,可重用性的問題也解決了,但是書寫起來會更復雜一些,對代碼審查要求也更嚴格,因為太容易寫成方案一那種形式了。
回過頭來再看看P,我們也可以按照V的方式處理,但針對接口和實現類有些不同,接口只要繼承即可,但是實現類因為不支持多繼承,只能通過依賴其他模塊的P來實現功能,如此一來就要求每個模塊的P也分成兩層,整體結構如下所示:
看起來通過這樣的方式解決了問題,但是每個MVP單元其實變成了MVVPP,復雜度大大增加了,這給實際工作造成很大麻煩,因此我們還需要進一步尋找更合適的方案。
4. MVVPP到MVP-VP
我們已經解決了好幾個棘手的問題,但現在的這個問題比之前的都要棘手很多倍,一方面我們不希望增加每個MVP單元的復雜度,另一方面也希望能夠保持最少知道原則,同時減少重復性。嘗試多種方案而不可得,給我們帶來了沉痛的打擊,但是也不全是壞消息,因為當極致的黑暗到來時,黎明也就不遠了。
我們在P和V之間盤旋太久了,以致于都忘記了MVP其實是三部分組成的,M被遺忘是我們的過失,但也給我們提供了一個完全不同的思路。M不起眼的原因就是獨立性,那么我們在多模塊使用時,是不是也可以提供一個獨立的V和P,既不增加MVP單元的復雜度,又能提高復用性呢?
為了管理上的便利,我們還是通過繼承來定義P和V的接口層,當然也可以完全另寫一個接口,這不會造成多大的影響。我們定義的接口如下:
public interface SharedVipView{
void getUserVip(String vipId);
void getVipSales(VipSales sales);
}
public interface VipView extends SharedVipView{
void upgradeVip(boolean isSuccessful);
}
public interface SharedVipPresenter{
void getUserVip(String userId);
void getVipSales(String vipId);
}
public interface VipPresenter extends SharedVipPresenter{
void upgradeVip();
}
主要的差別在于P的實現,我們要讓兩個P之間毫無關聯,即使功能是完全一致的,也要各自有自己的實現。它們可以定義如下:
public class SharedVipPresenterImpl implements SharedVipPresenter{
@Override
public void getUserVip(String userId){
VipInfo vipInfo = mVipModel.getUserVip(userId);
if(vipInfo != null){
mSharedVipView.getUserVip(vipInfo.getVipId());
}
}
// ...
}
public class VipPresenterImpl implements VipPresenter{
@Override
public void getUserVip(String userId){
VipInfo vipInfo = mVipModel.getUserVip(userId);
if(vipInfo != null){
mVipView.getUserVip(vipInfo.getVipId());
}
}
// ...
@Override
public void upgradeVip(){
// ...
}
}
暫時忽略兩個實現中幾乎一致的代碼,我們也感受到了明顯的區別,通過共用一個M,跨模塊與非跨模塊形成了兩個獨立的MVP單元,也就是從MVVPP轉變成了MVP-VP結構,VP的耦合與復雜度問題都不見了。另外還有一個重要的改變是問題被壓縮在了兩個P的實現里,整體的結構卻是簡潔而優雅的,僅這一點便足夠我們長舒一口氣了。接下來只要搞定這兩段類似的代碼就大功告成了。
5. P職責的再劃分
讓我們把這兩段代碼單獨拿出來看,它們是不是已經沒有可優化的空間了呢?
@Override
public void getUserVip(String userId){
VipInfo vipInfo = mVipModel.getUserVip(userId);
if(vipInfo != null){
mSharedVipView.getUserVip(vipInfo.getVipId());
}
}
@Override
public void getUserVip(String userId){
VipInfo vipInfo = mVipModel.getUserVip(userId);
if(vipInfo != null){
mVipView.getUserVip(vipInfo.getVipId());
}
}
這兩段代碼除了依賴的V不同,其余部分完全一致,我們是不是會率先想到抽取一下,把V當成變量傳遞進來,就像這樣:
// 含義尚不明確,不知道如何取名字
public class VipPresenterXxx<V extends SharedVipView>{
private V mView;
public VipPresenterXxx(V view){
mView = view;
}
public void getUserVip(String userId){
VipInfo vipInfo = mVipModel.getUserVip(userId);
if(vipInfo != null){
mView.getUserVip(vipInfo.getVipId());
}
}
}
看似沒有問題,但實際上出了大問題,我們知道VipPresenter包含了更多的方法,而VipPresenterXxx只實現公共方法的話,它就必然對VipPresenter造成了侵入,你需要知道何時調用VipPresenterXxx來實現,何時需要自己實現。而如果VipPresenterXxx實現了VipPresenter的全部方法,它就必須時刻知道V是SharedVipView還是VipView的實例,把兩個不同的V糅合在一起明顯不是一個聰明的決策。
現實逼迫著我們進行抽取,又不能混合著兩個V使用,所以我們只能抽取,但是留下V。我們把例子舉的稍微復雜一點,這樣可以看得更清楚:
@Override
public void getUserVipId(String userId){
VipInfo vipInfo = mVipModel.getUserVip(userId);
// 要根據用戶ID和當前的VIP信息,經過復雜的計算得到一個當前需要的vipId
String vipId = mVipModel.calculateVipIdForUser(userId, vipInfo);
if(vipId != null){
mVipView.getUserVipId(vipId);
// mSharedVipView.getUserVipId(vipId);
}
}
最開始我們就說過C的作用不僅是傳遞數據到V,它還要中間負責對數據的加工,P是C的升級版,自然也有一樣的職責。如果我們只抽取部分,而留下V,可以發現它們被完美的拆分成了數據加工和頁面渲染兩部分:
// 數據加工部分
VipInfo vipInfo = mVipModel.getUserVip(userId);
// 要根據用戶ID和當前的VIP信息,經過復雜的計算得到一個當前需要的vipId
String vipId = mVipModel.calculateVipIdForUser(userId, vipInfo);
// 頁面渲染部分
mVipView.getUserVipId(vipId);
// mSharedVipView.getUserVipId(vipId);
我們從一個P里拆出了兩個完全不相干的概念,這說明P是多職責的,我們可以依據單一職責原則優化它!頁面渲染很好理解,它是V的部分,但是對數據加工算什么呢?嚴格來說它不是純粹的業務,它是根據需要改造過的業務,但無論如何改變不了它操作的是數據的事實。所以我們可以總結,P由兩部分組成,一是作為M和V的橋梁,二是處理業務,身兼多職是它得以膨脹和傲視他人的根源。而我們接下來要做的就是斬斷它的“左膀右臂”之一,只讓它做橋梁就好了。
我們要給M重新下定義,把原有的M,也就是純粹的業務,稱為NSM(narrow sense model
),把包含了業務加工的M,稱為BSM(broad sense model),NSM是BSM不需要加工時的特例。現在我們的M更像一個工廠,它不僅生產業務,還加工業務,或許叫M Factory更合適,但是Factory容易被誤解為工廠模式,所以我們換個名字,就叫M Repository吧(是的,我們也叫Repository,但是和你看到的谷歌的Repository還不太一樣,我們的Repository更強大)。
6. Repository——M的東山再起
我們給M升級,自然會減輕P的負擔,使得M、V、P三部分可以“三足鼎立”,再不是P一個說了算。升級后的M依然可以保持獨立性,因此也不會破壞任一個MVP單元。一個Repository由純粹的業務和對業務加工兩部分組成,因為NSM是BSM的特例,因此外界對這兩部分是零感知的。還是以Vip為例,我們的Repository定義如下:
public interface VipModel{
VipInfo getUserVip(String userId);
VipSales getVipSales(String vipId);
String calculateVipIdForUser(String userId, VipInfo vipInfo);
boolean upgradeVip();
}
// VipRepository 擁有 VipModel 的全部能力,還擁有加工的能力
public interface VipRepository extends VipModel{
String calculateVipIdForUserByUserId(String userId);
}
其中,NSM部分的能力依然由原始的M完成,而BSM的能力則由Repository來完成,所以Repository的實例會依賴M,實現如下:
public class VipRepositoryImpl implements VipRepository{
private VipModel mVipModel;
public VipRepositoryImpl(){
mVipModel = new VipModelImpl();
}
@Override
public VipInfo getUserVip(String userId){
return mVipModel.getUserVip(userId);
}
// ...
@Override
public String calculateVipIdForUserByUserId(String userId){
VipInfo vipInfo = mVipModel.getUserVip(userId);
// 要根據用戶ID和當前的VIP信息,經過復雜的計算得到一個當前需要的vipId
String vipId = mVipModel.calculateVipIdForUser(userId, vipInfo);
return vipId;
}
}
VipRepository完全有能力同時為VipPresenter和SharedVipPresenter提供服務,它們只需要分別調用Repository的方法,分發給各自的View即可。聽起來是不是還是有些小瑕疵,這兩個P做的事情還是極其相似的?但是我們已經盡力了,如果真的十全十美,想來也是一種遺憾吧。
最后看下我們現在的模型吧,它一定比之前好了太多太多:
7. MVP的生命周期
生命周期是Android平臺的特性,MVP雖然是架構思想,也要適應這種特性,這里我們暫不展開敘述如何解決生命周期的問題,在下一篇文章分析Jetpack時會對此進行詳盡的闡述,現在只要知道問題的具體情況就好了。
MVP在生命周期中遇到問題的主要是P,我們都知道Activity有一段從onCreate到onDestory的生命周期,一般情況下,只要在onCreate時生成P,在onDestory時銷毀P,就可以保證P不會引發內存泄漏的問題。但是Activity會在很多情況下,例如屏幕旋轉時重走一次onCreate到onDestory的過程,此時我們的P也會跟著銷毀并重建。
P銷毀并重建,就意味著業務邏輯要重新執行一遍,如果是從網絡加載就需要再次請求一次網絡,不僅浪費用戶流量,還有可能因請求失敗導致頁面顯示錯誤的信息。當然我們有很多方式避免這一情況的發生,也可以簡單地對此視而不見,畢竟這種情況在所有操作中所占據的比例不會太高。后續我們會介紹每種處理方式,并比較它們的優缺點,但現在讓我們加快腳步,因為架構中還有另一道迷人的景色等待我們欣賞呢。
新時代的弄潮兒——MVVM
MVP已經足夠應對任何場景了,但是它終究只解決了MVC的一個缺點,P和V依然無法無天地炫耀他們的關系。而且當我們把業務加工也歸并到M后,P的唯一職責就是保持M和V的通訊,而這個通訊要是僅僅P通知V還好,實際卻是V要主動調用P,P再主動回調V,雙方都深受其苦。V要喊P干活貌似是必須的,但是如果V可以做一個“監工”,時刻盯著P干活,等結束后自己拿著結果走人,豈不是雙方都省事?不然P在那邊“埋頭苦干”,V在一邊“曬太陽”,如此壓榨勞動力也許某天P就造反了。
好在有個設計模式叫觀察者模式,簡直就是為“監工”量身定做,而且監工也不需要時刻盯著,這邊完成工作后自己就會發出信號,就像水燒開了自己會響一樣,V收到P發的信號就過來收作業,真是皆大歡喜呀。在Android中,MVVM架構做的就是這樣的事情。
MVVM,并不是四個單詞的組合,前面的MV和MVC、MVP中的概念都是一致的,最后一個VM是ViewModel的意思,和C、P屬于同一層級,VM可以理解為V+M,它對應的就是我們剛剛提取出的Repository的概念,用來對純粹業務進行加工的。那么P哪去了呢?奧秘就在于VM雖然和Repository同概念,但是它是可以被觀察的,V可以通過觀察VM自動更新自己,所以P就可以退出歷史舞臺了。V觀察VM,所以VM并不知曉V的存在,這是一個單向的依賴關系。MVVM的事件流向是這樣的:
可以看到,MVVM和MVP最大的區別就是PV是互相引用的,而VM和V是單向引用的,整體結構更為簡潔,沒有一點冗余,VM不再依賴V,它的復用就更簡單了。
V是如何觀察VM的,這里我們就不再展開了,簡單來說通過回調就可以實現。但是在Android上V的實現由Activity+xml組合完成,因此通過回調只能通知到Activity層,再由Activity來更新xml,而想要更進一步直接更新xml,就要借助databinding這一招大殺器了,它的出現使得MVVM在Android平臺上以星火燎原之勢迅速發展起來。
讓我們還是以簡單的登錄為例,感受一下MVVM給我們帶來的新驚喜。首先書寫VM,為了簡潔說明問題,全部操作都是模擬的:
public class LoginViewModel extends BaseObservable {
public final ObservableField<String> mLoginResultState = new ObservableField<>();
public final ObservableField<Boolean> mIsLoading = new ObservableField<>();
public void login(final String username, final String password) {
new Thread(new Runnable() {
@Override
public void run() {
mIsLoading.set(true);
if (!isUserNameValid(username)) {
mIsLoading.set(false);
mLoginResultState.set("username is not valid.");
return;
}
if (!isPasswordValid(password)) {
mIsLoading.set(false);
mLoginResultState.set("password is not valid.");
return;
}
// 模擬耗時操作
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
mLoginResultState.set("login succeed!");
mIsLoading.set(false);
}
}).start();
}
// ...
}
在VM中只要給ObservableField賦值,通過databinding工具就會自動通知訂閱者(Activity或xml)。我們希望xml可以自己感知是否正在加載中和加載結果,所以xml大致是這樣的:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewmodel"
type="com.common.mvvmsample.ui.login.LoginViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...>
<EditText
android:id="@+id/username"
... />
<EditText
android:id="@+id/password"
... />
<Button
android:id="@+id/login"
... />
<ProgressBar
android:id="@+id/loading"
android:visibility="@{viewmodel.mIsLoading ? View.VISIBLE : View.GONE}"
... />
<TextView
android:id="@+id/result"
android:text="@{viewmodel.mLoginResultState}"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
雖然和我們平時寫的不大一樣,但也只是加了一層而已,整體還是很好理解的,接下來是Activity部分:
public class LoginActivity extends AppCompatActivity {
private LoginViewModel loginViewModel;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityLoginBinding activityLoginBinding = DataBindingUtil.setContentView(this, R.layout.activity_login);
loginViewModel = new LoginViewModel();
activityLoginBinding.setViewmodel(loginViewModel);
// ...
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loginViewModel.login(usernameEditText.getText().toString(),
passwordEditText.getText().toString());
}
});
}
}
好了,示例到此結束,我們只是展示了databinding最基礎的用法,databinding的實現原理其實就是回調,在build下有自動生成的代碼,感興趣可以自行查看。我們演示的主要目的其實不是說明databinding的原理,而是展現它不好的一面。
結合上述示例,可以發現databinding一個顯著的問題,那就是它使得xml無法復用,因為每個view都和model綁定在一起了。還有一個問題比較隱晦,由于V和VM自動同時更新的原因(V和VM是雙向綁定的,VM可以更新V,V也可以更新VM,感興趣的話可以多用用databinding,自己去發現哈),這時候如果出現了bug,到底先從VM看還是先從V看就成了“玄學”的問題了。
誠然,MVVM并不是一定得結合databinding使用,但是若只是單純的將P替換成VM,相當于去掉了原有的PV互相引用的問題,卻又加入了觀察者回調,等于是換湯不換藥,所產生的提升是極其有限的。也許等某一天有了更好的工具可以取代databinding,MVVM就會遙遙領先,成為Android中的“架構之王”。
總結一
經過了對MVC、MVP、MVVM的分析,我們發現三層結構是架構的基石,將業務與界面分離,并提供一個中轉站似乎是目前最好的途徑。MVC開創了時代,MVP解決了M和V的微弱聯系,MVVM則是把P和V的聯系也減弱了,它們都是站在MVC的肩膀上發展起來的,并且在Android這個平臺上發光發熱。讓我們對這一段歷程做一個簡要的總結吧。
MVC的優缺點
- 優點
使用三層結構,分離了業務與UI,使得業務保持獨立性,可以進行單元測試,大大提高了可復用性,大大降低了耦合性;
- 缺點
M和V依然保持一定的聯系;
C和V聯系緊密,無法單獨復用;
MVP的優缺點
- 優點
徹底分離了M和V,且M、V、P三層均為抽象,將單元測試的范圍大大擴展;
通過對P的優化,可以極好地支持模塊化,滿足最少知道原則、單一職責原則;
- 缺點
將MV分離后,加重了P和V的聯系,造成了和則生分則死的局面;
P和V之間需要大量的接口,書寫困難,復雜性提高;
MVVM的優缺點
- 優點
在MVP的基礎上,減弱了VM和V之間的聯系,從而解開PV“生死相依”的局面;
VM不再需要V,復用性較P而言大大提高;
- 缺點
VM和V雙向綁定,數據從一處傳到了另一處,給debug增加了難度;
由于Activity+xml的模式,databinding工具使xml無法復用;
代碼不如MVP直觀,人往往對可見的信息更敏感,如此會增加出錯的概率;
MVVM還有進一步發展的空間,如果能夠解決它的缺點,它將可能是史上最強架構,在未來很長的時間里發光發熱,真是叫人期待呢。
總結二
架構是一種思想,一種對代碼的設計,永遠不應該是一種束縛,一種條條框框的東西。例如并不是一定要定義MVP的三層接口,也不一定非要使用MVP,比方說某個頁面只有一個功能,就是檢查一下當前APP是不是最新版本,而且這是一個次級功能,很可能長時間不會有改動,使用MVP和直接自上而下寫代碼能有多少區別呢?這個頁面有沒有問題一眼就可以看出來,這種情況下還硬是要套模式,就是自討苦吃了。當然同樣的問題在核心流程上還是有一定必要使用優秀設計的,比方說登錄的功能,它一旦出問題會產生不可估量的損失,老老實實地做分層架構并進行全方位的測試,才是更穩妥的方案。
總之在使用架構上,一定要靈活,結合每個頁面每個功能點量身定做,不要搞出一套“官僚主義”來,處處走形式。當類變得很大,就拆分,很小就不需要拆分,不需要復用就直接定義實現類,等需要復用了再抽象…,只有使用合適的方式做合適的事情,才是真正的優秀的架構。
我是飛機醬,如果您喜歡我的文章,可以關注我~
編程之路,道阻且長。唯,路漫漫其修遠兮,吾將上下而求索。