php设计模式范例

php设计模式范例

  • 转载于:戳这

  • 非原创,侵权即删。转自 LaraBase

我认为人们对于设计模式抱有的问题在于大家都了解它们却不知道该如何在实际中使用它们。

设计模式的关键在于设计,意味着你已经开始想创造一个东西,比如:写一个新框架。

设计模式体现的是抽象和设计能力,写出一种巧妙,可以伸缩,可以维护的代码架构。

目录

  • 创建型设计模式
    • 单例模式:保证一个类只有一个实例化对象
    • 简单工厂模式:不用 new 关键字来获得实例,而是把业务类放进一个工厂类里,由工厂类生产出对应的实例
    • 工厂方法模式:允许多个工厂存在,相当于给多个工厂分组
    • 抽象工厂模式:抽象工厂一般使用接口,特点是层层约束的,用来统一标准
    • 对象池模式:与单例模式的区别就是,相当于一个对象池管理多个单例
    • 原型模式:实质就是对对象的复制
  • 结构型设计模式
    • 适配器模式:将第三方类适配成自己需要的类
    • 桥接模式:将规定好的接口抽象方法以一定规则组装使用
    • 组合模式:在批量处理多个实现类时,感觉在使用一个类一样
    • 装饰器模式:当你 extend 用过后又遇到需要再次 extend 的情况时
    • 依赖注入:把一个类『 不可能更换的部分 』和 『 可更换的部分 』分离开来
    • 门面模式: 由一个门面 (入口) 把所有子系统隐藏起来,只需要操作门面就可以
    • 链式操作: 里面的方法每次都返回 return $this
    • 代理模式:代理和被代理的类必须实现同一接口,可以在代理对象中添加逻辑而不影响到被代理类
    • 注册器模式: 很多类的实例,起个别名,然后按照 key - value 的形式放在注册类里,以便之后统一调用
  • 行为性模式
    • 观察者模式: 当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
    • 责任链模式:当你有一个请求,你不知道用哪个方法 ( handler ) 来处理这个请求时,可以用责任链轮询
    • 模板方法:父类提供一系列模板方法,有的实现了逻辑,有的只是一个接口,子类对接口进行不同实现
    • 策略模式: 对行为进行依赖反转,对多层 if else 的实现改为依赖于接口而不是具体的实现
    • 访问者模式: 用一个类来新增方法
    • 遍历模式:实现 laravel 中的集合,用 foreach 来遍历对象
    • 空对象模式:避免报错
    • 状态模式
    • 命令模式

创建型设计模式

在软件工程中,创建型设计模式承担着对象创建的职责,尝试创建适合程序上下文的对象。

单例模式

作用:保证一个类只有一个实例化对象,提供一个全局访问点

最简单的设计模式,很容易理解。

最常见的场景就是一个数据库的连接。

我们每次请求只需要连接一次,也就是说如果我们用类来写的话,只需要一个实例就够了(多了浪费)。

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

class MySql
{
private static $connect; // 保存连接实例

// 构造方法访问设置为private,防止类被实例化
private function __construct()
{
self::$connect = mysqli_connect('localhost', 'root', ''); // 执行数据库连接
}

// 创建一个用来实例化对象的方法
// 如果不存在一个这个类的实例属性,就创建一个
// 否则就取这个实例属性
public static function getInstance()
{
if (!(self::$connect instanceof self)) {
self::$connect = new self;
}

return self::$connect;
}

// 防止对象被复制
public function __clone()
{
trigger_error('Clone is not allowed !');
}

// 防止反序列化后创建对象
private function __wakeup()
{
trigger_error('Unserialized is not allowed !');
}
}

//只能这样取得实例,不能new 和 clone
$mysql = Mysql::getInstance();

简单工厂模式

本来我们要获取一个类的实例,需要用到 new 关键字。

但是如果 new 直接写到业务代码里,一个类在很多地方都实例化过,以后要是这个类出了什么问题。比如:修改个名字(实际中,你更多的可能是修改构造函数方法),那么就尴尬了,需要改很多地方。

工厂模式,顾名思义,就是不用 new 来获得实例,而是把业务类放进一个工厂类里,由工厂类『 生产 』出对应的实例。

SimpleFactory.php

1
2
3
4
5
6
7
8
9
10
11
<?php

namespace DesignPatterns\Creational\SimpleFactory;

class SimpleFactory
{
public function createBicycle(): Bicycle
{
return new Bicycle();
}
}

Bicycle.php

1
2
3
4
5
6
7
8
9
10
<?php
namespace DesignPatterns\Creational\SimpleFactory;

class Bicycle
{
public function driveTo(string $destination)
{
echo $destnation;
}
}

使用:

1
2
3
$factory = new SimpleFactory();
$bicycle = $factory->createBicycle();
$bicycle->driveTo('Paris');

我们需要什么实例,就到工厂的实例方法里去拿。

我们看到,业务代码里没有出现 new 以及那个具体的业务类,这样业务类我们可以随便更改,以后更新的时候只要在工厂类里修改一次,就可以一对多的在各处生效了。

这个方法的名字 $factory->createBicycle()

这个名字要起得好,如果你要改这个名字,还是得到多个地方改的。

工厂方法模式

  • 概念: 工厂方法模式简单工厂模式 非常接近,唯一不同的是允许有多工厂的存在,相当于给工厂分组。

  • 规则

    • 每个工厂都必须继承一个抽象类或接口类,使之成为多态
    • 每个产品也必须继承一个抽象类或接口类,也使之成为多态
    • 每个工厂必须有一个工厂方法返回产品的实例

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

