MVP模式在攜程酒店的應用和擴展
前言
酒店業務部門是攜程旅行的幾大業務之一,其業務邏輯復雜,業務需求變動快,經過多年的研發,已經是一個代碼規模龐大的工程,如何規范代碼,將代碼按照其功能進行分類,將代碼寫到合適的地方對項目的迭代起著重要的作用。
MVP模式是目前客戶端比較流行的框架模式,攜程在很早之前就開始探索使用該模式進行相關的業務功能開發,以提升代碼的規范性和可維護性,積累了一定的經驗。本文將探討一下該模式在實際工程中的優點和缺陷,并介紹攜程面對這些問題時的思考,解決方案以及在實踐經驗基礎上對該模式的擴展模式MVCPI。
一、從MVC說起
MVC已經是非常成熟的框架模式,甚至不少人認為它過時陳舊老氣,在實踐中,很多同事會抱怨,MVC會使得代碼非常臃腫,尤其是Controller很容易變成大雜燴,預期的可維護性變得很脆弱,由此導致一方面希望有新框架模式可以解決現在的問題,但同時對框架模式又有些懷疑,新的框架模式是否能真正解決現在的問題?會不會重蹈覆轍?會不會過度設計?會不會掉進一個更深的坑?總之,這些類似“一朝被蛇咬,十年怕井繩”的擔憂顯得不無道理。但不管如何,我們需要仔細耐心的做工作。
1.1、被誤解的MVC
在MVP模式逐漸流行之前,不管我們有意識或無意識地,我們使用的就是MVC模式。以Android為例,我們來看看MVC是什么樣子。
public class HotelActivity extends Activity {
private TextView mNameView;
private TextView mAddressView;
private TextView mStarView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mNameView = (TextView) findViewById(R.id.hotel_view);
mAddressView = (TextView) findViewById(R.id.address_view);
mStarView = (TextView) findViewById(R.id.star_view);
HotelModel hotel = HotelLoader.loadHotelById(1000);
mHotelNameView.setText(hotel.hotelName);
mHotelAddressView.setText(hotel.hotelAdress);
mHotelStarView.setText(hotel.hotelStar);
}
}
上面的代碼,概括了Android MVC的基本結構,從筆者的經驗來看,很多應用都存在這樣的代碼風格,也就是大部分人認為的MVC:
- Model:
Hotel,HotelLoader
- Controller:
HotelActivity
- View:
mHotelNameView
mHotelAddressViewmHotelStarView
可以試想一下如果這個界面展示的數據非常的多話,MainActivity必然會變得非常龐大,就像大部分人所抱怨的那樣。誠然,上面的demo是MVC模式,但是,它僅是從系統框架的角度來看,如果從應用框架來看,它不是。下面來看一下,從應用框架來看一下MVC正確的結構:
1.2、MVC的正確姿勢
應用中的MVC應該在系統的MVC框架上根據業務的自身的需要進行進一步封裝,也就是說,如果在我們宣稱我們是使用MVC框架模式的時候,代表我們的主要工作是封裝自己的MVC組件。它看起來應該是像下面的風格:
public class HotelActivity extends Activity {
private HotelView mHotelView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mHotelView = (HotelView) findViewById(R.id.hotel_view);
HotelModel hotel = HotelLoader.loadHotelById(1000);
mHotelView.setHotel(hotel);
}
}
跟之前的代碼相比,基本結構是相似的,如下:
- Model:
Hotel,HotelLoader
- Controller:
HotelActivity
- View:
mHotelView
僅僅View層發生了變化,這是因為,Model和Controller相對是大家容易理解的概念,在面臨任何一個業務需求的時候,自然就能產生的近乎本能的封裝(盡管Model的基本封裝大部分工程師都可完成,但不可否認Model的設計是至關重要而有難度的);而對View的看法,可能就是“能正確布局和展示就行”。但這正是關鍵所在:我們需要對界面進行全方位的封裝,包括View。具體來說,一個真正的MVC框架應該具備下面的特點:
- 數據都由Model進行封裝
- View綁定業務實體,view.setXXX
- Controller不管理與業務無關的View
1.3 MVC模式的問題所在
前面說到,很多人抱怨采用MVC模式使得Controller變得很臃腫,我相信,Controller變得臃腫是事實,但其歸結于采用MVC模式是不正確的,這個鍋不應該由MVC來背,因為,這個論點會導致我們走向錯誤的方向從而無法發現MVC真正的問題所在。為什么這么說呢,那是因為在本人了解到的很多情況下,大家并沒有正確理解MVC框架模式,如采用前文中第一種模式,自然會使得Controller臃腫,但是如果采用第二種模式,Controller的代碼和邏輯也會非常清晰,至少不至于如此多的抱怨。因此如果只是想解決Controller臃腫的話,MVC就夠了,毋庸質疑。那MVC的問題是什么呢?我想只有深刻的理解了這個問題,我們才有必要考慮是否需要引入新的框架模式,以及避免新的模式中可能出現的問題。
View強依賴于Model是MVC的主要問題。由此導致很多控件都是根據業務定制,從Android的角度來看,原本可以由一個通用的layout就能實現的控件,由于要綁定實體模型,現在必須要自定義控件,這導致出現大量不必要的重復代碼。因此有必要將View和Model進行解耦,而MVP的主要思想就是解耦View和Model。由此引入MVP就顯得很自然。
二、 Android MVP
2.1、參考實現
Android 官方提供的MVP參考實現,大致思想如下:
1、抽象出IView接口,規范控件訪問方法,而不限View具體來源
public interface IHotelView {
public TextView getNameView();
public TextView getAddressView();
public TextView getStarView();
}
2、抽象出IPresenter接口,定義IView 和 Model的綁定接口
public interface IHotelPresenter {
public void setView(IHotelView hotelView);
public void setData(HotelMotel hotel);
}
3、IPresenter的實現類,實施數據和IView的綁定,并負責相關的業務處理
public class HotelPresenter implements IHotelPresenter {
private IHotelView hotelView;
public void setView(IHotelView hotelView) {
this.hotelView = hotelView;
}
public void setData(HotelModel hotel) {
hotelView.getNameView().setText(hotel.hotelName);
hotelView.getAddressView().setText(hotel.hotelAddress);
hotelView.getStarView().setText(hotel.hotelStart);
}
}
4、Activity實現IView,角色轉變為View,弱化Controller的功能
public class HotelActivity extends Activity implements IHotelView {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
HotelModel hotel = HotelLoader.loadHotelById(1000);
IPresenter presenter = new Presenter();
presenter.setView(this);
presenter.setData(hotel);
}
@Override
public TextView getNameView() {
return (TextView)findViewById(R.id.hotel_name_view);
}
@Override
public TextView getAddressView() {
return (TextView)findViewById(R.id.hotel_address_view);
}
@Override
public TextView getStarView() {
return (TextView)findViewById(R.id.hotel_address_view);
}
}
上述代碼,主要的特點可以概括為:
- 面向接口
- View – Model 解耦
- Activity角色轉換
就目前了解到的情況來看,很多采用MVP模式的應用基本上和android參考實現方案差別不大,說明該模式的應用場景也是很廣泛的。
2.2 Android MVP存在的問題
盡管已經有了大量的應用,但不可否認該模式的還是存在一些問題,這些問題在攜程的使用過程中也得到了體現。比如,上下文丟失問題,生命周期問題,內存泄露問題以及大量的自定義接口,回調鏈變長等問題。可以歸納為:
- 業務復雜時,可能使得Activity變成更加復雜,比如要實現N個IView,然后寫更多個模版方法。
- 業務復雜時,各個角色之間通信會變得很冗長和復雜,回調鏈過長。
- Presenter處理業務,讓業務變得很分散,不能全局掌握業務,很難去回答某個業務究竟是在哪里處理的。
- 用Presenter替代Controller是一個危險的做法,可能出現內存泄漏,生命周期不同步,上下文丟失等問題。
以下面的這個需求來看幾個具體的示例:
詳情按鈕的展示需要服務端下發標記位控制,展示時點擊需要請求一個服務,服務返回時toast提示用戶
public class HotelPresenter {
private IHotelView mHotelView;
private Handler handler = new Handler(getMainLooper());
public void setData(HotelModel hotelModel) {
View button = mHotelView.getButtonView();
int visibility = hotelModel.showButton ? .VISIBLE :GONE;
button.setVisibility(visibility);
if (hotelModel.showButton) {
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendRequest();
}
});
}
private void sendRequest() {
new Thread() {
public void run() {
Thread.sleep(15*1000);
handler.post(new Runnable() {
public void run() {
Toast.makeText(???) //Where is Context?
}
});
}
}.start();
}
}
上述代碼表明,HotelPresenter可以處理大部分的業務,但是在最后需要使用上下文的時候,出現了困難,因為脫離了上下文,展示一個Toast都不能實現
為了避免這樣的尷尬,因此改進方案如下:
public class HotelPresenter {
private IHotelView mHotelView;
private Fragment mFragment;
private HotelPresenter(Fragment fragment) {
this.mFragment = fragment;
}
private Handler handler = new Handler(Looper.getMainLooper());
public void setData(HotelModel hotelModel) {
View button = mHotelView.getButtonView();
button.setVisibility(hotelModel.showButton ? VISIBLE :GONE);
if (hotelModel.showButton) {
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendRequest();
}
});
}
}
private void sendRequest() {
new Thread() {
public void run() {
Thread.sleep(15*1000);
handler.post(new Runnable() {
public void run() {
Context context = mFragment.getActivity();
int duration = LENGTH_SHORT;
//NullPointerException will occur
Toast.makeText(context,"成功”,duration).show();
}
});
}
}.start();
}
}
改進的方案中,考慮到需要使用上下文,因此新增了接口傳入Fragment作為上下文,在Presenter需要時可以使用,但是,由于Fragment生命周期會了變化,可能會導致空指針問題。
于是新的問題又需要解決。主要是兩個思路,一個是為Presenter增加生命周期方法,在Fragment的生命周期方法里調用Presenter對應的生命周期函數,但這就讓Presenter看起來像Fragment的孫子;另外一個就是承認Presenter其實不太合適承擔Controller的職責,從而提供接口給外部處理;如下:
public class HotelPresenter {
private IHotelView mHotelView;
private Handler handler = new Handler(Looper.getMainLooper());
public void setData(HotelModel hotelModel) {
View button = mHotelView.getButtonView();
button.setVisibility(hotelModel.showButton ? VISIBLE :GONE);
if (hotelModel.showButton) {
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mCallback != null) {
mCallback.onSendButtonClicked();
}
});
}
}
public interface Callback {
public void onSendButtonClicked();
}
private Callback mCallback;
public void setCallback(Callback callback) {
mCallack = callback;
}
}
這個方案很穩定,似乎成為了最佳的選擇。但是自定接口和回調始終有那么一點痛。
三、MVP的擴展模式MVCPI
由于前面的分析,MVP參考實現并不是萬能的,攜程酒店并沒有完全采用參考實現方案,而是結合自身的實踐經驗思考之后設計出來的擴展方案。我們主要考慮了一下的幾個問題:
- 如何定義View接口?
- 如何定位Presenter ?
- 如何對待Controller?
- 如何解決長長的回調鏈?
通過對上述問題的思考,提出對應的解決方法,規避前面論述的各種問題,形成了攜程酒店的MVCPI框架模式,并在多個業務場景運行,取得了較為滿意的效果。下面,詳細介紹MVCPI模式。
3.1、 IView
和Android 參考實現不一樣的是,我們并沒有采用強類型的接口作為表達View的方式,而是采用弱類型的接口來定義View。具體定義方式如下:
public interface IView {
//用于展示酒店名稱的控件
int NAME_VIEW = R.id.name_view;
//用于展示酒店地址的控件
int ADDRESS_VIEW = R.id.address_view;
//用于展示酒店星級的控件
int STAR_VIEW = R.id.star_view;
//用于展示酒店詳情入口的的控件
int DETAIL_BUTTON = R.id.detail_button;
}
上面的接口簡潔的描述了作為業務控件的View需要具備的子控間ID,并不需要具體的實現類。因此也不需要Activity去實現這個接口,只需要在layout中申明這幾個ID的即可,極大的簡化了代碼。
3.2、 Presenter
與參考實現的定位不一樣,我們認為由Presenter取代Controller并不是一個好的做法,Presenter應是Controller的補充,主要起到View和Model解耦和數據綁定的作用,所負責的控件的上的業務還是有Controller決定如何去處理。另外setView接受的參數是一般的View,而非一個接口類型,內部根據IView定義的ID去查找子控件。如下:
public class CtripHotelPresenter {
TextView mNameView;
TextView mAddressView;
TextView mStarView;
Button mDetailButton;
public void setView(View view) {
mNameView = (TextView)mView.findViewById(IView.NAME_VIEW);
mAddressView = (TextView)mView.findViewById(IView.ADDRESS_VIEW);
mStarView = (TextView) mView.findViewById(IView.STAR_VIEW);
mDetailButton = (Button) mView.findViewById(IView.DETAIL_BUTTON);
}
public void setData(HotelModel hotel) {
mNameView.setText(hotel.hotelName);
mAddressView.setText(hotel.hotelAdress);
mStarView.setText(hotel.hotelStar);
int v = hotel.showButton ? View.VISIBLE : View.GONE;
mDetailButton.setVisibility(v);
}
}
3.3、 Interactor
Interactor是我們定義出來的擴展元素,在MVP和MVC中都沒有對應的角色。為了闡述它的含義,我們先來看看兩個非常常見的場景。
回調鏈過長
在前面介紹過,Presenter自定義接口是很多候選方案中較為合理的選擇,但相比MVC而言,MVP更容易出現如上圖的一種調用和回調關系(甚至更長)。維護這種回調鏈通常來說是一件非常頭痛的事情,從View的角度來看,很難知道某個事件到最后究竟完成了什么業務,Acitivity也不知道到要裝配哪些回調。某個未知的新需求可能需要將該鏈條上的每個環節都增加回調。
下面來是另外一種場景,大家可以腦補一下采用上面的回調方案,回調鏈會是什么情況。
交互集中型界面
在該界面有幾個特點:
- 幾十種動態交互需求,
- 分布于不同的模塊
- 分布于不同深度的嵌套層次中
經過大量版本迭代后,無論產品經理,研發或者測試,都不清楚到底有哪些需求,業務邏輯是什么,寫在什么地方等等……
上述兩個場景可以得出兩個結論:
- 排查問題非常耗時
- 增加功能成本高,容易引致其他問題
為了解決上述兩個比較棘手的問題,我們引入了Interactor,用于描述整個界面的交互,一舉解決上述兩個問題。我們認為交互模型是一個功能模塊的重要邏輯單元,相對于實體模型來說,交互模型更加抽象,在大多數的情況,并不能引起大家的注意,但它確實是如實體一樣的存在,正是因為沒有對交互進行系統的描述,才導致上面兩種突出的問題。盡管抽象,但是交互模型本質非常簡單,它有著和實體模型有相似的結構,示例如下:
public class HotelOrderDetailListeners {
public View.OnClickListener mBackListener; // 返回按鈕點擊事件監聽者
public View.OnClickListener mShareClickListener;//分享按鈕事件監聽者
public View.OnClickListener mConsultClickListener;//咨詢按鈕事件監聽者
……
}
通過對界面整體分析后,我們建立如上的交互模型,所有的交互都在交互模型進行注冊,由交互模型統一管理,進而可以對整個界面的交互進行宏觀把控;然后在頁面的所有元素中共享同一個交互模型,進而各個元素不再需要自定義接口和避免建立回調鏈。最后由Controller負責組裝,進一步加強Controller的控制能力。
3.4、 MVCPI全貌
最后,整體介紹一下MVCPI的代碼結構
1、首先定義整個界面中有哪些用戶交互,本例中就一個詳情按鈕交互
public class HotelInteractor {
//點擊詳情的事件處理器
public View.OnClickListener mDetail;
}
2、Presenter構造時需要傳入交互模型,內部定義了IView接口,傳入的View中需要包含它定義的ID的控件,在bindData時,詳情按鈕的點擊不是通過匿名內部類去處理,而是直接引用交互模型中定義的mDetail
public class HotelPresenter {
private View hotelView;
private HotelInteractor mInteractor;
private Button mDetailButton;
public HotelPresenter(HotelInteractor interactor) {
this.mInteractor = interactor;
}
private interface IView {
int DETAIL= R.id.detail_button;
……
}
public void setView(View hotelView) {
this.hotelView = hotelView;
mDetailButton= (Button)findViewById(IView. DETAIL );
}
public void setData(HotelModel hotel) {
if (hotel.showButton) {
mDetailButton.setVisibility(View.Visibile);
mDetailButton.setOnClickListener(mInteractor.mDetail);
}
}
}
3、Controller負責界面各個元素(包括交互模型)的初始化和裝配
public class HotelActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
HotelInteractor interactor = new HotelInteractor();
interactor.mDetail = new View.OnClickListener() {
public void onClick(View view) {
viewHotelDetail();//處理詳情業務;
}
};
HotelModel model= HotelLoader.loadHotelById(1000);
HotelPresenter presenter = new HotelPresenter (interactor);
View view= findViewById(R.id.hotel_view);
presenter.setView(view);
presenter.setData(hotel);
}
}
四、結論
通過對MVC、MVP的介紹和研究,我們發現二者的關系并不是相互取代的關系,而是一種演化和改進的關系。經實踐證明,MVC仍然具有強大的生命力,試圖用MVP取代MVC幾乎都會失敗。攜程在MVC模式基礎上,結合MVP思想,加入Interactor元素搭建的MVCPI框架模式,一方面將數據綁定邏輯從Controller(或者View)中分離出去,另一方面將交互模型的控制納入進來,進一步加強了Controller的控制能力。無論從代碼的簡潔性,維護性,擴展性來看,都具有較大優勢,具有一定的實踐推廣價值。
當然,任何框架模式都不是全能的,MVCPI也存在它不足,如果有好的意見和建議,歡迎加入,一起討論推進框架模式的發展。
來自:http://mobile.51cto.com/android-544249.htm