前言
我们全部都使用别人设计好的库或者框架。我们讨论库与框架,利用它们的API编译成我们的程序、享受运用别人的代码所带来的优点。但是,库与框架无法帮助我们将应用组织成容易了解、容易维护、具有弹性的框架,所以需要设计模式。
设计模式不会直接进入你的代码中,而是先进入你的大脑中,一旦你先在脑海中装入了许多关于模式的知识,就能够开始在新设计中采用他们,并当你的旧代码变得如同搅和成一团没有弹性的意大利面时,可用他们重做旧代码。
以我目前的理解看来,设计模式的确在很多方面提高了代码的复用性、可读性,并减少了重复编码的工作,开发中想要用好它们并不容易。在一些情况下,当你不了解设计模式优缺点的时候,你可以按照OO准则来编程,如对拓展开放对修改关闭、针对接口编程而不是针对实现编程,多用组合少用继承等等。
本文中所涉及的模式,没有提到如何理解它们,想要理解它们并不容易并且也需要例子来说明,我这里更多的是总结而非理解,我推荐一本书《Head First 设计模式》,可以看这本书理解设计模式。
依赖注入模式
先解释两个名词:
依赖-- 如果在 Class A 中,有 Class B 的实例,则称 Class A 对 Class B 有一个依赖。
注入–非自己主动初始化依赖,而通过外部来传入依赖的方式,称为注入。
更详细的解释,可以看这篇文章:点我
这个是被依赖的对象类:
class DatabaseConfiguration
{
private $host;
private $port;
private $username;
private $password;
public function __construct(string $host, int $port, string $username, string $password)
{
$this->host = $host;
$this->port = $port;
$this->username = $username;
$this->password = $password;
}
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;
}
}
看看如何注入
class DatabaseConnection
{
/**
* @var DatabaseConfiguration
*/
private $configuration;
/**
* @param DatabaseConfiguration $config
*/
public function __construct(DatabaseConfiguration $config)
{
// 这里DatabaseConfiguation 被注入
$this->configuration = $config;
}
public function getDsn(): string
{
// 通过 configuation 获得 DatabaseConfiguation的信息
return sprintf(
'%s:%s@%s:%d',
$this->configuration->getUsername(),
$this->configuration->getPassword(),
$this->configuration->getHost(),
$this->configuration->getPort()
);
}
}
策略模式
有一个设计原则
找出应用中可能会变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起
代码故事:我有一个鸭子抽象类,拥有鸭子的共性,但我想让鸭子飞,有的鸭子用翅膀飞,有的鸭子坐着火箭飞,有的鸭子腾云驾雾,这种情况下,鸭子子类覆盖父类实现自己的飞行方法就可以了。但有很多鸭子不会飞,比如模型鸭、诱饵鸭,如果他们继承鸭子抽象类,自身仍需覆盖fly(),但什么也不做,这种设计显然不太合理。
我希望设计更有弹性,再这里除了飞行之外,鸭子的其他部分不需要改变,我将它和鸭子类分开,建立一组新的类代表行为。
再看第二个设计原则
针对接口编程,而不是针对实现编程(这里的“接口”准确来说应该是超类型,通常是一个抽象类或者接口)
然后,我把类设计成这样
先看Fly接口
<?php
namespace Strategy;
interface Fly
{
public function fly();
}
定义了飞行的行为,所有具体的飞行方式都要继承这个接口。
<?php
namespace Strategy;
class FlyWithRocket implements Fly
{
public function fly()
{
echo '借助火箭飞行';
echo '<br>';
}
}
以及用翅膀飞的行为类
<?php
namespace Strategy;
class FlyWithWinds implements Fly
{
public function fly()
{
echo '用翅膀飞';
echo '<br>';
}
}
鸭子类,也很简单。
<?php
namespace Strategy;
abstract class MyDuck
{
private $fly;
public function __construct(Fly $fly)
{
$this->fly = $fly;
}
/**
* 执行被委托的飞行方法
*
* @return mixed
*/
public function doFly(){
return $this->fly->fly();
}
/**
* 动态的改变飞行的行为
*
* @param Fly $setFly
*/
public function setFly(Fly $setFly){
$this->fly=$setFly;
}
public function others(){
// 鸭子的其他方法
}
}
具体的鸭子类,这里简单起见,什么也不做。
<?php
namespace Strategy;
class YellowDuck extends MyDuck
{
/**
* 简化逻辑,这里什么也不做
*
* 实际情况会有自己的方法或者覆盖父类方法
*/
}
然后看看实现:
<?php
include("Fly.php");
include("MyDuck.php");
include("FlyWithRocket.php");
include("FlyWithWinds.php");
include("YellowDuck.php");
use Strategy\FlyWithRocket;
use Strategy\YellowDuck;
use Strategy\FlyWithWinds;
// 初始化时,用翅膀飞行
$yellow_duck=new YellowDuck(new FlyWithWinds());
$yellow_duck->doFly();
// 改成用火箭飞
$yellow_duck->setFly(new FlyWithRocket());
$yellow_duck->doFly();
如我所料,他的输出也是这样的:
用翅膀飞
借助火箭飞行
每个鸭子有一个Fly,鸭子的飞行行为委托他们进行代理。当你将两个类结合起来一起使用,叫组合,这是一个很总要的技巧,其实这也是一个设计原则:
多用组合,少用继承
到这里一个简写的策略模式就介绍完了,看一下策略模式的定义:
定义算法族,分别封装起来,让他们之间可以相互替换,让算法的变化独立于使用算法的客户。
如果你开发中找不到合适的模式,就采用这些面向对象的原则:
封装变化。
多用组合,少用继承。
针对接口编程,不针对实现编程。
观察者模式
我们来看看报纸和杂志的订阅是怎么回事:
- 报社的业务就是出版报纸。
- 向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。
- 当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
- 只要报社还再运营,就会一直有人向他们订阅报纸或者取消订阅报纸。
观察者模式就像订阅报纸,知识名称不太一样:出版社改为“主题”(Subject),订阅者称为“观察者”(Observer)。实现观察者模式的方式不只一种,但是包含Subject和Observer接口的类设计的做法最常见。
主题可以注册、注销观察者,当数据改变时,可以通知观察者;所有的观察者必须继承观察者接口,这个接口只有一个update()方法,当主题的状态被改变时调用。主题继承 SplSubject,这个是PHP内置观察者模式主题接口;观察者继承SplObserver;同时,主题中用SplObjectStorage来存储观察者列表。
这个是主题:
<?php
namespace Observer2;
use SplObserver;
class Subject implements \SplSubject
{
// 观察者对象
private $observers;
private $upd_data = 10;
private $upd_msg = '还不该吃饭';
public function __construct()
{
// 数据结构对象容器
$this->observers = new \SplObjectStorage();
}
/**
* 注册观察者
*
* Attach an SplObserver
* @link https://php.net/manual/en/splsubject.attach.php
* @param SplObserver $observer <p>
* The <b>SplObserver</b> to attach.
* </p>
* @return void
* @since 5.1.0
*/
public function attach(SplObserver $observer)
{
$this->observers->attach($observer);
}
/**
* 注销观察者
*
* Detach an observer
* @link https://php.net/manual/en/splsubject.detach.php
* @param SplObserver $observer <p>
* The <b>SplObserver</b> to detach.
* </p>
* @return void
* @since 5.1.0
*/
public function detach(SplObserver $observer)
{
$this->observers->detach($observer);
}
/**
* 通知观察者
* Notify an observer
* @link https://php.net/manual/en/splsubject.notify.php
* @return void
* @since 5.1.0
*/
public function notify()
{
// 循环调用观察者自身的update方法
if ($this->observers->count() > 0) {
/** @var \SplObserver $observer */
foreach ($this->observers as $observer) {
$observer->update($this); // 注意这个 $this 再观察者中的对应位置
}
}
}
/**
* 数据改变,同时通知观察者
*
* @param $upd_data
*/
public function updData($upd_data)
{
$this->upd_data = $upd_data;
// 当数据改变时,再需要通知观察者的地方调用 notify
$this->notify();
}
/**
* 消息改变,同时通知观察者
*
* @param $upd_msg
*/
public function updMsg($upd_msg)
{
$this->upd_msg = $upd_msg;
// 这个 notify 也可以不写在方法中,灵活运用
$this->notify();
}
/**
* @return mixed
*/
public function getUpdData()
{
return $this->upd_data;
}
/**
* @return mixed
*/
public function getUpdMsg()
{
return $this->upd_msg;
}
}
有一个猫猫观察者:
<?php
namespace Observer2;
use SplSubject;
class CatObserver implements \SplObserver
{
/**
* 从主题那里得到通知
*
* Receive update from subject
* @link https://php.net/manual/en/splobserver.update.php
* @param SplSubject $subject <p>
* The <b>SplSubject</b> notifying the observer of an update.
* </p>
* @return void
* @since 5.1.0
*/
public function update(\SplSubject $subject)
{
// 这个 $subject 一开始比较难理解,最后发现确实很聪明
// 我觉得这种实现方式最好,通过主题的实例,获取具体要得到哪些更新,去看主题的notify()
// 观察者不知道主题的细节,只知道实现了主题的接口
/**
* @var Subject $subject
*/
$listen_data = $subject->getUpdData();
$listen_msg = $subject->getUpdMsg();
echo 'I am 猫猫观察者,I get new data:' . $listen_data . ';msg:' . $listen_msg;
echo '<br>';
}
}
还有一个狗狗观察者:
<?php
namespace Observer2;
class DogObserver implements \SplObserver
{
public function update(\SplSubject $subject)
{
/**
* @var Subject $subject
*/
$listen_data = $subject->getUpdData();
$listem_msg = $subject->getUpdMsg();
echo 'I am 狗狗观察者,data:' . $listen_data . ';msg:' . $listem_msg;
echo '<br>';
}
}
来看一下调用吧:
<?php
include 'CatObserver.php';
include 'Subject.php';
include 'DogObserver.php';
// 观察者
$catObserver = new \Observer2\CatObserver();
$dogObserver=new Observer2\DogObserver();
// 主题
$subject = new \Observer2\Subject();
// 注册观察者
$subject->attach($catObserver);
$subject->attach($dogObserver);
// 主题有所变化,观察者得到对应的变化
$subject->updData(12);
$subject->updMsg('你妈妈喊你回家吃饭');
显示结果:
I am 猫猫观察者,I get new data:12;msg:还不该吃饭
I am 狗狗观察者,data:12;msg:还不该吃饭
I am 猫猫观察者,I get new data:12;msg:你妈妈喊你回家吃饭
I am 狗狗观察者,data:12;msg:你妈妈喊你回家吃饭
把我的例子弄懂,你也应该能理解观察者模式了。
来看一下你学到了什么:
00原则–对象之间松耦合设计。
观察者模式–在对象之间定义一对多的依赖,这样一来,当一个对象改变状态,依赖他的对象就会收到通知,并自动更新。
- 观察者模式定义了对象之间的一对多关系。
- 观察者不知道观察者的细节,也不知道主题的细节,只知道实现了观察者接口。
装饰者模式
代码故事:我要开一个咖啡馆,我卖的咖啡有:拿铁、摩卡、卡布奇诺、猫屎、猫尿…,没种咖啡有自己的价格和描述。于是我的一开始这样设计我的咖啡:
下单时,也可以要求加入各种调料,如牛奶、奶泡、巧克力、焦糖等等,所以订单系统必须要考虑到调料的价钱,cost()方法计算出咖啡+调料的总价。
简直是类爆炸了有木有。这种方案肯定是不合理的,于是我将父类进行改良,把是否加调料及调料的计价方法写再父类中:
但这样做也不合理,比如会带来以下问题
- 一但出现新的调料,就需要增加新的方法,并修改超类的cost()方法
- 父类中的方法,子类中并不都适用
- 顾客要双倍牛奶、双倍巧克力怎么办
- 该你了…
这个时候就要提到一个设计原则:开放-关闭原则
类应该对拓展开放,对修改关闭
(我们的目标是使类容易拓展,在不修改现有代码的情况下,就可以搭配新的行为)
既然本篇是介绍装饰者模式,还是先看看设计吧:
看下php的代码实现吧
饮料接口
<?php
namespace Decorator;
interface Beverage
{
/**
* 得到描述
*
* @return mixed
*/
public function getDescription();
/**
*
*
* @return mixed
*/
public function cost();
}
深度烘焙饮料:
<?php
namespace Decorator;
class DarkRoast implements Beverage
{
private $price = 10;
private $des = '深度烘焙';
/**
* 计算价格的方法
*
* @return mixed
*/
public function cost()
{
return $this->setPrice(22);
}
/**
* 改变咖啡的价格
*
* @param $price
* @return int
*/
public function setPrice($price)
{
$this->price = $price;
return $this->price;
}
/**
* 得到描述
*
* @return mixed
*/
public function getDescription()
{
return $this->des . '¥' . $this->price;
}
}
来看调料装饰者,我把它定义成抽象类
<?php
namespace Decorator;
/**
* 调料装饰者,扩展自 Beverage
*
* 调料装饰者类继承自饮料,所以调料装饰者能够取代饮料
*
* Class CondimentDecorator
* @package Decorator
*/
abstract class CondimentDecorator implements Beverage
{
protected $price;
protected $description;
// 饮料实例
protected $beverage;
/**
* 构造方法
*
* CondimentDecorator constructor.
* @param Beverage $beverage
*/
public function __construct(Beverage $beverage)
{
$this->beverage = $beverage;
}
}
两种调料,继承自调料装饰者的抽象类。猫屎调料:
<?php
namespace Decorator;
class MaoShi extends CondimentDecorator
{
public function __construct(Beverage $beverage)
{
parent::__construct($beverage);
}
/**
* 得到描述
*
* @return mixed
*/
public function getDescription()
{
$this->description = '添加猫屎';
return $this->beverage->getDescription() . '+' . $this->description;
}
/**
*
*
* @return mixed
*/
public function cost()
{
$this->price = 5.8;
return $this->price + $this->beverage->cost();
}
}
牛奶调料:
<?php
namespace Decorator;
class Milk extends CondimentDecorator
{
public function __construct(Beverage $beverage)
{
parent::__construct($beverage);
}
/**
* 得到描述
*
* @return mixed
*/
public function getDescription()
{
$this->description = '添加牛奶';
return $this->beverage->getDescription() . '+' . $this->description;
}
/**
*
*
* @return mixed
*/
public function cost()
{
$this->price = 10;
// 调料的价格+杯装饰者的价格
return $this->price + $this->beverage->cost();
}
}
来来了一个客户,他要:“深度烘焙的猫屎咖啡,还要加一份牛奶”。来看看运行代码:
<?php
include 'Beverage.php';
include 'CondimentDecorator.php';
include 'DarkRoast.php';
include 'MaoShi.php';
include 'Milk.php';
use Decorator\DarkRoast;
use Decorator\Milk;
use Decorator\MaoShi;
// 客户点单:深度烘焙的猫屎咖啡,哦,再加一份牛奶
$darkRoast = new DarkRoast();
// 用猫屎装饰它
$beverage_with_maoshi = new MaoShi($darkRoast);
// 再用牛奶装饰它
$coffee_done = new Milk($beverage_with_maoshi);
var_dump('点单:' . $coffee_done->getDescription());
var_dump('总价格:¥' . $coffee_done->cost());
看看是否满足客户的要求了?嗯,好像是这回事,哈哈。
点单:深度烘焙¥10+添加猫屎+添加牛奶
总价格:¥37.8
来总结一下吧,看看你是否真的理解了。
装饰者模式:动态地将责任附加到对象上。想要拓展功能,装饰者提供有别于继承的另一种选择。
要点:
- 我们应允许行为可以被拓展,而无需修改现有的代码。
- 除了继承,装饰者模式可以让我们拓展行为。
- 装饰者模式意味着一群装饰者类包装具体的组件。
- 装饰者和被装饰者的类型相同。
- 装饰者可以再被装饰者的行为前面(或后面)加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。
- 装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。
如果你再开发中不知道使用哪种设计模式,就遵守这些设计原则:
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之间的松耦合设计而努力
对拓展开放,对修改关闭
单例模式
目的:在调用的时候,只能由一个对象实例。
示例代码:
class Singleton {
private static $instance = null;
/**
* 获取唯一对象实例的方法,只能由类调用 Singleton::getInstance
*
* @return Singleton
*/
public static function getInstance() {
// 如果没有创建对象则创建
if (!self::$instance) {
self::$instance = new Singleton();
}
// 返回唯一的对象
return self::$instance;
}
// 私有的构造方法,外部无法new对象
private function __construct() {
}
// 防止外部克隆对象
private function __clone() {
}
}
但是以上代码在JAVA多线程中可能会产生多个对象实例,下面我划出简单示意图:
虽然不常见,但为避免这种情况,可以在判断前先创建好对象,而不是延迟实例化。
如果是JAVA代码,可以尝试这样改写
private static Singleton instance=new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
由于PHP本身不支持多线程,暂时不考虑这种情况。