SlideShare a Scribd company logo
1 of 171
Download to read offline
購物車程式架構簡介
打造易擴充的系統架構
Jace Ju
從程式面來看購物車
從程式面來看購物車

       購物車



             包含了計算邏輯
從程式面來看購物車

          購物車




           收集瀏覽器回傳的資料


   加入商品
從程式面來看購物車

                  購物車

   Session
  Database
               將計算結果存到
                 Session




        加入商品
從程式面來看購物車

                    購物車

   Session
  Database
               如果要長期保留就
                改用 Database




        加入商品
從程式面來看購物車

                購物車

   Session
  Database
                        更新數量可以
                      透過 AJAX 或是直接
                      POST 後重新載入,
                       不要在前端直接用
                       JavaScript 計算

        加入商品   更新數量
從程式面來看購物車

                購物車

   Session
  Database             移除商品與更新數量
                        是相似的動作




        加入商品   更新數量   移除商品
從程式面來看購物車             最後進入金流並完成訂單




                購物車
                             結帳
   Session
  Database




        加入商品   更新數量   移除商品
從程式面來看購物車

                購物車
                             結帳
   Session
  Database
                       這些只是購物車的
                        基本功能而已...




        加入商品   更新數量   移除商品
從程式面來看購物車

                購物車
                             結帳
   Session
  Database
                        最可怕的是...




        加入商品   更新數量   移除商品
從程式面來看購物車

                購物車
                             結帳
   Session
  Database




        加入商品   更新數量   移除商品
促銷活動的種類
促銷活動的種類
• 折價券   應該沒有比這個更常見的了
促銷活動的種類
• 折價券
• 紅利兌換   數學要很好...
促銷活動的種類
• 折價券
• 紅利兌換
• 滿 XXX 免運費   還算簡單
促銷活動的種類
•   折價券
•   紅利兌換
•   滿 XXX 免運費
•   買 A 送 B 有點麻煩...如果要拿掉 A 時,也要同時拿掉 B
促銷活動的種類
•   折價券
•   紅利兌換
•   滿 XXX 免運費
•   買A送B
•   紅綠標商品       介面很難做
促銷活動的種類
•   折價券
•   紅利兌換
•   滿 XXX 免運費
•   買A送B
•   紅綠標商品
•   限量搶購        庫存很難搞
促銷活動的種類
•   折價券
•   紅利兌換
•   滿 XXX 免運費
•   買A送B
•   紅綠標商品
•   限量搶購
•   ...
        更多折磨工程師的行銷手段!!
每個人心裡的 OS
每個人心裡的 OS
     請交給我們一個超多功能的系統!
客戶
每個人心裡的 OS
      請交給我們一個超多功能的系統!
客戶


      請給我們一個安心購物的平台!
消費者
每個人心裡的 OS
      請交給我們一個超多功能的系統!
客戶


      請給我們一個安心購物的平台!
消費者



      請賜死...不對

工程師
每個人心裡的 OS
      請交給我們一個超多功能的系統!
客戶


      請給我們一個安心購物的平台!
消費者



      請賜給我們...一個容易擴充的購物車架構!

工程師
常見的購物車架構
常見的購物車架構



       購物車
常見的購物車架構
     儲存購物車中的商品資訊



  Session
               購物車
常見的購物車架構


  Session
                  購物車

 Database



      在資料庫中尋找商品
常見的購物車架構

                         功能 1

  Session
            購物車          功能 2

 Database
                         功能 3



              其他的購物車功能
如何讓購物車容易測試與擴充?
如何讓購物車容易測試與擴充?
• 主架構只保留最基本的計算邏輯

            簡化架構以求測試容易
如何讓購物車容易測試與擴充?
• 主架構只保留最基本的計算邏輯
▫ 抽離 Session 與 Database
如何讓購物車容易測試與擴充?
• 主架構只保留最基本的計算邏輯
▫ 抽離 Session 與 Database
▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin
如何讓購物車容易測試與擴充?
• 主架構只保留最基本的計算邏輯
 ▫ 抽離 Session 與 Database
 ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin
• 測試時
        儘量避免測試要依賴系統
如何讓購物車容易測試與擴充?
• 主架構只保留最基本的計算邏輯
 ▫ 抽離 Session 與 Database
 ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin
• 測試時
 ▫ Session 改用 Storage
如何讓購物車容易測試與擴充?
• 主架構只保留最基本的計算邏輯
 ▫ 抽離 Session 與 Database
 ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin
• 測試時
 ▫ Session 改用 Storage
 ▫ Database 改用 Dao
如何讓購物車容易測試與擴充?
• 主架構只保留最基本的計算邏輯
 ▫ 抽離 Session 與 Database
 ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin
• 測試時
 ▫ Session 改用 Storage
 ▫ Database 改用 Dao
 ▫ 主架構與 Plugin 分開測試
Storage
Storage
• 用最單純的 Array 來做為介質   避免自動測試工具
                       不支援 Session
Storage
• 用最單純的 Array 來做為介質
• 提供操作介質的介面       將操作抽象化
                就可以封裝介質的不同
Storage
• 用最單純的 Array 來做為介質
• 提供操作介質的介面
• 真正寫入 Session 的部份交給子類別
                  在實際運作時,再改用子類別
// Storage 類別長什麼樣子?
// Storage 類別長什麼樣子?

class Cart_Storage    {




}
// Storage 類別長什麼樣子?

class Cart_Storage implements ArrayAccess, Countable {

                             為了能像操作 array 一般地操作 Storage ,
                            所以實作 ArrayAccess 及 Countable 兩種介面




}
// Storage 類別長什麼樣子?

class Cart_Storage implements ArrayAccess, Countable {
    protected $_contanier = null;

                                       儲存介質




}
// Storage 類別長什麼樣子?

class Cart_Storage implements ArrayAccess, Countable {
    protected $_contanier = null;

    public function __construct()
    {
        $this->_container = array();
    }
                                       介質初始化,在測試時不需載入資料




}
// Storage 類別長什麼樣子?

class Cart_Storage implements ArrayAccess, Countable {
    protected $_contanier = null;

    public function __construct()
    {
        $this->_container = array();
    }

    public function clear()
    {
        $this->_container = array();
    }
                    清除介質




}
// Storage 類別長什麼樣子?

class Cart_Storage implements ArrayAccess, Countable {
    protected $_contanier = null;

    public function __construct()
    {
        $this->_container = array();
    }

    public function clear()
    {
        $this->_container = array();
    }

    public   function   offsetSet($offset, $value) {}
    public   function   offsetExists($offset) {}
    public   function   offsetUnset($offset) {}
    public   function   offsetGet($offset) {}

                             ArrayAccess 介面提供的方法,
                               在這裡加入操作介質的程式

}
// Storage 類別長什麼樣子?

class Cart_Storage implements ArrayAccess, Countable {
    protected $_contanier = null;

    public function __construct()
    {
        $this->_container = array();
    }

    public function clear()
    {
        $this->_container = array();
    }

    public   function   offsetSet($offset, $value) {}
    public   function   offsetExists($offset) {}
    public   function   offsetUnset($offset) {}
    public   function   offsetGet($offset) {}

    public function count()
    {
        return count($this->_container);
    }                                            Countable 介面提供的方法,
}
                                                    這裡回傳介質的大小
// Storage 的 Session 子類別長什麼樣子?
// Storage 的 Session 子類別長什麼樣子?

class Cart_Storage_Session       {




}
// Storage 的 Session 子類別長什麼樣子?

class Cart_Storage_Session extends Cart_Storage {


                                   繼承 Cart_Storage 以沿用處理介質的方法




}
// Storage 的 Session 子類別長什麼樣子?

class Cart_Storage_Session extends Cart_Storage {
    public function __construct()
    {
        if (!isset($_SESSION)) { session_start(); }
        if (isset($_SESSION['CART'])) {
            $this->_container = $_SESSION['CART'];
        } else {
            $this->_container = array();
        }
    }
                              物件建構時,從 Session 取回資料置入介質




}
// Storage 的 Session 子類別長什麼樣子?

class Cart_Storage_Session extends Cart_Storage {
    public function __construct()
    {
        if (!isset($_SESSION)) { session_start(); }
        if (isset($_SESSION['CART'])) {
            $this->_container = $_SESSION['CART'];
        } else {
            $this->_container = array();
        }
    }

    public function __destruct()
    {
        $_SESSION['CART'] = $this->_container;
    }

                                在物件消滅前,把介質存回 Session



}
// Storage 的 Session 子類別長什麼樣子?

class Cart_Storage_Session extends Cart_Storage {
    public function __construct()
    {
        if (!isset($_SESSION)) { session_start(); }
        if (isset($_SESSION['CART'])) {
            $this->_container = $_SESSION['CART'];
        } else {
            $this->_container = array();
        }
    }

    public function __destruct()
    {
        $_SESSION['CART'] = $this->_container;
    }

    public function clear()
    {
        $_SESSION['CART'] = $this->_container = array();
    }
}
                                覆蓋清除的方法
// 以加入購物車為例:
// 以加入購物車為例:

class Cart {




}
// 以加入購物車為例:

class Cart {
    protected $_storage = null;

    public function setStorage(Cart_Storage $storage)
    {
        $this->_storage = $storage;
    }
                                         利用 setStorage 方法讓我們可以
                                           從外部注入 Storage 物件




}
// 以加入購物車為例:

class Cart {
    protected $_storage = null;

    public function setStorage(Cart_Storage $storage)
    {
        $this->_storage = $storage;
    }

    public function addItem($itemKey)
    {
        $this->_storage[$itemKey] = array(
            'quantity' => 1,
        );                            購物車商品資訊
    }
}                                 改用 Storage 物件存放
// 以加入購物車為例:

class Cart {
    protected $_storage = null;

    public function setStorage(Cart_Storage $storage)
    {
        $this->_storage = $storage;
    }

    public function addItem($itemKey)
    {
        $this->_storage[$itemKey] = array(
            'quantity' => 1,
        );
    }
}

$cart = new Cart();

                      建立一個新購物車來測試
// 以加入購物車為例:

class Cart {
    protected $_storage = null;

    public function setStorage(Cart_Storage $storage)
    {
        $this->_storage = $storage;
    }

    public function addItem($itemKey)
    {
        $this->_storage[$itemKey] = array(
            'quantity' => 1,
        );
    }
}

$cart = new Cart();
$cart->setStorage(new Cart_Storage());

                                   用外部注入方式來使用 Storage 物件
// 以加入購物車為例:

class Cart {
    protected $_storage = null;

    public function setStorage(Cart_Storage $storage)
    {
        $this->_storage = $storage;
    }

    public function addItem($itemKey)
    {
        $this->_storage[$itemKey] = array(
            'quantity' => 1,
        );
    }
}

$cart = new Cart();
$cart->setStorage(new Cart_Storage_Session());

                                  也可以改用 Session Storage
Dao
Dao
• 目的是把 Database 與主架構做分離
                      這樣就不再依賴資料庫
Dao
• 目的是把 Database 與主架構做分離
• 僅提供 API ,不關心資料從何處取得
                     我們只關心回傳給購物
                      車的格式是否正確
Dao
• 目的是把 Database 與主架構做分離
• 僅提供 API ,不關心資料從何處取得
• 真正存取 Database 的部份交給子類別
                  在實際運作時,再改用子類別
// Dao 類別長什麼樣子?
// Dao 類別長什麼樣子?

class Cart_Dao {




}
// Dao 類別長什麼樣子?

class Cart_Dao {
    public function find($key)
    {
        return array();
    }

                      用來取得一筆資料用的方法


}
// Dao 類別長什麼樣子?

class Cart_Dao {
    public function find($key)
    {
        return array();
    }

