作者:Harry Fuecks 翻譯:Easy Chen
MVC模式在網站架構中十分常見。它允許我們建立一個三層結構的應用程式,從代碼中分離出有用的層,協助設計師和開發人員協同工作以及提高我們維護和擴充既有程式的能力。
視圖(View)
“視圖”主要指我們送到Web瀏覽器的最終結果——比如我們的指令碼產生的HTML。當說到視圖時,很多人想到的是模版,但是把模板方案叫做視圖的正確性是值得懷疑的。
對視圖來說,最重要的事情可能是它應該是“自我意識(self aware)”的,視圖被渲染(render)時,視圖的元素能意識到自己在更大架構中的角色。
以XML為例,可以說XML在被解析時,DOM API有著這樣的認知——一個DOM樹裡的節點知道它在哪裡和它包含了什麼。 (當一個XML文檔中的節點用SAX解析時只有當解析到該節點時它才有意義。)
絕大多數模板方案使用簡單的過程語言和這樣的模板標籤:
<p>{some_text}</p>
<p>{some_more_text}</p>
它們在文檔中沒有意義,它們代表的意義只是PHP將用其他的東西來替換它。
如果你同意這種對視圖的鬆散描述,你也就會同意絕大多數模板方案並沒有有效分離視圖和模型。模板標籤將被替換成什麼存放在模型中。
在你實現視圖時問自己幾個問題:“全體視圖的替換容易嗎?”“實現一個新視圖要多久?” “能很容易的替換視圖的描述語言嗎?(比如在同一個視圖中用SOAP文檔替換HTML文檔)”
模型(Model)
模型代表了程式邏輯。(在企業級程式中經常稱為業務層(business layer))
總的來說,模型的任務是把原有資料轉換成包含某些意義的資料,這些資料將被視圖所顯示。通常,模型將封裝資料查詢,可能通過一些抽象資料類(資料訪問層)來實現查詢。舉例說,你希望計算英國年度降雨量(只是為了給你自己找個好點的度假地),模型將接收十年中每天的降雨量,計算出平均值,再傳遞給視圖。
控制器(controller)
簡單的說控制器是Web應用中進入的HTTP請求最先調用的一部分。它檢查收到的請求,比如一些GET變數,做出合適的反饋。在寫出你的第一個控制器之前,你很難開始編寫其他的PHP代碼。最常見的用法是index.php中像switch語句的結構:
<?php
switch ($_GET['viewpage']) {
case "news":
$page=new NewsRenderer;
break;
case "links":
$page=new LinksRenderer;
break;
default:
$page=new HomePageRenderer;
break;
}
$page->display();
?>
這段代碼混用了面向過程和對象的代碼,但是對於小的網站來說,這通常是最好的選擇。雖然上邊的代碼還可以最佳化。
控制器實際上是用來觸發模型的資料和視圖元素之間的綁定的控制項。
例子
這裡是一個使用MVC模式的簡單例子。
首先我們需要一個資料庫訪問類,它是一個普通類。
<?php
/**
* A simple class for querying MySQL
*/
class DataAccess {
/**
* Private
* $db stores a database resource
*/
var $db;
/**
* Private
* $query stores a query resource
*/
var $query; // Query resource
//! A constructor.
/**
* Constucts a new DataAccess object
* @param $host string hostname for dbserver
* @param $user string dbserver user
* @param $pass string dbserver user password
* @param $db string database name
*/
function DataAccess ($host,$user,$pass,$db) {
$this->db=mysql_pconnect($host,$user,$pass);
mysql_select_db($db,$this->db);
}
//! An accessor
/**
* Fetches a query resources and stores it in a local member
* @param $sql string the database query to run
* @return void
*/
function fetch($sql) {
$this->query=mysql_unbuffered_query($sql,$this->db); // Perform query here
}
//! An accessor
/**
* Returns an associative array of a query row
* @return mixed
*/
function getRow () {
if ( $row=mysql_fetch_array($this->query,MYSQL_ASSOC) )
return $row;
else
return false;
}
}
?>
在它上邊放上模型。
<?php
/**
* Fetches "products" from the database
*/
class ProductModel {
/**
* Private
* $dao an instance of the DataAccess class
*/
var $dao;
//! A constructor.
/**
* Constucts a new ProductModel object
* @param $dbobject an instance of the DataAccess class
*/
function ProductModel (&$dao) {
$this->dao=& $dao;
}
//! A manipulator
/**
* Tells the $dboject to store this query as a resource
* @param $start the row to start from
* @param $rows the number of rows to fetch
* @return void
*/
function listProducts($start=1,$rows=50) {
$this->dao->fetch("SELECT * FROM products LIMIT ".$start.", ".$rows);
}
//! A manipulator
/**
* Tells the $dboject to store this query as a resource
* @param $id a primary key for a row
* @return void
*/
function listProduct($id) {
$this->dao->fetch("SELECT * FROM products WHERE PRODUCTID='".$id."'");
}
//! A manipulator
/**
* Fetches a product as an associative array from the $dbobject
* @return mixed
*/
function getProduct() {
if ( $product=$this->dao->getRow() )
return $product;
else
return false;
}
}
?>
有一點要注意的是,在模型和資料訪問類之間,它們的互動從不會多於一行——沒有多行被傳送,那樣會很快使程式慢下來。同樣的程式對於使用模式的類,它只需要在記憶體中保留一行(Row)——其他的交給已儲存的查詢資源(query resource)——換句話說,我們讓MYSQL替我們保持結果。
接下來是視圖——我去掉了HTML以節省空間的,你可以查看這篇文章的完整代碼。
<?php
/**
* Binds product data to HTML rendering
*/
class ProductView {
/**
* Private
* $model an instance of the ProductModel class
*/
var $model;
/**
* Private
* $output rendered HTML is stored here for display
*/
var $output;
//! A constructor.
/**
* Constucts a new ProductView object
* @param $model an instance of the ProductModel class
*/
function ProductView (&$model) {
$this->model=& $model;
}
//! A manipulator
/**
* Builds the top of an HTML page
* @return void
*/
function header () {
}
//! A manipulator
/**
* Builds the bottom of an HTML page
* @return void
*/
function footer () {
}
//! A manipulator
/**
* Displays a single product
* @return void
*/
function productItem($id=1) {
$this->model->listProduct($id);
while ( $product=$this->model->getProduct() ) {
// Bind data to HTML
}
}
//! A manipulator
/**
* Builds a product table
* @return void
*/
function productTable($rownum=1) {
$rowsperpage='20';
$this->model->listProducts($rownum,$rowsperpage);
while ( $product=$this->model->getProduct() ) {
// Bind data to HTML
}
}
//! An accessor
/**
* Returns the rendered HTML
* @return string
*/
function display () {
return $this->output;
}
}
?>
最後是控制器,我們將把視圖實現為一個子類。
<?php
/**
* Controls the application
*/
class ProductController extends ProductView {
//! A constructor.
/**
* Constucts a new ProductController object
* @param $model an instance of the ProductModel class
* @param $getvars the incoming HTTP GET method variables
*/
function ProductController (&$model,$getvars=null) {
ProductView::ProductView($model);
$this->header();
switch ( $getvars['view'] ) {
case "product":
$this->productItem($getvars['id']);
break;
default:
if ( empty ($getvars['rownum']) ) {
$this->productTable();
} else {
$this->productTable($getvars['rownum']);
}
break;
}
$this->footer();
}
}
?>
注意這不是實現MVC的唯一方式——比如你可以用控制器實現模型同時整合視圖。這隻是示範模式的一種方法。
我們的index.php 檔案看起來像這樣:
<?php
require_once('lib/DataAccess.php');
require_once('lib/ProductModel.php');
require_once('lib/ProductView.php');
require_once('lib/ProductController.php');
$dao=& new DataAccess ('localhost','user','pass','dbname');
$productModel=& new ProductModel($dao);
$productController=& new ProductController($productModel,$_GET);
echo $productController->display();
?>
漂亮而簡單。
我們有一些使用控制器的技巧,在PHP中你可以這樣做:
$this->{$_GET['method']}($_GET['param']);
一個建議是你最好定義程式URL的名字空間形式(namespace),那樣它會比較規範比如:
"index.php?class=ProductView&method=productItem&id=4"
通過它我們可以這樣處理我們的控制器:
$view=new $_GET['class'];
$view->{$_GET['method']($_GET['id']);
有時候,建立控制器是件很困難的事情,比如當你在開發速度和適應性之間權衡時。一個獲得靈感的好去處是Apache group 的Java Struts,它的控制器完全是由XML文檔定義的。