現實生活中的工廠是做什麼用的呢?「麵包工廠」生產麵包、蛋糕、吐司等等。「煉油工廠」生產汽油、柴油等等。
而在物件導向程式設計中的「工廠」,則是生產物件。它是一個類別,專門用來生產其它類別實例化的物件。在物件導向程式設計中,工廠模式是最常見的模式,
最常用在操作資料庫、快取、日誌等等。
我們直接以實戰中可能會遇到的情況來當範例吧!
情境:主管要我們寫一個快取的功能來存放後台 API 產生的結果,以免每次都得進行多次的資料庫查詢來組裝 API 回給前端的資料結構。
筆者剛剛寫了一支非常簡易的快取驅動器 (cache driver),可以用來儲存 key 對 value 型式的快取資料。這是一支採用 MySQL 資料庫為存取媒介的快取驅動器。
資料表:
原始碼: Day 7 - Example 1
class Cache
{
protected $db;
protected $table = 'cache_data';
public function __construct($setting)
{
$host = 'mysql' .
':host=' . $setting['host'] .
';dbname=' . $setting['dbname'] .
';charset='. $setting['charset'];
$user = $setting['user'];
$pass = $setting['pass'];
$this->db = new PDO($host, $user, $pass);
$this->table = $setting['table'];
}
public function get($key)
{
$sql = 'SELECT cache_value FROM ' . $this->table . '
WHERE cache_key = :cache_key';
$query = $this->db->prepare($sql);
$query->bindValue(':cache_key', $key);
$query->execute();
$resultData = $query->fetch($this->db::FETCH_ASSOC);
if (!empty($resultData)) {
return $resultData['cache_value'];
}
return false;
}
public function set($key, $value)
{
$cacheData = $this->get($key);
$data = [
'cache_key' => $key,
'cache_value' => $value,
];
if ($cacheData !== false) {
$sql = 'UPDATE ' . $this->table . '
SET cache_value = :cache_value
WHERE cache_key = :cache_key';
$query = $this->db->prepare($sql);
$query->execute($data);
} else {
$sql = 'INSERT INTO ' . $this->table . ' (cache_key, cache_value) VALUES (:cache_key, :cache_value)';
$query = $this->db->prepare($sql);
$query->execute($data);
}
}
}
使用方法:
<?php
include 'Cache.php';
$setting['host'] = '127.0.0.1';
$setting['dbname'] = 'test';
$setting['charset'] = 'UTF8';
$setting['user'] = 'shieldon';
$setting['pass'] = 'taiwan';
$setting['table'] = 'cache_data';
$cache = new Cache($setting);
$cache->set('foo', 'bar');
echo $cache->get('foo');
// Result: bar
當我們實例化 Cache
類別之後,就可以自由使用 set
和 get
這兩個方法來存放資料。
但有假如有一天,主管要我們改用 Redis 這個熱門的 NoSQL 資料庫來放快取資料。又或者聽說 Redis 效能非常好,你也想試試。那麼來寫支以 Redis 資料庫為存取媒介的資料驅動器。
class CacheRedis
{
protected $db;
public function __construct($setting)
{
$host = $setting['host'];
$port = $setting['port'];
$this->db = new Redis();
$this->db->connect($host, $port);
}
public function get($key)
{
return $this->db->get($key);
}
public function set($key, $value)
{
return $this->db->set($key, $value);
}
}
使用方法:
<?php
include 'CacheRedis.php';
$setting['host'] = '127.0.0.1';
$setting['port'] = 6379;
$cache = new CacheRedis($setting);
$cache->set('foo', 'bar la');
echo $cache->get('foo');
// Result: bar la
假如以後還要加上 MongoDB、SQLite、Memcache 等存取方式呢,有沒有更方便、更好拓展整個架構的好方法呢?
我們來蓋工廠,讓工廠負責產生快取的物件吧!
原始碼: Day 7 - Example 2
這個範例我們把,例 1 的 Cache
類別更名為 CacheMysql
,檔名同樣也是。
class CacheFactory
{
public function createDriver($type, $setting)
{
$setting = $setting[$type];
$className = 'Cache' . ucfirst(strtolower($type));
$classFilePatch = __DIR__ . '/' . $className . '.php';
if (file_exists($classFilePatch)) {
include_once $classFilePatch;
}
return new $className($setting);
}
}
用快取工廠來產生快取物件。
<?php
include 'CacheFactory.php';
$setting['mysql']['host'] = '127.0.0.1';
$setting['mysql']['dbname'] = 'test';
$setting['mysql']['charset'] = 'UTF8';
$setting['mysql']['user'] = 'shieldon';
$setting['mysql']['pass'] = 'taiwan';
$setting['mysql']['table'] = 'cache_data';
$setting['redis']['host'] = '127.0.0.1';
$setting['redis']['port'] = 6379;
$factory = new CacheFactory();
$cache = $factory->createDriver('redis', $setting);
$cache->set('hello', 'world');
echo $cache->get('hello') . "\n";
這個例子,我們使用 Redis 資料庫的驅動器唷。
如果要改用 MySQL,只要把 redis 改成 mysql 就可以囉。如下的程式碼:
$cache = $factory->createDriver('mysql', $setting);
假如想再加不同資料庫的驅動器,只要新增類別,例如 CacheMongo
,並存成檔名 CacheMongo.php
,新增準備要實化該物件的設定值於 $setting['mongo']
,以此類推,及可不必修改類別的程式碼,就能一直擴充。
是不是很方便,是不是,是不是?
這支工廠的程式碼,筆者逐行說明:
class CacheFactory
{
public function createDriver($type, $setting)
{
$setting = $setting[$type];
$className = 'Cache' . ucfirst(strtolower($type));
$classFilePatch = __DIR__ . '/' . $className . '.php';
if (file_exists($classFilePatch)) {
include_once $classFilePatch;
return new $className($setting);
} else {
throw new Exception('Driver ' . $className . ' not found.');
}
}
}
第一個參數 $type
為快取要用的驅動器類型名稱,第二個參數 $setting
為驅動器的設定值。
$setting = $setting[$type];
$setting
是存放設定值的陣列,$type
為 reids
表示取得 Redis 資料庫要用的連線設定。
$className = 'Cache' . ucfirst(strtolower($type));
$classFilePatch = __DIR__ . '/' . $className . '.php';
if (file_exists($classFilePatch)) {
include_once $classFilePatch;
return new $className($setting);
} else {
throw new Exception('Driver ' . $className . ' not found.');
}
$type
和前輟字串 Cache
組成類別的名稱,並確認是否有這個類別的檔案,如果有的話則載入該 php 檔,並實例化該類別。找不到則丟出異常訊息。
工廠模式的實作除了筆者的寫法以外,還有工廠方法、抽象工廠等等,不同的寫法有不同的趣味,讀者有興趣的話也可買專書來好好研究喔。
明天筆者要介紹的是,依賴注入 (dependency injection),會把今天的範例再次改寫。我們明天見。
本文同步更新於 TerryL 部落格 PHP 設計模式:工廠 (Factory),歡迎前往討論。