    public function findAll($keyList)
    {
        return array();
    }
}
                          用來取得多筆資料用的方法
// Dao 的 Db 子類別長什麼樣子?
// Dao 的 Db 子類別長什麼樣子?

class Cart_Dao_Product   {




}
// Dao 的 Db 子類別長什麼樣子?

class Cart_Dao_Product extends Cart_Dao {


                                    繼承 Cart_Dao 類別




}
// Dao 的 Db 子類別長什麼樣子?

class Cart_Dao_Product extends Cart_Dao {
    public function find($key)
    {
                            覆寫父類別的 find 方法




    }




}
// Dao 的 Db 子類別長什麼樣子?

class Cart_Dao_Product extends Cart_Dao {
    public function find($key)
    {
        $productTable = new Products();
        $productData = array();
        if ($productRow =
                $productTable->fetchRow(array('sn = ?' => $key))) {
            $productData = $productRow->toArray();
        }
        return $productData;
    }                            找到 $key 對應的商品後就回傳




}
// Dao 的 Db 子類別長什麼樣子?

class Cart_Dao_Product extends Cart_Dao {
    public function find($key)
    {
        $productTable = new Products();
        $productData = array();
        if ($productRow =
                $productTable->fetchRow(array('sn = ?' => $key))) {
            $productData = $productRow->toArray();
        }
        return $productData;
    }

    public function findAll($keyList)
    {

                               覆寫父類別的 findAll 方法




    }
}
// Dao 的 Db 子類別長什麼樣子?

class Cart_Dao_Product extends Cart_Dao {
    public function find($key)
    {
        $productTable = new Products();
        $productData = array();
        if ($productRow =
                $productTable->fetchRow(array('sn = ?' => $key))) {
            $productData = $productRow->toArray();
        }
        return $productData;
    }

    public function findAll($keyList)
    {
        $productTable = new Products();
        $productRowset = $productTable->fetchRowsetBySn($keyList);


                                透過 Zend_Db_Table 來取得 Rowset


    }
}
// Dao 的 Db 子類別長什麼樣子?

class Cart_Dao_Product extends Cart_Dao {
    public function find($key)
    {
        $productTable = new Products();
        $productData = array();
        if ($productRow =
                $productTable->fetchRow(array('sn = ?' => $key))) {
            $productData = $productRow->toArray();
        }
        return $productData;
    }

    public function findAll($keyList)
    {
        $productTable = new Products();
        $productRowset = $productTable->fetchRowsetBySn($keyList);
        $resultDataList = array();
        foreach ($productRowset as $productRow) {
            $resultDataList[$productRow->sn] = $productRow->toArray();
        }
        return $resultDataList;
    }                                將取得的 Rowset 轉換為 Array
}
// 以加入購物車為例:
// 以加入購物車為例:

class Cart {




}
// 以加入購物車為例:

class Cart {
    protected $_dao = null;

    public function setDao(Cart_Dao $dao)
    {
        $this->_dao = $dao;
    }

                              利用 setDao 方法讓我們可以
                                從外部注入 Dao 物件




}
// 以加入購物車為例:

class Cart {
    protected $_dao = null;

    public function setDao(Cart_Dao $dao)
    {
        $this->_dao = $dao;
    }

    public function addItem($itemKey)
    {
        if ($productData = $this->_dao->find($itemKey)) {
            $this->_storage[$itemKey] = array(
                'quantity' => 1,
            );
        }                   有抓到商品資料再存到 Storage
                                             裡
    }
}                             沒有的話就做錯誤處理 (這邊省略)
// 以加入購物車為例:

class Cart {
    protected $_dao = null;

    public function setDao(Cart_Dao $dao)
    {
        $this->_dao = $dao;
    }

    public function addItem($itemKey)
    {
        if ($productData = $this->_dao->find($itemKey)) {
            $this->_storage[$itemKey] = array(
                'quantity' => 1,
            );
        }
    }
}

$cart = new Cart();

                       建立一個測試用的購物車物件
// 以加入購物車為例:

class Cart {
    protected $_dao = null;

    public function setDao(Cart_Dao $dao)
    {
        $this->_dao = $dao;
    }

    public function addItem($itemKey)
    {
        if ($productData = $this->_dao->find($itemKey)) {
            $this->_storage[$itemKey] = array(
                'quantity' => 1,
            );
        }
    }
}

$cart = new Cart();
$cart->setDao(new Cart_Dao());

                                       用外部注入的方式使用 Dao 物件
// 以加入購物車為例:

class Cart {
    protected $_dao = null;

    public function setDao(Cart_Dao $dao)
    {
        $this->_dao = $dao;
    }

    public function addItem($itemKey)
    {
        if ($productData = $this->_dao->find($itemKey)) {
            $this->_storage[$itemKey] = array(
                'quantity' => 1,
            );
        }
    }
}

$cart = new Cart();
$cart->setDao(new Cart_Dao_Product());

                                         改從資料庫抓取商品資料
Plugin 模式
Plugin 模式
• 在主程式裡呼叫 Plugin     流程還是由主程式控制
                   Plugin 只負責提供額外功能
Plugin 模式
                            複合模式
• 在主程式裡呼叫 Plugin
• 有點像 Template Method 和 Observer 的混血
Plugin 模式
• 在主程式裡呼叫 Plugin
• 有點像 Template Method 和 Observer 的混血
               定義好主流程,其他的實作透過
                子類別 Hook 方法來完成

                 Don't call me, I call you.
Plugin 模式
• 在主程式裡呼叫 Plugin
• 有點像 Template Method 和 Observer 的混血

         一旦有動作,會立即通知
            訂閱者做改變
Plugin 模式
• 在主程式裡呼叫 Plugin
• 有點像 Template Method 和 Observer 的混血
• 最好在設計階段就考慮進去
                        跟 AOP 很像,
                   但插入機制是寫死在程式裡;
                    而 AOP 則是可以動態加入
Plugin 模式
•   在主程式裡呼叫 Plugin
•   有點像 Template Method 和 Observer 的混血
•   最好在設計階段就考慮進去
•   與 PoEAA 定義的 Plugin Pattern 是不一樣的
           Patterns of Enterprise Application Architecture
                          by Martin Fowler
Plugin 模式
•   在主程式裡呼叫 Plugin
•   有點像 Template Method 和 Observer 的混血
•   最好在設計階段就考慮進去
•   與 PoEAA 定義的 Plugin Pattern 是不一樣的
                       在不同的運行環境,
                        執行不一樣的動作
Plugin 模式圖解
Plugin 模式圖解


購物車
Plugin 模式圖解

      用戶點選
購物車   頁面連結
      加入商品
Plugin 模式圖解

      用戶點選    尋找商品資料
購物車   頁面連結     並驗證是否
      加入商品      可以加入
Plugin 模式圖解

      用戶點選    尋找商品資料
購物車                    寫入 Session
      頁面連結     並驗證是否
                       或 Database
      加入商品      可以加入
Plugin 模式圖解


購物車
Plugin 模式圖解


購物車

             加入 Plugin 機制



 Plugin 1

      Plugin 2

            Plugin 3
Plugin 模式圖解

                 用戶點選
購物車              頁面連結
                 加入商品




 Plugin 1

      Plugin 2

            Plugin 3
Plugin 模式圖解

                            這時購物車會呼叫
                 用戶點選         Plugin
購物車              頁面連結
                 加入商品




 Plugin 1               加入購物車前

      Plugin 2           加入購物車前

            Plugin 3       加入購物車前
Plugin 模式圖解

                 用戶點選        尋找商品資料
購物車              頁面連結         並驗證是否
                 加入商品          可以加入




 Plugin 1               加入購物車前

      Plugin 2           加入購物車前

            Plugin 3       加入購物車前
Plugin 模式圖解

                                        購物車再次呼叫
                 用戶點選        尋找商品資料       Plugin
購物車              頁面連結         並驗證是否
                 加入商品          可以加入




 Plugin 1               加入購物車前      加入購物車後

      Plugin 2           加入購物車前       加入購物車後

            Plugin 3       加入購物車前      加入購物車後
Plugin 模式圖解                         繼續完成動作




                 用戶點選        尋找商品資料
購物車                                          寫入 Session
                 頁面連結         並驗證是否
                                             或 Database
                 加入商品          可以加入




 Plugin 1               加入購物車前       加入購物車後

      Plugin 2           加入購物車前        加入購物車後

            Plugin 3       加入購物車前        加入購物車後
// 以加入購物車為例:
// 以加入購物車為例:

class Cart {
               購物車類別




}
// 以加入購物車為例:

class Cart {


    public function addItem($itemKey) {
                                          加入購物車方法




        // 真正加入購物車
        (...略...)




    }
}
// 以加入購物車為例:

class Cart {
    protected $_pluginList = array();

    public function addItem($itemKey) {    用陣列來記住
                                          已載入的 Plugin



        // 真正加入購物車
        (...略...)




    }
}
// 以加入購物車為例:

class Cart {
    protected $_pluginList = array();
                                            在這裡插入
    public function addItem($itemKey) {   加入購物車前的程式
        // 加入購物車之前
        foreach ($this->_pluginList => $plugin) {
            $plugin->beforeAddItem($itemKey);
        }

        // 真正加入購物車
        (...略...)




    }
}
// 以加入購物車為例:

class Cart {
    protected $_pluginList = array();

    public function addItem($itemKey) {
        // 加入購物車之前
        foreach ($this->_pluginList => $plugin) {
            $plugin->beforeAddItem($itemKey);
        }

        // 真正加入購物車                            在這裡插入
        (...略...)
                                            加入購物車後的程式
        // 加入購物車之後
        foreach ($this->_pluginList => $plugin) {
            $plugin->afterAddItem($itemKey);
        }
    }
}
// Plugin 類別長什麼樣子??
// Plugin 類別長什麼樣子??

abstract class Cart_Plugin {

                               首先要有一個抽象類別




}
// Plugin 類別長什麼樣子??

abstract class Cart_Plugin {
    protected $_cart = null;

                               為了可直接取得購物車的資訊
                                 所以有一個購物車屬性




}
// Plugin 類別長什麼樣子??

abstract class Cart_Plugin {
    protected $_cart = null;

    public function __construct(Cart $cart) {
        $this->_cart = $cart;
    }
                                       購物車屬性可由建構式
                                         的參數帶入



}
// Plugin 類別長什麼樣子??

abstract class Cart_Plugin {
    protected $_cart = null;

    public function __construct(Cart $cart) {
        $this->_cart = $cart;
    }

    public   function beforeAddItem($itemKey) {}
    public   function afterAddItem($itemKey) {}
    public   function beforeUpdateItem($itemKey) {}
    public   function afterUpdateItem($itemKey) {}
    // ...   (其他方法)
}
                                             最後定義各項 Hook 方法
                                               (預設為空實作)
// 如何加入 Plugin ??
// 如何加入 Plugin ??

class Cart {
    protected $_pluginList = array();   剛剛的購物車類別




}
// 如何加入 Plugin ??

class Cart {
    protected $_pluginList = array();

    public function registerPlugin($name)
    {
                                  加入註冊 Plugin 的方法




    }
}
// 如何加入 Plugin ??

class Cart {
    protected $_pluginList = array();

    public function registerPlugin($name)
    {
        $name = ucfirst($name);
        $pluginName = 'Cart_Plugin_' . $name;

                                           轉換成實際 Plugin 的
                                              類別名稱

    }
}
// 如何加入 Plugin ??

class Cart {
    protected $_pluginList = array();

