23种设计模式
(一)、创建型模式
对象实例化的模式,创建型模式用于解耦对象的实例化过程。
- 单例模式:某个类只能有一个实例,提供一个全局的访问点。
- 工厂方法模式:一个工厂类根据传入的参量决定创建出哪一种产品类的实例。
- 抽象工厂模式:创建相关或依赖对象的家族,而无需明确指定具体类。
- 建造者模式:封装一个复杂对象的创建过程,并可以按步骤构造。
- 原型模式:通过复制现有的实例来创建新的实例。
(二)、结构型模式
把类或对象结合在一起形成一个更大的结构。
- 装饰器模式:动态的给对象添加新的功能。
- 代理模式:为其它对象提供一个代理以便控制这个对象的访问。
- 桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。
- 适配器模式:将一个类的方法接口转换成客户希望的另一个接口。
- 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。
- 外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。
- 享元模式:通过共享技术来有效的支持大量细粒度的对象。
(三)、行为型模式
类和对象如何交互,及划分责任和算法。
- 策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。
- 模板方法模式:定义一个算法结构,而将一些步骤延迟到子类实现。
- 命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。
- 迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。
- 观察者模式:对象间的一对多的依赖关系。
- 中介者(仲裁者)模式:用一个中介对象来封装一系列的对象交互。
- 备忘录模式:在不破坏封装的前提下,保持对象的内部状态。
- 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。
- 状态模式:允许一个对象在其对象内部状态改变时改变它的行为。
- 责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。
- 访问者模式:不改变数据结构的前提下,增加作用于一组对象元素的新功能。
(一)创建型模式1 - 单例模式(Singleton Pattern)
顾名思义,就是只有一个实例。作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
为什么要使用单例模式
1,php的应用主要在于数据库应用, 一个应用中会存在大量的数据库操作, 在使用面向对象的方式开发时, 如果使用单例模式,
则可以避免大量的new 操作消耗的资源,还可以减少数据库连接这样就不容易出现 too many connections情况。
2,如果系统中需要有一个类来全局控制某些配置信息, 那么使用单例模式可以很方便的实现。
3,在一次页面请求中, 便于进行调试, 因为所有的代码(例如数据库操作类db)都集中在一个类中, 我们可以在类中设置钩子, 输出日志,从而避免到处var_dump, echo
单例模式结构图
单例模式的实现(三私一公)
公有化静态方法作为提供对象的接口,私有属性用于存放唯一一个单例对象。私有化构造方法,私有化克隆方法保证只存在一个单例。
ps:但实际上,虽然我们无法通过new 关键字和clone出一个新的对象,但我们若想得到一个新对象。还是有办法的,那就是通过序列化和反序列化得到一个对象。私有化sleep()和wakeup()方法依然无法阻止通过这种方法得到一个新对象。或许真得要阻止,你只能去__wakeup添加删除一个实例的代码,保证反序列化增加一个对象,你就删除一个。
代码实现
class Singleton{
//存放实例
private static $_instance = null;
//私有化构造方法、
private function __construct(){
echo "单例模式的实例被构造了";
}
//私有化克隆方法
private function __clone(){
}
//公有化获取实例方法
public static function getInstance(){
if (!(self::$_instance instanceof Singleton)){
self::$_instance = new Singleton();
}
return self::$_instance;
}
}
$singleton=Singleton::getInstance();
优点和缺点
优点
(1) 由于单例模式在内存中只有一个实例,减少内存开支,特别是一个对象需要频繁地创建销毁时,而且创建或销毁时性能又无法优化,单例模式就非常明显了
(2) 由于单例模式只生成一个实例,所以,减少系统的性能开销,当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
(3) 单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
(4) 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。
缺点
(1) 单例模式没有抽象层,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
(2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。
(3) 滥用单例将带来一些负面问题,如:为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
又比如:在多个线程中操作单例类的成员时,但单例中并没有对该成员进行线程互斥处理。
使用Trait关键字实现类似于继承单例类的功能
Trait Singleton{
//存放实例
private static $_instance = null;
//私有化克隆方法
private function __clone(){
}
//公有化获取实例方法
public static function getInstance(){
$class = __CLASS__;
if (!(self::$_instance instanceof $class)){
self::$_instance = new $class();
}
return self::$_instance;
}
}
class DB {
private function __construct(){
echo __CLASS__.PHP_EOL;
}
}
class DBhandle extends DB {
use Singleton;
private function __construct(){
echo "单例模式的实例被构造了";
}
}
$handle=DBhandle::getInstance();
//注意若父类方法为public,则子类只能为pubic,若父类为private,子类为public ,protected,private都可以。
(一)创建型模式2&3 - 工厂模式(Factor Pattern) & 抽象工厂模式( Abstract Factor Pattern)
就是负责生成其他对象的类或方法,也叫 工厂方法模式
抽象工厂模式( Abstract Factor Pattern),可简单理解为工厂模式的升级版
三种工厂模式
为什么需要工厂模式
1,工厂模式可以将对象的生产从直接new 一个对象,改成通过调用一个工厂方法生产。这样的封装,代码若需修改new的对象时,不需修改多处new语句,只需更改生产对象方法。
2,若所需实例化的对象可选择来自不同的类,可省略if-else多层判断,给工厂方法传入对应的参数,利用多态性,实例化对应的类。
工厂模式结构图
工厂方法模式
抽象工厂模式
简单工厂实现代码
//工厂类
class Factor{
//生成对象方法
static function createDB(){
echo '我生产了一个DB实例';
return new DB;
}
}
//数据类
class DB{
public function __construct(){
echo __CLASS__.PHP_EOL;
}
}
$db=Factor::createDB();
实现一个运算器
//抽象运算类
abstract class Operation{
abstract public function getVal($i,$j);//抽象方法不能包含方法体
}
//加法类
class OperationAdd extends Operation{
public function getVal($i,$j){
return $i+$j;
}
}
//减法类
class OperationSub extends Operation{
public function getVal($i,$j){
return $i-$j;
}
}
//计数器工厂
class CounterFactor {
private static $operation;
//工厂生产特定类对象方法
static function createOperation(string $operation){
switch($operation){
case '+' : self::$operation = new OperationAdd;
break;
case '-' : self::$operation = new OperationSub;
break;
}
return self::$operation;
}
}
$counter = CounterFactor::createOperation('+');
echo $counter->getVal(1,2);
简单工厂缺点
若是再增加一个乘法运算,除了增加一个乘法运算类之外,还得去工厂生产方法里面添加对应的case代码,违反了开放-封闭原则。
解决方法1 - 通过传入指定类名(工厂方法模式)
//计算器工厂
class CounterFactor {
//工厂生产特定类对象方法
static function createOperation(string $operation){
return new $operation;
}
}
class OperationMul extends Operation{
public function getVal($i,$j){
return $i*$j;
}
}
$counter = CounterFactor::createOperation('OperationMul');
解决方法2 - 通过抽象工厂模式
抽象高于实现
其实我们完全可以抽象出一个抽象工厂,然后将对应的对象生产交给子工厂实现。代码如下
//抽象运算类
abstract class Operation{
abstract public function getVal($i,$j);//抽象方法不能包含方法体
}
//加法类
class OperationAdd extends Operation{
public function getVal($i,$j){
return $i+$j;
}
}
//乘法类
class OperationMul extends Operation{
public function getVal($i,$j){
return $i*$j;
}
}
//抽象工厂类
abstract class Factor{
abstract static function getInstance();
}
//加法器生产工厂
class AddFactor extends Factor {
//工厂生产特定类对象方法
static function getInstance(){
return new OperationAdd;
}
}
//减法器生产工厂
class MulFactor extends Factor {
static function getInstance(){
return new OperationMul;
}
}
//文本输入器生产工厂
class TextFactor extends Factor{
static function getInstance(){}
}
$mul = MulFactor::getInstance();
echo $mul->getVal(1,2);
(一)创建型模式4 - 建造者模式(Builder Pattern)
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式是一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。建造者模式属于对象创建型模式。根据中文翻译的不同,建造者模式又可以称为 生成器模式 。
为什么需要建造者模式
1,对象的生产需要复杂的初始化,比如给一大堆类成员属性赋初值,设置一下其他的系统环境变量。使用建造者模式可以将这些初始化工作封装起来。
2,对象的生成时可根据初始化的顺序或数据不同,而生成不同角色。
建造者模式结构图
模式应用
在很多游戏软件中,地图包括天空、地面、背景等组成部分,人物角色包括人体、服装、装备等组成部分,可以使用建造者模式对其进行设计,通过不同的具体建造者创建不同类型的地图或人物
设计实例
如果我们想创造出有一个person类,我们通过实例化时设置的属性不同,让他们两人一个是速度快的小孩,一个是知识深的长者
class person {
public $age;
public $speed;
public $knowledge;
}
//抽象建造者类
abstract class Builder{
public $_person;
public abstract function setAge();
public abstract function setSpeed();
public abstract function setKnowledge();
public function __construct(Person $person){
$this->_person=$person;
}
public function getPerson(){
return $this->_person;
}
}
//长者建造者
class OlderBuider extends Builder{
public function setAge(){
$this->_person->age=70;
}
public function setSpeed(){
$this->_person->speed="low";
}
public function setKnowledge(){
$this->_person->knowledge='more';
}
}
//小孩建造者
class ChildBuider extends Builder{
public function setAge(){
$this->_person->age=10;
}
public function setSpeed(){
$this->_person->speed="fast";
}
public function setKnowledge(){
$this->_person->knowledge='litte';
}
}
//建造指挥者
class Director{
private $_builder;
public function __construct(Builder $builder){
$this->_builder = $builder;
}
public function built(){
$this->_builder->setAge();
$this->_builder->setSpeed();
$this->_builder->setKnowledge();
}
}
//实例化一个长者建造者
$oldB = new OlderBuider(new Person);
//实例化一个建造指挥者
$director = new Director($oldB);
//指挥建造
$director->built();
//得到长者
$older = $oldB->getPerson();
var_dump($older);
总结
使用建造者模式时,我们把创建一个person实例的过程分为了两步.
一步是先交给对应角色的建造者,如长者建造者。这样的好处就把角色的属性设置封装了起来,我们不用在new一个person时,因为要得到一个older角色的实例,而在外面写了一堆$older->age=70。
另一步是交给了一个建造指挥者,调了一个built方法,通过先设置age,再设置Speed的顺序,初始化这个角色。当然在这个例子中,初始化的顺序,是无所谓的。但是如果对于一个建造汉堡,或是地图,初始化的顺序不同,可能就会得到不同的结果。
也许,你会说,我直接设置也很方便呀。是的,对于某些情况是这样的。但是如果你考虑,我现在想增加一个青年人角色呢?如果我现在想让建造有初始化有三种不同的顺序呢?
如果你使用了建造者模式,这两个问题就简单了,增加一个青年人角色,那就增加一个青年年建造者类。初始化三种不同的顺序,那么就在指挥建造者中增加两种建造方法。
(一)创建型模式5 - 原型模式(Prototype Pattern)
与工厂模式类似,都是用来创建对象的。利用克隆来生成一个大对象,减少创建时的初始化等操作占用开销
为什么需要原型模式
1,有些时候,我们需要创建多个类似的大对象。如果直接通过new对象,开销很大,而且new完还得进行重复的初始化工作。可能把初始化工作封装起来的,但是对于系统来说,你封不封装,初始化工作还是要执行。,
2,原型模式则不同,原型模式是先创建好一个原型对象,然后通过clone这个原型对象来创建新的对象,这样就免去了重复的初始化工作,系统仅需内存拷贝即可。
原型模式结构图
简单实例
如果说,我们现在正开发一个游戏,有不同的地图,地图大小都是一样的,并且都有海洋,但是不同的地图温度不一样。
<?php
//抽象原型类
Abstract class Prototype{
abstract function __clone();
}
//具体原型类
class Map extends Prototype{
public $width;
public $height;
public $sea;
public function setAttribute(array $attributes){
foreach($attributes as $key => $val){
$this->$key = $val;
}
}
public function __clone(){}
}
//海洋类.这里就不具体实现了。
class Sea{}
//使用原型模式创建对象方法如下
//先创建一个原型对象
$map_prototype = new Map;
$attributes = array('width'=>40,'height'=>60,'sea'=>(new Sea));
$map_prototype->setAttribute($attributes);
//现在已经创建好原型对象了。如果我们要创建一个新的map对象只需要克隆一下
$new_map = clone $map_prototype;
var_dump($map_prototype);
var_dump($new_map);
通过上面的代码,我们可以发现利用原型模式,只需要实例化并初始化一个地图原型对象。以后生产一个地图对象,都可以直接通过clone原型对象产生。省去了重新初始化的过程。
但是上面的代码还是存在一些问题。那就是它只是一个浅拷贝,什么意思呢?map原型对象有一个属性sea存放了一个sea对象,在调用setAttribute的时候,对象的赋值方式默认是引用。而当我们克隆map对象时,直接克隆了map的sea属性,这就使得克隆出来的对象与原型对象的sea属性对指向了,同一个sea对象的内存空间。如果这个时候,我们改变了克隆对象的sea属性,那么原型对象的sea属性也跟着改变。
这显然是不合理的,我们想要的结果应该是深拷贝,也就是改变克隆对象的所有属性,包括用来存放sea这种其他对象的属性时,也不影响原型对象。
当然,讲到这里你可以当我在胡说。但我还是建议你打印下原型对象和克隆对象,看一下他们的sea属性吧,然后去好好了解一下什么叫深拷贝和浅拷贝。
深拷贝的实现
深拷贝的实现,其实也简单,我们只要实现Map类的克隆方法就行了。这就是我们为什么要定义一个抽象原型类的原因。我们利用抽象类,强制所有继承的具体原型类都必须来实现这个克隆方法。改进如下:
//具体原型类
class Map extends Prototype{
public $width;
public $height;
public $sea;
public function setAttribute(array $attributes){
foreach($attributes as $key => $val){
$this->$key = $val;
}
}
//实现克隆方法,用来实现深拷贝
public function __clone(){
$this->sea = clone $this->sea;
}
}
到这里原型模式就算实现了,但是我觉还可以进一步进行封装,利用工厂模式或建造者模式的思想。
延伸
举个例子,如果我们在克隆这个地图对象的同时我们还需要进行一下系统设置,或是说我们想给原型对象的clone_id属性赋值当前已经拷贝了多少个对象的总数量?
我们可以把clone这个动作封装到一个类似的工厂方法里面去,简单地实现一下,虽然不咋严谨。
<?php
//抽象原型类
Abstract class Prototype{
abstract function __clone();
}
//具体原型类
class Map extends Prototype{
public $clone_id=0;
public $width;
public $height;
public $sea;
public function setAttribute(array $attributes){
foreach($attributes as $key => $val){
$this->$key = $val;
}
}
//实现克隆方法,用来实现深拷贝
public function __clone(){
$this->sea = clone $this->sea;
}
}
//海洋类.这里就不具体实现了。
class Sea{}
//克隆机器
class CloneTool{
static function clone($instance,$id){
$instance->clone_id ++;
system_write(get_class($instance));
return clone $instance;
}
}
//系统通知函数
function system_write($class){
echo "有人使用克隆机器克隆了一个{$class}对象".PHP_EOL;
}
//使用原型模式创建对象方法如下
//先创建一个原型对象
$map_prototype = new Map;
$attributes = array('width'=>40,'height'=>60,'sea'=>(new Sea));
$map_prototype->setAttribute($attributes);
//现在已经创建好原型对象了。如果我们要创建一个新的map对象只需要克隆一下
$new_map = CloneTool::clone($map_prototype,1);
var_dump($map_prototype);
var_dump($new_map);
模型应用
多用于创建大对象,或初始化繁琐的对象。如游戏中的背景,地图。web中的画布等等
(二)结构型模式1 - 装饰器模式(Decorator Pattern)
允许向一个已有的对象添加新的功能或部分内容,同时又不改变其结构。属于结构型模式,它是作为现有的类的一个包装。
为什么需要装饰器模式
1,我们要对一个已有的对象添加新功能,又不想修改它原来的结构。
2,使用子类继承的方法去实现添加新功能,会不可避免地出现子类过多,继承链很长的情况。而且不少书籍都规劝我们竭力保持一个对象的父与子关系不超过3个。
3,装饰器模式,可以提供对对象内容快速非侵入式地修改。
装饰器模式UML图
简单实例
<?php
/*游戏原来的角色类
class Person{
public function clothes(){
echo "长衫".PHP_EOL;
}
}
*/
//装饰器接口
interface Decorator
{
public function beforeDraw();
public function afterDraw();
}
//具体装饰器1-宇航员装饰
class AstronautDecorator implements Decorator
{
public function beforeDraw()
{
echo "穿上T恤".PHP_EOL;
}
function afterDraw()
{
echo "穿上宇航服".PHP_EOL;
echo "穿戴完毕".PHP_EOL;
}
}
//具体装饰器2-警察装饰
class PoliceDecorator implements Decorator{
public function beforeDraw()
{
echo "穿上警服".PHP_EOL;
}
function afterDraw()
{
echo "穿上防弹衣".PHP_EOL;
echo "穿戴完毕".PHP_EOL;
}
}
//被装饰者
class Person{
protected $decorators = array(); //存放装饰器
//添加装饰器
public function addDecorator(Decorator $decorator)
{
$this->decorators[] = $decorator;
}
//所有装饰器的穿长衫前方法调用
public function beforeDraw()
{
foreach($this->decorators as $decorator)
{
$decorator->beforeDraw();
}
}
//所有装饰器的穿长衫后方法调用
public function afterDraw()
{
$decorators = array_reverse($this->decorators);
foreach($decorators as $decorator)
{
$decorator->afterDraw();
}
}
//装饰方法
public function clothes(){
$this->beforeDraw();
echo "穿上长衫".PHP_EOL;
$this->afterDraw();
}
}
//警察装饰
$police = new Person;
$police->addDecorator(new PoliceDecorator);
$police->clothes();
//宇航员装饰
$astronaut = new Person;
$astronaut->addDecorator(new AstronautDecorator);
$astronaut->clothes();
//混搭风
$madman = new Person;
$madman->addDecorator(new PoliceDecorator);
$madman->addDecorator(new AstronautDecorator);
$madman->clothes();
当然,上面的代码没有严格地按照UML图,这是因为当被装饰者只有一个时,那 Component也就是ConcreteComponent。同理,如果,只有一个装饰器,那也没必要实现一个implment接口。
如果我们有两个不同的被装饰者,那当然就应该抽象出一个Component,让这两个被装饰者去继承它。也许,那继承不是又来了吗。既然外面都用到继承,直接把装饰器的方法放到继承里面不就行了。
可是你想,如果,直接通过继承的话,那装饰过的被装饰者就应该继承自被装饰者,而且被装饰者因为装饰的不同还要有很多不同类型的子类。而使用装饰者模式的话,继承链缩短了,而且不同的装饰类型还可以动态增加。
(二)结构型模式2 - 代理模式(Proxy Pattern)
构建了透明置于两个不同对象之内的一个对象,从而能够截取或代理这两个对象间的通信或访问。
为什么需要代理模式
1,远程代理,也就是为了一个对象在不同地址空间提供局部代表。隐藏一个对象存在于不同地址空间的事实。
2,虚拟代理,根据需要来创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象。
3,安全代理,用来控制真实对象的访问对象。
4,智能指引,当调用真实对象的时候,代理处理一些事情。
代理模式UML图
简单实例
案例一:你想买一张学友哥的新唱片,以前你都是在县城CD店里买的。现在CD行业不景气,没得卖了。你只能找人去香港帮你代购一张。
<?php
//代理抽象接口
interface shop{
public function buy($title);
}
//原来的CD商店,被代理对象
class CDshop implements shop{
public function buy($title){
echo "购买成功,这是你的《{$title}》唱片".PHP_EOL;
}
}
//CD代理
class Proxy implements shop{
public function buy($title){
$this->go();
$CDshop = new CDshop;
$CDshop->buy($title);
}
public function go(){
echo "跑去香港代购".PHP_EOL;
}
}
//你93年买了张 吻别
$CDshop = new CDshop;
$CDshop->buy("吻别");
//14年你想买张 醒着做梦 找不到CD商店了,和做梦似的,不得不找了个代理去香港帮你代购。
$proxy = new Proxy;
$proxy->buy("醒着做梦");
案例二:通过代理实现MySQL的读写分离,如果是读操作,就连接127.0.0.1的数据库,写操作就读取127.0.0.2的数据库
<?php
class Proxy
{
protected $reader;
protected $wirter;
public function __construct(){
$this->reader = new PDO('mysql:host=127.0.0.1;port=3306;dbname=CD;','root','password');
$this->writer = new PDO('mysql:host=127.0.0.2;port=3306;dbname=CD;','root','password');
}
public function query($sql)
{
if (substr($sql, 0, 6) == 'select')
{
echo "读操作: ".PHP_EOL;
return $this->reader->query($sql);
}
else
{
echo "写操作:".PHP_EOL;
return $this->writer->query($sql);
}
}
}
//数据库代理
$proxy = new Proxy;
//读操作
$proxy->query("select * from table");
//写操作
$proxy->query("INSERT INTO table SET title = 'hello' where id = 1");
当然对于数据库来说,这里应该使用单例模式的方法来存放$reader和$writer,但我只是举个例子,不想把单例加进来把代码搞复杂。
但是如果你要实现这样的一个数据库代理,我觉得还是有必要用上单例模式的知识
一句话来说,代理模式,就是在访问对象时通过一个代理对象去访问你想访问的对象。 而在代理对象中,我们可以实现对访问对象的截断或权限控制等操作。
(二)结构型模式3 - 桥接模式(Bridge Pattern)
将抽象与实现解耦,使得两者可以独立的变化
为什么需要桥接模式
1,如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
2,抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
3,虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
桥接模式UML图
Abstraction为抽象化角色,主要职责是定义该角色的行为,同时保存一个对实现化角色的引用。
Implementor为实现化角色,它是一个接口或抽象类。在PHP中一般是抽象类。
简单实例
如果我现在是小米公司的雷布斯,小米公司旗下有小米mix和小米note手机,现在有一个底层的语音输出软件,由于小米mix的全面屏设计没有开孔使用了骨传导,所以和小米note有些不同。那么我们要如何来设计这个输出功能呢?
传统的做法就应该是,一个抽象手机品牌,mix和note手机品牌继承抽象手机品牌。然后有一个mix品牌的输出软件继承自mix品牌。如果这时候有一个redmi的牌子的输出软件就应继承自redmi品牌,redmi继承抽象手机品牌。
如果除了这个语音输出软件,我们还有其它的软件呢,一个手机有很多软件,那么这种继承,如果画出继承链的话,那一个品牌下得有多少子类?
如果我们使用桥接模式的话,就可以把这个软件的具体实现与抽象品牌分离出来,这样也能是我们动态增减实现化角色时更为方便。
<?php
//抽象化角色
abstract class MiPhone{
protected $_audio; //存放音频软件对象
abstract function output();
public function __construct(Audio $audio){
$this->_audio = $audio;
}
}
//具体手机
class Mix extends MiPhone{
//语音输出功能
public function output(){
$this->_audio->output();
}
}
class Note extends MiPhone{
public function output(){
$this->_audio->output();
}
}
//实现化角色 功能实现者
abstract class Audio{
abstract function output();
}
//具体音频实现者 -骨传导音频输出
class Osteophony extends Audio{
public function output(){
echo "骨传导输出的声音-----哈哈".PHP_EOL;
}
}
//普通音频输出---声筒输出
class Cylinder extends Audio{
public function output(){
echo "声筒输出的声音-----呵呵".PHP_EOL;
}
}
//让小米mix和小米note输出声音
$mix = new Mix(new Osteophony);
$mix->output();
$note = new Note(new Cylinder);
$note->output();
这样写的好处是把抽象化角色手机和实现化角色手机的具体功能音频输出 分离了出来。如果现在最新的小米note系列也要用上骨传导输出,那么我们只需实例化时传入的声筒音频类改为骨传导音频类。如果我们还有一个扬声器输出,小米mix和小米note都有这个功能。我们只需要再添加一个扬声器输出类继承Audio类,然后谁使用就保存这个实例在属性中。没有桥接模式的话,我们可是要写两个,一个是小米mix的扬声器输出,一个是小米note。
如果扬声器输出对小米mix和小米note不一样,那么我们确实需要写两个扬声器输出类。使用桥接模式,依然比原来直接继承好,就是因为有一天扬声器技术驱动更新了,我们要更新扬声器不用修改手机类代码,而只需传入一个更新的扬声器类。
(二)结构型模式4 - 适配器模式(Adapter Pattern)
将某个对象的接口适配为另一个对象所期望的接口。
为什么需要适配器模式
1,某个操作数据库的有两套不同的数据库操作方法,我们通过适配器统一成一个接口。例如,我们待会把mysql和mysqli统一成一个接口。
2,我们有多套数据库对应了多种数据库操作,例如MySQL,SqlServer,Oralce,Redis都有对应的操作函数,或操作类。PDO把这些都统一成一个接口。
3,系统的增加一些新功能,创建了一个新的接口,但是老的接口并不想废弃。可以使用适配器模式,对用户隐藏这两个接口,提供用户所希望的接口。
适配器UML图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5zxlo2Kq-1687685254734)(null)]
设计实例
把MySQL和mysqli统一成一个接口,用户可以调用同样的方法使用MySQL和mysqli操作数据库。
<?php
//MySQL待操作适配类
class MySQLAdaptee implements Target
{
protected $conn; //用于存放数据库连接句柄
//实现连接方法
public function connect($host, $user, $passwd, $dbname)
{
$conn = mysql_connect($host, $user, $passwd);
mysql_select_db($dbname, $conn);
$this->conn = $conn;
}
//查询方法
public function query($sql)
{
$res = mysql_query($sql, $this->conn);
return $res;
}
//关闭方法
public function close()
{
mysql_close($this->conn);
}
}
//MySQLi操作待适配类
class MySQLiAdaptee
{
protected $conn;
public function connect($host, $user, $passwd, $dbname)
{
$conn = mysqli_connect($host, $user, $passwd, $dbname);
$this->conn = $conn;
}
public function query($sql)
{
return mysqli_query($this->conn, $sql);
}
public function close()
{
mysqli_close($this->conn);
}
}
//用户所期待的接口
Interface Target{
public function connect($host, $user, $passwd, $dbname);
public function query($sql);
public function close();
}
//用户期待适配类
Class DataBase implements Target {
protected $db ; //存放MySQLiAdapter对象或MySQLAdapter对象
public function __construct($type){
$type = $type."Adapter" ;
$this->db = new $type ;
}
public function connect($host, $user, $passwd, $dbname){
$this->db->connect($host, $user, $passwd, $dbname);
}
public function query($sql){
return $this->db->query($sql);
}
public function close(){
$this->db->close();
}
}
//用户调用同一个接口,使用MySQL和mysqli这两套不同示例。
$db1 = new DataBase('MySQL');
$db1->connect('127.0.0.1','root','1234','myDB');die;
$db1->query('select * from test');
$db1->close();
$db2 = new DataBase('MySQLi');
$db2->connect('127.0.0.1','root','1234','myDB');
$db2->query('select * from test');
$db2->close();
上面的代码只是一个示例,如果你运行以上的代码报了mysql函数不存在或是被废弃的错误。这是正常的,因为MySQL这套函数在PHP5.5以上的版本已经被废弃了。感兴趣的还可以去了解一下PDO的实现。
通过上面的代码,我们可以看到,使用适配器可以把不同的操作接口封装起来,对外显示成用户所期望的接口。
这就好比你家墙上有一个电源三相插孔,但是插孔的孔距之间太小。你的电器三相插头插脚距太大的插不进去,或许你还有个两相的插头,或许你还有条USB线和type-C线,这些都没法插到三相接口里。
于是你买了个插脚适合插到你墙上的排插,然后这个排插是这些年新出的,USB也能插。于是你把你的三相插头,两相插头,USB线,type-c线都插到排插上。实际上就是间接地连在了你墙壁上的三相插孔上。
没错,适配器要做的就是这么回事。
有些书也把适配器模式分为:类的适配器模式,对象的适配器模式,接口的适配器模式
(二)结构型模式5 - 组合模式(Composite Pattern)
将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。组合模式也叫 合成模式 ,有时候又叫做 部分-整体模式 。
为什么需要组合模式
1,使我们在树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
2,组合模式让你可以优化处理递归或分级数据结构。
组合模式UML图
Component是组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。
Leaf 在组合中表示叶子结点对象,叶子结点没有子结点。
Composite定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除(remove)等。
简单实例
如果我们在做一个OA系统,公司的人事管理该如何设计呢。传统的就是树状结构。经理下面有部门主管,然后是员工。
人事部门图
<?php
class Manager{
public $name;
protected $c_nodes = array();//存放子节点,部门经理,普通员工等
public function __construct($name){
$this->name = $name;
}
//添加部门经理
public function addGm(GM $gm){
$this->c_nodes[] = $gm;
}
//添加普通员工
public function addStaff(Staff $staff){
$this->c_nodes[] = $staff;
}
//获取全部子节点
public function get_C_nodes(){
return $this->c_nodes;
}
}
//部门经理 就用general manager 简写 GM
Interface Gm{
public function add(Staff $staff);
public function get_c_nodes();
}
//销售经理
class Sgm implements Gm{
public $name;
protected $c_nodes = array();
public function __construct($name){
$this->name = $name;
}
//添加员工
public function add(Staff $staff){
$this->c_nodes = $staff;
}
//获取子节点
public function get_C_nodes(){
return $this->c_nodes;
}
//区别于其他经理,销售经理有一个销售方法
public function sell(){
echo "安利一下我司的产品";
}
}
//员工接口
Interface staff{
public function work();
}
//销售部员工
class Sstaff implements staff{
public $name;
public function work(){
echo '在销售经理带领下,安利全世界';
}
}
//实例化
$manager = new Manager("总经理");
$sgm = new Sgm("销售经理");
$staff = new Sstaff("何在");
//组装成树
$manager->addGm($sgm);
$sgm->add($staff);
我们想象一下,如果我们的层级非常深,如销售经理下面还有,销售主管,分区经理,分区主管。那怎么办?我们new的时候,就要new很多不同的类。而且如果要加一个任职期限的属性,还得每个类去添加一遍。
我们想做的是,可以把树形结构,当成“部分-整体结构”来处理,通俗地讲,就是把一个树形结构当成一个关系型的结构。例如:数据库存储的一行行的方式。
上面这张图把中国 -湖南-(长沙)(衡阳)-这个树状图以关系型的层级方式存储于数据库中。对于公司人事,其实我们也是可以用这种方法的。那就是利用组合模式。再去看一眼UML图把和人事管理图吧。我们发现总经理和部门经理还是有很多相同的地方。组合模式主要就是把根节点和所有树枝节点归结到一起去,这样就隐藏了树形的层级。
<?php
//抽象构件
Abstract class Component{
public $name;
abstract function doSomething();
public function __construct($name){
$this->name = $name;
}
}
//普通员工 树叶构件 不能添加子节点
class Leaf extends Component{
public $lever;
public function doSomething(){
echo "层级--{$this->lever}--work";
}
}
//总经理 部门经理 主管等 树枝构件
class Composite extends Component{
public $c_nodes = array();
public $lever = 1;
//添加子节点
public function add(Component $component){
$component->lever = $this->lever + 1;
$this->c_nodes[] = $component;
}
public function doSomething(){
echo "我是层级--{$this->lever}--".PHP_EOL;
}
}
$manager = new Composite("总经理");
$sgm = new Composite("销售经理");
$staff = new Leaf("何在");
//组装成树
$manager->add($sgm);
$sgm->add($staff);
这样,我们就把根节点(总经理)和所有树枝节点(部门经理,主管)的树枝结构隐藏了,通过$lever属性来区分。如果对于不同树枝节点有不同的方法,我们也可以在Composite 类中的doSomething()方法中延迟绑定具体的方法实现,使不同层级具有不同能力
public function doSomething(){
switch($this->lever){
case 1:$this->manager();
break;
case 2:$this->sell();
}
}
private function manager(){}
private function sell(){}
当然,当层级太深时,若有多个层级有相同的doSomething能力。这种方法还是可以的。但是当层级太深且不同层级具有不同的doSomething能力时,就会导致一个类中空置了多个不用的
private方法,而doSomething只调用一个。
$lever 的另一个功能就是便于递归遍历出所有公司人员
//遍历树 - 函数
function display(Composite $composite){
$composite->doSomething();
foreach($composite->c_nodes as $c_node)
$c_node instanceof Leaf ? $c_node->doSomething() : display($c_node);
}
display($manager);
当然这种遍历方法只能前序遍历,即从根节点总经理向下找,没法从任何一个员工向上找出他的上级。如果,你想实现后序遍历,可以在Component类中添加一个parent属性,并在composite 的add方法中设置子节点的parent属性。
组合模式也分为透明模式和安全模式,上面的例子是安全模式。透明模式是把composite的方法也放到抽象类component中。
有许多关于分级数据结构的例子,使得组合模式非常有用武之地。关于分级数据结构的一个普遍性的例子是你每次使用电脑时所遇到的:文件系统。文件系统由目录和文件组成。每个目录都可以装内容。目录的内容可以是文件,也可以是目录。按照这种方式,计算机的文件系统就是以递归结构来组织的。如果你想要描述这样的数据结构,那么你可以使用组合模式Composite。
(二)结构型模式6 - 外观模式(Facade Pattern)
为子系统中的一组接口提供一个一致的界面,定义一个高层接口,这个接口使得这一子系统更加容易使用。
为什么需要外观模式
1,开发阶段,子系统越来越复杂,增加外观模式提供一个简单的调用接口。
2,维护一个大型遗留系统的时候,可能这个系统已经非常难以维护和扩展,但又包含非常重要的功能,为其开发一个外观类,以便新系统与其交互。
3,外观模式可以隐藏来自调用对象的复杂性。
外观模式UML图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sShikc9V-1687685254387)(null)]
简单实例
比如说我们去医院就诊,医院有医生员工系统,有药品系统,有患者资料系统。但是我们只是在前台挂个号,就能在其他系统里都看到我们。外观系统就差不多这样。
如果没有挂号系统的话,我们就先要去医生系统通知一下医生,
然后去患者系统取一下患者资料交给医生,再去药品系统登记一下,最后到药房领药。
<?php
//医院医生员工系统
class DoctorSystem{
//通知就诊医生
static public function getDoctor($name){
echo __CLASS__.":".$name."医生,挂你号".PHP_EOL;
return new Doctor($name);
}
}
//医生类
class Doctor{
public $name;
public function __construct($name){
$this->name = $name;
}
public function prescribe($data){
echo __CLASS__.":"."开个处方给你".PHP_EOL;
return "祖传秘方,药到必死";
}
}
//患者系统
class SufferSystem {
static function getData($suffer){
$data = $suffer."资料";
echo __CLASS__.":".$suffer."的资料是这些".PHP_EOL ;
return $data;
}
}
//医药系统
class MedicineSystem {
static function register($prescribe){
echo __CLASS__.":"."拿到处方:".$prescribe."------------通知药房发药了".PHP_EOL;
Shop::setMedicine("砒霜5千克");
}
}
//药房
class shop{
static public $medicine;
static function setMedicine($medicine){
self::$medicine = $medicine;
}
static function getMedicine(){
echo __CLASS__.":".self::$medicine.PHP_EOL;
}
}
//如果没有挂号系统,我们就诊的第一步
//通知就诊医生
$doct = DoctorSystem::getDoctor("顾夕衣");
//患者系统拿病历资料
$data = SufferSystem::getData("何在");
//医生看病历资料,开处方
$prscirbe = $doct->prescribe($data);
//医药系统登记处方
MedicineSystem::register($prscirbe);
//药房拿药
Shop::getMedicine();
echo PHP_EOL.PHP_EOL."--------有了挂号系统以后--------".PHP_EOL.PHP_EOL;
//挂号系统
class Facade{
static public function regist($suffer,$doct){
$doct = DoctorSystem::getDoctor($doct);
//患者系统拿病历资料
$data = SufferSystem::getData($suffer);
//医生看病历资料,开处方
$prscirbe = $doct->prescribe($data);
//医药系统登记处方
MedicineSystem::register($prscirbe);
//药房拿药
Shop::getMedicine();
}
}
//患者只需要挂一个号,其他的就让挂号系统去做吧。
Facade::regist("叶好龙","贾中一");
外观模式,也叫 门面模式 。它多用于在多个子系统之间,作为中间层。用户通过Facade对象,直接请求工作,省去了用户调用多个子系统的复杂动作。
外观模式常举的一个例子,就是我们买了好多支股票,但是时间有限。盯盘很复杂,我们搞得一团糟。所以,我们干脆买了股票基金。股票基金就好比于外观模式的Facade对象,而子系统就是股票基金投的各支股票。
(二)结构型模式7 - 享元模式 (Flyweight Pattern)
池技术的重要实现方式, 运用共享技术有效的支持大量的细粒度对象,用于减少创建对象的数量,以减少内存占用和提高性能。
为什么需要享元模式
1,在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
2,系统有大量相似对象。
3,需要缓冲池的场景。
享元模式UML图
简单实例
这里引用《设计模式之禅》的例子,就是如果有一个报考信息系统,每个考生进来就实例化一个对象,传入他要报考的考点。由于考生分为30个考点,所以其实我们没有必要不停的实例化对象并释放,我们完全可以做一个缓冲池,若缓冲池中没有与这个考生相同考点的对象,则实例化一个,使用完不需要释放。下一个相同考点的考生进来后,只需复用这个对象。
<?php
//抽象享元对象
abstract class Flyweight{
//考点
public $address;
//享元角色必修设置考点
public function __construct($address){
$this->address = $address;
}
}
//具体享元角色 考生类
class ConcreteFlyweight extends Flyweight{
//报考动作
public function register(){
echo "我的报考点是:{$this->address}".PHP_EOL;
}
//退出
public function quit(){
unset($this);
}
}
//享元工厂 缓冲池
class FlyweightFactor{
static private $students = array();
static public function getStudent($address){
$students =self::$students;
//判断该键值是否存在
if(array_key_exists($address,$students)){
echo "缓冲池有考点为{$address},从池中直接取".PHP_EOL;
}else{
echo "缓冲池没有,创建了考点为{$address}的对象并放到池中".PHP_EOL;
self::$students[$address] = new ConcreteFlyweight($address);
}
return self::$students[$address];
}
}
//实例化学生对象
$student_1 = FlyweightFactor::getStudent('广州');
//报考
$student_1 ->register();
// 退出
$student_1->quit();
//第二个学生进来
$student_2 = FlyweightFactor::getStudent('东莞');
//报考
$student_2 ->register();
// 退出
$student_2->quit();
//第三个学生进来
$student_3 = FlyweightFactor::getStudent('广州');
//报考
$student_3 ->register();
// 退出
$student_3->quit();
这里我们可以看到,当第三个学生进来时,由于和第一个学生的报考地点一致,所以只需从缓冲池取出。
ps:虽然ConcreteFlyweight的quit方法unset了$this,但是由于在FlyweightFactor中的students还存放着这个对象,所以unset只释放了变量$student_1,并没有完全删除这个对象,这就是student_3进来还能从缓冲池取得对象的原因
享元模式在PHP中可能比较少遇,但在Java中常常有这种情况。就是代码产生了大量的对象,虽然使用完有释放,但是由于垃圾回收需要一定时间,导致内存被耗尽。PHP经常应用web编程,脚本执行时间很短(30秒)。所以很少遇见这种情况,甚至我们使用完变量,连unset()函数都不用调用,就等着脚步执行结束后,自动释放。但是如果你试过在cli模式下运行PHP脚本,做一些socket通信,发送邮件等长耗时或是多连接任务时,就难免会遇到这种情况。
(三)行为型模式1 - 策略模式 ( Strategy Pattern )
定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为 政策模式(Policy)
为什么需要策略模式
1,在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
2,利用面向对象的继承和多态机制,将多个算法解耦。避免类中出现太多的if-else语句
策略模式UML图
Context(应用场景):
1、需要使用ConcreteStrategy提供的算法。
2、 内部维护一个Strategy的实例。
3、 负责动态设置运行时Strategy具体的实现算法。
4、负责跟Strategy之间的交互和数据传递。
Strategy(抽象策略类):定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,Context使用这个接口调用不同的算法,一般使用接口或抽象类实现。
ConcreteStrategy(具体策略类):实现了Strategy定义的接口,提供具体的算法实现。
简单实例
如果我们现在在做一个商城,用户来了,我们想向其推荐对应的商品。比如说一个女性用户登录的话,我们就问她需要衣服,男性用户就推荐螺丝刀,不确定性别就推荐《葵花宝典》
<?php
//抽象策略接口
abstract class Strategy{
abstract function peddle();
}
//具体封装的策略方法 女性用户策略
class ConcreteStrategyA extends Strategy
{
public function peddle(){
echo '美女穿这件衣服肯定很好看'.PHP_EOL;
}
}
//男性用户策略
class ConcreteStrategyB extends Strategy
{
public function peddle(){
echo '每一个男人都需要一个工具箱,充实工具箱,从购买各种螺丝刀开始'.PHP_EOL;
}
}
//未知性别用户策略
class ConcreteStrategyC extends Strategy
{
public function peddle(){
echo '骨骼清奇,这本《葵花宝典》最适合你'.PHP_EOL;
}
}
//环境类
class Context{
protected $strategy;
public function __construct(Strategy $strategy)
{
$this->strategy = $strategy;
}
public function request()
{
$this->strategy->peddle($this);
}
}
//若处于女性用户环境
$female_context = new Context(new ConcreteStrategyA);
$female_context->request();
//若处于男性用户环境
$male_context = new Context(new ConcreteStrategyB);
$male_context->request();
//若处于未知性别用户环境
$unknow_context = new Context(new ConcreteStrategyC);
$unknow_context->request();
这里我们可以看到,我们可以通过不同的环境选择不同的策略,用户性别的判断 如果对于一个商城系统来说,完全可以在用户登陆的时候通过数据库的用户信息得到,然后做出判断,选择适合的策略。
而且策略模式最显而易见的优势,就是如果我想增加一个新的策略怎么办?比如说,如果用户是小孩,就给他推荐《五年中考三年模拟》。是的,相信你们心里已经有答案了。那就是,再增加一个具体策略类。
(三)行为型模式2 - 模板模式 (Template Pattern)
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
为什么需要模板模式
1,一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
2, 多个子类有相同的方法,逻辑基本相同时。可将相同的逻辑代码提取到父类
3,重构时,将相同代码抽取到父类,然后通过钩子函数约束其行为
模板模式UML图
简单实例
这里举《PHP设计模式》的一个例子:一个银行可以有许多不同类型的银行账户,但是所有账户的处理方式基本相同。假设我们现在有两类账户,一类是普通账户,一类是信用卡账户。现在进行支付,信用卡允许透支,普通账户不允许透支,即账户金额不允许小于零
<?php
//抽象模板类
abstract class Template{
protected $balance = 100; //账户余额,为测试方便,直接赋初值100
//结算方法
abstract protected function adjust($num);
//支付信息显示
abstract protected function display($num);
final public function apply($num){
$this->adjust($num);
$this->display($num);
}
}
//普通账户
class Account extends Template{
protected $falg; //用于判断支付是否成功
protected function adjust($num){
if($this->balance > $num){//只有余额大于所需支付金额才允许支付
$this->balance-=$num;
$this->falg = true;
}else{
$this->falg = false;
}
}
protected function display($num){
if($this->falg){
echo '支付成功,所剩余额为'.$this->balance.PHP_EOL;
}else{
echo '余额不足,支付失败,所剩余额为'.$this->balance.PHP_EOL;
}
}
}
//信用卡用户
class Credit extends Template{
protected function adjust($num){
$this->balance-=$num;
}
protected function display($num){
echo '感谢您使用信用支付,所剩余额为'.$this->balance.PHP_EOL;
}
}
//普通账户使用
$account = new Account;
//普通账户使用
$account -> apply(80);
//普通账户透支
$account -> apply(30);
//信用卡账户使用
$credit = new Credit;
$credit -> apply(200);
模板模式的好处在于行为由父类控制,而具体的实现由子类实现。这就可以把一个操作延迟绑定到子类上。还有另一种应用是把复杂的核心代码设计为 模板方法 ,周边的相关细节则由子类实现。
(三)行为型模式3 - 命令模式 (Command Pattern)
将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为 动作(Action)模式 或 事务(Transaction)模式 。
为什么需要命令模式
1, 使用命令模式,能够让请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活。
2,使用命令模式可以比较容易地设计一个命令队列和宏命令(组合命令),而且新的命令可以很容易地加入系统
命令模式UML图
简单实例
有关命令模式的一个经典的例子,就是电视机与遥控器。如果按照命令模式的UML图,那么就有这样的对应关系:电视机是请求的接收者,遥控器是请求的发送者。遥控器上有一些按钮,不同的按钮对应电视机的不同操作。这些按钮就是对应的具体命令类。抽象命令角色由一个命令接口来扮演,有三个具体的命令类实现了抽象命令接口,这三个具体命令类分别代表三种操作:打开电视机、关闭电视机和切换频道。
<?php
//抽象命令角色
abstract class Command{
protected $receiver;
function __construct(TV $receiver)
{
$this->receiver = $receiver;
}
abstract public function execute();
}
//具体命令角色 开机命令
class CommandOn extends Command
{
public function execute()
{
$this->receiver->action();
}
}
//具体命令角色 关机机命令
class CommandOff extends Command
{
public function execute()
{
$this->receiver->action();
}
}
//命令发送者 --遥控器
class Invoker
{
protected $command;
public function setCommand(Command $command)
{
$this->command = $command;
}
public function send()
{
$this->command->execute();
}
}
//命令接收者 Receiver =》 TV
class TV
{
public function action()
{
echo "接收到命令,执行成功".PHP_EOL;
}
}
//实例化命令接收者 -------买一个电视机
$receiver = new TV();
//实例化命令发送者-------配一个遥控器
$invoker = new Invoker();
//实例化具体命令角色 -------设置遥控器按键匹配电视机
$commandOn = new CommandOn($receiver);
$commandOff = new CommandOff($receiver);
//设置命令 ----------按下开机按钮
$invoker->setCommand($commandOn);
//发送命令
$invoker->send();
//设置命令 -----------按下关机按钮
$invoker->setCommand($commandOff);
//发送命令
$invoker->send();
在实际使用中,命令模式的receiver经常是一个抽象类,就是对于不同的命令,它都有对应的具体命令接收者。命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
(三)行为型模式4 - 迭代模式(Iterator Pattern)
迭代器模式可帮组构造特定的对象,那些对象能够提供单一的标准接口循环或迭代任何类型的可计数数据。
为什么需要迭代模式
1,我们想要向遍历数组那样,遍历对象,或是遍历一个容器。
2,迭代器模式可以隐藏遍历元素所需的操作-
迭代模式UML图
上图是截取了《PHP设计模式》中迭代器的UML图,仅供参考。在PHP中我们对迭代模式的使用,通常只需实现Iterator接口即可。
下图给出实现 Interator接口 所需实现的方法
简单实例
现在我们把一个类当做一个容器,让其实现Iterator接口,使其可以被遍历。
<?php
class ArrayContainer implements Iterator
{
protected $data = array();
protected $index ;
public function __construct($data)
{
$this->data = $data;
}
//返回当前指针指向数据
public function current()
{
return $this->data[$this->index];
}
//指针+1
public function next()
{
$this->index ++;
}
//验证指针是否越界
public function valid()
{
return $this->index < count($this->data);
}
//重置指针
public function rewind()
{
$this->index = 0;
}
//返回当前指针
public function key()
{
return $this->index;
}
}
//初始化数组容器
$arr = array(0=>'唐朝',1=>'宋朝',2=>'元朝');
$container = new ArrayContainer($arr);
//遍历数组容器
foreach($container as $a => $dynasty){
echo '如果有时光机,我想去'.$dynasty.PHP_EOL;
}
迭代器其实也类似于数据库的游标,可以在容器内上下翻滚,遍历它所需要查看的元素。
通过实现Iterator接口,我们就可以把一个对象里的数据当一个数组一样遍历。也许你会说我把一个数组直接遍历不就行了吗,为什么你还要把数组扔给容器对象,再遍历容器对象呢?是这样的,通过容器对象,我们可以隐藏我们foreach的操作。比如说,我想遍历时,一个元素输出,一个元素不输出怎么办呢?利用迭代器模式,你只需把容器对象中的next方法中的index++语句改为index+=2即可。这点,你可以试试。
为何实现一个Iterator接口就必须实现current那些方法呢?其实foreach容器对象的时候,PHP是自动帮我们依次调用了,valid,next这些方法。具体的调用顺序,还有Iterator接口的实现,其实是属于SPL(Standard PHP Library)的内容。
(三)行为型模式5 - 观察者模式 (observer Pattern)
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。也叫发布-订阅模式
为什么需要观察者模式
1,一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作
2,完美的将观察者和被观察的对象分离开,使得每个类将重点放在某一个功能上,一个对象只做一件事情。
3,观察者模式在模块之间划定了清晰的界限,提高了应用程序的可维护性和重用性。
观察者模式UML图
简单实例
观察者模式也叫发布订阅模式,如果说我们现在在做一个系统。我们让所有客户端订阅我们的服务端,那么当我们的服务端有更新信息的时候,就通知客户端去更新。这里的服务端就是被观察者,客户端就是观察者。
<?php
//抽象被观察者
abstract class Subject{
//定义一个观察者数组
private $observers = array();
//增加观察者方法
public function addObserver(Observer $observer){
$this->observers[] = $observer;
echo "添加观察者成功".PHP_EOL;
}
//删除观察者方法
public function delObserver(Observer $observer){
$key = array_search($observer,$this->observers); //判断是否有该观察者存在
if($observer===$this->observers[$key]) { //值虽然相同 但有可能不是同一个对象 ,所以使用全等判断
unset($this->observers[$key]);
echo '删除观察者成功'.PHP_EOL;
} else{
echo '观察者不存在,无需删除'.PHP_EOL;
}
}
//通知所有观察者
public function notifyObservers(){
foreach($this->observers as $observer){
$observer->update();
}
}
}
//具体被观察者 服务端
class Server extends Subject{
//具体被观察者业务 发布一条信息,并通知所有客户端
public function publish(){
echo '今天天气很好,我发布了更新包'.PHP_EOL;
$this->notifyObservers();
}
}
//抽象观察者接口
Interface Observer{
public function update();
}
//具体观察者类
//微信端
class Wechat implements Observer{
public function update(){
echo '通知已接收,微信更新完毕'.PHP_EOL;
}
}
//web端
class Web implements Observer{
public function update(){
echo '通知已接收,web端系统更新中'.PHP_EOL;
}
}
//app端
class App implements Observer{
public function update(){
echo '通知已接收,APP端稍后更新'.PHP_EOL;
}
}
//实例化被观察者
$server = new Server ;
//实例化观察者
$wechat = new Wechat ;
$web = new Web ;
$app = new App;
//添加被观察者
$server->addObserver($wechat);
$server->addObserver($web);
$server->addObserver($app);
//被观察者 发布信息
$server->publish();
//删除观察者
$server->delObserver($wechat);
//再次发布信息
$server->publish();
//尝试删除一个未添加成观察者的对象
$server->delObserver(new Web);
//再次发布信息
$server->publish();
观察者模式的一个关键词就是触发,被观察者的动作触发观察者的做出对应的响应。观察者模式的另一个常用领域在于插件系统。
在PHP中观察者的另一种实现方式,是通过实现SplSubject接口和SplObserver。
(三)行为型模式6 - 中介者模式(Mediator Pattern)
用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。
为什么需要中介者模式
1,中介者模式可以使对象之间的关系数量急剧减少。
2,中转作用(结构性):通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,通过中介者即可。该中转作用属于中介者在结构上的支持。
3,协调作用(行为性):中介者可以更进一步的对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。该协调作用属于中介者在行为上的支持。
中介者模式UML图
简单实例
中介者模式的思想在现实生活中也很常见,比如说交换机。没有交换机存在的时代,每个电话之间都需要电话线连接才能进行通话。如果一个台电话要和其它100台电话通话,那么它就必须要有100条电话线与其它100个电话连接。
后来为了解决这种麻烦,交换机出现了。每个电话只需连入交换机,通话时。只需构建一条电话-交换机-电话的链路,就可以进行通话。所以现在我们的电话理论上可以同世界上任何一台电话通话,但是只需一条电话线。当然现在用电话的人少了,但是手机呀,计算机网络的实现也是在传统通信网的设计上进行演进的。
其实交换机对应的就是中介者模式的中介者,而电话机就是中介者中的同事。下面,就让我们用代码来实现这个思想。
<?php
//抽象同事类 --------电话机
abstract class Colleague{
protected $mediator; //用于存放中介者
abstract public function sendMsg($num,$msg);
abstract public function receiveMsg($msg);
//设置中介者
final public function setMediator(Mediator $mediator){
$this->mediator = $mediator;
}
}
//具体同事类 ---------座机
class Phone extends Colleague
{
public function sendMsg($num,$msg)
{
echo __class__.'--发送声音:'.$msg.PHP_EOL;
$this->mediator->opreation($num,$msg);
}
public function receiveMsg($msg)
{
echo __class__.'--接收声音:'.$msg.PHP_EOL;
}
}
//具体同事类----------手机
class Telephone extends Colleague
{
public function sendMsg($num,$msg)
{
echo __class__.'--发送声音:'.$msg.PHP_EOL;
$this->mediator->opreation($num,$msg);
}
//手机接收信息前 会智能响铃
public function receiveMsg($msg)
{
echo '响铃-------'.PHP_EOL;
echo __class__.'--接收声音:'.$msg.PHP_EOL;
}
}
//抽象中介者类
abstract class Mediator{
abstract public function opreation($id,$message);
abstract public function register($id,Colleague $colleague);
}
//具体中介者类------交换机
class switches extends Mediator
{
protected $colleagues = array();
//交换机业务处理
public function opreation($num,$message)
{
if (!array_key_exists($num,$this->colleagues)) {
echo __class__.'--交换机内没有此号码信息,无法通话'.PHP_EOL;
}else{
$this->colleagues[$num]->receiveMsg($message);
}
}
//注册号码
public function register($num,Colleague $colleague)
{
if (!in_array($colleague, $this->colleagues)) {
$this->colleagues[$num] = $colleague;
}
$colleague->setMediator($this);
}
}
//实例化固话
$phone = new Phone;
//实例化手机
$telephone = new Telephone;
//实例化交换机
$switches = new Switches;
//注册号码 ---放号
$switches->register(6686668,$phone);
$switches->register(18813290000,$telephone);
//通话
$phone->sendMsg(18813290000,'hello world');
$telephone->sendMsg(6686668,'请说普通话');
$telephone->sendMsg(6686660,'你好');
(三)行为型模式7 - 备忘录模式(Memento Pattern)
在不破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。又叫做 快照模式(Snapshot Pattern) 或 Token模式
为什么需要备忘录模式
1,有时一些发起人对象的内部信息必须保存在发起人对象以外的地方,但是必须要由发起人对象自己读取,这时,使用备忘录模式可以把复杂的发起人内部信息对其他的对象屏蔽起来,从而可以恰当地保持封装的边界。
2,本模式简化了发起人类。发起人不再需要管理和保存其内部状态的一个个版本,客户端可以自行管理他们所要的这些状态的版本。
备忘录模式UML图
简单实例
备忘录模式往简单了说,就是打副本。这里我们给出一个备忘录模式的小例子,备份一个游戏角色,也就是发起者的初始状态,并恢复。
<?php
//发起人,所需备份者
class Originator{
//内部状态
private $state;
//设置状态
public function setState($state){
$this->state = $state ;
}
//查看状态
public function getState(){
echo $this->state,PHP_EOL;
}
//创建一个备份录
public function createMemento(){
return new Memento($this->state);
}
//恢复一个备份
public function restoreMemento(Memento $memento){
$this->state = $memento->getState();
}
}
//备忘录角色
class Memento{
private $state; //用于存放发起人备份时的状态
public function __construct($state){
$this->state = $state;
}
public function getState(){
return $this->state;
}
}
//备忘录管理者
class Caretaker{
private $menento;
//存档备忘录
public function setMemento(Memento $memento){
$this->memento = $memento;
}
//取出备忘录
public function getMemento(){
return $this->memento;
}
}
//实例化发起人 假如是个游戏角色
$role = new Originator;
//设置状态 满血
$role->setState('满血');
//备份
//创建备份录管理者
$caretaker = new Caretaker;
//创建备份
$caretaker->setMemento($role->createMemento());
//状态更改
$role->setState('死亡');
$role->getState();
//恢复备份
$role->restoreMemento($caretaker->getMemento());
//重新查看状态
$role->getState();
可能最后那段恢复备份的代码有点绕,这是因为我们引入了备份管理者。其实如果对于只有一个备份,那么我们也可以不用备份管理者。而备份管理者存在的好处,当然是管理多个备份了。如果对于多个备份,我们可以把备份管理者的memento属性改为数组变量,就可以存放多个备份了。
其实备份在原型模式我们也提过,我们完全可以通过clone关键字来备份,但是备忘录模式相对于原型模式更精简,可能有些时候我们只想备份的就只有这一个属性呢。而且从本质上说备忘录模式恢复备份后还是原来那个对象,而原型模式就不一定了。如果原型模式恢复备份是直接使用clone出来的对象副本,那么其实它就不算原来那个对象了,虽然它和被clone的对象几乎一模一样,使用无差别,但是对于var_dump,它的object#id肯定是不一样的。
(三)行为型模式8 - 解释器模式(Interpreter Pattern)
提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等
为什么需要解释器模式
可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
解释器模式UML图
简单实例
解释器模式是开发中最少使用的,因为我们亲自编写语法解析的时候总是非常非常少。而且我能想到的解释器模式例子,几乎都是代码繁多到我自己怕。所以这里我只给出解释器模式的UML图通用代码,感兴趣的话可以自己去实现
<?php
//抽象表达式
abstract class Expression{
//任何表达式子类都应该有一种解析任务
abstract public function interpreter($context);
}
//抽象表达式是生成语法集合(语法树)的关键,每个语法集合完成指定语法解析任务
//抽象表达式通过递归调用的方法,最终由最小语法单元进行解析完成
//终结符表达式 通常指运算变量
class TerminalExpression extends Expression{
//终结符表达式通常只有一个
public function interpreter($context){
return null; //视具体业务实现
}
}
//非终结符表达式 通常指运算的符号
class NonterminalExpression extends Expression{
public function interpreter($context){
return null;
}
}
(三)行为型模式9 - 状态模式 (State Pattern)
允许一个对象在其内部状态改变时改变它的行为,让不同状态的对象看起来似乎修改了它的类,或者说是看起来不是来自同一个类。
为什么需要状态模式
1,将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
2,本模式简化了发起人类。发起人不再需要管理和保存其内部状态的一个个版本,客户端可以自行管理他们所要的这些状态的版本。
状态模式UML图
简单实例
状态模式一个最妙的应用就是通过变化状态拥有不同的能力。比如我们以水为例,水如果是固态,那么它就能融化成液态,如果是液态那么它就能蒸发成气态,而气态也能凝华成固态。现在就让我们用程序来模拟这个过程。
<?php
//抽象状态类
abstract class State{
abstract function handle();
}
//固态
class Solid extends State{
public function handle(){
echo '固态 =>融化 =>液态转化中'.PHP_EOL;
}
}
class Liquid extends State{
public function handle(){
echo '液态 =>蒸发 =>气态转化中'.PHP_EOL;
}
}
class Gas extends State{
public function handle(){
echo '气态 =>凝华 =>固态转化中'.PHP_EOL;
}
}
//context环境类 -----water
class Water{
protected $states = array();
protected $current=0;
public function __construct()
{
$this->states[]=new Solid;
$this->states[]=new Liquid;
$this->states[]=new Gas;
}
//水的变化
public function change(){
//告知当前状态
echo '当前所处状态'.get_Class($this->states[$this->current]).PHP_EOL;
//当前状态能力
$this->states[$this->current]->handle();
//状态变化
$this->changeState();
}
//状态变化
public function changeState()
{
$this->current++ == 2 && $this->current = 0;
}
}
//实例化具体环境角色-----水
$water = new Water;
//水的能力变化 ---与它的状态相关
$water->change();
$water->change();
$water->change();
$water->change();
当然我们这里只是一个简单的示例,你完全可以让一个状态有多个能力,或者通过给water给一个对外的接口,通过传参使其转化为你指定的状态。
(三)行为型模式10 - 责任链模式(Chain of Responsibility Pattern)
为请求创建了一个接收者对象的链,并沿这条链传递该请求,直到有对象处理它为止。这种模式能够给予请求的类型,对请求的发送者和接收者进行解耦。
为什么需要责任链模式
1,将请求的发送者和请求的处理者解耦了。责任链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递。
2, 发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任
责任链模式 UML图
简单实例
如果我们现在做一个员工系统,如果说公司有规定说员工有请示,要根据请示的级别由不同人批示。比如请假由组长批示,休假由主管批示,辞职由总经理批示。好了,如果我们使用代码如何实现呢?员工类,组长类,主管类,总经理类。那么员工对象的请示直接交给总经理吗,显然是不好。如果我们按照现实世界的逻辑来实现,那应该是怎样的呢?(我很赞同一句话:任何代码的逻辑都在现实世界能找到与之对应的场景,如果没有,就是你的逻辑有问题)
<?php
//抽象处理者
abstract class Handler{
private $next_handler ;//存放下一处理对象
private $lever = 0; //处理级别默认0
abstract protected function response(); //处理者回应
//设置处理级别
public function setHandlerLevel($lever){
$this->lever = $lever ;
}
//设置下一个处理者是谁
public function setNext(Handler $handler){
$this->next_handler = $handler;
$this->next_handler->setHandlerLevel($this->lever+1);
}
//责任链实现主要方法,判断责任是否属于该对象,不属于则转发给下一级。使用final不允许重写
final public function handlerMessage(Request $request){
if($this->lever == $request->getLever()){
$this->response();
}else{
if($this->next_handler != null){
$this->next_handler->handlerMessage($request);
}else{
echo '洗洗睡吧,没人理你'.PHP_EOL;
}
}
}
}
//具体处理者
// headman 组长
class HeadMan extends Handler{
protected function response(){
echo '组长回复你:同意你的请求'.PHP_EOL;
}
}
//主管director
class Director extends Handler{
protected function response(){
echo '主管回复你:知道了,退下'.PHP_EOL;
}
}
//经理manager
class Manager extends Handler{
protected function response(){
echo '经理回复你:容朕思虑,再议'.PHP_EOL;
}
}
//请求类
class Request{
protected $level = array('请假'=>0,'休假'=>1,'辞职'=>2);//测试方便,默认设置好请示级别对应关系
protected $request;
public function __construct($request){
$this->request = $request;
}
public function getLever(){
//获取请求对应的级别,如果该请求没有对应级别 则返回-1
return array_key_exists($this->request,$this->level)?$this->level[$this->request]:-1;
}
}
//实例化处理者
$headman = new HeadMan;
$director = new Director;
$manager = new Manager;
//责任链组装
$headman->setNext($director);
$director->setNext($manager);
//传递请求
$headman->handlerMessage(new Request('请假'));
$headman->handlerMessage(new Request('休假'));
$headman->handlerMessage(new Request('辞职'));
$headman->handlerMessage(new Request('加薪'));
具体的处理者即对应UML图中的ConcreteHandler,请求类对应UML图的client。使用责任链,可以使对应的处理者有对应的单一的response方法。请求只需交给最低级的处理者,由属性$lever判断,一层层传递到与请求级别相同的处理者中做出对应的回应。请求无需知道,要交给对应的那个处理者。
当然,当责任链过长时也会引起性能问题。对此我们应避免使用过长的责任链。
(三)行为型模式11 - 访问者模式(Visitor Pattern)
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
为什么需要访问者模式
访问者模式能够在不更改对象的情况下向该对象添加新的功能性
访问者模式UML图
访问者模式UML图通常是比较复杂,如果对于只有一个元素和一种访问者,我们其实也可以不用抽象元素和抽象访问者,不要objectStruct。
下图给出《PHP设计模式》中的访问者模式UML图。
简单实例
用《PHP设计模式》一书中的UML图,实现如何在不更改对象的情况下向该对象添加新的功能性。
<?php
//具体元素
class Superman{
public $name;
public function doSomething(){
echo '我是超人,我会飞'.PHP_EOL;
}
public function accept(Visitor $visitor){
$visitor->doSomething($this);
}
}
//具体访问者
class Visitor{
public function doSomething($object){
echo '我可以返老还童到'.$object->age = 18;
}
}
//实例化具体对象
$man = new Superman;
//使用自己的能力
$man->doSomething();
//通过添加访问者,把访问者能能力扩展成自己的
$man->accept(new Visitor);
我们可以看到,通过调用accept方法接收一个访问者,具体对象可以把访问者的doSomething能力也扩展为自己能力。当然如果你需要多个扩展能力,你可以有多个访问者。而accept方法调用访问者的dosomething方法时,传入$this是为了能使用Superman的属性和方法,使其感觉扩展完就是Superman的真正一部分。
23种设计模式之外
(一)、创建型模式
对象实例化的模式,创建型模式用于解耦对象的实例化过程。
- 注册树(注册器)模式:为应用中经常使用的对象创建一个中央存储器来存放这些对象。
(二)、结构型模式
把类或对象结合在一起形成一个更大的结构。
- 数据映射模式:描述如何创建提供透明访问任何数据源的对象。
(三)、行为型模式
类和对象如何交互,及划分责任和算法。
- 用一个空对象取代 NULL,减少对实例的检查。这样的空对象可以在数据不可用的时候提供默认的行为
(二)结构型模式8 - 数据映射模式(Data Mapper Pattern)
描述如何创建提供透明访问任何数据源的对象。数据映射模式,也叫数据访问对象模式,或数据对象映射模式。
为什么需要数据映射模式
数据映射模式的目的是让持久化数据存储层、驻于内存的数据表现层、以及数据映射本身三者相互独立、互不依赖。这个数据访问层由一个或多个映射器(或者数据访问对象)组成,用于实现数据传输。通用的数据访问层可以处理不同的实体类型,而专用的则处理一个或几个。
数据映射模式UML图
简单实例
通过数据对象映射模式,我们可以实现一个对象对应一条数据库记录,对象的属性对应记录的字段。但对象的属性改变时,自动更新数据库记录。
例如我们有一个用户类与数据库的用户表对应
<?php
//数据模式映射类
class User
{
protected $id;
protected $data;
protected $db;
protected $change = false;
public function __construct($id)
{
$this->id = $id;
//实例化数据库对象,这里使用了工厂方法
$this->db = Factory::getDatabase();
//从数据库查询数据,存放到data属性中
$this->data = $this->db->query("select * from user where id = $id limit 1");
}
public function __get($key)
{
if (isset($this->data[$key]))
{
return $this->data[$key];
}
}
public function __set($key, $value)
{
$this->data[$key] = $value;
$this->change = true;
}
//析构方法
public function __destruct()
{
//如果对象属性改变过,则change属性为true 则调更新方法更新数据库
$this->change && $this->update();
}
//更新记录方法
public function update(){
foreach ($this->data as $k => $v)
{
$fields[] = "$k = '{$v}'";
}
$this->db->query("update user set " . implode(', ', $fields) . "where
id = {$this->id} limit 1");
}
}
//实例化对象
$user = new User(1);
//改变名字
$user->name = 'admin';
(一)创建型模式6 - 注册树模式(Registry Pattern)
注册树模式为应用中经常使用的对象创建一个中央存储器来存放这些对象 —— 通常通过一个只包含静态方法的抽象类来实现(或者通过单例模式)。也叫做注册器模式
为什么需要注册树模式
解决常用对象的存放问题,实现类似于全局变量的功能。
注册树模式UML图
无
简单实例
<?php
//User类用于测试
class User{}
//注册树类
class Registry
{
protected static $objects; //用于存放实例
//存入实例方法
static public function set($key, $object)
{
self::$objects[$key] = $object;
}
//获取实例方法
static public function get($key)
{
if (!isset(self::$objects[$key]))
{
return false;
}
return self::$objects[$key];
}
//删除实例方法
static public function _unset($key)
{
unset(self::$objects[$key]);
}
}
$user = new User;
//存入实例
Registry::set('User',$user);
//查看实例
var_dump(Registry::get('User'));
//删除实例
Registry::_unset('User');
//再次查看实例
var_dump(Registry::get('User'));
注册树经常与单例模式一起使用,先查看注册树上是否有该实例,有就直接使用,没有就生成一个实例,并挂到树上。有些时候我们还可以这样做,让get方法如果get不到实例的时候就自动new一个存放起来,这样我们使用时就不用管有没有存放过这个实例,反正没有的话get方法也会帮我们存放。
//获取实例方法
static public function get($key)
{
if (!isset(self::$objects[$key]))
{
self::$objects[$key] = new $key;
}
return self::$objects[$key];
}
当然使用这种方式的话,查看实例是否存在,就不能使用get方法了。因为调用get方法以后,不存在也会生成一个实例。
(三)行为型模式12 - 空对象模式(Null Object Pattern)
用一个空对象取代 NULL,减少对实例的检查。这样的空对象可以在数据不可用的时候提供默认的行为
为什么需要空对象模式
解决在需要一个对象时返回一个null值,使其调用函数出错的情况
空对象模式UML图
上图是Java的空对象模式UML图,网上很多PHP设计模式的代码实现都是照着上面这个UML图
实际上PHP在空对象模式的实现上比Java更加简单些。因为PHP有美妙的语法糖,魔术方法__call方法。
我们只要实现空对象的__call方法就可以实现空对象模式,并不需要使用nullobject去继承对应的抽象object。
简单实例
假设现在我们有这么一段代码
<?php
//测试类
class Person{
public function code(){
echo 'code makes me happy'.PHP_EOL;
}
}
//定义一个生成对象函数,只有PHPer才允许生成对象
function getPerson($name){
if($name=='PHPer'){
return new Person;
}
}
$phper = getPerson('PHPer');
$phper->code();
是的,现在这段代码会输出code makes me happy。如果有时候这个函数是别人调用的,它并没传入合适的参数呢?
$phper = getPerson('Javaer');
$phper->code();
这个时候就会报错了error : Call to a member function code() on null。是的$phper现在是一个null值,所以调用code方法就会报错
这种情况很常见,系统并没有返回一个我们期待的对象,而是返回了一个null值。所以多数情况下,我们的代码都要这样写
$phper = getPerson('Javaer');
if(!is_null($phper)){
$phper->code();
}
或者是
if(is_object($phper)){
$phper->code();
}
这样让太多的if判断不可避免地存在于我们的代码中
如果我们使用NullObject模式的话,我们就可以让函数没有返回值时返回一个nullobject对象。而不是一个null值(没有return 默认null值)
//测试类
class Person{
public function code(){
echo 'code makes me happy'.PHP_EOL;
}
}
//空对象模式
class NullObject{}
//定义一个生成对象函数,只有PHPer才允许生成对象
function getPerson($name){
if($name=='PHPer'){
return new Person;
}else{
return new NullObject;
}
}
$phper = getPerson('PHer');
$phper->code();
这个时候就不会再报一个null调用函数的错误了,但是会报一个call to undefined method的错误,这是由于NullObject对象没有code方法。这个时候我们只需实现魔术方法__call,就不会报错了。
//空对象模式
class NullObject{
public function __call($method,$arg){
echo 'this is NullObject';
}
}
我们可以通过返回一个NullObject对象来取代返回null,这样就不用在调用方法时判断是否为null,而且只要你实现了call方法,不管真正的对象它原来是调用那个方法的,NullObject都可以去调用而且不报错(实际是隐式调用了魔术方法call)。当然,如果你原本的逻辑是返回对象是null的话什么都不做,那么你可以让__call()什么都不做。或者你也可以让它抛出一个异常。
评论 (0)