/* 工厂和产品接口 */
interface CarFactory
{
public function makeCar();
}
interface Car
{
public function getType();
}

/* 工厂和产品实现 */
class SedanFactory implements CarFactory
{
public function makeCar()
{
return new Sedan();
}
}
class Sedan implements Car
{
public function getType()
{
return 'Sedan';
}
}
/* 客户端 */
$factory = new SedanFactory();
$car = $factory->makeCar();
print $car->getType();

照着这个思路,你还可以搞一个 SuvFactory(),然后生产 Suv汽车。

你可能会问,这样做的意义?如何运用?

建议暂时记住定义,搞清楚不同模式的区别(比如说得出简单工厂和工厂方法的区别就可以了),到遇到具体场景你想起来再查找就明白了。

抽象工厂模式

  • 区别:抽象工厂模式工厂方法模式 在某种程度上是一样的,区别在于子工厂必须全部继承或实现同一个抽象类或者接口。
  • 规则
    • 每个工厂必须继承同一个抽象类或实现同一个接口
    • 每个工厂必须包含多个工厂方法
    • 每个工厂的方法必须一致,每个方法返回的实例,必须是继承或实现了同一个抽象或接口的多态类

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

abstract class Button{}

abstract class Border{}

class MacButton extends Button{}
class WinButton extends Button{}

class MacBorder extends Border{}
class WinBorder extends Border{}

interface AbstractFactory
{
public function CreateButton();
public function CreateBorder();
}

class MacFactory implements AbstractFactory
{
public function CreateButton()
{
return new MacButton();
}

public function CreateBorder()
{
return new MacBorder();
}
}
class WinFactory implements AbstractFactory
{
public function CreateButton()
{
return new WinButton();
}

public function CreateBorder()
{
return new WinBorder();
}
}

抽象工厂一般使用接口,特点是层层约束的,缺点是增加产品比较困难。

比如再增加个 CreateLine() , 接口和实现都得改。

优点是增加固定类型产品不同品牌比较方便,比如我要再加一个 Linux 品牌,那么再建议一个 LinuxFactory 就可以了。

你想做统一标准的时候,比如写了一个框架,数据库操作定义了一套接口,你自己写了一个 MySQL 的实现,那么其它人参与开发,比如另一个人写了一个 Oracle 的实现,那么这种标准的价值就体现出来了,它会让你的代码非常一致,不会被别人写乱。

此模式其实比较难理解,还是建议先记住区别,等遇到真实场景就知道怎么运用了。

对象池模式

场景:

正常情况下,一个对象随着请求产生,也会随着请求结束被销毁。

有一些对象,需要依赖外部资源,比如说 MySQL 数据库的连接, socket 的连接, memcahed 的连接,以及一些大对象,比如说图片,字体对象等, 每次创建的时候耗时比较长,极大的影响系统性能,而且上述这些案例有些影响全局的。

那我们有没有一种自动管理实例创建,如果有实例就不再重复创建呢?

听上去很耳熟,这不是单例模式吗?是的,单例模式可以解决这个问题。

这里要介绍的是单例模式的一种升级:对象池模式

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

class ObjectPool
{
private $instances = [];

public function get($key)
{
if (isset($this->instances[$key])) {
return $this->instances[$key];
} else {
$item = $this->make($key);
$this->instances[$key]=$item;

return $item;
}
}
public function add($object, $key)
{
$this->instances[$key] = $object;
}

public function make($key)
{
if($key =='mysql') {
return new Mysql();
} elseif($key =='socket') {
return new Socket();
}
}
}

如果对象池里有 MySQL 的对象实例,就拿出来,如果没有就新建一个,这样无论如何 MySQL实例只会被创建一次,并且会被保存在内存中,以便复用。

与单例模式的区别就是,相当于一个对象池管理多个单例。

原型模式

实质就是对 对象 的复制

对一些大型对象,每次去 new ,初始化开销很大,这个时候我们先 new 一个模板对象,然后其它实例都去 clone 这个模板,这样可以节约不少性能。

这个所谓的模板,就是原型。

当然,原型模式比单纯的 Clone 要稍微升级一下。

普通 Clone

new 和 clone 都是用来创建对象的方法。

在php中,对象的赋值操作实际上是引用操作 (实际上,绝大部分的编程语言也是如此)

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php


class myclass
{
public $data;
}
$obj1 = new myclass();
$obj1->data = "aaa";
$obj2 = $obj1;
$obj2->data ="bbb"; //$obj1->data的值也会变成"bbb"

var_dump($obj1->data); // bbb
var_dump($obj2->data); // bbb

但是如果你不是直接引用,而是 clone ,那么相当于做了一个独立的副本

1
2
$obj2 = clone $obj1;
$obj2->data ="bbb"; //$obj1->data的值还是"aaa",不会关联

这样就会得到一个和被复制对象完全没有纠葛的新对象,但两个对象长得一模一样。

深复制

深复制非常简单,在被复制对象中加一个魔术方法就可以了。

1
2
3
4
5
6
7
8
9
10
class myclass 
{
public $data;

public $item;

public function __clone() {
$this->item = clone $this->item;
}
}

这个技巧就是被复制对象一旦被复制的时候,就放弃自己的属性,把属性给要求的对象,然后自己存一个属性的副本。

这样我们复制这个对象的时候,后续对象就不会因为引用的关系而改变源对象了。

现在正是来讲原型模式,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