    public function registerPlugin($name)
    {
        $name = ucfirst($name);
        $pluginName = 'Cart_Plugin_' . $name;
        if (class_exists($pluginName, true)) {
            $this->_pluginList[$name] = new $pluginName($this);
        }

                 判斷 Plugin 類別是否存在?有的話就建立實體,
    }            並利用建構式把購物車物件傳給 Plugin 物件使用
}
// 如何加入 Plugin ??

class Cart {
    protected $_pluginList = array();

    public function registerPlugin($name)
    {
        $name = ucfirst($name);
        $pluginName = 'Cart_Plugin_' . $name;
        if (class_exists($pluginName, true)) {
            $this->_pluginList[$name] = new $pluginName($this);
        } else {
            throw new Exception("Plugin: $pluginName does not exist!");
        }
    }
}                            Plugin 類別不存在的話,就丟出異常
// 如何加入 Plugin ??

class Cart {
    protected $_pluginList = array();

    public function registerPlugin($name)
    {
        $name = ucfirst($name);
        $pluginName = 'Cart_Plugin_' . $name;
        if (class_exists($pluginName, true)) {
            $this->_pluginList[$name] = new $pluginName($this);
        } else {
            throw new Exception("Plugin: $pluginName does not exist!");
        }
    }
}

$cart = new Cart();

                      建立一個新購物車來測試
// 如何加入 Plugin ??

class Cart {
    protected $_pluginList = array();

    public function registerPlugin($name)
    {
        $name = ucfirst($name);
        $pluginName = 'Cart_Plugin_' . $name;
        if (class_exists($pluginName, true)) {
            $this->_pluginList[$name] = new $pluginName($this);
        } else {
            throw new Exception("Plugin: $pluginName does not exist!");
        }
    }
}

$cart = new Cart();
$cart->registerPlugin('plugin1');
$cart->registerPlugin('plugin2');

                          加入 Plugin
// 計算小計與總計金額的 Plugin 類別範例
// 計算小計與總計金額的 Plugin 類別範例

class Cart_Plugin_Total
{




}
// 計算小計與總計金額的 Plugin 類別範例

class Cart_Plugin_Total extends Cart_Plugin
{
                                    繼承抽象的 Cart_Plugin 類別




}
// 計算小計與總計金額的 Plugin 類別範例

class Cart_Plugin_Total extends Cart_Plugin
{
    protected $_total = 0;

                            用來記住總金額的屬性




}
// 計算小計與總計金額的 Plugin 類別範例

class Cart_Plugin_Total extends Cart_Plugin
{
    protected $_total = 0;

    public function beforeRefreshCart(&$iterator)
    {
        $this->_total = 0;
    }

                  更新購物車前先將總金額歸零




}
// 計算小計與總計金額的 Plugin 類別範例

class Cart_Plugin_Total extends Cart_Plugin
{
    protected $_total = 0;

    public function beforeRefreshCart(&$iterator)
    {
        $this->_total = 0;
    }

    public function afterRefreshItem($itemKey, &$iterator)
    {
        $itemData = $iterator[$itemKey];
        $iterator[$itemKey]['subtotal'] = $itemData['quantity'] *
$itemData['price'];
        $this->_total += $iterator[$itemKey]['subtotal'];
    }
                       在更新商品項目後,更新商品小計與總金額



}
// 計算小計與總計金額的 Plugin 類別範例

class Cart_Plugin_Total extends Cart_Plugin
{
    protected $_total = 0;

    public function beforeRefreshCart(&$iterator)
    {
        $this->_total = 0;
    }

    public function afterRefreshItem($itemKey, &$iterator)
    {
        $itemData = $iterator[$itemKey];
        $iterator[$itemKey]['subtotal'] = $itemData['quantity'] *
$itemData['price'];
        $this->_total += $iterator[$itemKey]['subtotal'];
    }

    public function getValue()
    {
        return $this->_total;
    }
}                           取得總金額
// 如何存取 Plugin 的值??
// 如何存取 Plugin 的值??

class Cart {




}
// 如何存取 Plugin 的值??

class Cart {
    public function __call($name, $args)
    {
               使用 __call 魔術方法來間接呼叫 plugin 的方法
                這樣一來就不用寫過多的 setter / getter




    }
}
// 如何存取 Plugin 的值??

class Cart {
    public function __call($name, $args)
    {
        // 設定值
        $name = preg_replace('/^set/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'setValue'), $args);
        }

              如果方法名為 set 開頭,就呼叫 plugin 的 setValue 方法




    }
}
// 如何存取 Plugin 的值??

class Cart {
    public function __call($name, $args)
    {
        // 設定值
        $name = preg_replace('/^set/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'setValue'), $args);
        }
        // 取得值
        $name = preg_replace('/^get/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'getValue'), $args);
        }
              如果方法名為 get 開頭,就呼叫 plugin 的 getValue 方法
    }
}
// 如何存取 Plugin 的值??

class Cart {
    public function __call($name, $args)
    {
        // 設定值
        $name = preg_replace('/^set/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'setValue'), $args);
        }
        // 取得值
        $name = preg_replace('/^get/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'getValue'), $args);
        }
        return null;
    }
}                  預設 return null
// 如何存取 Plugin 的值??

class Cart {
    public function __call($name, $args)
    {
        // 設定值
        $name = preg_replace('/^set/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'setValue'), $args);
        }
        // 取得值
        $name = preg_replace('/^get/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'getValue'), $args);
        }
        return null;
    }
}

$cart = new Cart();

                        建立測試用的購物車物件
// 如何存取 Plugin 的值??

class Cart {
    public function __call($name, $args)
    {
        // 設定值
        $name = preg_replace('/^set/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'setValue'), $args);
        }
        // 取得值
        $name = preg_replace('/^get/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'getValue'), $args);
        }
        return null;
    }
}

$cart = new Cart();
$cart->registerPlugin('total');

                                  註冊 Total Plugin
// 如何存取 Plugin 的值??

class Cart {
    public function __call($name, $args)
    {
        // 設定值
        $name = preg_replace('/^set/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'setValue'), $args);
        }
        // 取得值
        $name = preg_replace('/^get/', '', $name);
        if (array_key_exists($name, $this->_pluginList)) {
            return call_user_func_array(array($this->_pluginList[$name],
'getValue'), $args);
        }
        return null;
    }
}

$cart = new Cart();
$cart->registerPlugin('total');
echo $cart->getTotal();

                           取得 Total Plugin 的值
Plugin 模式的優點
Plugin 模式的優點
• 不用把過多的功能塞在一個類別上
               例如活動、金流、折價券等,
               可以分開獨立成不同 Plugin
Plugin 模式的優點
• 不用把過多的功能塞在一個類別上
• 新增移除功能非常容易
            像是系統要需要新增金流,
           只要加入新的 Plugin 與畫面即可
Plugin 模式的優點
• 不用把過多的功能塞在一個類別上
• 新增移除功能非常容易
• Plugin 的職責明確
     每個 Plugin 只做自己該做的,
        不會影響到其他程式
Plugin 模式的缺點
Plugin 模式的缺點
• 產生過多的類別
        因為每個功能都會需要一個類別
Plugin 模式的缺點
• 產生過多的類別
• 可能會產生重複的資源
        相同的資源,卻分別在不同的 Plugin 產生
Plugin 模式的缺點
• 產生過多的類別
• 可能會產生重複的資源
• 一般開發者沒有經過說明不易理解
        再加上如果對物件導向不夠熟悉,
         就更難對這樣的架構進行維護
Plugin 模式要注意的地方
Plugin 模式要注意的地方
• 流程細膩度
      切得越細, Plugin 可 Hook 的地方越多,
          但也會造成開發上的困擾
Plugin 模式要注意的地方
• 流程細膩度
• Plugin 安插的優先順序
            例如免運費是需要先知道總金額,
            所以總金額 Plugin 就要放在前面
Plugin 模式要注意的地方
• 流程細膩度
• Plugin 安插的優先順序
• Plugin 之間的相依性
             例如免運費是需要知道總金額,
           所以免運費 Plugin 相依於總金額 Plugin
Plugin 模式要注意的地方
•   流程細膩度
•   Plugin 安插的優先順序
•   Plugin 之間的相依性
•   Plugin 存取外部資源的部份也要分離
            可以用 Dao 來分離資料庫或是外部金流程
                  式,讓測試容易進行
測試購物車
測試購物車
• 用 PHPUnit 來做測試框架
        PHPUnit 是目前較常見的測試框架
測試購物車
• 用 PHPUnit 來做測試框架
• 只測試重要的類別
            因為測試購物車的目的是在於驗證複雜的計算
測試購物車
• 用 PHPUnit 來做測試框架
• 只測試重要的類別
• 使用假的 Stub 類別來做測試
           因為 PHPUnit 提供的 Mock 機制操作上太繁瑣
整合到 MVC
整合到 MVC
• 購物車本身與 MVC 架構無關
             任何 Framework 甚至純 PHP 都可以整合
整合到 MVC
• 購物車本身與 MVC 架構無關
• Cart 即 Model
        要注意資料庫存取的方式
整合到 MVC
• 購物車本身與 MVC 架構無關
• Cart 即 Model
• Controller 呼叫 Cart 方法
                 Controller 也只會看到 Cart
整合到 MVC
•   購物車本身與 MVC 架構無關
•   Cart 即 Model
•   Controller 呼叫 Cart 方法
•   View 取得 Cart 資料
                 所以畫面也可以客制化
問題與討論
問題與討論
• 為什麼 Plugin 需要包含 Cart 物件,但 Storage
  和 Dao 不需要?
問題與討論
• 為什麼 Plugin 需要包含 Cart 物件,但 Storage
  和 Dao 不需要?
• Plugin 模式的應用
問題與討論
• 為什麼 Plugin 需要包含 Cart 物件,但 Storage
  和 Dao 不需要?
• Plugin 模式的應用
 ▫ Zend Framework Dispatcher
問題與討論
• 為什麼 Plugin 需要包含 Cart 物件,但 Storage
  和 Dao 不需要?
• Plugin 模式的應用
 ▫ Zend Framework Dispatcher
 ▫ 是否可用在 Template 上?
問題與討論
• 為什麼 Plugin 需要包含 Cart 物件,但 Storage
  和 Dao 不需要?
• Plugin 模式的應用
 ▫ Zend Framework Dispatcher
 ▫ 是否可用在 Template 上?
• 現有架構可否加入 Plugin 模式?
問題與討論
• 為什麼 Plugin 需要包含 Cart 物件,但 Storage
  和 Dao 不需要?
• Plugin 模式的應用
 ▫ Zend Framework Dispatcher
 ▫ 是否可用在 Template 上?
• 現有架構可否加入 Plugin 模式?
• 實際應用時,效能的考量?
謝謝

More Related Content

What's hot

「クックパッドとZaimのグロースハックについて」
「クックパッドとZaimのグロースハックについて」「クックパッドとZaimのグロースハックについて」
「クックパッドとZaimのグロースハックについて」Kato Kyosuke
 
Designing Effective Tests with React Testing Library - React Day Berlin 2022
Designing Effective Tests with React Testing Library - React Day Berlin 2022Designing Effective Tests with React Testing Library - React Day Berlin 2022
Designing Effective Tests with React Testing Library - React Day Berlin 2022Josh Justice
 
Torry Harris API and Application Integration Governance Framework
Torry Harris API and Application Integration Governance FrameworkTorry Harris API and Application Integration Governance Framework
Torry Harris API and Application Integration Governance FrameworkShubaS4
 
Grails Simple Login
Grails Simple LoginGrails Simple Login
Grails Simple Loginmoniguna
 
