現實生活中的工廠是做什麼用的呢?「麵包工廠」生產麵包、蛋糕、吐司等等。「煉油工廠」生產汽油、柴油等等。

而在物件導向程式設計中的「工廠」,則是生產物件。它是一個類別,專門用來生產其它類別實例化的物件。在物件導向程式設計中,工廠模式是最常見的模式,最常用在操作資料庫、快取、日誌等等。

我們直接以實戰中可能會遇到的情況來當範例吧!

實戰

情境

主管要我們寫一個快取的功能來存放後台 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 類別之後,就可以自由使用 setget 這兩個方法來存放資料。

但有假如有一天,主管要我們改用 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'],以此類推,及可不必修改類別的程式碼,就能一直擴充。

是不是很方便,是不是,是不是?

CacheFactory 說明

這支工廠的程式碼,筆者逐行說明:

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 是存放設定值的陣列,$typereids 表示取得 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),會把今天的範例再次改寫。我們明天見。

最後修改日期: 2022-02-01

作者

留言

撰寫回覆或留言

發佈留言必須填寫的電子郵件地址不會公開。