interface Prototype
{
public function copy();
}
class ConcretePrototype implements Prototype
{
private $_name;

public function __construct($name)
{
$this->_name = $name;
}

public function copy()
{
return clone $this;
}
}

class Demo
{

}
// client
$demo = new Demo();
$object1 = new ConcretePrototype($demo);
$object2 = $object1->copy();

从上面的例子不难看出,所谓原型模式就是不直接用 clone 这种关键字写法,而是创建一个原型类。

把需要被复制的对象丢进原型类里面,然后这个类具有复制自己能力(方法),并且可以继承原型的一些公共属性和方法。

如果你用过 Carbon 这个处理时间的包,它里面的 copy() 方法就是采用了这个模式。

但是上述案例不能解决浅复制的问题。

原型模式的来龙去脉就是这么回事,怎么合理使用可以遇到问题的时候回来查看这个例子。

结构性设计模式

在软件工程中,结构型设计模式集是用来抽象真实程序之中的对象实体之间的关系,并使这种关系可被描述,概括和具体化。

适配器模式

适配器有点像电源适配器,把110V的电源转换成220V。

有些第三方类并没有按照规定的接口来实现,而是有自己不同的方法。

这个时候我们就需要有一个适配器类,来处理一下这个异类。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
interface Database
{
public function connect();

public function query();

public function close();
}
class Mysql implements Database
{
public function connect()
{
//mysql 的逻辑
};

public function query()
{
//mysql 的逻辑
};

public function close()
{
//mysql 的逻辑
};
}
class Pdo implements Database
{
public function connect()
{
//Pdo 的逻辑
};
public function query()
{
//Pdo 的逻辑
};
public function close()
{
//Pdo 的逻辑
};
}
//使用
$database = new Mysql(); //切换数据库只要改这一行就行了,因为后面的都是标准接口方法,不管哪个数据库都一样。
$database->connect();
$database->query();
$database->close();

异类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//第三方数据库类
class Oracle
{
public function oracleConnect()
{
//Oracle 的逻辑
};

public function oracleQuery()
{
//Oracle 的逻辑
};

public function oracleClose()
{
//Oracle 的逻辑
};
}

适配器类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Adapter implements Database 
{
private $adaptee;

function __construct($adaptee)
{
$this->adaptee = $adaptee;
}
//这里把异类的方法转换成了 接口标准方法,下同
public function connect()
{
$this->adaptee->oracleConnect();
};

public function query()
{
$this->adaptee->oracleQuery();
};

public function close()
{
$this->adaptee->oracleClose();
};
}

使用:

1
2
3
4
5
$adaptee = new Oracle();  
$database = new Adapter($adaptee);//只要改这个类就行了,后面的都可以不用改;
$database->connect();
$database->query();
$database->close();

这就是适配器模式,很简单,也很实用。

桥接模式

我们知道一个类可以实现多个接口,一个接口对应多个实现。

在不同的实现类中,它实现接口方法的逻辑是不一样的。

有时候我们需要对这些抽象方法进行一些组合,修改,但是又能适用于所有实现类

这时候我们需要做一个桥,连接不同的实现类并统一标准。

一个接口多个实现:

FormatterInterface.php

1
2
3
4
interface FormatterInterface
{
public function format(string $text);
}

PlainTextFormatter.php

1
2
3
4
5
6
7
class PlainTextFormatter implements FormatterInterface
{
public function format(string $text)
{
return $text;
}
}

HtmlFormatter.php

1
2
3
4
5
6
7
class HtmlFormatter implements FormatterInterface
{
public function format(string $text)
{
return sprintf('<p>%s</p>', $text);
}
}

桥接:

Service.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class Service
{
protected $implementation;
//初始化一个FormatterInterface的实现
public function __construct(FormatterInterface $printer)
{
$this->implementation = $printer;
}
// 可以跟换实现
public function setImplementation(FormatterInterface $printer)
{
$this->implementation = $printer;
}
//桥接抽象方法
abstract public function get();
}

HelloWordService

1
2
3
4
5
6
7
8
9
class HelloWorldService extends Service
{
//桥接抽象方法的实现
//这个方法是关键,因为它不在受限于原有的接口方法,而是可以自由组合修改,并且你可以编写多个类似的方法,这样就和原接口解耦了。
public function get()
{
return $this->implementation->format('Hello World').'-这是修改的后缀';
}
}

使用:

1
2
3
4
5
$service = new HelloWorldService(new PlainTextFormatter());
echo $service->get(); //Hello World-这是修改的后缀
//在这里切换实现很轻松
$service->setImplementation(new HtmlFormatter());
echo $service->get(); //<p>Hello World</p>-这是修改的后缀

组合模式

大概什么意思:

一个接口对于多个实现,并且这些实现中都拥有相同的方法(名)。

有时候你只需要运行一个方法,就让不同实现类的某个方法或某个逻辑执行一遍。

在批量处理多个实现类时,感觉在使用一个类一样。

实现:

顶层渲染接口 RenderableInterface.php

1
2
3
4
interface RenderableInterface
{
public function render(): string;
}

表单构造器 Form.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//必须继承顶层渲染接口
class Form implements RenderableInterface
{
private $elements;
//这里很关键,相当于是批量处理接口实现类
public function render(): string
{
$formCode = '<form>';
foreach ($this->elements as $element) {
$formCode .= $element->render();
}
$formCode .= '</form>';
return $formCode;
}
//这个方法用来注册 接口实现类
public function addElement(RenderableInterface $element)
{
$this->elements[] = $element;
}
}