ヤフーを支えるレコメンデーションエンジン #yjtc / YJTC21 A-5
ヤフーを支えるレコメンデーションエンジン #yjtc / YJTC21 A-5ヤフーを支えるレコメンデーションエンジン #yjtc / YJTC21 A-5
ヤフーを支えるレコメンデーションエンジン #yjtc / YJTC21 A-5Yahoo!デベロッパーネットワーク
 
Karate for Complex Web-Service API Testing by Peter Thomas
Karate for Complex Web-Service API Testing by Peter ThomasKarate for Complex Web-Service API Testing by Peter Thomas
Karate for Complex Web-Service API Testing by Peter Thomasintuit_india
 
The Many Ways to Test Your React App
The Many Ways to Test Your React AppThe Many Ways to Test Your React App
The Many Ways to Test Your React AppAll Things Open
 
Bounty Craft: Bug bounty reports how do they work, @sushihack presents at Nu...
Bounty Craft: Bug bounty reports  how do they work, @sushihack presents at Nu...Bounty Craft: Bug bounty reports  how do they work, @sushihack presents at Nu...
Bounty Craft: Bug bounty reports how do they work, @sushihack presents at Nu...HackerOne
 
Let's discover React and Redux with TypeScript
Let's discover React and Redux with TypeScriptLet's discover React and Redux with TypeScript
Let's discover React and Redux with TypeScriptMathieu Savy
 
Bootiful Development with Spring Boot and Angular - Connect.Tech 2017
 Bootiful Development with Spring Boot and Angular - Connect.Tech 2017 Bootiful Development with Spring Boot and Angular - Connect.Tech 2017
Bootiful Development with Spring Boot and Angular - Connect.Tech 2017Matt Raible
 
A brief primer on OpenAI's GPT-3
A brief primer on OpenAI's GPT-3A brief primer on OpenAI's GPT-3
A brief primer on OpenAI's GPT-3Ishan Jain
 
Generative AI For Everyone on AWS.pdf
Generative AI For Everyone on AWS.pdfGenerative AI For Everyone on AWS.pdf
Generative AI For Everyone on AWS.pdfManjunatha Sai
 
ChordアルゴリズムによるDHT入門
ChordアルゴリズムによるDHT入門ChordアルゴリズムによるDHT入門
ChordアルゴリズムによるDHT入門Hiroya Nagao
 
데이터분석 기반 게임봇과 작업장 탐지 (NDC 2017)
데이터분석 기반 게임봇과 작업장 탐지 (NDC 2017)데이터분석 기반 게임봇과 작업장 탐지 (NDC 2017)
데이터분석 기반 게임봇과 작업장 탐지 (NDC 2017)Korea University
 
Domain Modeling Made Functional (DevTernity 2022)
Domain Modeling Made Functional (DevTernity 2022)Domain Modeling Made Functional (DevTernity 2022)
Domain Modeling Made Functional (DevTernity 2022)Scott Wlaschin
 
[MTC 2021] Criando testes automatizados rápidos e robustos com cypress - Walm...
[MTC 2021] Criando testes automatizados rápidos e robustos com cypress - Walm...[MTC 2021] Criando testes automatizados rápidos e robustos com cypress - Walm...
[MTC 2021] Criando testes automatizados rápidos e robustos com cypress - Walm...minastestingconference
 

What's hot (20)

「クックパッドとZaimのグロースハックについて」
「クックパッドとZaimのグロースハックについて」「クックパッドとZaimのグロースハックについて」
「クックパッドとZaimのグロースハックについて」
 
Designing Effective Tests with React Testing Library - React Day Berlin 2022
Designing Effective Tests with React Testing Library - React Day Berlin 2022Designing Effective Tests with React Testing Library - React Day Berlin 2022
Designing Effective Tests with React Testing Library - React Day Berlin 2022
 
Torry Harris API and Application Integration Governance Framework
Torry Harris API and Application Integration Governance FrameworkTorry Harris API and Application Integration Governance Framework
Torry Harris API and Application Integration Governance Framework
 
Grails Simple Login
Grails Simple LoginGrails Simple Login
Grails Simple Login
 
ヤフーを支えるレコメンデーションエンジン #yjtc / YJTC21 A-5
ヤフーを支えるレコメンデーションエンジン #yjtc / YJTC21 A-5ヤフーを支えるレコメンデーションエンジン #yjtc / YJTC21 A-5
ヤフーを支えるレコメンデーションエンジン #yjtc / YJTC21 A-5
 
Karate for Complex Web-Service API Testing by Peter Thomas
Karate for Complex Web-Service API Testing by Peter ThomasKarate for Complex Web-Service API Testing by Peter Thomas
Karate for Complex Web-Service API Testing by Peter Thomas
 
The Many Ways to Test Your React App
The Many Ways to Test Your React AppThe Many Ways to Test Your React App
The Many Ways to Test Your React App
 
Bounty Craft: Bug bounty reports how do they work, @sushihack presents at Nu...
Bounty Craft: Bug bounty reports  how do they work, @sushihack presents at Nu...Bounty Craft: Bug bounty reports  how do they work, @sushihack presents at Nu...
Bounty Craft: Bug bounty reports how do they work, @sushihack presents at Nu...
 
Burp suite
Burp suiteBurp suite
Burp suite
 
Let's discover React and Redux with TypeScript
Let's discover React and Redux with TypeScriptLet's discover React and Redux with TypeScript
Let's discover React and Redux with TypeScript
 
Microservices at Mercari
Microservices at MercariMicroservices at Mercari
Microservices at Mercari
 
Bootiful Development with Spring Boot and Angular - Connect.Tech 2017
 Bootiful Development with Spring Boot and Angular - Connect.Tech 2017 Bootiful Development with Spring Boot and Angular - Connect.Tech 2017
Bootiful Development with Spring Boot and Angular - Connect.Tech 2017
 
Angular vs react
Angular vs reactAngular vs react
Angular vs react
 
A brief primer on OpenAI's GPT-3
A brief primer on OpenAI's GPT-3A brief primer on OpenAI's GPT-3
A brief primer on OpenAI's GPT-3
 
Generative AI For Everyone on AWS.pdf
Generative AI For Everyone on AWS.pdfGenerative AI For Everyone on AWS.pdf
Generative AI For Everyone on AWS.pdf
 
ChordアルゴリズムによるDHT入門
ChordアルゴリズムによるDHT入門ChordアルゴリズムによるDHT入門
ChordアルゴリズムによるDHT入門
 
데이터분석 기반 게임봇과 작업장 탐지 (NDC 2017)
데이터분석 기반 게임봇과 작업장 탐지 (NDC 2017)데이터분석 기반 게임봇과 작업장 탐지 (NDC 2017)
데이터분석 기반 게임봇과 작업장 탐지 (NDC 2017)
 
Domain Modeling Made Functional (DevTernity 2022)
Domain Modeling Made Functional (DevTernity 2022)Domain Modeling Made Functional (DevTernity 2022)
Domain Modeling Made Functional (DevTernity 2022)
 
SpringBootTest入門
SpringBootTest入門SpringBootTest入門
SpringBootTest入門
 
[MTC 2021] Criando testes automatizados rápidos e robustos com cypress - Walm...
[MTC 2021] Criando testes automatizados rápidos e robustos com cypress - Walm...[MTC 2021] Criando testes automatizados rápidos e robustos com cypress - Walm...
[MTC 2021] Criando testes automatizados rápidos e robustos com cypress - Walm...
 

Viewers also liked

PHP 物件導向 - 基礎觀念篇
PHP 物件導向 - 基礎觀念篇PHP 物件導向 - 基礎觀念篇
PHP 物件導向 - 基礎觀念篇Jace Ju
 
What happens in laravel 4 bootstraping
What happens in laravel 4 bootstrapingWhat happens in laravel 4 bootstraping
What happens in laravel 4 bootstrapingJace Ju
 
PHP 語法基礎與物件導向
PHP 語法基礎與物件導向PHP 語法基礎與物件導向
PHP 語法基礎與物件導向Shengyou Fan
 
開發環境建置
開發環境建置開發環境建置
開發環境建置Shengyou Fan
 
深入淺出 MVC
深入淺出 MVC深入淺出 MVC
深入淺出 MVCJace Ju
 
常見設計模式介紹
常見設計模式介紹常見設計模式介紹
常見設計模式介紹Jace Ju
 
jQuery 實戰經驗講座
jQuery 實戰經驗講座jQuery 實戰經驗講座
jQuery 實戰經驗講座Jace Ju
 
Composer 套件管理
Composer 套件管理Composer 套件管理
Composer 套件管理Shengyou Fan
 
如何打造 WebMVC Framework - 基礎概念篇
如何打造 WebMVC Framework - 基礎概念篇如何打造 WebMVC Framework - 基礎概念篇
如何打造 WebMVC Framework - 基礎概念篇Jace Ju
 
购物车图例:例子和最佳实践
购物车图例:例子和最佳实践购物车图例:例子和最佳实践
购物车图例:例子和最佳实践大璋 王
 
PHP 防駭 - 基礎觀念篇
PHP 防駭 - 基礎觀念篇PHP 防駭 - 基礎觀念篇
PHP 防駭 - 基礎觀念篇Jace Ju
 
Refactoring with Patterns in PHP
Refactoring with Patterns in PHPRefactoring with Patterns in PHP
Refactoring with Patterns in PHPJace Ju
 
Beginning PHPUnit
Beginning PHPUnitBeginning PHPUnit
Beginning PHPUnitJace Ju
 
Patterns in Library Design (套件設計裡的模式)
Patterns in Library Design (套件設計裡的模式)Patterns in Library Design (套件設計裡的模式)
Patterns in Library Design (套件設計裡的模式)Jace Ju
 
如何選擇合適的網站製作平台
如何選擇合適的網站製作平台如何選擇合適的網站製作平台
如何選擇合適的網站製作平台Hui Chieh Chiu
 
PHPUnit 入門介紹
PHPUnit 入門介紹PHPUnit 入門介紹
PHPUnit 入門介紹Jace Ju
 
Big Data Taiwan 2014 Track1-1: 群體智慧‧想像無限 ─ 精準推薦解決方案
Big Data Taiwan 2014 Track1-1: 群體智慧‧想像無限 ─ 精準推薦解決方案Big Data Taiwan 2014 Track1-1: 群體智慧‧想像無限 ─ 精準推薦解決方案
Big Data Taiwan 2014 Track1-1: 群體智慧‧想像無限 ─ 精準推薦解決方案Etu Solution
 
Open Cart 模組架站教學
Open Cart 模組架站教學Open Cart 模組架站教學
Open Cart 模組架站教學蘇姵欣 PeiSu
 
電子商務專題:課程簡介 Course Introduction
電子商務專題:課程簡介 Course Introduction電子商務專題:課程簡介 Course Introduction
電子商務專題:課程簡介 Course IntroductionRay Wang
 
Why to choose laravel framework
Why to choose laravel frameworkWhy to choose laravel framework
Why to choose laravel frameworkBo-Yi Wu
 

Viewers also liked (20)

PHP 物件導向 - 基礎觀念篇
PHP 物件導向 - 基礎觀念篇PHP 物件導向 - 基礎觀念篇
PHP 物件導向 - 基礎觀念篇
 
What happens in laravel 4 bootstraping
What happens in laravel 4 bootstrapingWhat happens in laravel 4 bootstraping
What happens in laravel 4 bootstraping
 
PHP 語法基礎與物件導向
PHP 語法基礎與物件導向PHP 語法基礎與物件導向
PHP 語法基礎與物件導向
 
開發環境建置
開發環境建置開發環境建置
開發環境建置
 
深入淺出 MVC
深入淺出 MVC深入淺出 MVC
深入淺出 MVC
 
常見設計模式介紹
常見設計模式介紹常見設計模式介紹
常見設計模式介紹
 
