大话PHP设计模式
一、PHP三种基本的设计模式:
- 工厂模式:工厂方法或者类生成对象,而不是在代码中直接new.
- 单列模式:使某个类的对象仅允许创建一个.
- 注册模式:全局共享和交换对象.
1. 工厂模式
在平时的代码书写中,总是会调用对象,这样每次在声明一个对象实例的时候就会需要进行一个new的过程。假如我们new了十个对象实例,但是我们现在想要修改这个对象的名称,则需要修改10次,而如果我们使用工厂模式进行生成的话则只需要进行一次修改。
class Factory
{
static function createDataBase()
{
$db =new DataBase();
return $db;
}
}
这样当我们需要调用的时候就不需要使用
$obj = new Imooc\DataBase();
而是可以直接
$db = \Imooc\Factory::createDataBase();
2. 单例模式
考虑一下这种情况,放我们在调用数据库的时候,其实我们只需要进行一个数据库类的对象实例即可,否则就会造成资源的浪费。当这个时候,就可以使用单例模式来避免这种情况的发送。
使用单例模式,首先需要先将构造方法私有化:
private function __construct()
{
}
然后,创造一个静态方法来进行逻辑判断,实现单例模式:
protected $db;
static function getInstance()
{
if (self::db)
{
return self::$db;
}else{
self::$db = new self();
return self::db;
}
}
这样当我们需要调用这个对象的时候,无论调用多少次,都只new一个实例:
//实现单例模式,无论调用多少次,都只会调用一次
$db = \Imooc\DataBase::getInstance();
$db = \Imooc\DataBase::getInstance();
$db = \Imooc\DataBase::getInstance();
$db = \Imooc\DataBase::getInstance();
同时可以修改上文的工厂模式,使其与单例模式相结合:
class Factory
{
static function createDataBase()
{
//将单例模式与工厂模式相结合
$db =DataBase::getInstance();
return $db;
}
}
3. 注册模式
-
什么是注册树模式?
注册树模式当然也叫注册模式,注册器模式。之所以我在这里矫情一下它的名称,是因为我感觉注册树这个名称更容易让人理解。像前两篇一样,我们这篇依旧是从名字入手。注册树模式通过将对象实例注册到一棵全局的对象树上,需要的时候从对象树上采摘的模式设计方法。 这让我想起了小时候买糖葫芦,卖糖葫芦的将糖葫芦插在一个大的杆子上,人们买的时候就取下来。不同的是,注册树模式摘下来还会有,能摘很多次,糖葫芦摘一次就没了。。。
-
为什么要采用注册树模式?
-
单例模式解决的是如何在整个项目中创建唯一对象实例的问题,
-
工厂模式解决的是如何不通过new建立实例对象的方法。
-
那么注册树模式想解决什么问题呢? 在考虑这个问题前,我们还是有必要考虑下前两种模式目前面临的局限。 首先,单例模式创建唯一对象的过程本身还有一种判断,即判断对象是否存在。存在则返回对象,不存在则创建对象并返回。 每次创建实例对象都要存在这么一层判断。 工厂模式更多考虑的是扩展维护的问题。 总的来说,单例模式和工厂模式可以产生更加合理的对象。怎么方便调用这些对象呢?而且在项目内如此建立的对象好像散兵游勇一样,不便统筹管理安排啊。因而,注册树模式应运而生。不管你是通过单例模式还是工厂模式还是二者结合生成的对象,都统统给我“插到”注册树上。我用某个对象的时候,直接从注册树上取一下就好。这和我们使用全局变量一样的方便实用。 而且注册树模式还为其他模式提供了一种非常好的想法。
-
-
如何实现注册树?
所有的对象“插入”到注册树上。这个注册树应该由一个静态变量来充当。而且这个注册树应该是一个二维数组。这个类应该有
- 一个插入对象实例的方法(set())
- 当让相对应的就应该有一个撤销对象实例的方法(_unset())
- 当然最重要的是还需要有一个读取对象的方法(get())
namespace Imooc;
//注册树
class Register
{
protected static $objects;
static function set($alias, $obj)
{
self::$objects[$alias] = $obj;
}
static function get($alias)
{
return self::$objects[$alias];
}
static function _unset($alias)
{
unset(self::$objects[$alias]);
}
}
这个时候,修改一下我们前文实现的工厂模式,将我们创建的对象“插到”注册树上:
class Factory
{
static function createDataBase()
{
$db =DataBase::getInstance();
//将创建出来的对象插入到注册树上
Register::set('obj1',$db);
return $db;
}
}
这样当我们调用这个对象的时候,可以直接调用:
$db = \Imooc\Register::get('obj1');
二、适配器模式
适配器模式可以将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。
比如说PHP的数据库操作有mysql、mysqli、pdo三种,他们实现操作的方法各不相同,这个时候就可以使用适配器模式进行统一操作。
类似的场景还有cache适配器,将memcache,redis,file,apc等不同的缓存函数,统一成一致。
那么如何使用适配器模式呢?
首先,需要定义一个统一的接口类:
interface IDataBase{
public function connect($host,$user,$passwd,$dbname);
public function query($sql);
public function close();
}
然后分别创建不同的数据库类,使其适配这个接口类,并且实现其方法:
- MySQL类
//MySQL类
class MySQL implements IDataBase
{
protected $conn;
public function connect($host, $user, $passwd, $dbname)
{
$conn = mysql_connect($host, $user, $passwd);
mysql_select_db($dbname);
$this->conn = $conn;
}
public function query($sql)
{
$res = mysql_query($sql,$this->conn);
return $res;
}
public function close()
{
mysql_close($this->conn);
}
}
- MySQLi类
//MySQLi类
class MySQLi implements IDataBase
{
protected $conn;
public function connect($host, $user, $passwd, $dbname)
{
$this->conn = mysqli_connect($host,$user,$passwd,$dbname);
}
public function query($sql)
{
return mysqli_query($this->conn,$sql);
}
public function close()
{
mysqli_close();
}
}
- PDO类
//PDO类
class PDO implements IDataBase
{
protected $conn;
public function connect($host, $user, $passwd, $dbname)
{
$this->conn = new \PDO("mysql:host=$host;dbname=$dbname",$user,$passwd);
}
public function query($sql)
{
return $this->conn->query($sql);
}
public function close()
{
unset($this->conn);
}
}
由上述三类代码可以看出,分别适配了IDataBase这个适配器,并且实现了connect、query、close方法。
三、策略模式
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。
- 策略模式,就是将一组特定的行为和算法封装成类,以适应某些特定的上下文环境,这种模式就是策略模式。
- 实际应用举例,加入一个电商网站系统,针对男性女性用户要求各自跳转到不同的商品类目,并且所有广告位展示不同的广告。在这种情景环境下,如果使用
if-else
硬编码的方式,则会导致一种情况,即在将来加入需要添加儿童用户,则需要多次添加if-else,这显然是不科学的。 - 使用策略模式可以实现Ioc,依赖倒置、控制反转。
这里引用一下https://blog.youkuaiyun.com/hel12he/article/details/52003433的博客内容:
让算法独立于使用它的客户而变化。
这是什么意思?也就是说实现一个功能,有多个方法,而选择这个方法的控制权不要交给客户端,也就说了,我换了实现方法,客户端是不需要改代码的。
那么要做到这样子,必然提供给客户端的一个稳定的调用类(称为环境类),首先调用这个类能够产生一个具体算法的实例,其次这个调用类,还需要公布一个接口,让客户端调用实现具体功能。
那么做到以上,无论实现多少种双方,客户端的调用都是不变的。控制权都在这个调用类里边,由它来决定到底采用哪种算法。
下面来接着说算法部分。如果需要 环境类 提供一个实现具体功能的接口,那么这些算法必然实现了一个公共接口(称为抽象策略类)。才能确保有相同的方法提供出来。然后具体的算法都要实现这个接口。这也就是上面定义中的 将每一个算法封装起来 每一个具体的算法称为:具体策略类
类图演示
策略模式包含的角色如下:
- Context: 环境类
- Strategy: 抽象策略类
- ConcreteStrategy: 具体策略类
上文的引用是引用的博客里面的内容,其实说白了,策略模式就是尽可能的使客户端与功能实现方法相隔离开来,客户端只需要访问一个固定类的固定方法就能够实现需求而不需要进行代码的修改,未来功能的修改工作则交给这个类来操作。
下面使用代码来展示一下整个策略模式的操作流程。
//定义了一个用户的Strategy
interface UserStrategy
{
public function setAd();
public function setCate();
}
首先实现了Strategy这个抽象策略类,它的主要目的就是**规范必须要实现的方法,让环境类Context依赖这个接口进行编程。**接下来就是各种算法的实现,举例现在有男性和女性。
Tips:所有的算法都需要实现策略类接口Strategy.
//女性策略类的算法实现
class FemaleUserStrategy implements UserStrategy
{
public function setAd()
{
echo '化妆品打折啦';
}
public function setCate()
{
echo '女装';
}
}
//男性策略类的算法实现
class MaleUserStrategy implements UserStrategy
{
public function setAd()
{
echo 'iphone要出新手机啦';
}
public function setCate()
{
echo '数码';
}
}
可以看出,现在有两种策略类算法,一种是针对女性的FemaleUserStrategy
算法,一种是针对男性的MaleUserStrategy
算法。
接下来就是对于环境类Context的代码编写,对于不同的用户有着不同的需求,而客户端不需要对于这种不同进行选择,转而让环境类进行选择,如此就能够保证客户端的最简化。
//策略模式的环境类
class UserContext
{
private $user;
//该方法主要进行了不同用户类别的判断,进行数据的实例化
public function init($cate)
{
switch ($cate) {
case 'female':
$this->user = new FemaleUserStrategy();
break;
case 'male':
$this->user = new MaleUserStrategy();
break;
default:
$this->user = null;
break;
}
}
public function setAd()
{
if (is_null($this->user)) {
exit('初始化错误');
}
$this->user->setAd();
}
public function setCate()
{
if (is_null($this->user)) {
exit('初始化错误');
}
$this->user->setCate();
}
}
以上,策略算法就基本完成了。最后对于客户端来说,实现的代码就非常简单了。
$user = trim($_GET['user']);
$context = new \Imooc\UserContext();
//初始化用户实例
$context->init($user);
//调用功能
$context->setAd();
$context->setCate();
通过上面的整体代码我们不难发现这个模式很好的实现了开闭原则。
比方说,现在新增了一个儿童用户类别。那么中需要添加一个儿童的策略算法,同时在UserContext
中把对应的实力初始化加进去,其他地方都不需要变动。
四、数据对象映射模式
是将对象和数据存储映射起来,对一个对象的操作会映射为对数据存储的操作。例如在代码中 new 一个对象,使用数据对象映射模式就可以将对象的一些操作比如设置一些属性,就会自动保存到数据库,跟数据库中表的一条记录对应起来。
在代码中实现数据对象映射模式,我们将实现一个 ORM(对象关系映射 Object Relational Mapping) 类,将复杂的 SQL 语句映射成对象属性的操作。同时结合【工厂模式】和【注册模式】使用。
假设数据库有一个表user
,表中有4个字段分别是:id
, name
, phone
, regTime
.
这个时候就需要创建一个类User:
class User
{
public $id;
public $name;
public $phone;
public $regTime;
protected $db;
public function __construct($id)
{
$this->db = Factory::createUser();
// $this->db = new MySQLi();
$res = $this->db->query("select * from user where id = $id limit 1");
$data = $res->fetch_assoc();
$this->id = $data['id'];
$this->name = $data['name'];
$this->phone = $data['phone'];
$this->regTime = $data['regTime'];
}
public function __destruct()
{
$this->db->query("update user set id='{$this->id}',name='{$this->name}',phone='{$this->phone}',regTime='{$this->regTime}'where id={$this->id} limit 1");
}
}
并且结合工厂模式在工厂类Factory中进行工厂化:
//工厂模式
class Factory
{
static function createUser()
{
//注意,这里使用到了注册模式
$db = Register::get("userdb");
if (is_null($db)){
$db = DataBase::getInstance();
Register::set('userdb',$db);
}
return $db;
}
}
这样,在客户端中,
$user = new \Imooc\User(1);
$user->name = 'admin';
$user->phone = '12345678910';
$user->regTime = time('Y-m-d H:m:s');
就可以在构造的时候进行数据的获取,并且在析构的时候进行数据的修改。
五、观察者模式
- 观察者模式,当一个对象状态发生改变的时候,依赖它的对象全部会收到通知,并自动更新。
- 场景:一个事情发生后,要执行一连串的更新操作。传统的编程方式,就是在事件的代码之后直接加入处理逻辑。当更新的逻辑增多之后,代码会变得难为维护。这种方式是耦合的,侵入式的,增加新的逻辑需要修改事件主体的代码
- 观察者模式实现了低耦合,非侵入式的通知与更新机制
相信屏幕前的小伙伴们看到上面对于观察者模式的介绍,都在想你说的什么XX玩意啊,老纸要是能看懂就奇了怪了(反正我是没看懂),所以我就去百度上找了很多的文章,最后发现其实很简单。
举个小例子来说,现在你的boss给了你一个任务,那就是完成用户的登陆功能,你牛逼哄哄的三下五除二就把它KO了;过了两天,boss说功能太单调了,我现在想让你在用户登录完成之后给他推送最新消息,好啊,这简单,直接改代码,在logiin()方法里面添加功能呗;又过了两天,boss又觉得不行,我还要给用户推送适合他的产品,没办法,你只能再在login方法里面添添改改了。就这样,一个月过去了,你再看之前的代码,就在想这尼玛谁写的傻逼玩意啊,我看,代码真的是又臭又长,一点也不优雅。
其实,仔细看我们的业务逻辑就会发现,只要用户一登录,很多功能就开始自动实现了,就好比一个小人推开门之后,该敲锣的敲锣,该唱歌的唱歌。
具体的代码实现如下:
首先,创建观察者接口类Observer,其实就是站那要敲锣和唱歌的人的boss,他规定了敲锣的和唱歌的都要完成的动作update方法
// 观察者接口类
interface Observer
{
public function update($event_info = null);
}
接着,就要创建敲锣小人Observer1和唱歌小人Observer2了
// 观察者1
class Observer1 implements Observer
{
public function update($event_info = null)
{
echo "我是个敲锣的,我要开始敲锣了 收到执行通知 执行完毕! \n";
}
}
// 观察者2
class Observer2 implements Observer
{
public function update($event_info = null)
{
echo "我是个唱歌的,我要开始唱歌了 收到执行通知 执行完毕! \n";
}
}
然后,需要创建活动生产者EventGenerator,他实际上就是统一存放这些观察者和通着他们干活的类
abstract class EventGenerator
{
private $Observers = [];
// 增加观察者
public function addObserver($Observer)
{
$this->Observers[] = $Observer;
}
// 事件通知
public function notify()
{
// 小人开门了,赶快通知所有的观察者给我动起来,干你们各自该干的事情
foreach ($this->Observers as $observer) {
$observer->update();
}
}
}
最后,创建事件类Event,他规定了触发事件,就是小人进门的事件login.
class Event extends EventGenerator
{
// 小人进门的事件
public function login()
{
//小人进门了,要赶紧通知观察者了
$this->notify();
}
}
这样,整体大概就算完成了,如果我们想要给小人进门的时候添加敲锣和唱歌的旁观者的话,
// 创建一个事件
$event = new Event();
// 为事件添加旁观者
$event->addObserver(new Observer1()); //添加了敲锣的旁观者,都站那等着呢
$event->addObserver(new Observer2()); //添加了唱歌的旁观者,都站那等着呢
// 执行事件,通知旁观者
$event->login(); //小人进门了,大家伙都给我动起来!!!
六、原型模式
- 与工厂模式作用类似,都是用来创建对象
- 与工厂模式的实现不同,原型模式是先创建好一个原型对象,然后通过clone原型对象来创建新的对象。这样就免去了类创建时候重复的初始化操作。
- 原型模式适用于大对象的创建。创建一个大对象需要很大的开销,如果每次new就会消耗很大,原型模式仅仅需要内存拷贝就可以了。
打个比方,现在有一个画布的类Canvas,它有两个需要初始化的方法init()和serColor(),假如我们需要创建两个画布,那么原始的方法是:
// 画布1
$canvas1 = new Canvas();
$canvas1->init();
$canvas1->setColor();
$canvas1->draw();
// 画布2
$canvas2 = new Canvas();
$canvas2->init();
$canvas2->setColor();
$canvas2->draw();
我们要进行两次画布的初始化方法的操作,如果这些初始化操作占用资源很大,那么每new一个对象就是一个资源的占用,显然是不合理的,如果使用原型模式的话:
// 创建原型对象
$prototype = new Canvas();
$prototype->init();
$prototype->setColor();
// 画布1
$canvas1 = clone $prototype;
$canvas1->draw();
// 画布2
$canvas2 = clone $prototype;
$canvas2->draw();
只需要首先创建一个原型对象,实现初始化方法,当我们需要的时候直接clone这个原型对象就可以了。
七、装饰器模式
- 装饰器模式(Decorator),可以动态地添加修改类的功能
- 一个类提供了一项功能,如果要在修改并添加额外的功能,传统的编程模式,需要写一个子类继承它,并重新实现类的方法。
- 使用装饰器模式,仅需要在运行的时候添加一个装饰器对象就可以实现,可以实现最大的灵活性。
想要实现装饰器模式,需要
- 一个装饰器接口
- 一个被装饰的对象
- 若干个实现装饰器接口的具体类
1. 装饰器接口
装饰器接口提供装饰器所需要实现的通用的方法
interface CanvasDecorator{
//接口中的方法
function berfore_draw();
function after_draw();
}
2. 被装饰的对象
被装饰的对象中包含了装饰器的通用方法,并且提供了添加装饰器的功能
class Canvas
{
private $decorators = [];
// 装饰对象的draw方法
public function draw()
{
$this->before_draw();
echo '画画';
$this->after_draw();
}
// 添加装饰器
public function addDecorate(CanvasDecorator $decorator){
$this->decorators[] = $decorator;
}
// 遍历装饰器,调用装饰器方法
private function before_draw()
{
foreach ($this->decorators as $decorator) {
$decorator->before_draw();
}
}
// 遍历装饰器,调用装饰器方法
private function after_draw()
{
foreach (array_reverse($this->decorators) as $decorator) {
$decorator->after_draw();
}
}
}
3. 创建具体的装饰器
class CanvasDecorator1 implements CanvasDecorator
{
function berfore_draw()
{
echo '装饰器1';
}
function after_draw()
{
echo '装饰器1';
}
}
class CanvasDecorator2 implements CanvasDecorator
{
function berfore_draw()
{
echo '装饰器2';
}
function after_draw()
{
echo '装饰器2';
}
}
具体展示
// 实例化被装饰的对象
$canvas = new Canvas();
// 添加装饰器
$canvas->addDecorate(new CanvasDecorator1());
$canvas->addDecorate(new CanvasDecorator2());
// 允许被装饰的方法
$canvas->draw();
八、迭代器模式
- 迭代器模式,在不需要了解内部实现的前提下,遍历一个聚合对象的内部元素。
- 相比传统的编程模式,迭代器模式可以隐藏遍历元素的所需操作。
要实现迭代器,首先需要继承PHP的Iterator接口,进行批量操作。
- current():返回当前元素
- key():返回当前元素的值
- next():向前移动到下一个元素
- rewind():返回到迭代器的第一个元素
class AllUser implements \Iterator
{
protected $index = 0;
protected $data = [];
public function __construct()
{
$link = mysqli_connect('192.168.0.91', 'root', '123', 'xxx');
$rec = mysqli_query($link, 'select id from doc_admin');
$this->data = mysqli_fetch_all($rec, MYSQLI_ASSOC);
}
//1 重置迭代器
public function rewind()
{
$this->index = 0;
}
xxx
//2 验证迭代器是否有数据
public function valid()
{
return $this->index < count($this->data);
}
//3 获取当前内容
public function current()
{
$id = $this->data[$this->index];
return User::find($id);
}
//4 移动key到下一个
public function next()
{
return $this->index++;
}
//5 迭代器位置key
public function key()
{
return $this->index;
}
}
具体实现过程
//实现迭代遍历用户表
$users = new AllUser();
//可实时修改
foreach ($users as $user) {
$user->add_time = time();
$user->save();
}
九、代理模式
在客户端与实体之间创建一个代理对象(proxy),客户端对实体进行操作全部委派都给代理对象,隐藏实体的具体实现细节。
Proxy还可以与业务代码分离,部署到另外的服务器,业务代理中通过RPC来委派任务。
在原始代码编程中,如果想要对slave数据库进行读取操作,对master数据库进行写的操作,就需要知道他们的具体实现过程,而使用代理模式则只需要创建代理对象,调用代理对象中的方法而不需要理会具体逻辑过程。