DAO(Data Access Object)是一個(應該算是)行之有年的模式,利用他可以把很快地把商業邏輯從主程式拆出來。
除了拆出商業邏輯,DAO的設計是依賴抽象的,所以當實作要抽換時,就很方便。
DAO其實很簡單,就是定義某個資料存取方法的物件。通常實作的方式是:
結束,真的很簡單。不過因為是依賴定義好的介面,而主程式是依賴介面的定義操作DAO,所以可以利用類似前面抽換View的方式,抽換實作。大概就是這樣。
在使用DAO拆開商業邏輯前,回頭看一下目前的程式:
<?php
require 'bootstrap.php';
if(isset($_SESSION['user']['account'])) {
$member = true;
$name = $_SESSION['user']['name'];
} else $member = false;
if(isset($_SESSION['msg'])) {
$message = mysql_real_escape_string($_SESSION['msg']);
unset($_SESSION['msg']);
}
$sql = "SELECT f.*,count(a.forums_id) AS count FROM forums f LEFT JOIN articles a ON a.forums_id = f.id GROUP BY a.forums_id ORDER BY f.id";
$result = mysql_query($sql, $conn);
$count = 0;
$data = array();
while($row = mysql_fetch_array($result)) {
if($count%2==0) {
$style = "#EEFFEE";
} else {
$style = "#FFFFFF";
}
$sql = "SELECT * FROM articles WHERE forums_id=".$row['id']." ORDER BY id DESC LIMIT 1";
$result1 = mysql_query($sql, $conn);
$row1 = mysql_fetch_array($result1);
$data[] = array('id'=>$row['id'], 'name'=>$row['name'], 'count'=>$row['count'], 'title'=>$row1['title'], 'style'=>$style);
$count++;
}
$view = Fillano\Core\View::getInstance('views');
$view->assign(array(
'member'=>$member,
'data'=>$data,
'name'=>$name,
'message'=>$message
));
$view->render('index', 'html');
仔細看一下程式,會發現$style其實跟商業邏輯沒關係,他是頁面就可以決定的,所以先來做簡單的調整,把跟商業邏輯無關的東西整理到正確的地方。其實$style是根據奇數與偶數列來定義style,這個邏輯在Twig可以用迴圈的變數實作出來。所以先改一下View:
{% extends "base.html" %}
{% block navbar %}我的論壇{% endblock %}
{% block content %}
<table width="100%" border="1" cellspacing="0" cellpadding="5">
<tr bgcolor="#DDEEFF">
<th>論壇名稱</th>
<th>文章數</th>
<th>最新文章</th>
<th>操作</th>
</tr>
{% for row in data %}
{% if loop.index0%2==0 %}
{% set style="#EEFFEE" %}
{% else %}
{% set style="#FFFFFF" %}
{% endif %}
<tr bgcolor="{{style}}">
<td>{{row.name}}</td>
<td>{{row.count}}</td>
<td>{{row.title}}</td>
<td>
<button onclick="document.location.href='forum.php?id={{row.id}}'">進入</button>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
然後把程式中的$style拿掉:
<?php
require 'bootstrap.php';
if(isset($_SESSION['user']['account'])) {
$member = true;
$name = $_SESSION['user']['name'];
} else $member = false;
if(isset($_SESSION['msg'])) {
$message = mysql_real_escape_string($_SESSION['msg']);
unset($_SESSION['msg']);
}
$sql = "SELECT f.*,count(a.forums_id) AS count FROM forums f LEFT JOIN articles a ON a.forums_id = f.id GROUP BY a.forums_id ORDER BY f.id";
$result = mysql_query($sql, $conn);
$data = array();
while($row = mysql_fetch_array($result)) {
$sql = "SELECT * FROM articles WHERE forums_id=".$row['id']." ORDER BY id DESC LIMIT 1";
$result1 = mysql_query($sql, $conn);
$row1 = mysql_fetch_array($result1);
$data[] = array('id'=>$row['id'], 'name'=>$row['name'], 'count'=>$row['count'], 'title'=>$row1['title']);
}
$view = Fillano\Core\View::getInstance('views');
$view->assign(array(
'member'=>$member,
'data'=>$data,
'name'=>$name,
'message'=>$message
));
$view->render('index', 'html');
接下來就可以開始寫DAO,首先定義操作的介面Fillano\Models\IIndex:
<?php
namespace Fillano\Models;
Interface IIndex {
function getForumList();
}
然後實作一個類別,因為目前都是用原生的mysql函數,所以用Mysql前綴來命名。(訂好這樣的Name Convention,未來就容易抽換實作)在這個類別的getForumList方法中,把主程式處理資料的部份直接移過去:
<?php
namespace Fillano\Models;
class MysqlIndex implements IIndex
{
private $conn;
public function __construct($conn)
{
$this->conn = $conn;
}
public function getForumList()
{
$sql = "SELECT f.*,count(a.forums_id) AS count FROM forums f LEFT JOIN articles a ON a.forums_id = f.id GROUP BY a.forums_id ORDER BY f.id";
$result = mysql_query($sql, $this->conn);
$data = array();
while($row = mysql_fetch_array($result)) {
$sql = "SELECT * FROM articles WHERE forums_id=".$row['id']." ORDER BY id DESC LIMIT 1";
$result1 = mysql_query($sql, $this->conn);
$row1 = mysql_fetch_array($result1);
$data[] = array('id'=>$row['id'], 'name'=>$row['name'], 'count'=>$row['count'], 'title'=>$row1['title']);
}
return $data;
}
}
需要改的地方,只有$conn改成$this->conn。
主程式就呼叫這個DAO來取得資料:
<?php
require 'bootstrap.php';
if(isset($_SESSION['user']['account'])) {
$member = true;
$name = $_SESSION['user']['name'];
} else $member = false;
if(isset($_SESSION['msg'])) {
$message = mysql_real_escape_string($_SESSION['msg']);
unset($_SESSION['msg']);
}
$model = new Fillano\Models\MysqlIndex($conn);
$data = $model->getForumList();
$view = Fillano\Core\View::getInstance('views');
$view->assign(array(
'member'=>$member,
'data'=>$data,
'name'=>$name,
'message'=>$message
));
$view->render('index', 'html');
現在主程式只剩幾行了,一目了然。而且因為只是把程式移過去,沒有動到邏輯,所以幾乎不會出錯。(測試的時候,忘了去改$conn...還是出錯了)不過這樣主程式中還是有MysqlIndex這個類別名字,將來要改動資料操作的實作(例如改用PDO、ORM等等),仍然要動到主程式。所以還是仿照之前實作View的方式,寫一個工廠方法來產出Model:
<?php
namespace Fillano\Core;
class ModelFactory
{
public static function getInstance($name, $params)
{
if(!defined(MODEL_IMPL)) {
die('Model Implementation constant "MODEL_IMPL" not defined in config.php');
}
//糟糕,寫不下去
}
}
...糟糕,MysqlIndex需要一個connection resource作為建構式的參數,但是這種狀況不是每一種實作都會有的,放在ModelFactory裡會有問題...嗯,考慮了一下,既然只有實作才知道要怎樣建構實例,那就需要每個實作定義一個自己的工廠方法才對。所以另外在寫一個實作的工廠類別,名稱是MODEL_IMPL加上ModelFactory,所以目前是用Mysql這個前綴,這個類別就叫做MysqlModelFactory吧:
<?php
namespace Fillano\Core;
class MysqlModelFactory
{
public static function getInstance($name) {
if(!defined('MODEL_IMPL')) {
die('Model Implementation constant "MODEL_IMPL" not defined in config.php');
}
$class = 'Fillano\\Models\\'.MODEL_IMPL.$name;
if(class_exists($class)) {
$conn = $GLOBALS['conn'];
return new $class($conn);
} else {
throw new \Exception("class $class not found.");
}
}
}
然後ModelFactory就這樣寫:
<?php
namespace Fillano\Core;
class ModelFactory
{
public static function getInstance($name)
{
if(!defined('MODEL_IMPL')) {
die('Model Implementation constant "MODEL_IMPL" not defined in config.php');
}
$class = 'Fillano\\Core\\'.MODEL_IMPL."ModelFactory";
if(class_exists($class)) {
return $class::getInstance($name);
} else {
throw new \Exception("class $class not found.");
}
}
}
總之就是委派給實作的工廠類別就是了。
最後主程式只有一行變動:
<?php
require 'bootstrap.php';
if(isset($_SESSION['user']['account'])) {
$member = true;
$name = $_SESSION['user']['name'];
} else $member = false;
if(isset($_SESSION['msg'])) {
$message = mysql_real_escape_string($_SESSION['msg']);
unset($_SESSION['msg']);
}
//$model = new Fillano\Models\MysqlIndex($conn);
$model = Fillano\Core\ModelFactory::getInstance('Index');
$data = $model->getForumList();
$view = Fillano\Core\View::getInstance('views');
$view->assign(array(
'member'=>$member,
'data'=>$data,
'name'=>$name,
'message'=>$message
));
$view->render('index', 'html');
打開瀏覽器跑一下,看起來跟之前一樣,操作也沒問題。
======
DAO是很容易實作的模式,把它應用到程式中的過程也不困難,但是使用以後,立刻就可以把原先在主程式中的商業邏輯獨立出來。用這個方式,就可以逐步改善程式的架構,小步前進。
不過在考慮抽換實作與程式依賴問題後,會需要做一些tricky的設計,裡面會累積更多的依賴關係,在大型的系統中,這樣比較容易出問題。有需要時,其實PHP已經有不少成熟的IoC/DI容器的實作,可以協助做這方面的管理。不過我先不去碰這個了,時間也不太夠。(在網路上找了一些:http://r.je/dice.html、https://code.google.com/p/substrate-php/、https://github.com/koriym/Ray.Di、https://github.com/packfire/fuelblade、http://marcelog.github.io/Ding/等等,許多framework也內建這個機制,例如Laravel)
Mysql函式庫有可能在未來被移出PHP,既然目前已經有辦法抽換DAO的實作,那明天就用PDO改寫一下目前的程式,並且驗證一下是否真的不用動到主程式。