jQuery 實戰經驗講座
jQuery 實戰經驗講座jQuery 實戰經驗講座
jQuery 實戰經驗講座
 
Composer 套件管理
Composer 套件管理Composer 套件管理
Composer 套件管理
 
如何打造 WebMVC Framework - 基礎概念篇
如何打造 WebMVC Framework - 基礎概念篇如何打造 WebMVC Framework - 基礎概念篇
如何打造 WebMVC Framework - 基礎概念篇
 
购物车图例:例子和最佳实践
购物车图例:例子和最佳实践购物车图例:例子和最佳实践
购物车图例:例子和最佳实践
 
PHP 防駭 - 基礎觀念篇
PHP 防駭 - 基礎觀念篇PHP 防駭 - 基礎觀念篇
PHP 防駭 - 基礎觀念篇
 
Refactoring with Patterns in PHP
Refactoring with Patterns in PHPRefactoring with Patterns in PHP
Refactoring with Patterns in PHP
 
Beginning PHPUnit
Beginning PHPUnitBeginning PHPUnit
Beginning PHPUnit
 
Patterns in Library Design (套件設計裡的模式)
Patterns in Library Design (套件設計裡的模式)Patterns in Library Design (套件設計裡的模式)
Patterns in Library Design (套件設計裡的模式)
 
如何選擇合適的網站製作平台
如何選擇合適的網站製作平台如何選擇合適的網站製作平台
如何選擇合適的網站製作平台
 
PHPUnit 入門介紹
PHPUnit 入門介紹PHPUnit 入門介紹
PHPUnit 入門介紹
 
Big Data Taiwan 2014 Track1-1: 群體智慧‧想像無限 ─ 精準推薦解決方案
Big Data Taiwan 2014 Track1-1: 群體智慧‧想像無限 ─ 精準推薦解決方案Big Data Taiwan 2014 Track1-1: 群體智慧‧想像無限 ─ 精準推薦解決方案
Big Data Taiwan 2014 Track1-1: 群體智慧‧想像無限 ─ 精準推薦解決方案
 
Open Cart 模組架站教學
Open Cart 模組架站教學Open Cart 模組架站教學
Open Cart 模組架站教學
 
電子商務專題:課程簡介 Course Introduction
電子商務專題:課程簡介 Course Introduction電子商務專題:課程簡介 Course Introduction
電子商務專題:課程簡介 Course Introduction
 
Why to choose laravel framework
Why to choose laravel frameworkWhy to choose laravel framework
Why to choose laravel framework
 

Similar to 購物車程式架構簡介

WooCommerce CRUD and Data Store by Akeda Bagus
WooCommerce CRUD and Data Store by Akeda BagusWooCommerce CRUD and Data Store by Akeda Bagus
WooCommerce CRUD and Data Store by Akeda BagusWordCamp Indonesia
 
Symfony World - Symfony components and design patterns
Symfony World - Symfony components and design patternsSymfony World - Symfony components and design patterns
Symfony World - Symfony components and design patternsŁukasz Chruściel
 
Caching and Scaling WordPress using Fragment Caching
Caching and Scaling WordPress using Fragment CachingCaching and Scaling WordPress using Fragment Caching
Caching and Scaling WordPress using Fragment CachingErick Hitter
 
Speed Things Up with Transients
Speed Things Up with TransientsSpeed Things Up with Transients
Speed Things Up with TransientsCliff Seal
 
WordPress as an application framework
WordPress as an application frameworkWordPress as an application framework
WordPress as an application frameworkDustin Filippini
 
Why is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenariosWhy is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenariosDivante
 
Doctrine For Beginners
Doctrine For BeginnersDoctrine For Beginners
Doctrine For BeginnersJonathan Wage
 
Decouple Your Code For Reusability (International PHP Conference / IPC 2008)
Decouple Your Code For Reusability (International PHP Conference / IPC 2008)Decouple Your Code For Reusability (International PHP Conference / IPC 2008)
Decouple Your Code For Reusability (International PHP Conference / IPC 2008)Fabien Potencier
 
[PHPCon 2023] “Kto to pisał?!... a, to ja.”, czyli sposoby żeby znienawidzić ...
[PHPCon 2023] “Kto to pisał?!... a, to ja.”, czyli sposoby żeby znienawidzić ...[PHPCon 2023] “Kto to pisał?!... a, to ja.”, czyli sposoby żeby znienawidzić ...
[PHPCon 2023] “Kto to pisał?!... a, to ja.”, czyli sposoby żeby znienawidzić ...Mateusz Zalewski
 
Chaining and function composition with lodash / underscore
Chaining and function composition with lodash / underscoreChaining and function composition with lodash / underscore
Chaining and function composition with lodash / underscoreNicolas Carlo
 
Writing extensible Word press-plugins
Writing extensible Word press-pluginsWriting extensible Word press-plugins
Writing extensible Word press-pluginsAllenSnook
 
WordCamp San Francisco 2011: Transients, Caching, and the Complexities of Mul...
WordCamp San Francisco 2011: Transients, Caching, and the Complexities of Mul...WordCamp San Francisco 2011: Transients, Caching, and the Complexities of Mul...
WordCamp San Francisco 2011: Transients, Caching, and the Complexities of Mul...andrewnacin
 
Dependency Injection with PHP and PHP 5.3
Dependency Injection with PHP and PHP 5.3Dependency Injection with PHP and PHP 5.3
Dependency Injection with PHP and PHP 5.3Fabien Potencier
 
Dependency injection in Drupal 8
Dependency injection in Drupal 8Dependency injection in Drupal 8
Dependency injection in Drupal 8Alexei Gorobets
 
Dependency Injection with PHP 5.3
Dependency Injection with PHP 5.3Dependency Injection with PHP 5.3
Dependency Injection with PHP 5.3Fabien Potencier
 
Chaining et composition de fonctions avec lodash / underscore
Chaining et composition de fonctions avec lodash / underscoreChaining et composition de fonctions avec lodash / underscore
Chaining et composition de fonctions avec lodash / underscoreNicolas Carlo
 
Dependency Injection - ConFoo 2010
Dependency Injection - ConFoo 2010Dependency Injection - ConFoo 2010
Dependency Injection - ConFoo 2010Fabien Potencier
 

Similar to 購物車程式架構簡介 (20)

WooCommerce CRUD and Data Store by Akeda Bagus
WooCommerce CRUD and Data Store by Akeda BagusWooCommerce CRUD and Data Store by Akeda Bagus
WooCommerce CRUD and Data Store by Akeda Bagus
 
Symfony World - Symfony components and design patterns
Symfony World - Symfony components and design patternsSymfony World - Symfony components and design patterns
Symfony World - Symfony components and design patterns
 
Caching and Scaling WordPress using Fragment Caching
Caching and Scaling WordPress using Fragment CachingCaching and Scaling WordPress using Fragment Caching
Caching and Scaling WordPress using Fragment Caching
 
Speed Things Up with Transients
Speed Things Up with TransientsSpeed Things Up with Transients
Speed Things Up with Transients
 
Event Sourcing with php
Event Sourcing with phpEvent Sourcing with php
Event Sourcing with php
 
WordPress as an application framework
WordPress as an application frameworkWordPress as an application framework
WordPress as an application framework
 
Why is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenariosWhy is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenarios
 
Doctrine For Beginners
Doctrine For BeginnersDoctrine For Beginners
Doctrine For Beginners
 
Decouple Your Code For Reusability (International PHP Conference / IPC 2008)
Decouple Your Code For Reusability (International PHP Conference / IPC 2008)Decouple Your Code For Reusability (International PHP Conference / IPC 2008)
Decouple Your Code For Reusability (International PHP Conference / IPC 2008)
 
[PHPCon 2023] “Kto to pisał?!... a, to ja.”, czyli sposoby żeby znienawidzić ...
[PHPCon 2023] “Kto to pisał?!... a, to ja.”, czyli sposoby żeby znienawidzić ...[PHPCon 2023] “Kto to pisał?!... a, to ja.”, czyli sposoby żeby znienawidzić ...
[PHPCon 2023] “Kto to pisał?!... a, to ja.”, czyli sposoby żeby znienawidzić ...
 
Chaining and function composition with lodash / underscore
Chaining and function composition with lodash / underscoreChaining and function composition with lodash / underscore
Chaining and function composition with lodash / underscore
 
Writing extensible Word press-plugins
Writing extensible Word press-pluginsWriting extensible Word press-plugins
Writing extensible Word press-plugins
 
WordCamp San Francisco 2011: Transients, Caching, and the Complexities of Mul...
WordCamp San Francisco 2011: Transients, Caching, and the Complexities of Mul...WordCamp San Francisco 2011: Transients, Caching, and the Complexities of Mul...
WordCamp San Francisco 2011: Transients, Caching, and the Complexities of Mul...
 
Dependency Injection with PHP and PHP 5.3
Dependency Injection with PHP and PHP 5.3Dependency Injection with PHP and PHP 5.3
Dependency Injection with PHP and PHP 5.3
 
Dependency injection in Drupal 8
Dependency injection in Drupal 8Dependency injection in Drupal 8
Dependency injection in Drupal 8
 
Dependency Injection with PHP 5.3
Dependency Injection with PHP 5.3Dependency Injection with PHP 5.3
Dependency Injection with PHP 5.3
 
Symfony 1, mi viejo amigo
Symfony 1, mi viejo amigoSymfony 1, mi viejo amigo
Symfony 1, mi viejo amigo
 
Chaining et composition de fonctions avec lodash / underscore
Chaining et composition de fonctions avec lodash / underscoreChaining et composition de fonctions avec lodash / underscore
Chaining et composition de fonctions avec lodash / underscore
 
Dependency Injection - ConFoo 2010
Dependency Injection - ConFoo 2010Dependency Injection - ConFoo 2010
Dependency Injection - ConFoo 2010
 
Dependency Injection
Dependency InjectionDependency Injection
Dependency Injection
 

Recently uploaded

The Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsThe Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsPixlogix Infotech
 
From Family Reminiscence to Scholarly Archive .
From Family Reminiscence to Scholarly Archive .From Family Reminiscence to Scholarly Archive .
From Family Reminiscence to Scholarly Archive .Alan Dix
 
SAP Build Work Zone - Overview L2-L3.pptx
SAP Build Work Zone - Overview L2-L3.pptxSAP Build Work Zone - Overview L2-L3.pptx
SAP Build Work Zone - Overview L2-L3.pptxNavinnSomaal
 
Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Mattias Andersson
 
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxMerck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxLoriGlavin3
 
H2O.ai CEO/Founder: Sri Ambati Keynote at Wells Fargo Day
H2O.ai CEO/Founder: Sri Ambati Keynote at Wells Fargo DayH2O.ai CEO/Founder: Sri Ambati Keynote at Wells Fargo Day
H2O.ai CEO/Founder: Sri Ambati Keynote at Wells Fargo DaySri Ambati
 
Search Engine Optimization SEO PDF for 2024.pdf
Search Engine Optimization SEO PDF for 2024.pdfSearch Engine Optimization SEO PDF for 2024.pdf
Search Engine Optimization SEO PDF for 2024.pdfRankYa
 
Advanced Computer Architecture – An Introduction
Advanced Computer Architecture – An IntroductionAdvanced Computer Architecture – An Introduction
Advanced Computer Architecture – An IntroductionDilum Bandara
 
Ensuring Technical Readiness For Copilot in Microsoft 365
Ensuring Technical Readiness For Copilot in Microsoft 365Ensuring Technical Readiness For Copilot in Microsoft 365
Ensuring Technical Readiness For Copilot in Microsoft 3652toLead Limited
 
