2017年4月25日 星期二

你的系統是MVC pattern還是MVC anti-pattern?


MVC是一種軟體架構模式,通常也是大多數程式設計新手第一個接觸的架構模式,因此也往往流於一種被濫用的名詞,許多新手常把「我的系統是採MVC架構設計的」這樣的話掛在嘴邊:,又或者找了一個open source framework,就以為follow他們的設計就是MVC了,這也是我寫下這篇文章的原因:提醒自己不要犯類似的錯誤,也希望能做到教學相長的作用。


MVC模式(Model-View-Controller pattern)

MVC模式的詳細介紹,可以參考維基百科:


其目的是為了使程式重複使用(reuse)、去耦合(de-coupling)、易維護性(maintainability)、清晰(clear)、 平行開發(parallel development)。 


之所以提出目的,是因為MVC並沒有一個標準的定義在,所以我個人的淺見是別執著於定義,而是將思考的點放在,如何使用MVC達到上述的目的,如此便容易釐清程式中的anti-pattern,並加以改善。

以下是我個人對MVC的理解:

  • Model:
    • 強調封裝(encapsulation),重點是what is it? how to use it?而不是how does it work? how did it do that?:根據problem domain描述應用程式的行為,並直接管理應用程式的資料、邏輯和規則,其必須與使用者介面無關,不依賴controller。
    • model設計的重點是要如何modling?將其抽象化,成為一個domain或subject,讓需要的controller去使用它
    • model依據controller的要求存取所要擷取的資料,至於資料內容是什麼往往得依據view或其他需求去設計。
  • View:
    • 可以是任何輸出表現形式,可以簡單以JSON格式表現,也可用複雜的圖表呈現,相同的資訊擁有多種view是可能的。
    • 隨著Web Service技術普及,view可以依據使用者權限、裝置類型、請求的不同等多種狀況,呈現不一樣的資訊內容或不同的視覺化表示法。 
  • Controller
    • 接受輸入,並轉換成給model的命令或給view的資料。 
    • 能發出命令,更新model的狀態(如資料庫的CRUD),將model取得的資料傳遞給對應的view去呈現。

以PHP的Codeigniter為例,其提供了一個完善的MVC軟體架構,但如果只知MVC所然,而不知MVC所以然的開發人員往往會寫出各種ani-pattern,如以下程式碼:

//這是一個處理會員登入的model
class Login_model extends CI_Model
{
    //取得會員資料
    public function select_join($post)
    {        
        $this->db->select($post['field']);  //要取得的欄位
        $this->db->from($post['table']);    //要取得的資料表名稱
        $this->db->where($post['where']     //要取得的資料表篩選條件
        foreach($post['order_by'] as $field=>$sort)
        {    
            $this->db->order_by($field,$sort);//排序        
        }
           
        //…

        $query = $this->db->get();            //取的資料庫資料
        return $query->result_array();        //查詢結果陣列資料
    }
}

//這是一個處理會員登入的Controller
class Admin_login extends CI_Controller {
    public function __construct()
    {
        parent::__construct();
        //載入login_model
        $this->load->model('login_model','login_model');
    }
    
    public function login()
    {
        //將欲查詢的資料表與使用者帳戶密碼以JSON格式封裝
        $post = array(
                  'table'=>'member',
                  'where'=> array(
                  'member.account' => 
                    $this->input->post('account'),
                  'member.password'=>
                    encryp($this->input->post('password'))
                 );

        //查詢會員是否在資料庫中
        $member_data = $this->login_model->select_join($post);
        
        //…

        redirect(‘member_page', 'refresh’); //載入對應的view
    }


這個MVC設計有幾個問題:我們沒辦法很直覺的理解model的用意,當我們沒辦法理解how to use this model?what is this function?的時候,model就很難reuse。

而這個model應該負責處理會員相關資料,但select_join這個function卻沒有將相關的資料表包含在其中,而是利用controller將要查詢的table與columns作為參數傳入select_join()中,這樣又導致controller與model會coupling,使其日後維護變得相當複雜,而我一般會希望能透過controller看出系統的運作流程,所以傾向於controller能簡單的表達抽象且同一層級的邏輯即可。

我會希望controller只需要傳入view取得的會員帳戶與密碼,並將其傳入model對應的function中,即可取得對應的帳戶資料:

$post = array(
        'account' =>$this->input->post('account'),
        'password’=>$this->input->post('password')
        );
$member_data = $this->login->model->get_member_data($post);


其中,明碼加密這件事(原本由encrpt()這個函數處理),應該屬於model層級的規則與邏輯,所以也應該併入model中處理,而不是在controller中處理。接著model->select_join()應該更換為更具有可讀性的名字,如get_member_data(),如此一來我們甚至不需要註解,也能輕鬆的從controller中看出整個controller的處理流程與邏輯。

另外許多工程師也會直接將判斷邏輯寫在view中:
//view for user login
<?php 
if($email_enable=="off"){?>
    <p>
    您的信箱尚未驗證,若未收到驗證信,請至修改會員資料頁面,重新發送驗證
    </p>
<?php }
?>

如果能將判斷邏輯放在controller中,更能降低view與controller之間的coupling且命名盡量使用直覺一點的命名,使UI Designer能專注在設計上,而不是留下一堆疑問等著問你:
//view for user login
<?php 
    <div style=“display:<?=$visible_for_email_verify; // return block or none?>”>
    <p>
    您的信箱尚未驗證,若未收到驗證信,請至修改會員資料頁面,重新發送驗證
    </p>
    </div>
?>


以上是小弟個人的淺見與經驗談,歡迎各位批評指教。 

沒有留言:

張貼留言