關於MVC、MVP架構


這裏開始記錄下來自己對MVC、MVP、MVVM這三種框架模式的理解,本文從以下幾個方面來梳理。

  • 架構的目的
  • 框架模式、設計模式
  • MVC設計的介紹
  • MVC在Android中的應用
  • MVC該如何設計
  • MVP設計的介紹
  • MVP在Android中的應用

1. 架構的目的

當我們在進行OOP編程時,一切對象來源於需求,對象結合業務邏輯通過多態、繼承、等封裝成各個業務模塊。我們通過設計將程序模塊化,使模塊內部高內聚和模塊之間低耦合。這樣做的好處是,當我們進行開發或者測試時,我們只需要專注於一點,而不用考慮牽一髮動全身之類的問題,從而大大提高了我們的工作效率。但是設計是服務於目的的,就是說我們的需求決定着設計,不同的場景有些不同的需求,那麼其應用的設計模式必然是不盡相同的。萬不可爲了設計而設計,導致忘記了設計的初衷。
我們舉個例子,假使我們的程序只需要實現一個極其簡單的功能以至於只有寥寥數行代碼,那這時候去考慮引入設計模式是不切實際的。但相反,對於系列龐大、功能衆多的app我們就必須考慮引入一套符合情景的模式以梳理開發、簡化維護。


2. 框架、設計模式

框架和設計模式是完全的兩個概念,框架通常是代碼的重用,而設計模式是設計的重用,架構則介於兩者之間,部分代碼重用,部分設計重用。
可能以上說法比較晦澀難懂,在此摘錄一些從業者對它們的理解。

  • 框架通常是代碼重用,模式是設計重用;
  • 設計模式是思考的過程,框架是思考的結果;
  • 框架需要用到設計模式,比如我們封裝了一套retrofit+okhttp+rxjava網絡請求框架,我們可能用到很多設計模式。比如:代理模式、觀察者模式等;
  • 脫離實際需求談框架都是耍流氓。
  • 框架是麪包、麪條、餃子…,設計模式是做麪包麪條餃子的手藝(至於怎麼做、做成什麼形狀、味道…主要看手藝);
  • 設計模式是大量場景總結出來的模版,如果你有相應的場景就可以往上套。

我們今天談到的MVX系列是框架而不是設計模式,因爲MVX框架是用來解決實際需求的(比如View、Model如何分離相互控制)。


3. MVC設計的介紹

這裏寫圖片描述

MVC的全稱是Model View Controller,如圖是模型(model)-視圖(view)-控制器(controller)的縮寫。一種軟件設計典範,用一種業務邏輯、數據、界面顯示分離的方法組織代碼,在改進和個性化定製界面及用戶交互的同時,不需要重新編寫業務邏輯。
其中M層處理數據,業務邏輯等;V層處理界面的顯示結果;C層起到橋樑的作用,來控制V層和M層通信以此來達到分離視圖顯示和業務邏輯層。


4. MVC在Android中的應用

這裏寫圖片描述

我們在文章的最開始說了框架的作用就是爲了解決代碼的臃腫,提高代碼的複用率。在MVC模式中我們可以發現View、Model層都是遵循此原則設計的。
對於View層來講,我們通常在做一個項目時封裝出許多自定義控件或者動畫,封裝較好的情況下在別的項目中我們可以直接拿來用,這是View層代碼複用的體現。
對於Model層呢?我們可以講Model理解爲用來存儲業務數據,當我們的同一app需要跨平臺時(比如騰訊視頻的apk版和騰訊雲極光TV版)我們考慮是不是Model可以共用?
對於Controller層來講,幾乎不可複用,因爲它和業務邏輯聯繫太緊密。下面我們就來談談MVC在Android中的體現。

