设计模式是一把双刃剑,在工做场景中,只有不多的设计模式会被在业务场景中使用到,最经常使用的就是单例模式。相反,工做中使用的框架则在底层使用到了大量的设计模式,这样作提升了框架的性能、更好的解耦各个模块之间的功能、提供更好的拓展性、最大程度简化了开发者使用成本。实际上,在业务场景中使用合适的设计模式,也会达到事半功倍的效果。不过要想使用好设计模式,必需要深入理解它设计的初衷和适用的场景分别是什么,这并非一件容易的事情!本文结合四种常见的设计模式,聊一聊在php应用开发场景中,怎样更好的使用设计模式来简化业务逻辑,使得需求可拓展,代码更容易维护。本文使用到的案例放在:设计模式 ,先来学习一下基本知识:设计模式的七大原则、分类和UML类图的使用。php
这七大原则为咱们设计程序提供了指导,能够说是优秀程序设计的方法论。 不过理论每每又是简短而抽象的,你们想要理解并熟练用它们去指导程序设计,还需从大量的实践中去领悟。下边咱们介绍使用设计模式的时候也会根据这七大原则设计。html
设计模式自己是很是丰富的,通常将面向对象的设计模式分为三类:建立型、结构型和结构型。mysql
1.2.1 建立型git
建立对象时,再也不由咱们直接实例化对象;而是根据特定场景,由程序来肯定建立对象的方式,从而保证更大的性能、更好的架构优点。 建立型模式主要有:程序员
1.2.2 结构型github
用于帮助将多个对象组织成更大的结构。面试
结构型模式主要有:sql
1.2.3 行为型数据库
用于帮助系统间各对象的通讯,以及如何控制复杂系统中流程。设计模式
行为型模式主要有:
不少东西使用文字表述是苍白无力的,尤为是设计模式这种抽象的理论。咱们使用UML类图来增长咱们的表达力。类图(Class diagram)主要用于描述系统的结构化设计。类图也是最经常使用的UML图,用类图能够显示出类、接口以及它们之间的静态结构和关系。
在UML类图中,常见的有如下几种关系:
这六种类关系中,组合、聚合和关联的代码结构同样,能够从关系的强弱来理解,各种关系从强到弱依次是:继承→实现→组合→聚合→关联→依赖。(目前本身的理解)除了继承和实现,其余类关系较弱,通常是经过在一个类中调用其余类来实现。
咱们本节介绍的四种设计模式分别是:工厂模式、装饰器模式、发布/订阅模式、迭代器模式。选择这四种设计模式是由于在PHP业务场景中会常常用到他们,笔者还整理了一些相关的一些案例。
“工厂模式”简单来说就是将建立对象的任务交给工厂,根据抽象层次的不一样,又分为:简单工厂、工厂方法和抽象工厂。
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类建立型模式。在简单工厂模式中,能够根据参数的不一样返回不一样类的实例。简单工厂模式专门定义一个类来负责建立其余类的实例,被建立的实例一般都具备共同的父类。它的抽象层次低,工厂类通常也不会包含复杂的对象生成逻辑,只能适用于生成结构比较简单,扩展性要求较低的对象。
工厂方法模式(Factory Method Pattern): 又称为工厂模式,也叫虚拟构造器(Virtual Constructor)模式或者多态工厂(Polymorphic Factory)模式,它属于类建立型模式。在工厂方法模式中,工厂父类负责定义建立产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样作的目的是将产品类的实例化操做延迟到工厂子类中完成,即经过工厂子类来肯定究竟应该实例化哪个具体产品类。
抽象工厂模式(Abstract Factory Pattern) :提供一个建立一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象建立型模式。抽象工厂模式包含以下角色:
咱们使用工厂模式的业务场景是这样的:一个专门作“运动户外”新零售的公司,想要在本身的APP上向用户推荐不一样的服装、鞋帽搭配套装,推荐的策略根据用户性别的不一样有所不一样,男生推荐鞋子和背包,女生推荐外套和裤子;根据产品策略的不一样,又有不一样的推荐版本,好比第一个版本只推荐阿迪达斯的产品,第二个版本则推荐耐克的产品。这个业务场景就很是适合使用“抽象工厂模式”。咱们先来看一下UML类图:
是的,使用抽象工厂模式确实有一个缺点:类特别多。可是带来的好处也是很明显的,咱们先来看一下代码实现:
首先是抽象工厂类:
abstract class recommendFactory
{
public function createRecommendClass($sex) {}
}
复制代码
咱们假设每个版本的推荐都有一个相关的工厂类,他们都继承抽象工厂类:
class concreteRecommendFactoryV1 extends recommendFactory
{
public function createRecommendClass($sex) {
switch ($sex) {
case 'man':
return new concreteRecommendClassV1man();
case 'women':
return new concreteRecommandClassV1women();
}
}
}
......
class concreteRecommendFactoryV2 extends recommendFactory
{
public function createRecommendClass($sex) {
switch ($sex) {
case 'man':
return new concreteRecommendClassV2man();
case 'women':
return new concreteRecommendClassV2women();
}
}
}
复制代码
工厂类用来生产产品对象,根据用户的性别生产不一样的对象实例,产品对象也都有继承的抽象类,咱们来看一下产品的抽象类:
abstract class recommendClass
{
public function recommendRun() {}
}
复制代码
而后工厂类生产出来根据产品类获得的产品实例。咱们来分别看一下产品类的内容:
class concreteRecommendClassV1man extends recommendClass
{
public function recommendRun() {
return '推荐耐克鞋子和背包';
}
}
......
class concreteRecommandClassV1women extends recommendClass
{
public function recommendRun() {
return '推荐耐克上衣和裤子';
}
}
......
class concreteRecommendClassV2man extends recommendClass
{
public function recommendRun() {
return '推荐阿迪达斯鞋子和背包';
}
}
......
class concreteRecommendClassV2women extends recommendClass
{
public function recommendRun() {
return '推荐阿迪达斯上衣和裤子';
}
}
复制代码
这样咱们就能够根据不一样的版本,用户不一样的性别来展现不一样的推荐信息了:
include './recommendClass.php';
include './concreteRecommendClassV1man.php';
include './concreteRecommendClassV2man.php';
include './concreteRecommandClassV1women.php';
include './concreteRecommendClassV2women.php';
include './recommendFactory.php';
include './concreteRecommendFactoryV1.php';
include './concreteRecommendFactoryV2.php';
$sex = $_GET['sex'];
$version = $_GET['version'];
switch ($version) {
case 'version1' :
$factory = new concreteRecommendFactoryV1();
break;
case 'version2' :
$factory = new concreteRecommendFactoryV2();
}
$recommendClass = $factory->createRecommendClass($sex);
$recommendContent = $recommendClass->recommendRun();
echo $recommendContent.'<br/>';
复制代码
虽然咱们实现逻辑中用到的类比较的多,可是代码的可拓展性很强。再次发布不一样的版本推荐策略时,咱们只要建立相应的工厂,生产对应的类便可。
抽象工厂模式隔离了具体类的生成,因为这种隔离,更换一个具体工厂就变得相对容易。全部的具体工厂都实现了抽象工厂中定义的那些公共接口,所以只需改变具体工厂的实例,就能够在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式能够实现高内聚低耦合的设计目的,所以抽象工厂模式获得了普遍的应用。
装饰器模式属于结构型模式,装饰器模式能够动态的给一个对象添加额外的功能,就增长功能来讲,装饰器模式比生成子类更为灵活;它容许向一个现有的对象添加新的功能,同时又不改变其结构。读过Laravel源码的人应该都知道,Laravel中间件(Middleware)的实现就是使用的装饰器模式;Koa.js 最为人所知的基于 洋葱模型 的HTTP中间件处理流程也是装饰器模式。关于Laravel中间件源码的实现,笔者专门整理了一篇博文,感兴趣的读者能够读一下:Lumen中间件源码解析
咱们如今有这样一个需求:如今有一家餐馆,入住美团以后提供外卖服务,经过让顾客选套餐的方式,提供给用户选择的多样性,以此来增长销量。其中套餐能够由:主食、素菜、饮料、荤菜组成,其中主食和素材是基本套餐项,顾客能够再次基础上再选择添加饮料和荤菜,简单列一下菜系:
明确了需求,咱们来看一下案例中使用到装饰器所用到的UML类图:
根据UML类图,咱们来看一下代码实现,首先是添加菜品的接口:
<?php
/**
* Description: 抽象接口,真实对象和装饰对象具备相同的接口
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-20
*/
interface AbstractComponent{
public function addCategory(array &$dishes, Closure $next);
}
复制代码
一个具体要装饰的对象ConcreteComponent实现了这个接口:
<?php
/**
* Description: 具体的对象
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-20
*/
class ConcreteComponent implements AbstractComponent
{
public function addCategory(array &$dishes, Closure $next)
{
return function(&$dishes) use ($next) {
$dishes[] = ['米饭', '馒头'];
$dishes[] = ['土豆丝', '番茄鸡蛋', '炒豆角'];
return $next($dishes);
};
}
}
复制代码
咱们这里直接定义了主食和素材做为基础套餐,接下来对它进行装饰。 接下来就是定义了装饰器的抽象类了:
<?php
/**
* Description: 装饰类,继承了Component,从外类来扩展Component类的功能。
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-20
*/
abstract class AbstractDecorator implements AbstractComponent
{
private function initCategory() {}
public function addCategory(array &$dishes, Closure $next) {}
}
复制代码
抽象类中定义了一个私有方法initCategory,用来管理菜系的菜品,好比说素菜有:土豆丝、番茄鸡蛋、炒豆角,未来还可能添加豆腐、海带等其余素菜菜品。addCategory就是装饰器,将菜品添加到用户选择的菜系中。荤菜类和饮料类菜品继承了抽象装饰器:
<?php
/**
* Description: 荤菜装饰器,为菜品添加荤菜
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-20
*/
class ChivesDecorator extends AbstractDecorator
{
private function initCategory()
{
return ['回锅肉', '牛排', '羊排'];
}
public function addCategory(array &$dishes, Closure $next)
{
$dishes[] = $this->initCategory();
return $next($dishes);
}
}
......
/**
* Description: 饮料装饰器,为菜品添加类
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-20
*/
class DrinkDecorator
{
private function initCategory()
{
return ['雪碧', '可乐', '酸梅汤'];
}
public function addCategory(array &$dishes,Closure $next)
{
$dishes[] = $this->initCategory();
return $next($dishes);
}
}
复制代码
基本的类都实现了以后,咱们来看一下怎样在基础套餐之上进行装饰吧:
<?php
include __DIR__.'/AbstractComponent.php';
include __DIR__.'/AbstractDecorator.php';
include __DIR__.'/ConcreteComponent.php';
include __DIR__.'/DrinkDecorator.php';
include __DIR__.'/ChivesDecorator.php';
//将用户选好的套餐排列组合
$output = function ($dishes) {
$items = [];
$init = array_shift($dishes);
foreach($init as $item) {
$items[] = [$item];
}
do{
$elements = array_shift($dishes);
$temp = [];
foreach($elements as $element) {
foreach($items as $item) {
$item[] = $element;
$temp[] = $item;
}
}
$items = $temp;
} while(count($dishes));
return $items;
};
//组合函数,将装饰器函数一层层包装
function combinationFunc()
{
return function($stack, $decorators){
return function (&$dishes) use ($stack, $decorators) {
return $decorators->addCategory($dishes, $stack);
};
};
}
$dishes = [];
$baseCategory = (new ConcreteComponent())->addCategory($dishes, $output);
print '加荤菜和饮料后的套餐组合<br/>';
$addCategoryDecorators = [new DrinkDecorator(), new ChivesDecorator()];
$go = array_reduce(array_reverse($addCategoryDecorators), combinationFunc(), $baseCategory);
var_dump($go($dishes));
复制代码
首先咱们将用户选择的套餐放到数组$dishes中,咱们向套餐中添加基础套餐 $baseCategory = (new ConcreteComponent())->addCategory($dishes, $output);
其中$output是排列组合函数,用户选择套餐以后,对用户选择的菜品进行排列组合,好比用户选择了主食、素材和荤菜,那么咱们就从这三种菜系中各挑出一个菜进行组合造成套餐。提及来容易,可是对二维数组排列组合不是一件简单的事情,笔者这里的方法读者能够参考一下:
$output = function ($dishes) {
$items = [];
$init = array_shift($dishes);
foreach($init as $item) {
$items[] = [$item];
}
do{
$elements = array_shift($dishes);
$temp = [];
foreach($elements as $element) {
foreach($items as $item) {
$item[] = $element;
$temp[] = $item;
}
}
$items = $temp;
} while(count($dishes));
return $items;
};
复制代码
接下来咱们用荤菜类和饮料类对基本的套餐类进行装饰:
print '加荤菜和饮料后的套餐组合<br/>';
$addCategoryDecorators = [new DrinkDecorator(), new ChivesDecorator()];
$go = array_reduce(array_reverse($addCategoryDecorators), combinationFunc(), $baseCategory);
var_dump($go($dishes));
复制代码
这样就达到了咱们想要的效果,若是读者对array_reduce函数不是很理解的话能够看一下官方文档。这样一个选择套餐的装饰器就完成了,咱们能够根据用户选择的套餐信息来组合装饰器,好比这里的$addCategoryDecorators是饮料类和荤菜类,未来还能够增长其余用户选择的菜系。这种装饰器模式并无经过继承来拓展基础类套餐,而是经过组合横向拓展了类的能力,后期代码也方便维护,装饰器模式也能够称为责任链模式、管道模式,在项目中也常常会使用到。
发布/订阅模式(又称为观察者模式,属于行为型模式的一种,它是将行为独立模块化,下降了行为和主体的耦合性。它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知全部的观察者对象,使他们可以自动更新本身。熟悉js操做DOM的读者应该知道,为DOM元素绑定一个事件可使用addEventListener方法,其实这就是一种发布/订阅模式,当事件发生时,各个监听的组建就会收到通知,继而产生交互效果。发布/订阅模式在项目中使用的很是多,例如Lumen框架中想要监听数据库的操做,并把数据库的每一次操做都记录到日志当中去,未来能够分析慢查询问题,就可使用以下方法:
\DB::listen(function ($query) {
....//这里是对sql语句的操做
});
复制代码
php底层的不少实现机制也都使用到了发布/订阅模式,好比信号处理、错误处理等。和生产者、消费者模式不一样的是,生产者、消费者每每是在两个进程中进行的,是一种异步处理的策略,发布/订阅模式则是当主体对象发生改变时,实时通知到观察者。
发布/订阅模式如此重要,以致于SPL直接给出了实现方案,咱们本例中也是经过SPL提供的接口SplSubject/SplObserver来实现的,例子也是最普通的:学校中当有新书上架时,将新书上架的消息通知给老师和学生。
咱们先来看一下发布/订阅模式的UML类图:
其中SplSubject类提供添加监听者(attach),删除监听者(detach),通知监听者(notify)三个方法,Book类实现了这个接口,咱们来看一下代码:
<?php
/**
* Description: 订阅与发布模式的实现
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-27
*/
class Book implements SplSubject
{
private $author = null; //做者
private $price = null; //售价
private $name = null; //书名
private $observers = null;
public function __construct($name, $author, $price)
{
$this->name = $name;
$this->author = $author;
$this->price = $price;
//初始化观察者为spl对象存储
$this->observers = new SplObjectStorage();
}
/**
* 添加观察者
* @param SplObserver $observer
*/
public function attach(SplObserver $observer)
{
$this->observers->attach($observer);
}
/**
* 删除观察者
* @param SplObserver $observer
*/
public function detach(SplObserver $observer)
{
$this->observers->detach($observer);
}
/**
* 通知观察者
*/
public function notify()
{
$params = [
'name' => $this->name,
'author' => $this->author,
'price' => $this->price
];
foreach ($this->observers as $observer) {
$observer->update($this, $params);
}
}
}
复制代码
咱们这里是用到了SplObjectStorage对象来存储观察者对象,这样就省去了底层数组的不少操做细节,好比in_array判断观察者对象是否已经存在了,这是一种委托设计模式。接下来咱们再来实现订阅者,订阅者只须要实现SplObserver的update方法便可:
<?php
/**
* Description: 学生类观察者
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-27
*/
class Student implements SplObserver
{
public function update(\Splsubject $subject)
{
// TODO: Implement update() method.
if(func_num_args() == 2) {
$params = func_get_arg(1);
echo '学生已经收到',$params['name'],'上架的信息','做者:',$params['author'],'订价:',$params['price'],"<br>";
}
}
}
......
class Teacher implements SplObserver
{
public function update(SplSubject $subject)
{
// TODO: Implement update() method.
if(func_num_args() == 2) {
$params = func_get_arg(1);
echo '老师已经收到',$params['name'],'上架的信息','做者:',$params['author'],'订价:',$params['price'],"<br/>";
}
}
}
复制代码
学生类和老师类收到新书上架的信息后,会打印出接收通知,update接收一个SplSubject对象,其实在Observer类中接收Subject对象属性的方法更好的是在Subject对象中添加一个getParams的方法,不直接去访问对象的内部属性,这样作设计模式中开闭原则。本例中经过参数接收发布者传递过来的信息,有些取巧,不过也达到了效果。接下来只要给发布者添加监听对象就能够了:
<?php
include __DIR__.'/Book.php';
include __DIR__.'/Student.php';
include __DIR__.'/Teacher.php';
$student = new Student();
$teacher = new Teacher();
$book = new Book('<<钢铁是怎样炼成的>>', '奥斯特洛夫斯基', '79.00');
$book->attach($student);
$book->attach($teacher);
$book->notify();
复制代码
几乎全部的api框架中都会提供这样一种事件发布/订阅机制,Laravel中专门有本身的Event模块的设计,基于此能够实现事件的广播。发布/订阅模式实现相对简单,读者本身也能够进行实现。
迭代器模式(Iterator),又叫作游标(Cursor)模式。提供一种方法访问一个容器(Container)对象中各个元素,而又不需暴露该对象的内部细节。 当你须要访问一个聚合对象,并且无论这些对象是什么都须要遍历的时候,就应该考虑使用迭代器模式。另外,当须要对汇集有多种方式遍历时,能够考虑去使用迭代器模式。迭代器模式为遍历不一样的汇集结构提供如开始、下一个、是否结束、当前哪一项等统一的接口。 PHP标准库(SPL)中提供了迭代器接口 Iterator,要实现迭代器模式,实现该接口便可。咱们来看一个简单的例子,咱们使用对象从数据库中取到数据以后,想要遍历取出对象中的数据,就可使用迭代器模式,UML类图很是简单:
咱们先来实现一个简单的数据库链接查询类,php7之后移除了mysqli模块,推荐使用PDO操做数据库(笔者认为这也和设计模式有关系,php程序员已经习惯了提到php就想到mysql,而PDO却能够将操做mysql、sqlserver、Oracle其余数据库的操做封住成统一的接口,这是一种适配器的设计模式)。咱们例子使用PDO简单实现一下:
class PdoDB
{
private static $conn = null;
//禁止被实例化
private function __construct()
{
}
private static function connDB()
{
return new PDO('mysql:host=localhost;sort=3306;dbname=study;', 'root', 'root');
}
/**
* 获取数据库链接单例
* @return null|PDO
*/
public static function getInstance()
{
if (is_null(self::$conn)) {
self::$conn = self::connDB();
}
return self::$conn;
}
private function __clone()
{
// TODO: Implement __clone() method.
echo 'error!';
}
}
复制代码
咱们那的PdoDB使用了单例模式实现,单例模式中的构造函数是private,另外当用户对对象进行克隆的时候,应该报错(克隆对象的操做也是一种设计模式,叫原型模式,和单例模式不一样的是:原型模式是建立型模式,是建立新对象,而单例模式的目的是共用一个对象)。接下来咱们实现Users类,其中使用PdoDB操做数据库取出数据,并实现迭代器Iterator:
<?php
/**
* Description: 迭代类
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-27
*/
class Users implements Iterator
{
protected $data;
protected $index;
public function __construct()
{
$this->data = PdoDB::getInstance()->query('select * from users')->fetchAll();
}
public function current()
{
$current = $this->data[$this->index];
return $current;
}
public function next()
{
$this->index++;
}
public function key()
{
return $this->index;
}
public function valid()
{
return $this->index < count($this->data);
}
public function rewind()
{
$this->index = 0;
}
}
复制代码
而后咱们就能够对Users对象进行遍历了:
<?php
/**
* Description: File basic description here...
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-27
*/
include __DIR__.'/PdoDB.php';
include __DIR__.'/Users.php';
//对数据进行迭代
$users = new Users();
foreach($users as $user) {
var_dump($user);
}
复制代码
上边的例子很简单;说到迭代器,不得不提一下PHP另一个强大的特性:生成器,生成器是 PHP 5.5 引入的新特性,可是目前貌似不多人用到它。 下面试 PHP 官方文档上对生成器的解释: 生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大下降。 生成器容许你在 foreach 代码块中写代码来迭代一组数据而不须要在内存中建立一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你能够写一个生成器函数,就像一个普通的自定义函数同样, 和普通函数只返回一次不一样的是, 生成器能够根据须要 yield 屡次,以便生成须要迭代的值。
咱们来看一个简单的文件读取的例子:
<?php
/**
* Description: 生成器读取超大文件
* User: guozhaoran<guozhaoran@cmcm.com>
* Date: 2019-10-27
*/
header("content-type:text/html;charset=utf-8");
/*$startMemory = memory_get_usage();
$value = file_get_contents('./test.txt');
$useMemory = memory_get_usage() - $startMemory;
echo '一共占用了',$useMemory,'字节内存';*/
function readTxt()
{
$handle = fopen('./test.txt', 'rb');
while (feof($handle)===false) {
yield fgets($handle);
}
fclose($handle);
}
$startMemory = memory_get_usage();
foreach (readTxt() as $key => $value) {
$lineData = $value;
}
$useMemory = memory_get_usage() - $startMemory;
echo '一共占用了',$useMemory,'字节内存';
复制代码
运行上边案例,对比使用file_get_contents读取文件,咱们看到使用生成器节省了大量的内存,生成器的引入使得程序中的函数达到了中断的效果,实现迭代器也只须要使用yield关键字便可,yield返回的就是一个Iterator对象。总的来讲,迭代器模式在业务场景中不经常使用,可是颇有用,读者若是以前没接触过相似的概念,能够搜集资料学习一下并在项目中使用。
设计模式的内容多多少少仍是有些抽象的,它是一把双刃剑,很容易被滥用。不一样的设计模式适用于不一样的业务场景,只有经过大量的经验积累和学习理解,才能将设计模式用的恰到好处。设计模式的最终目的是为了使系统解耦,方便开发者维护代码。设计模式有不少,笔者认为,结合不一样的业务场景尝试使用合理的设计模式解决问题,是最好的学习方式。