iT邦幫忙

DAY 26
11

逐步提昇PHP技術能力系列 第 26

逐步提昇PHP技術能力 - 逐步改善軟體架構 - 用DAO初步分離資料邏輯

DAO(Data Access Object)是一個(應該算是)行之有年的模式,利用他可以把很快地把商業邏輯從主程式拆出來。

除了拆出商業邏輯,DAO的設計是依賴抽象的,所以當實作要抽換時,就很方便。
DAO其實很簡單,就是定義某個資料存取方法的物件。通常實作的方式是:

  1. 根據資料存取的需求,設計出存取的介面
  2. 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裡會有問題XD...嗯,考慮了一下,既然只有實作才知道要怎樣建構實例,那就需要每個實作定義一個自己的工廠方法才對。所以另外在寫一個實作的工廠類別,名稱是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.htmlhttps://code.google.com/p/substrate-php/https://github.com/koriym/Ray.Dihttps://github.com/packfire/fuelbladehttp://marcelog.github.io/Ding/等等,許多framework也內建這個機制,例如Laravel)

Mysql函式庫有可能在未來被移出PHP,既然目前已經有辦法抽換DAO的實作,那明天就用PDO改寫一下目前的程式,並且驗證一下是否真的不用動到主程式。


上一篇
逐步提昇PHP技術能力 - 逐步改善軟體架構 - 建立簡單的View
下一篇
逐步提昇PHP技術能力 - 逐步改善軟體架構 - 轉換到PDO
系列文
逐步提昇PHP技術能力30

1 則留言

0

我要留言

立即登入留言