具体实现类之一 TextElement.php

1
2
3
4
5
6
7
8
9
10
11
12
class TextElement implements RenderableInterface
{
private $text;
public function __construct(string $text)
{
$this->text = $text;
}
public function render(): string
{
return $this->text;
}
}

具体实现类之二 InputElement.php

1
2
3
4
5
6
7
class InputElement implements RenderableInterface
{
public function render(): string
{
return '<input type="text" />';
}
}

看如何使用:

1
2
3
4
5
6
7
8
9
10
//先建立一个表单
$form = new Form();
//在表单中增加一个Email元素
$form->addElement(new TextElement('Email:'));
$form->addElement(new InputElement());
//在表单中增加一个密码元素
$form->addElement(new TextElement('Password:'));
$form->addElement(new InputElement());
//把表单渲染出来
$form->render();

这个例子形象的介绍了组合模式,表单的元素可以动态增加,但是只要渲染一次,就可以把整个表单渲染出来。

表单构造器是经典的组合模式的应用,如果你也想实现一个也可以参考该设计模式。

装饰器模式

概述:

一个类中有一个方法,我需要经常修改它,而且会反反复复,改完了再改回去。

一般要么我们直接改原来的类中的方法,要么继承一下覆盖这个方法。

装饰器模式就是可以不用继承,只需要增加一个类进去就可以改掉那个方法。

场景:

1
2
3
4
5
6
7
8
9
10
11
class plainCoffee 
{
public function makeCoffee()
{
$this->addCoffee();
}
public function addCoffee()
{
// 加点咖啡
}
}

这是一个煮咖啡的程序,现在我还想加点糖,一般做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
class sweetCoffee extends plainCoffee 
{
public function makeCoffee()
{
$this->addCoffee();
$this->addSugar();
}

public function addSugar()
{
// 加点糖
}
}

好了,下面如果我还想加点奶,加点奶油,加点巧克力,加点海盐?

extend 到崩溃。

要想使用装饰器,需要对最早那个类进行改造

我们想改造 makeCoffee() 这个方法,无非是在它前面或后面加点逻辑,于是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class plainCoffee 
{
private function before()
{

}

private function after()
{

}

public function makeCoffee()
{
$this->before();
$this->addCoffee();
$this->after();
}
public function addCoffee()
{

}
}

那么我们怎么在 beforeafter 中加入逻辑呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class plainCoffee 
{
private $decorators;

public function addDecorator($decorator)
{
$this->decorators[] = $decorator;
}

private function before()
{
foreach($this->decorators as $decorator)
{
$decorator->before()
}
}

private function after()
{
foreach($this->decorators as $decorator)
{
$decorator->after()
}
}

public function makeCoffee()
{
$this->before();
$this->addCoffee();
$this->after();
}

public function addCoffee()
{

}
}

改造好了,我们来看看怎么写装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SweetCoffeeDecorator
{
public function before()
{

}

public function after()
{
$this->addSugar();
}

public function addSugar()
{

}
}

使用:

1
2
3
$coffee = new plainCoffee();
$coffee->addDecorator(new SweetCoffeeDecorator());
$coffee->makeCoffee();

当你需要在加糖的咖啡再加奶的话,就新建一个类似的装饰器:

1
2
3
4
$coffee = new plainCoffee();
$coffee->addDecorator(new SweetCoffeeDecorator());
$coffee->addDecorator(new MilkCoffeeDecorator());
$coffee->makeCoffee();

不难发现,在这里可以自由增加或注释掉修饰器,比较灵活。

当你 extend 用过后又遇到需要再次 extend 的情况时,不妨考虑一下装饰器模式。

依赖注入

这是一个著名的设计原则,其实它比其它设计模式都简单。

依赖注入的实质就是把一个类『 不可能更换的部分 』和 『 可更换的部分 』分离开来

通过注入的方式来使用,从而达到解耦的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MySql
{
private $host;

private $prot;

private $username;

private $password;

private $databasename;

public function __construct()
{
$this->host = '127.0.0.1';
$this->prot = '3306';
$this->username = 'root';
$this->password = '';
$this->databasename = 'db';
}

public function connect()
{
return mysqli_connect($this->host,$this->username ,$this->password,$this->db_name,$this->port);
}
}

显然,数据库的配置是可以更换的部分,因此我们需要把它拎出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MysqlConfiguration 
{
private $host;

private $prot;

private $username;

private $password;

private $databasename;

public function __construct(string $host, int $port, string $username, string $password, string $databasename)
{
$this->host = $host;
$this->prot = $port;
$this->username = $username;
$this->password = $password;
$this->databasename = $databasename;
}

public function getHost(): string
{
return $this->host;
}
public function getPort(): int
{
return $this->port;
}
public function getUsername(): string
{
return $this->username;
}
public function getPassword(): string
{
return $this->password;
}
public function getDatabaseName(): string
{
return $this->databasename;
}
}

不可更换的部分为:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MySql
{
private $configuration;

public function __construct(MysqlConfiguration $config)
{
$this->configuration = $config;
}
public function connect()
{
return mysqli_connect($this->configuration->getHost(), $this->configuration->getUsername(), $this->configuration->getPassword, $this->configuration->getDatabaseName(), $this->configuration->getPort());
}
}

这样就完成了配置文件和连接逻辑的分离。

使用:

1
2
3
$config = new MysqlConfiguration('127.0.0.1', 'root', 'db', '3306');
$db = new Mysql($config);
$connect = $db->connect();

config 是注入 Mysql 的,这就是所谓的依赖注入。