上圖爲MVC模式在android項目中的體現,View代表視圖層,Controller代表控制器,Model代表數據模型,app通過View視圖監聽控件根據控件表現調用相應的控制器處理業務邏輯,業務邏輯處理完之後控制器通知Model層改變,Model層改變帶動View視圖變化。
在android中一般採用xml文件進行界面描述,這些xml文件可以視爲app的View層,引用很簡單,並且通過良好構造也能達到一定的複用性。model對應着各種體現業務邏輯的數據類,它們體現了app的數據(對數據庫、網絡操作)。而聯通它們之間的橋樑就是Controller即Activity、Fragment,我們通過在activity/fragment來管理控制整個業務流程。

這裏舉個例子來形象地闡述下MVC在Android中的應用,比如一個飯店的做飯流程,做飯就要考慮做什麼?拿什麼做?這其中是如何分工的。飯店老闆戚總就好比我們的Controller,飯店的廚師老範相當於View(買什麼菜他說了算是需求的來源),採購小王相當於Model。廚師老範需要做個霸王別姬,需要一大堆食材,他把需求報告給戚總,戚總整理成清單要求小王去採購,小王採購完成後直接將食材交給廚師老範。這就是MVC的一個循環,當然飯店比較小,有的時候廚師老範只需要一根蔥這時候沒必要專門讓採購跑一趟了吧也省點油錢,戚總順手就買了,時間久了,戚總慢慢幹了點採購的活…

我們舉個簡單的例子。根據學生號碼查詢其名字、性別、年齡。

V層在xml文件中描述如下:
這裏寫圖片描述

M層數據類如下:
這裏寫圖片描述

C層如下:
這裏寫圖片描述

到這裏我們理解了MVC在android中的應用原理,那麼我們進一步思考一下,如果app的業務邏輯相對簡單,這麼負責調控app的activity/fragment中的代碼也相對簡單,但當處理業務邏輯非常繁瑣的app時,activity/fragment中的代碼就會變得相當臃腫不雅,非常不利於維護。並且在實際開發過程中,我們經常在activity/fragment中寫下這樣的代碼,如下

textView.setText(string);

這樣便違反了MVC的原則,在C中處理V,使得C的職責單一性被破壞,所以這時候MVP模型便走進了我們的視野。下節我們先不講MVP,當我們知道了MVC的現狀後,我們該如何合理地應用MVC模型。


5. MVC該如何設計

MVC 雖然只有三層,但是它並沒有限制你只能有三層。所以,我們可以將 Controller 裏面過於臃腫的邏輯抽取出來,形成新的可複用模塊或架構層次。

  • 將網絡請求抽象到單獨的類中;
  • 提供自定義控件封裝到專門的類中;
  • 構造 ViewModel;
  • 專門構造存儲類;

通過代碼的抽取,我們可以將原本的 MVC 設計模式中的 ViewController 進一步拆分,構造出 網絡請求層、ViewModel 層、Service 層、Storage 層等其它類,來配合 Controller 工作,從而使 Controller 更加簡單,我們的 App 更容易維護。
另外,不知道大家注意到沒,其實 Controller 層是非常難於測試的,如果我們能夠將 Controller 瘦身,就可以更方便地寫 Unit Test 來測試各種與界面的無關的邏輯。移動端自動化測試框架都不太成熟,但是將 Controller 的代碼抽取出來,是有助於我們做測試工作的。


6. MVP設計的介紹

還記得我們上面的飯店吧,時間久了戚總有時候爲了多省點油錢越來越多的擔任採購的活了,甚至還兼職抄起了菜…慢慢的隨着飯店的生意越來越好慢慢做大,戚總的事情越來越多開始忙不過來了,另一方面飯店的生意好了卻不見得多掙了多少錢。戚總想着會不會是廚師老範和採購小王串通坑他呢,於是乎,戚總想了一夜,哎嗨,一套新的管理機制來了。
View還是廚師老範,Model是採購小王,戚總把自己定位爲Presenter,以後廚師老範有需求了仍然告訴戚總,戚總安排給小王去採購,採購完之後把食材交給戚總檢查,數目賬單沒問題了戚總再將食材給廚師老範。這樣不讓廚師老範和採購小王就沒有任何機會溝通避免他們串通,同時戚總以後只負責吩咐兩人做事也不爲了省點油錢自己親自幹了畢竟生意做大了得像個老闆的樣子了,由此MVP模型初見雛形。其示意圖如下:

這裏寫圖片描述

這樣做它有何利弊呢?我們仍然結合飯店的例子加以說明。

首先使它的好處:

  • 分工明確,大家各司其職就好提高效率;
  • 利於以後的發展,比如要裁人換人等,像MVC模式我如果換掉廚師,就會同時影響老闆和採購的工作,MVP呢不管如何調整我們只調整P就行,其他模塊互不干涉影響。
  • 大大解放老闆的工作(是P層支付者調度減少代碼臃腫)。
  • 用程序語言就是更加高內聚、低耦合;

再來談談它的弊端:

  • 結構複雜了。

總體而言,對於業務邏輯複雜的項目,MVP的利要遠大於弊。


7. MVP在Android中的應用

下面我們就來談談MVP在Android之中的應用,既然是在Android中的應用,那麼我們就考慮從大哥那兒學一套本事%>_<%。
這是Google大哥的官方demo。
再來一篇分析demo的文章。

研究完了上面的,我們是時候來點實踐了。就模擬個登錄功能吧。
首先看清文件結構:

這裏寫圖片描述

google應用了BaseView、BasePresenter兩個類。 如下:

這裏寫圖片描述

這裏寫圖片描述

這兩個類作爲所有view和presenter的基類來使用。
BasePresenter中的start方法是用來load頁面時加載相應數據的,而登錄模塊暫時並不需要該方法,但是這個類畢竟是爲整個業務模塊服務的,別的業務可能需要,暫時保留。
BaseView中的setPresenter方法是爲了向fragment中傳遞activity中new出來的presenter對象。登錄模塊其實一個activity足以搞定,這個方法多餘,但是保留該方法,理由同上。

開始具體業務。這裏需要構建view和presenter。值得注意的點是,google將view和presenter放到了一個契約類中了。所以

這裏寫圖片描述

View中提供了四個相關操作,即登錄成功、登錄失敗、獲取用戶名、獲取密碼;
Presenter中只提供了login()登錄操作。

這裏寫圖片描述

這裏activity作爲mvp中的view層實現了契約類中的view接口。

public class LoginActivity extends Activity implements LoginContract.View{

    private Context mContext;
    private EditText mNameEt, mPasswordEt;
    private Button mLoginBtn;
    private LoginContract.Presenter mPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login_activity);
        mContext = this;
        mPresenter = new LoginPresenter(this);
        mNameEt = (EditText) findViewById(R.id.nameEt);
        mPasswordEt = (EditText) findViewById(R.id.passwordEt);
        mLoginBtn = (Button) findViewById(R.id.loginBtn);
        mLoginBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.login();
            }
        });
    }

    @Override
    public void loginSuccess() {
        Toast.makeText(mContext, "登錄成功", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void loginFailed() {
        Toast.makeText(mContext, "登錄失敗", Toast.LENGTH_SHORT).show();
    }

    @Override
    public String getLoginName() {
        return mNameEt.getText().toString();
    }

    @Override
    public String getLoginPassword() {
        return mPasswordEt.getText().toString();
    }

    @Override
    public void setPresenter(Object presenter) {

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

注意,如果你是fragment作爲view,一定要加上mView.setPresenter(this)把P傳遞過去。

public class LoginPresenter implements LoginContract.Presenter{

    private LoginContract.View mView;

    public LoginPresenter(LoginContract.View view){
        mView = view;
    }

    @Override
    public void login() {
        boolean successFlag = httpLogin(mView.getLoginName(), mView.getLoginPassword());
        if ( successFlag )
            mView.loginSuccess();
        else
            mView.loginFailed();
    }

    @Override
    public void start() {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

Google通過一個契約類使我們的項目結構更加清晰。
當然沒有絕對的MVP、MVC框架,比如在上面的登錄功能中我們利用MVC更加方便簡單,但是如果考慮以後的擴展性(比如增加第三方登錄等等)利用MVP更加合適。所以無論選擇哪種方式必定是建立在需求的基礎上的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章