What's New in Teams Calling, Meetings and Devices March 2024
What's New in Teams Calling, Meetings and Devices March 2024What's New in Teams Calling, Meetings and Devices March 2024
What's New in Teams Calling, Meetings and Devices March 2024Stephanie Beckett
 
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks..."LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...Fwdays
 
"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr BaganFwdays
 
Story boards and shot lists for my a level piece
Story boards and shot lists for my a level pieceStory boards and shot lists for my a level piece
Story boards and shot lists for my a level piececharlottematthew16
 
Human Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsHuman Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsMark Billinghurst
 
Scanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsScanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsRizwan Syed
 
Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!Commit University
 
TeamStation AI System Report LATAM IT Salaries 2024
TeamStation AI System Report LATAM IT Salaries 2024TeamStation AI System Report LATAM IT Salaries 2024
TeamStation AI System Report LATAM IT Salaries 2024Lonnie McRorey
 
DevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenDevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenHervé Boutemy
 
Powerpoint exploring the locations used in television show Time Clash
Powerpoint exploring the locations used in television show Time ClashPowerpoint exploring the locations used in television show Time Clash
Powerpoint exploring the locations used in television show Time Clashcharlottematthew16
 
Unleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubUnleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubKalema Edgar
 

Recently uploaded (20)

The Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsThe Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and Cons
 
From Family Reminiscence to Scholarly Archive .
From Family Reminiscence to Scholarly Archive .From Family Reminiscence to Scholarly Archive .
From Family Reminiscence to Scholarly Archive .
 
SAP Build Work Zone - Overview L2-L3.pptx
SAP Build Work Zone - Overview L2-L3.pptxSAP Build Work Zone - Overview L2-L3.pptx
SAP Build Work Zone - Overview L2-L3.pptx
 
Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?
 
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxMerck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
 
H2O.ai CEO/Founder: Sri Ambati Keynote at Wells Fargo Day
H2O.ai CEO/Founder: Sri Ambati Keynote at Wells Fargo DayH2O.ai CEO/Founder: Sri Ambati Keynote at Wells Fargo Day
H2O.ai CEO/Founder: Sri Ambati Keynote at Wells Fargo Day
 
Search Engine Optimization SEO PDF for 2024.pdf
Search Engine Optimization SEO PDF for 2024.pdfSearch Engine Optimization SEO PDF for 2024.pdf
Search Engine Optimization SEO PDF for 2024.pdf
 
Advanced Computer Architecture – An Introduction
Advanced Computer Architecture – An IntroductionAdvanced Computer Architecture – An Introduction
Advanced Computer Architecture – An Introduction
 
Ensuring Technical Readiness For Copilot in Microsoft 365
Ensuring Technical Readiness For Copilot in Microsoft 365Ensuring Technical Readiness For Copilot in Microsoft 365
Ensuring Technical Readiness For Copilot in Microsoft 365
 
What's New in Teams Calling, Meetings and Devices March 2024
What's New in Teams Calling, Meetings and Devices March 2024What's New in Teams Calling, Meetings and Devices March 2024
What's New in Teams Calling, Meetings and Devices March 2024
 
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks..."LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
"LLMs for Python Engineers: Advanced Data Analysis and Semantic Kernel",Oleks...
 
"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan"ML in Production",Oleksandr Bagan
"ML in Production",Oleksandr Bagan
 
Story boards and shot lists for my a level piece
Story boards and shot lists for my a level pieceStory boards and shot lists for my a level piece
Story boards and shot lists for my a level piece
 
Human Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR SystemsHuman Factors of XR: Using Human Factors to Design XR Systems
Human Factors of XR: Using Human Factors to Design XR Systems
 
Scanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsScanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL Certs
 
Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!
 
TeamStation AI System Report LATAM IT Salaries 2024
TeamStation AI System Report LATAM IT Salaries 2024TeamStation AI System Report LATAM IT Salaries 2024
TeamStation AI System Report LATAM IT Salaries 2024
 
DevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenDevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache Maven
 
Powerpoint exploring the locations used in television show Time Clash
Powerpoint exploring the locations used in television show Time ClashPowerpoint exploring the locations used in television show Time Clash
Powerpoint exploring the locations used in television show Time Clash
 
Unleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding ClubUnleash Your Potential - Namagunga Girls Coding Club
Unleash Your Potential - Namagunga Girls Coding Club
 