门面模式

首先来了解一下 Facade 这个单词的意思,建筑的正面,门面,由于以前法国,意大利的建筑只注重修葺临街的一面,十分精美,而背后却十分简陋,所以这个词的引申是 表象,假象。

在设计模式中,其实 Facade 这个概念十分简单。

它主要讲的是设计一个接口来统领所有子系统的功能。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Cpu
{
public function freeze()
{
......
}

public function jump()
{
......
}

public function excute()
{
......
}
}

class HardDrive
{
public function read($bootSector, $sectorSize)
{
......
}
}

class Memory
{
public function load($bootAddress, $hdData)
{

}
}

CPU,硬盘,内存这三个类是电脑中的子系统,我们需要写一个总系统来组织它们之间的关系,其实这就是 Facade:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ComputerFacade
{
private $cpu;

private $ram;

private $hd;

public function __construct()
{
$this->cpu = new Cpu();
$this->ram = new Memory();
$this->hd = new HardDrive();
}

public function start()
{
$this->cpu->freeze();
$this->ram->load(BOOT_ADDRESS, $this->hd->read(BOOT_SECTOR, SECTOR_SIZE));
$this->cpu->jump(BOOT_ADDRESS);
$this->cpu->execute();
}
}

使用:

1
2
$computer = new ComputerFacade();
$computer->start();

门面模式其实就是这么回事,由一个门面 (入口) 把所有子系统隐藏起来,只需要操作门面就可以。

Laravel 中的 Facade

要使用某个类中的方法,必须先实例化它。

Laravel 中的 Facade 作用是避免使用 new 关键字实例化类,而是通过一个假静态方法最快的使用某一个类中的某个方法。

比如:

1
2
3
4
5
use Illuminate\Support\Facades\Cache;

Route::get('/cache', function () {
return Cache::get('key');
});

如果你到 Illuminate\Support\Facades\Cache 这个类里去看,会发现根本不存在一个 get 静态方法。

但是这个 Illuminate\Support\Facades\Cache 的父类中,有一个魔术方法 __callStatic() ,当调用不存在的静态方法时,会激活这一个魔术方法,在这个魔术方法里,会通过 Ioc 容器找到真正的 Cache 类,并调用真实存在的 get 方法。

所以这里的 Facade,就是个假的静态,从语言的意思上,其实更符合 Facade 的语义。

链式操作

在 laravel 中,ORM的一系列 sql 操作就是链式操作,特点是每次都返回一个 QueryBuilder。

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

class Employee
{
public $name;

public $surName;

public $salary;

public function setSurname($surname)
{
$this->surName = $surname;

return $this;
}

public function setName($name)
{
$this->name = $name;

return $this;
}

public function setSalary($salary)
{
$this->salary = $salary;

return $this;
}

public function __toString()
{
$employeeInfo = 'Name: ' . $this->name . PHP_EOL;
$employeeInfo .= 'Surname: ' . $this->surName . PHP_EOL;
$employeeInfo .= 'Salary: ' . $this->salary . PHP_EOL;

return $employeeInfo;
}
}

使用:

1
2
3
4
5
6
7
8
9
10
//链式操作的效果
$employee = (new Employee())
->setName('Tom')
->setSurname('Smith')
->setSalary('100');
echo $employee;
# 输出结果
# Name: Tom
# Surname: Smith
# Salary: 100

这里面能够连续链式操作的关键就在于每个方法都返回 return $this

代理模式

概念:

这个模式比较简单,就是你想访问一个类的时候,不直接访问,而是找这个类的一个代理。

代理就是中介,有中介就意味着有解耦。

在代理模式下,代理对象和被代理的对象,有个重要特点:必须继承同一个接口

之前说过的适配器模式和代理模式非常非常像。

只不过在适配器模式下,适配器和它要适配的类没有继承同一个接口。

适配器就是要把这个第三方类变成符合接口规范。

适配器也是个中介,所以它们很像。

实现:

接口

1
2
3
4
interface Image 
{
public function getWidth();
}

真实对象

1
2
3
4
5
6
7
class RawImage implements Image
{
public function getWidth()
{
return "100x100";
}
}

