PHP设计模式

前言

我们全部都使用别人设计好的库或者框架。我们讨论库与框架,利用它们的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本身不支持多线程,暂时不考虑这种情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值