購物車程式架構簡介

  • 3. 從程式面來看購物車 購物車 包含了計算邏輯
  • 4. 從程式面來看購物車 購物車 收集瀏覽器回傳的資料 加入商品
  • 5. 從程式面來看購物車 購物車 Session Database 將計算結果存到 Session 加入商品
  • 6. 從程式面來看購物車 購物車 Session Database 如果要長期保留就 改用 Database 加入商品
  • 7. 從程式面來看購物車 購物車 Session Database 更新數量可以 透過 AJAX 或是直接 POST 後重新載入, 不要在前端直接用 JavaScript 計算 加入商品 更新數量
  • 8. 從程式面來看購物車 購物車 Session Database 移除商品與更新數量 是相似的動作 加入商品 更新數量 移除商品
  • 9. 從程式面來看購物車 最後進入金流並完成訂單 購物車 結帳 Session Database 加入商品 更新數量 移除商品
  • 10. 從程式面來看購物車 購物車 結帳 Session Database 這些只是購物車的 基本功能而已... 加入商品 更新數量 移除商品
  • 11. 從程式面來看購物車 購物車 結帳 Session Database 最可怕的是... 加入商品 更新數量 移除商品
  • 12. 從程式面來看購物車 購物車 結帳 Session Database 加入商品 更新數量 移除商品
  • 14. 促銷活動的種類 • 折價券 應該沒有比這個更常見的了
  • 17. 促銷活動的種類 • 折價券 • 紅利兌換 • 滿 XXX 免運費 • 買 A 送 B 有點麻煩...如果要拿掉 A 時,也要同時拿掉 B
  • 18. 促銷活動的種類 • 折價券 • 紅利兌換 • 滿 XXX 免運費 • 買A送B • 紅綠標商品 介面很難做
  • 19. 促銷活動的種類 • 折價券 • 紅利兌換 • 滿 XXX 免運費 • 買A送B • 紅綠標商品 • 限量搶購 庫存很難搞
  • 20. 促銷活動的種類 • 折價券 • 紅利兌換 • 滿 XXX 免運費 • 買A送B • 紅綠標商品 • 限量搶購 • ... 更多折磨工程師的行銷手段!!
  • 22. 每個人心裡的 OS 請交給我們一個超多功能的系統! 客戶
  • 23. 每個人心裡的 OS 請交給我們一個超多功能的系統! 客戶 請給我們一個安心購物的平台! 消費者
  • 24. 每個人心裡的 OS 請交給我們一個超多功能的系統! 客戶 請給我們一個安心購物的平台! 消費者 請賜死...不對 工程師
  • 25. 每個人心裡的 OS 請交給我們一個超多功能的系統! 客戶 請給我們一個安心購物的平台! 消費者 請賜給我們...一個容易擴充的購物車架構! 工程師
  • 28. 常見的購物車架構 儲存購物車中的商品資訊 Session 購物車
  • 29. 常見的購物車架構 Session 購物車 Database 在資料庫中尋找商品
  • 30. 常見的購物車架構 功能 1 Session 購物車 功能 2 Database 功能 3 其他的購物車功能
  • 34. 如何讓購物車容易測試與擴充? • 主架構只保留最基本的計算邏輯 ▫ 抽離 Session 與 Database ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin
  • 35. 如何讓購物車容易測試與擴充? • 主架構只保留最基本的計算邏輯 ▫ 抽離 Session 與 Database ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin • 測試時 儘量避免測試要依賴系統
  • 36. 如何讓購物車容易測試與擴充? • 主架構只保留最基本的計算邏輯 ▫ 抽離 Session 與 Database ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin • 測試時 ▫ Session 改用 Storage
  • 37. 如何讓購物車容易測試與擴充? • 主架構只保留最基本的計算邏輯 ▫ 抽離 Session 與 Database ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin • 測試時 ▫ Session 改用 Storage ▫ Database 改用 Dao
  • 38. 如何讓購物車容易測試與擴充? • 主架構只保留最基本的計算邏輯 ▫ 抽離 Session 與 Database ▫ 加入可擴充的機制,讓經常變動的功能變成 Plugin • 測試時 ▫ Session 改用 Storage ▫ Database 改用 Dao ▫ 主架構與 Plugin 分開測試
  • 40. Storage • 用最單純的 Array 來做為介質 避免自動測試工具 不支援 Session
  • 41. Storage • 用最單純的 Array 來做為介質 • 提供操作介質的介面 將操作抽象化 就可以封裝介質的不同
  • 42. Storage • 用最單純的 Array 來做為介質 • 提供操作介質的介面 • 真正寫入 Session 的部份交給子類別 在實際運作時,再改用子類別
  • 45. // Storage 類別長什麼樣子? class Cart_Storage implements ArrayAccess, Countable { 為了能像操作 array 一般地操作 Storage , 所以實作 ArrayAccess 及 Countable 兩種介面 }
  • 46. // Storage 類別長什麼樣子? class Cart_Storage implements ArrayAccess, Countable { protected $_contanier = null; 儲存介質 }
  • 47. // Storage 類別長什麼樣子? class Cart_Storage implements ArrayAccess, Countable { protected $_contanier = null; public function __construct() { $this->_container = array(); } 介質初始化,在測試時不需載入資料 }
  • 48. // Storage 類別長什麼樣子? class Cart_Storage implements ArrayAccess, Countable { protected $_contanier = null; public function __construct() { $this->_container = array(); } public function clear() { $this->_container = array(); } 清除介質 }
  • 49. // Storage 類別長什麼樣子? class Cart_Storage implements ArrayAccess, Countable { protected $_contanier = null; public function __construct() { $this->_container = array(); } public function clear() { $this->_container = array(); } public function offsetSet($offset, $value) {} public function offsetExists($offset) {} public function offsetUnset($offset) {} public function offsetGet($offset) {} ArrayAccess 介面提供的方法, 在這裡加入操作介質的程式 }
  • 50. // Storage 類別長什麼樣子? class Cart_Storage implements ArrayAccess, Countable { protected $_contanier = null; public function __construct() { $this->_container = array(); } public function clear() { $this->_container = array(); } public function offsetSet($offset, $value) {} public function offsetExists($offset) {} public function offsetUnset($offset) {} public function offsetGet($offset) {} public function count() { return count($this->_container); } Countable 介面提供的方法, } 這裡回傳介質的大小
  • 51. // Storage 的 Session 子類別長什麼樣子?
  • 52. // Storage 的 Session 子類別長什麼樣子? class Cart_Storage_Session { }
  • 53. // Storage 的 Session 子類別長什麼樣子? class Cart_Storage_Session extends Cart_Storage { 繼承 Cart_Storage 以沿用處理介質的方法 }
  • 54. // Storage 的 Session 子類別長什麼樣子? class Cart_Storage_Session extends Cart_Storage { public function __construct() { if (!isset($_SESSION)) { session_start(); } if (isset($_SESSION['CART'])) { $this->_container = $_SESSION['CART']; } else { $this->_container = array(); } } 物件建構時,從 Session 取回資料置入介質 }
  • 55. // Storage 的 Session 子類別長什麼樣子? class Cart_Storage_Session extends Cart_Storage { public function __construct() { if (!isset($_SESSION)) { session_start(); } if (isset($_SESSION['CART'])) { $this->_container = $_SESSION['CART']; } else { $this->_container = array(); } } public function __destruct() { $_SESSION['CART'] = $this->_container; } 在物件消滅前,把介質存回 Session }
  • 56. // Storage 的 Session 子類別長什麼樣子? class Cart_Storage_Session extends Cart_Storage { public function __construct() { if (!isset($_SESSION)) { session_start(); } if (isset($_SESSION['CART'])) { $this->_container = $_SESSION['CART']; } else { $this->_container = array(); } } public function __destruct() { $_SESSION['CART'] = $this->_container; } public function clear() { $_SESSION['CART'] = $this->_container = array(); } } 覆蓋清除的方法
  • 59. // 以加入購物車為例: class Cart { protected $_storage = null; public function setStorage(Cart_Storage $storage) { $this->_storage = $storage; } 利用 setStorage 方法讓我們可以 從外部注入 Storage 物件 }
  • 60. // 以加入購物車為例: class Cart { protected $_storage = null; public function setStorage(Cart_Storage $storage) { $this->_storage = $storage; } public function addItem($itemKey) { $this->_storage[$itemKey] = array( 'quantity' => 1, ); 購物車商品資訊 } } 改用 Storage 物件存放
  • 61. // 以加入購物車為例: class Cart { protected $_storage = null; public function setStorage(Cart_Storage $storage) { $this->_storage = $storage; } public function addItem($itemKey) { $this->_storage[$itemKey] = array( 'quantity' => 1, ); } } $cart = new Cart(); 建立一個新購物車來測試
  • 62. // 以加入購物車為例: class Cart { protected $_storage = null; public function setStorage(Cart_Storage $storage) { $this->_storage = $storage; } public function addItem($itemKey) { $this->_storage[$itemKey] = array( 'quantity' => 1, ); } } $cart = new Cart(); $cart->setStorage(new Cart_Storage()); 用外部注入方式來使用 Storage 物件
  • 63. // 以加入購物車為例: class Cart { protected $_storage = null; public function setStorage(Cart_Storage $storage) { $this->_storage = $storage; } public function addItem($itemKey) { $this->_storage[$itemKey] = array( 'quantity' => 1, ); } } $cart = new Cart(); $cart->setStorage(new Cart_Storage_Session()); 也可以改用 Session Storage
  • 64. Dao
  • 65. Dao • 目的是把 Database 與主架構做分離 這樣就不再依賴資料庫
  • 66. Dao • 目的是把 Database 與主架構做分離 • 僅提供 API ,不關心資料從何處取得 我們只關心回傳給購物 車的格式是否正確
  • 67. Dao • 目的是把 Database 與主架構做分離 • 僅提供 API ,不關心資料從何處取得 • 真正存取 Database 的部份交給子類別 在實際運作時,再改用子類別
  • 70. // Dao 類別長什麼樣子? class Cart_Dao { public function find($key) { return array(); } 用來取得一筆資料用的方法 }
  • 71. // Dao 類別長什麼樣子? class Cart_Dao { public function find($key) { return array(); } public function findAll($keyList) { return array(); } } 用來取得多筆資料用的方法
  • 72. // Dao 的 Db 子類別長什麼樣子?
  • 73. // Dao 的 Db 子類別長什麼樣子? class Cart_Dao_Product { }
  • 74. // Dao 的 Db 子類別長什麼樣子? class Cart_Dao_Product extends Cart_Dao { 繼承 Cart_Dao 類別 }
  • 75. // Dao 的 Db 子類別長什麼樣子? class Cart_Dao_Product extends Cart_Dao { public function find($key) { 覆寫父類別的 find 方法 } }
  • 76. // Dao 的 Db 子類別長什麼樣子? class Cart_Dao_Product extends Cart_Dao { public function find($key) { $productTable = new Products(); $productData = array(); if ($productRow = $productTable->fetchRow(array('sn = ?' => $key))) { $productData = $productRow->toArray(); } return $productData; } 找到 $key 對應的商品後就回傳 }
  • 77. // Dao 的 Db 子類別長什麼樣子? class Cart_Dao_Product extends Cart_Dao { public function find($key) { $productTable = new Products(); $productData = array(); if ($productRow = $productTable->fetchRow(array('sn = ?' => $key))) { $productData = $productRow->toArray(); } return $productData; } public function findAll($keyList) { 覆寫父類別的 findAll 方法 } }
  • 78. // Dao 的 Db 子類別長什麼樣子? class Cart_Dao_Product extends Cart_Dao { public function find($key) { $productTable = new Products(); $productData = array(); if ($productRow = $productTable->fetchRow(array('sn = ?' => $key))) { $productData = $productRow->toArray(); } return $productData; } public function findAll($keyList) { $productTable = new Products(); $productRowset = $productTable->fetchRowsetBySn($keyList); 透過 Zend_Db_Table 來取得 Rowset } }
  • 79. // Dao 的 Db 子類別長什麼樣子? class Cart_Dao_Product extends Cart_Dao { public function find($key) { $productTable = new Products(); $productData = array(); if ($productRow = $productTable->fetchRow(array('sn = ?' => $key))) { $productData = $productRow->toArray(); } return $productData; } public function findAll($keyList) { $productTable = new Products(); $productRowset = $productTable->fetchRowsetBySn($keyList); $resultDataList = array(); foreach ($productRowset as $productRow) { $resultDataList[$productRow->sn] = $productRow->toArray(); } return $resultDataList; } 將取得的 Rowset 轉換為 Array }
  • 82. // 以加入購物車為例: class Cart { protected $_dao = null; public function setDao(Cart_Dao $dao) { $this->_dao = $dao; } 利用 setDao 方法讓我們可以 從外部注入 Dao 物件 }
  • 83. // 以加入購物車為例: class Cart { protected $_dao = null; public function setDao(Cart_Dao $dao) { $this->_dao = $dao; } public function addItem($itemKey) { if ($productData = $this->_dao->find($itemKey)) { $this->_storage[$itemKey] = array( 'quantity' => 1, ); } 有抓到商品資料再存到 Storage 裡 } } 沒有的話就做錯誤處理 (這邊省略)
  • 84. // 以加入購物車為例: class Cart { protected $_dao = null; public function setDao(Cart_Dao $dao) { $this->_dao = $dao; } public function addItem($itemKey) { if ($productData = $this->_dao->find($itemKey)) { $this->_storage[$itemKey] = array( 'quantity' => 1, ); } } } $cart = new Cart(); 建立一個測試用的購物車物件
  • 85. // 以加入購物車為例: class Cart { protected $_dao = null; public function setDao(Cart_Dao $dao) { $this->_dao = $dao; } public function addItem($itemKey) { if ($productData = $this->_dao->find($itemKey)) { $this->_storage[$itemKey] = array( 'quantity' => 1, ); } } } $cart = new Cart(); $cart->setDao(new Cart_Dao()); 用外部注入的方式使用 Dao 物件
  • 86. // 以加入購物車為例: class Cart { protected $_dao = null; public function setDao(Cart_Dao $dao) { $this->_dao = $dao; } public function addItem($itemKey) { if ($productData = $this->_dao->find($itemKey)) { $this->_storage[$itemKey] = array( 'quantity' => 1, ); } } } $cart = new Cart(); $cart->setDao(new Cart_Dao_Product()); 改從資料庫抓取商品資料
  • 88. Plugin 模式 • 在主程式裡呼叫 Plugin 流程還是由主程式控制 Plugin 只負責提供額外功能
  • 89. Plugin 模式 複合模式 • 在主程式裡呼叫 Plugin • 有點像 Template Method 和 Observer 的混血
  • 90. Plugin 模式 • 在主程式裡呼叫 Plugin • 有點像 Template Method 和 Observer 的混血 定義好主流程,其他的實作透過 子類別 Hook 方法來完成 Don't call me, I call you.
  • 91. Plugin 模式 • 在主程式裡呼叫 Plugin • 有點像 Template Method 和 Observer 的混血 一旦有動作,會立即通知 訂閱者做改變
  • 92. Plugin 模式 • 在主程式裡呼叫 Plugin • 有點像 Template Method 和 Observer 的混血 • 最好在設計階段就考慮進去 跟 AOP 很像, 但插入機制是寫死在程式裡; 而 AOP 則是可以動態加入
  • 93. Plugin 模式 • 在主程式裡呼叫 Plugin • 有點像 Template Method 和 Observer 的混血 • 最好在設計階段就考慮進去 • 與 PoEAA 定義的 Plugin Pattern 是不一樣的 Patterns of Enterprise Application Architecture by Martin Fowler
  • 94. Plugin 模式 • 在主程式裡呼叫 Plugin • 有點像 Template Method 和 Observer 的混血 • 最好在設計階段就考慮進去 • 與 PoEAA 定義的 Plugin Pattern 是不一樣的 在不同的運行環境, 執行不一樣的動作
  • 97. Plugin 模式圖解 用戶點選 購物車 頁面連結 加入商品
  • 98. Plugin 模式圖解 用戶點選 尋找商品資料 購物車 頁面連結 並驗證是否 加入商品 可以加入
  • 99. Plugin 模式圖解 用戶點選 尋找商品資料 購物車 寫入 Session 頁面連結 並驗證是否 或 Database 加入商品 可以加入
  • 101. Plugin 模式圖解 購物車 加入 Plugin 機制 Plugin 1 Plugin 2 Plugin 3
  • 102. Plugin 模式圖解 用戶點選 購物車 頁面連結 加入商品 Plugin 1 Plugin 2 Plugin 3
  • 103. Plugin 模式圖解 這時購物車會呼叫 用戶點選 Plugin 購物車 頁面連結 加入商品 Plugin 1 加入購物車前 Plugin 2 加入購物車前 Plugin 3 加入購物車前
  • 104. Plugin 模式圖解 用戶點選 尋找商品資料 購物車 頁面連結 並驗證是否 加入商品 可以加入 Plugin 1 加入購物車前 Plugin 2 加入購物車前 Plugin 3 加入購物車前
  • 105. Plugin 模式圖解 購物車再次呼叫 用戶點選 尋找商品資料 Plugin 購物車 頁面連結 並驗證是否 加入商品 可以加入 Plugin 1 加入購物車前 加入購物車後 Plugin 2 加入購物車前 加入購物車後 Plugin 3 加入購物車前 加入購物車後
  • 106. Plugin 模式圖解 繼續完成動作 用戶點選 尋找商品資料 購物車 寫入 Session 頁面連結 並驗證是否 或 Database 加入商品 可以加入 Plugin 1 加入購物車前 加入購物車後 Plugin 2 加入購物車前 加入購物車後 Plugin 3 加入購物車前 加入購物車後
  • 109. // 以加入購物車為例: class Cart { public function addItem($itemKey) { 加入購物車方法 // 真正加入購物車 (...略...) } }
  • 110. // 以加入購物車為例: class Cart { protected $_pluginList = array(); public function addItem($itemKey) { 用陣列來記住 已載入的 Plugin // 真正加入購物車 (...略...) } }
  • 111. // 以加入購物車為例: class Cart { protected $_pluginList = array(); 在這裡插入 public function addItem($itemKey) { 加入購物車前的程式 // 加入購物車之前 foreach ($this->_pluginList => $plugin) { $plugin->beforeAddItem($itemKey); } // 真正加入購物車 (...略...) } }
  • 112. // 以加入購物車為例: class Cart { protected $_pluginList = array(); public function addItem($itemKey) { // 加入購物車之前 foreach ($this->_pluginList => $plugin) { $plugin->beforeAddItem($itemKey); } // 真正加入購物車 在這裡插入 (...略...) 加入購物車後的程式 // 加入購物車之後 foreach ($this->_pluginList => $plugin) { $plugin->afterAddItem($itemKey); } } }
  • 114. // Plugin 類別長什麼樣子?? abstract class Cart_Plugin { 首先要有一個抽象類別 }
  • 115. // Plugin 類別長什麼樣子?? abstract class Cart_Plugin { protected $_cart = null; 為了可直接取得購物車的資訊 所以有一個購物車屬性 }
  • 116. // Plugin 類別長什麼樣子?? abstract class Cart_Plugin { protected $_cart = null; public function __construct(Cart $cart) { $this->_cart = $cart; } 購物車屬性可由建構式 的參數帶入 }
  • 117. // Plugin 類別長什麼樣子?? abstract class Cart_Plugin { protected $_cart = null; public function __construct(Cart $cart) { $this->_cart = $cart; } public function beforeAddItem($itemKey) {} public function afterAddItem($itemKey) {} public function beforeUpdateItem($itemKey) {} public function afterUpdateItem($itemKey) {} // ... (其他方法) } 最後定義各項 Hook 方法 (預設為空實作)
  • 119. // 如何加入 Plugin ?? class Cart { protected $_pluginList = array(); 剛剛的購物車類別 }
  • 120. // 如何加入 Plugin ?? class Cart { protected $_pluginList = array(); public function registerPlugin($name) { 加入註冊 Plugin 的方法 } }
  • 121. // 如何加入 Plugin ?? class Cart { protected $_pluginList = array(); public function registerPlugin($name) { $name = ucfirst($name); $pluginName = 'Cart_Plugin_' . $name; 轉換成實際 Plugin 的 類別名稱 } }
  • 122. // 如何加入 Plugin ?? class Cart { protected $_pluginList = array(); public function registerPlugin($name) { $name = ucfirst($name); $pluginName = 'Cart_Plugin_' . $name; if (class_exists($pluginName, true)) { $this->_pluginList[$name] = new $pluginName($this); } 判斷 Plugin 類別是否存在?有的話就建立實體, } 並利用建構式把購物車物件傳給 Plugin 物件使用 }
  • 123. // 如何加入 Plugin ?? class Cart { protected $_pluginList = array(); public function registerPlugin($name) { $name = ucfirst($name); $pluginName = 'Cart_Plugin_' . $name; if (class_exists($pluginName, true)) { $this->_pluginList[$name] = new $pluginName($this); } else { throw new Exception("Plugin: $pluginName does not exist!"); } } } Plugin 類別不存在的話,就丟出異常
  • 124. // 如何加入 Plugin ?? class Cart { protected $_pluginList = array(); public function registerPlugin($name) { $name = ucfirst($name); $pluginName = 'Cart_Plugin_' . $name; if (class_exists($pluginName, true)) { $this->_pluginList[$name] = new $pluginName($this); } else { throw new Exception("Plugin: $pluginName does not exist!"); } } } $cart = new Cart(); 建立一個新購物車來測試
  • 125. // 如何加入 Plugin ?? class Cart { protected $_pluginList = array(); public function registerPlugin($name) { $name = ucfirst($name); $pluginName = 'Cart_Plugin_' . $name; if (class_exists($pluginName, true)) { $this->_pluginList[$name] = new $pluginName($this); } else { throw new Exception("Plugin: $pluginName does not exist!"); } } } $cart = new Cart(); $cart->registerPlugin('plugin1'); $cart->registerPlugin('plugin2'); 加入 Plugin
  • 127. // 計算小計與總計金額的 Plugin 類別範例 class Cart_Plugin_Total { }
  • 128. // 計算小計與總計金額的 Plugin 類別範例 class Cart_Plugin_Total extends Cart_Plugin { 繼承抽象的 Cart_Plugin 類別 }
  • 129. // 計算小計與總計金額的 Plugin 類別範例 class Cart_Plugin_Total extends Cart_Plugin { protected $_total = 0; 用來記住總金額的屬性 }
  • 130. // 計算小計與總計金額的 Plugin 類別範例 class Cart_Plugin_Total extends Cart_Plugin { protected $_total = 0; public function beforeRefreshCart(&$iterator) { $this->_total = 0; } 更新購物車前先將總金額歸零 }
  • 131. // 計算小計與總計金額的 Plugin 類別範例 class Cart_Plugin_Total extends Cart_Plugin { protected $_total = 0; public function beforeRefreshCart(&$iterator) { $this->_total = 0; } public function afterRefreshItem($itemKey, &$iterator) { $itemData = $iterator[$itemKey]; $iterator[$itemKey]['subtotal'] = $itemData['quantity'] * $itemData['price']; $this->_total += $iterator[$itemKey]['subtotal']; } 在更新商品項目後,更新商品小計與總金額 }
  • 132. // 計算小計與總計金額的 Plugin 類別範例 class Cart_Plugin_Total extends Cart_Plugin { protected $_total = 0; public function beforeRefreshCart(&$iterator) { $this->_total = 0; } public function afterRefreshItem($itemKey, &$iterator) { $itemData = $iterator[$itemKey]; $iterator[$itemKey]['subtotal'] = $itemData['quantity'] * $itemData['price']; $this->_total += $iterator[$itemKey]['subtotal']; } public function getValue() { return $this->_total; } } 取得總金額
  • 134. // 如何存取 Plugin 的值?? class Cart { }
  • 135. // 如何存取 Plugin 的值?? class Cart { public function __call($name, $args) { 使用 __call 魔術方法來間接呼叫 plugin 的方法 這樣一來就不用寫過多的 setter / getter } }
  • 136. // 如何存取 Plugin 的值?? class Cart { public function __call($name, $args) { // 設定值 $name = preg_replace('/^set/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'setValue'), $args); } 如果方法名為 set 開頭,就呼叫 plugin 的 setValue 方法 } }
  • 137. // 如何存取 Plugin 的值?? class Cart { public function __call($name, $args) { // 設定值 $name = preg_replace('/^set/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'setValue'), $args); } // 取得值 $name = preg_replace('/^get/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'getValue'), $args); } 如果方法名為 get 開頭,就呼叫 plugin 的 getValue 方法 } }
  • 138. // 如何存取 Plugin 的值?? class Cart { public function __call($name, $args) { // 設定值 $name = preg_replace('/^set/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'setValue'), $args); } // 取得值 $name = preg_replace('/^get/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'getValue'), $args); } return null; } } 預設 return null
  • 139. // 如何存取 Plugin 的值?? class Cart { public function __call($name, $args) { // 設定值 $name = preg_replace('/^set/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'setValue'), $args); } // 取得值 $name = preg_replace('/^get/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'getValue'), $args); } return null; } } $cart = new Cart(); 建立測試用的購物車物件
  • 140. // 如何存取 Plugin 的值?? class Cart { public function __call($name, $args) { // 設定值 $name = preg_replace('/^set/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'setValue'), $args); } // 取得值 $name = preg_replace('/^get/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'getValue'), $args); } return null; } } $cart = new Cart(); $cart->registerPlugin('total'); 註冊 Total Plugin
  • 141. // 如何存取 Plugin 的值?? class Cart { public function __call($name, $args) { // 設定值 $name = preg_replace('/^set/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'setValue'), $args); } // 取得值 $name = preg_replace('/^get/', '', $name); if (array_key_exists($name, $this->_pluginList)) { return call_user_func_array(array($this->_pluginList[$name], 'getValue'), $args); } return null; } } $cart = new Cart(); $cart->registerPlugin('total'); echo $cart->getTotal(); 取得 Total Plugin 的值
  • 143. Plugin 模式的優點 • 不用把過多的功能塞在一個類別上 例如活動、金流、折價券等, 可以分開獨立成不同 Plugin
  • 144. Plugin 模式的優點 • 不用把過多的功能塞在一個類別上 • 新增移除功能非常容易 像是系統要需要新增金流, 只要加入新的 Plugin 與畫面即可
  • 145. Plugin 模式的優點 • 不用把過多的功能塞在一個類別上 • 新增移除功能非常容易 • Plugin 的職責明確 每個 Plugin 只做自己該做的, 不會影響到其他程式
  • 147. Plugin 模式的缺點 • 產生過多的類別 因為每個功能都會需要一個類別
  • 148. Plugin 模式的缺點 • 產生過多的類別 • 可能會產生重複的資源 相同的資源,卻分別在不同的 Plugin 產生
  • 149. Plugin 模式的缺點 • 產生過多的類別 • 可能會產生重複的資源 • 一般開發者沒有經過說明不易理解 再加上如果對物件導向不夠熟悉, 就更難對這樣的架構進行維護
  • 151. Plugin 模式要注意的地方 • 流程細膩度 切得越細, Plugin 可 Hook 的地方越多, 但也會造成開發上的困擾
  • 152. Plugin 模式要注意的地方 • 流程細膩度 • Plugin 安插的優先順序 例如免運費是需要先知道總金額, 所以總金額 Plugin 就要放在前面
  • 153. Plugin 模式要注意的地方 • 流程細膩度 • Plugin 安插的優先順序 • Plugin 之間的相依性 例如免運費是需要知道總金額, 所以免運費 Plugin 相依於總金額 Plugin
  • 154. Plugin 模式要注意的地方 • 流程細膩度 • Plugin 安插的優先順序 • Plugin 之間的相依性 • Plugin 存取外部資源的部份也要分離 可以用 Dao 來分離資料庫或是外部金流程 式,讓測試容易進行
  • 156. 測試購物車 • 用 PHPUnit 來做測試框架 PHPUnit 是目前較常見的測試框架
  • 157. 測試購物車 • 用 PHPUnit 來做測試框架 • 只測試重要的類別 因為測試購物車的目的是在於驗證複雜的計算
  • 158. 測試購物車 • 用 PHPUnit 來做測試框架 • 只測試重要的類別 • 使用假的 Stub 類別來做測試 因為 PHPUnit 提供的 Mock 機制操作上太繁瑣
  • 160. 整合到 MVC • 購物車本身與 MVC 架構無關 任何 Framework 甚至純 PHP 都可以整合
  • 161. 整合到 MVC • 購物車本身與 MVC 架構無關 • Cart 即 Model 要注意資料庫存取的方式
  • 162. 整合到 MVC • 購物車本身與 MVC 架構無關 • Cart 即 Model • Controller 呼叫 Cart 方法 Controller 也只會看到 Cart
  • 163. 整合到 MVC • 購物車本身與 MVC 架構無關 • Cart 即 Model • Controller 呼叫 Cart 方法 • View 取得 Cart 資料 所以畫面也可以客制化
  • 165. 問題與討論 • 為什麼 Plugin 需要包含 Cart 物件,但 Storage 和 Dao 不需要?
  • 166. 問題與討論 • 為什麼 Plugin 需要包含 Cart 物件,但 Storage 和 Dao 不需要? • Plugin 模式的應用
  • 167. 問題與討論 • 為什麼 Plugin 需要包含 Cart 物件,但 Storage 和 Dao 不需要? • Plugin 模式的應用 ▫ Zend Framework Dispatcher
  • 168. 問題與討論 • 為什麼 Plugin 需要包含 Cart 物件,但 Storage 和 Dao 不需要? • Plugin 模式的應用 ▫ Zend Framework Dispatcher ▫ 是否可用在 Template 上?
  • 169. 問題與討論 • 為什麼 Plugin 需要包含 Cart 物件,但 Storage 和 Dao 不需要? • Plugin 模式的應用 ▫ Zend Framework Dispatcher ▫ 是否可用在 Template 上? • 現有架構可否加入 Plugin 模式?
  • 170. 問題與討論 • 為什麼 Plugin 需要包含 Cart 物件,但 Storage 和 Dao 不需要? • Plugin 模式的應用 ▫ Zend Framework Dispatcher ▫ 是否可用在 Template 上? • 現有架構可否加入 Plugin 模式? • 實際應用時,效能的考量?
  • 171. 謝謝