中介对象 (代理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ImageProxy implements Image
{
private $img

public function __construct()
{
$this->img = new RawImage();
}

public function getWidth()
{
return $this->img->getWidth();
}
}

使用

1
2
$proxy = new ImageProxy();
$proxy->getWidth();

作用:

显而易见,解耦。

因为代理对象和和被代理对象都实现同一接口,所以对于原真实对象,你无论怎么改都行。

同样,在代理对象中,除了如实反映真实对象的方法逻辑,你还可以添加点别的逻辑,怎么添加都行,不会影响到真实对象,添加后可以在所有使用过代理对象的业务逻辑中瞬时更新。

注册器模式

概述:

注册器模式是一种基础常见的设计模式,它的主要意思是把多个类的实例注册到一个注册器类中去,然后需要哪个类,由这个注册器统一调取。

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Register
{
protected static $objects;

public static function set($alias,$object)
{
if(!isset($objects[$alias])){
self::$objects[$alias]=$object;
}
}

public static function get($alias)
{
return self::$objects[$alias];
}

public static function _unset($alias)
{
unset(self::$objects[$alias]);
}
}

使用:

1
2
Register::set('rand',stdClass::class);
Register::get('rand');

就是把很多类的实例,起个别名,然后按照 key - value 的形式放在注册类里,以便之后统一调用。

你可能会想到 Laravel 的 Service Container,容器本质上也是种注册器,但 Laravel 中的实现要比这个例子复杂得多。

行为性模式

观察者模式

概念:

观察者是一种非常常用的模式,具体在 事件的设计上 体现得最明显。

定义对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

在 laravel 的事件设计中,我们知道有一个 listener 和一个 handler,当一个 listener 侦听到一个事件发生时,可能有多个 handler 会与之处对应自动处理各自的业务逻辑。

合理的设计:

声明一个抽象的事件发生者基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class EventGenerator
{
private $observers = array();
//添加观察者方法
function addobserver(Observer $observer)
{
$this->observers[] = $observer;
}
//对每个添加的观察者进行事件通知
function notify()
{
//对每个观察者逐个去更新
foreach($this->observers as $observer)
{
$observer->update();
}
}
}

声明一个观察者接口

1
2
3
4
interface observer
{
function update($eventInfo = null);
}

声明具体事件类,继承了主事件

1
2
3
4
5
6
7
8
class Event extends EventGenerator
{
function trigger()
{
echo "Event<br/>";
$this->notify();
}
}

声明多个观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Observer1 implements observer
{
function update($event_info = null)
{
echo "逻辑1<br/>";
}
}

class Observer2 implements observer
{
function update($event_info = null)
{
echo "逻辑2<br/>";
}
}

使用

1
2
3
4
$event = new Event;
$event->addObserver(new Observer1);
$event->addObserver(new Observer2);
$event->trigger();

仔细观察代码其实很简单的,Event 基类里的 foreach ,可以实现一个事件对应多个观察者;

在这里我们就搞明白了,所谓的观察者就是事件的 handler

它和事件怎么挂钩呢,其实就是需要注册一下:

1
2
$event->addObserver(new Observer1);
$event->addObserver(new Observer2);

而这个步骤

1
2
$event = new Event();
$event->trigger();

在 laravel 里被封装成了

1
event(new Event());

后记:

从设计原则来理解观察者模式

实例:拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。

  • 依赖倒置
    • 低层结构要依赖于高层结构
    • 在这里调用通知其他竞价者的地方就是高层结构
    • 而具体的方法就是低层结构
1
2
3
4
if($message) {
sendMessageToUser(); // 发送标价给竞价者
writeLog(); // 记录竞价信息
}

上面就是个错误例子。

让高层结构依赖于低层结构,程序便出现了耦合。

因为在触发了通知的时候,通知这个操作需要知道所有方法 (发送信合和记录竞价信息) 。

  • 不符合单一职责

    • 当有通知时必须知道触发了哪些任务时,便使职责不再单一
  • 开放封闭原则

    • 开放,对扩展开放
    • 封闭,对修改封闭
    • 这里扩展指的就是通知触发的具体的操作
    • 修改就是当通知触发后需要增加多一种方法时,不该修改系统已有的程序

责任链模式

概念:

责任链是一种比较高级的行为设计模式,就是当你有一个请求,你不知道用哪个方法 ( handler ) 来处理这个请求时,你可以把一个请求丢进一个责任链里 ( 里面有很多方法 ) ,这个 责任链会通过轮询的方式 自动找到对应的方法。

实践:

比如我要翻译一个单词,我写这个代码的时候,根本不知道用户会输入什么语言,所以我干脆就不管了,无论用户输入什么语言,我把它输入的内容丢进一个责任链,这个责任链里有德语翻译器、英语翻译器、法语翻译器和汉语翻译器等等,丢进去它就自动查找了,找到对应的语言就会自动翻译出来了。

laravel的管道模式、中间件的实现,其实就是责任链模式的变种。

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
abstract class TranslationResponsibility 
{ // 抽象责任角色
protected $next; // 下一个责任角色

protected $translator;

public function setNext(Responsibility $l)
{
$this->next = $l;
return $this;
}

public function canTranslate($input)
{
return $this->$translator == check($input)
}

public function check($input)
{
//写验证输入语言总类的逻辑
}

abstract public function translate($input); // 翻译方法
}

class EnglishTranslator extends TranslationResponsibility
{
public function __construct()
{
$this->translator = 'English'
}

public function translate($input)
{
//如果当前翻译器翻译不了,并且责任链上还有下一个翻译器可用,则让下一个翻译器试试
if (!is_null($this->next) && !$this->canTranslate($input)) {
$this->next->translate();
} else {
//翻译成英语逻辑
}
}
}

class FrenchTranslator extends TranslationResponsibility
{
public function __construct()
{
$this->translator = 'French'
}

public function translate($input)
{
//如果当前翻译器翻译不了,并且责任链上还有下一个翻译器可用,则让下一个翻译器试试
if (!is_null($this->next) && !$this->canTranslate($input)) {
$this->next->translate();
}else{
//翻译成法语逻辑
}
}
}

使用:

1
2
3
4
5
6
//组建注册链
$res_a = new EnglishTranslator();
$res_b = new FrenchTranslator();
$res_a->setNext($res_b);
//使用
$res_a->translate('Bonjour');

结果就是是,英语翻译器翻译不了,传递到法语翻译器翻译。

注意,这里为了简化说明,只展示了2个翻译器互为责任链的情况,如果你需要多个翻译器,还需要改造一下代码,让它能够轮询。

模板方法

最常见的设计模式:

其实质就是父类提供一系列模板方法,有的实现了逻辑,有的只是一个接口。

而子类继承大部分共有方法,同时对接口方法进行不同的实现,从而完成对父类模板的个性化改造,起到一对多的解耦目的。

可以说PHP的抽象类就是为了实现这个设计模式而推出的功能。

在PHP中,抽象类本身就是模板方法模式。

策略模式

概念:

策略模式是一个非常常用,且非常有用的设计模式。

简单的说,它是当你使用大量 if else 逻辑时的救星。

if else 就是一种判断上下文环境所作出的策略,如果你把 if else 写死,那么在复杂逻辑的时候你会发现代码超级长,而且最蛋疼的是,当你以后要新增策略的时候,再写一个 else if ???

万一这个逻辑还要修改20个地方呢?

策略模式就是用来解决这个问题的。

场景:

商城的首页,男的进来看男性商品,女的进来看女性商品,不男不女…… 以此类推,各种条件下用不同策略展示不同商品。

实现:

ShowStrategy.php 展示策略接口

1
2
3
4
interface ShowStrategy
{
public function showCategory();
}

maleShowStrategy.php 男性用户展示策略

1
2
3
4
5
6
class maleShowStrategy implements showStrategy 
{ // 具体策略A
public function showCategory(){
echo '展示男性商品目录';
}
}

femaleShowStrategy.php 女性用户展示策略

1
2
3
4
5
6
class femaleShowStrategy implements showStrategy 
{ // 具体策略B
public function showCategory(){
echo '展示女性商品目录';
}
}

Page.php 展示页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Page
{
private $_strategy;

public function __construct(Strategy $strategy)
{
$this->_strategy = $strategy;
}

public function showPage()
{
$this->_strategy->showCategory();
}
}

使用:

1
2
3
4
5
6
7
8
if(isset($_GET['male'])){
$strategy = new maleShowStrategy();
}elseif(isset($_GET['female'])){
$strategy = new femaleShowStrategy();
}
//注意看这里上下,Page类不再依赖一种具体的策略,而是只需要绑定一个抽象的接口,这就是传说中的控制反转(IOC)。
$question = new Page($strategy);
$question->showPage();

总结:

仔细看上面例子,不复杂,我们两个好处:

  1. 它把 if else 抽离出来了,不需要在每个类里写 if else
  2. 它成功的实现了控制反转,Page 类没有具体的依赖策略,这样我们可以随时添加和删除不同策略。

关于依赖反转可以参考:设计模式的六大原则

策略模式和工厂模式的区别在于:

工厂模式是为了得到不同的实例,重在创建。

策略模式重在行为,执行行为。

访问者模式

简单来说,就是本来你的类里有一个方法,后来因为种种原因,你需要增加新的方法,于是你不断修改这个类。

访问者模式,可以让你不用一直新增方法,不用改原来的类,而是通过新增一个注入原有类来实现新增方法的目的。

原有类:

Unit.php 基类

1
2
3
4
5
6
7
8
9
10
11
class Unit
{
//注意这个方法,非常关键,你现在可能没看懂,接着往下看,然后再回来看。
public function accept(Visitor $visitor)
{
$method = 'visit'. get_class($this);
if (method_exists($visitor, $method)) {
$visitor->$method($this);
}
}
}

User.php 具体类

1
2
3
4
5
6
class User extends Unit{
public function getName()
{
//获取名字
}
}

不修改 User 类,达到新增一个方法的目的:

如果不要修改 User 类,达到新增一个方法的目的,我们需要新增一个访问者类:

1
2
3
4
5
6
class getPhoneVistor
{
public function visitUser(){
//获取电话
}
}

使用:

1
2
3
4
5
$user = new User();
// 正常获取名字
$user->getName();
// 通过访问者获取电话
$user->accept(new getPhoneVistor());

总结

这其实是一个比较抽象的设计模式,如果你在百度搜教程,可能会被带偏,这里把它最小化精简了,并指出实质:用一个类来新增方法。

遍历模式

你真的了解 foreach 吗?

如果你是 laravel 的用户,那么你应该经常使用这个所谓的遍历模式,但不一定完全理解。

当你在使用 laravel 的 collection 的时候,你有没有想过为什么可以用一个 foreach 来遍历一个对象?foreach 不是用来遍历数组的吗?

是的,foreach 可以用来遍历类的属性

除了遍历数组,我们再来学习一下 foreach 的其他用法,比如,foreach 可以遍历一个类中所有可见属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class MyClass
{
public $var1 = 'value 1';
public $var2 = 'value 2';
public $var3 = 'value 3';
protected $protected = 'protected var';
private $private = 'private var';
function iterateAll() {
foreach($this as $key => $value) {
print "$key => $value\n";
}
}
}
$class = new MyClass();
foreach($class as $key => $value) {
print "$key => $value\n";
}
echo "\n";
$class->iterateAll();

结果:

1
2
3
4
5
6
7
8
9
10
//遍历可见属性
var1 => value 1
var2 => value 2
var3 => value 3
//遍历所有属性
var1 => value 1
var2 => value 2
var3 => value 3
protected => protected var
private => private var

集合

foreach 还可以用来遍历集合。

所谓集合,就是一个类包含多个类,并且这个集合可以像数组一样被遍历,可以进行各种集合运算操作。

典型例子,laravel 的 collection 对象。

要实现遍历操作,在设计模式上我们需要采用遍历模式;foreach 是一个封装好的方法,遍历模式就是把这个封装打开给你用。

在 PHP 中,我们需要实现 Standard PHP Library (SPL)标准库中一个遍历 ( Iterator ) 接口,它的形式如下:

Book.php // 单元对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Book
{
private $author;

private $title;

public function __construct(string $title, string $author)
{
$this->author = $author;
$this->title = $title;
}

public function getAuthor():string
{
return $this->author;
}

public function getTitle():string
{
return $this->title;
}
}

Collection.php 集合对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Collection implements \Countable, \Iterator
{
private $books = [];

private $currentIndex = 0;

public function addBook(Book $book)
{
$this->books[] = $book;
}

public function count(): int
{
return count($this->books);
}

public function current(): Book
{
return $this->books[$this->currentIndex];
}

public function key(): int
{
return $this->currentIndex;
}

public function next()
{
$this->currentIndex++;
}

public function rewind()
{
$this->currentIndex = 0;
}

public function valid(): bool
{
return isset($this->books[$this->currentIndex]);
}
}

这里我们实现了2个标准接口,\Countable\Iterator
如果你去遍历这个Collection对象,那么count(),current(),key(),next(),rewind(),valid()这些方法都会被调用,如果要深入研究,可以在这些方法里打印一下返回值。

这样处理后,可以对Collection对象进行遍历:

1
2
3
4
5
6
7
$bookList = new Collection();
$bookList->addBook(new Book('Learning PHP Design Patterns', 'William Sanders'));
$bookList->addBook(new Book('Professional Php Design Patterns', 'Aaron Saray'));
$bookList->addBook(new Book('Clean Code', 'Robert C. Martin'));
foreach ($bookList as $book) {
echo $book->getAuthor().'-'.$book->getTitle().'\n';
}

小结:

这个所谓模式和之前学习的模式不一样,有点抽象,先记住是什么意思,回头有时间的话,可以深入研究一下 Laravel Collection 的实现原理。

空对象模式

这简直不能算一种设计模式。

看完这个例子秒懂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface Animal {
public function makeSound();
}
class Dog implements Animal {
public function makeSound() {
echo "Woof..";
}
}
class Cat implements Animal {
public function makeSound() {
echo "Meowww..";
}
}
//这个就是空对象,里面的方法啥也不做,它的存在就是为了避免报错
class NullAnimal implements Animal {
public function makeSound() {
// silence...
}
}
$animalType = 'elephant';
switch($animalType) {
case 'dog':
$animal = new Dog();
break;
case 'cat':
$animal = new Cat();
break;
default:
$animal = new NullAnimal();
break;
}
$animal->makeSound(); // ..the null animal makes no sound

我们看,如果没有一个空对象作为『Place Holder』放在default这里,那么这个程序就要报错了,为了避免报错,我们可能需要写if 不等于猫狗等等,但是这样很麻烦,万一有100个动物呢,搞一个『空对象』放在这里就好了。

状态模式

状态模式就是对策略模式前面的 if else 这块逻辑进行升级,不再用 if else 判断,而是根据自身的状态判断。

实现:

shop.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Shop 
{
private $handler;
//这里$state设置一个状态值
public $state;
//设置默认状态,和默认处理器
public function __construct()
{
$this->state = 'male';
$this->handler = new maleHandler();
}

public function setHandler(Handler $handler)
{
$this->handler = $handler;
}

public function show()
{
$this->handler->handle($this);
}
}

handler.php 业务接口处理类

1
2
3
4
interface Handler 
{
public function handle(Shop $shop);
}

maleHandler.php 男性业务处理类

1
2
3
4
5
6
7
8
9
10
11
12
class maleHandler implements Handler  
{
public function handle(Shop $shop)
{
if($shop->state =="male"){
echo '展示男性商品目录';
}else{
$shop->setHandler(new femaleHandler());
$shop->show();
}
}
}

femaleHandler.php 女性业务处理类

1
2
3
4
5
6
7
8
9
10
11
12
class femaleHandler implements Handler  
{
public function handle(Shop $shop)
{
if($shop->state =="female"){
echo '展示女性商品目录';
}else{
$shop->setHandler(new maleHandler());
$shop->show();
}
}
}

使用:

1
2
3
4
5
6
7
$shop = new Shop;
$shop->state ="male";
$shop->show();
//展示男性商品目录
$shop->state ="female";
$shop->show();
//展示女性商品目录

我们看到这是一种更厉害的封装,在客户端这一块,if else不见了,甚至连策略类的注入也不见了。
看上去很清爽。
但其实,在后台的实现,比之前复杂了不少,你需要在每一个handler类里面进行条件判断。

命令模式

目的是什么:

我们想实现的是,只需要输入一个字符串式的指令,就可以执行相应的逻辑,而不用 if else 什么来判断。

PHP CLI 命令的设计就会用到这个模式。

实现:

我们来设计一个电视机开关的指令 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//命令接口
interface Command
{
public function excecute();
}

//开电视指令
class turnOnTVCommand extends Command
{
private $controller;

public function __construct(Controller $controller){
$this->controller = $controller;
}

public function excecute(){
$this->controller->turnOnTV();
}
}

//关电视指令
class turnOffTVCommand extends Command
{
private $controller;
public function __construct(Controller $controller){
$this->controller = $controller;
}
public function excecute(){
$this->controller->turnOffTV();
}
}

//指令库控制器(储存所有具体执行逻辑)
class Controller
{
public function turnOnTV()
{
echo '打开电视';
}
public function turnOffTV()
{
echo '关闭电视';
}
}

如果你想换一个指令,那么就修改$command_string就行了。

感谢您的阅读,本文由 Double-c 版权所有。如若转载,请注明出处:Double-c(https://double-c.github.io/2019/03/26/php-design/
设计模式的六大原则
关于代理两三事