工厂方法模式和单例模式在|工厂方法模式和单例模式在 Laravel 框架中 ORM 搜索功能中的应用

Laravel 框架中 ORM 搜索结果缓存的实现 标签: 设计模式 工厂方法模式 单例模式 Laravel PHP
在 Gof 总结的 24 种设计模式中,用来分离类的创建与调用的工厂模式和单例模式的应用非常广泛 ,今天我们就来看一下这些模式在 Laravel 框架的 ORM 搜索结果缓存功能中的应用。
ORM 模式介绍 在使用 Laravel 框架或者其他框架的时候,ORM 的搜索功能是很重要的一块。我们知道,ORM 是一种关系模型映射,它将数据库中的表和编程语言中的类,表的字段和类的属性,表中的记录和类的实例对应起来,记录的增加和删除对应类对象的创建与删除,记录的修改对应对象属性的修改,而记录的查找则通过 ORM 模型提供的对数据库的查找操作的方法来实现。ORM 模型在本质上还是框架中数据库的操作模块的进一步封装。
问题描述 在我们使用框架中的 ORM 模型进行开发的时候,有时候可能需要对 ORM 模型进行进一步的扩展,比如数据库中的 product 表对应的 Product 模型,我们可能需要在上面扩展业务层和产品相关的功能。有时候可能需要对搜索功能进行进一步的优化,比如对搜索结果添加缓存功能。由于 ORM 模型的实现本身就具有复杂性,我们很难在 ORM 模型的基础上修改代码添加缓存功能,因此我们考虑将 ORM 模型的搜索功能进行抽象,创建独立的搜索类来实现这些功能。
工厂方法模式 Gof 总结的设计模式中,对工厂方法模式的描述如下:

定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到子类。
单例模式 接下来我们来看一下单例模式的描述:
保证一个类仅有一个实例,并提供一个访问它的全局访问点
问题分析 我们面对的主要问题是对数据库搜索结果的缓存问题。我们可以创建一个抽象类来封装对数据库的查询、查询结果缓存等操作。在这个抽象类中,封装一个对数据库操作对象的属性,由其代理具体的数据库操作。同时创建一个抽象的工厂方法,让子类确定具体实例化哪一个 ROM 或者用户扩展的业务类实例。
因为我们搜索类属于工具类,原则上不应该管理有关上下文的数据信息,应该保证一个具体的类仅有一个具体的实例,因此我们用单例模式管理具体搜索类的实例,并提供访问具体唯一搜索类的全局访问接口。
代码实现 接下来我们先来看一下抽象的搜索类的实现
abstract class Search{ /* * 搜索抽象类,管理项目中 ORM 实例的搜索、缓存等操作 *///数据库操作对象的类实例 protected $db; //要搜索的数据表 protected $table; //要搜索的数据表的主键 protected $primaryKey; //是否对搜索结果进行缓存 protected $cache = true; //针对某一次具体的搜索行为是否要进行缓存 protected $realCache; //存储实例化好的具体搜索类的实例 protected static $_instance = []; protected function __construct() { $this->dbReset(); }public function __call($name, $arguments) { //使用魔术方法,将具体的数据库操作的方法调用代理到 $this->db 对象上 if(method_exists($this->db, $name)){ call_user_func_array([$this->db, $name], $arguments); } return $this; }/** * @return Search */ public static function Instance() { // 单例模式的访问接口,通过访问此方法,返回由静态属性 $_instance 管理的具体的搜索对象实例 //返回的是调用此方法的类对象的实例 $calledClass = get_called_class(); if(!isset(self::$_instance[$calledClass])){ self::$_instance[$calledClass] = new $calledClass(); }else{ self::$_instance[$calledClass]->dbReset(); } return self::$_instance[$calledClass]; }public function dbReset(){ //重置 $this->db 以及其他上下文管理属性,以防以前的搜索条件对此次搜索产生影响 unset($this->db); $this->db = DB::table($this->table)->select($this->primaryKey); $this->realCache = $this->cache; }public function cache($cache){ /* *设计这个方法和 $this->realCache 属性的目的是有时候在项目正式环境中,可能需要对搜索结果进行缓存,但在调试的时候需要关掉缓存调试。通过设置 $this->realCache 为 false 可以关闭此次搜索的缓存。 */ $this->realCache = $cache; return $this; }public function page($page, $pageRow){ //对分页操作进行封装 $this->db->skip(($page - 1) * $pageRow)->limit($pageRow); return $this; }protected function cacheRemember($cacheId, Closure $callback){ //对回调返回的数据进行缓存操作 if(!$this->realCache){ Cache::tags(['search', $this->table])->forget($cacheId); }if(Cache::tags(['search', $this->table])->has($cacheId)){ return Cache::tags(['search', $this->table])->get($cacheId); }else{ $data = https://www.it610.com/article/$callback(); if($this->cache){ Cache::tags(['search', $this->table])->put($cacheId, $data, Constant::CACHE_TIME); } return $data; } }public function getIds(){ //返回搜索的主键的集合,并进行缓存操作 $cacheId = 'search-get-ids-'.md5($this->db->toSql().json_encode($this->db->getBindings())); $ids = $this->cacheRemember($cacheId, function(){ $primaryKey = $this->primaryKey; if(strpos($this->primaryKey, '.') !== false){ list(,$primaryKey) = explode('.', $this->primaryKey); } return $this->db->select($this->primaryKey)->lists($primaryKey); }); return $ids; }public function count(){ //返回搜索结果的数目,并进行缓存操作 $cacheId = 'search-count-'.md5($this->db->toSql().json_encode($this->db->getBindings())); return $this->cacheRemember($cacheId, function(){ return $this->db->distinct()->count(); }); }public function sum($column){ //返回并缓存搜索结果某一列的和 $cacheId = 'search-sum-'.md5($this->db->toSql().json_encode($this->db->getBindings())); return $this->cacheRemember($cacheId, function()use($column){ return $this->db->sum($column); }); }public function avg($column){ //返回并缓存搜索结果某一列平均值 $cacheId = 'search-avg-'.md5($this->db->toSql().json_encode($this->db->getBindings())); return $this->cacheRemember($cacheId, function()use($column){ return $this->db->average($column); }); }public function getByIds($ids){ //根据主键返回模型实例集合 $items = []; if(!empty($ids)){ foreach($ids as $id){ $item = $this->find($id); if($item){ $items[] = $item; } } } //可以对搜索结果用 Collection 对象进行封装 return new Collection($items); }public function get(){ //返回根据搜索结果得到的模型实例集合 $result = $this->getByIds($this->getIds()); $this->dbReset(); return $result; }public function first(){ //返回搜索到的第一个实例 $this->limit(1); $ids = $this->getIds(); $this->dbReset(); return !empty($ids) ? $this->find($ids[0]) : null; }/** * @param $id * @return mixed * 抽象的工厂方法,将创建具体的 ORM 实例和对实例进行缓存的工作交给子类来实现。 */ abstract public function find($id); }

【工厂方法模式和单例模式在|工厂方法模式和单例模式在 Laravel 框架中 ORM 搜索功能中的应用】至此,我们已经实现了抽象类 Search,它的子类将实现针对具体 ORM 的搜索工具类。接下来我们来看一下搜索 product 表对应 ROM 对象实例的搜索字类如何实现。
class ProductSearch extends Search { /* * ProductSearch 类,提供针对 Product 表的对象搜索功能 */ protected $table = 'products as p'; protected $primaryKey = 'p.pid'; public function find($id) { // 返回以 $id 为主键的 Product 类的实例 return Product::find($id); } }

至此我们就实现了 ProductSearch 类的功能,我们就可以通过其提供的方法方便地进行 Product 类对象的搜索。比如通过调用 ProductSearch::Instance()->where('price','>',100)->get() 返回所有价格大于100的产品对象集合。
总结 为了实现项目中不同数据库对象的搜索功能,我们对数据库搜索功能进行抽象得到 Search 搜索类,其中数据库查询的功能由 Search 类的数据库操作属性来实现。并且通过工厂方法模式,我们将具体数据库对象的查询和实例化延迟到 Search 类的子类来实现,通过单例模式,我们提供了访问唯一的具体搜索子类的全局访问接口。通过以上这些方法,我们实现了灵活的数据库对象的搜索、缓存功能。

    推荐阅读