【PHP】常见的五种设计模式

单例模式

在整个应用中只生成一个实例,不允许重复创建实例。有利于减少重复创建实例的开销
应用场景:

  • 数据库连接
  • 日志记录
  • 文件锁
#文件结构
code
   |-- Singleton
          |-- Singleton.php
          |--Tests
              |-- SingletonTest.php
          |-- phpunit.xml
          |-- composer.json

#Singleton.php
<?php declare(strict_types=1);


namespace DesignPattern\Singleton;


class Singleton {
    private static ?Singleton $instance = null; #php7的类型提示,?Singleton 表示可以为null或Singleton

    /**
     * 惰性加载实例 (第一次使用时创建实例)
     */
    public static function getInstance(): Singleton
    {
        if (static::$instance === null) {
            static::$instance = new static();
        }

        return static::$instance;
    }

    /**
     * 避免从外部实例化
     * 只能有一种实例化方式:Singleton::getInstance()
     */
    private function __construct()
    {
    }

    /**
     * 避免被克隆 (会创建第二个实例)
     */
    private function __clone()
    {
    }

    /**
     * 避免反序列化unserialize() (会创建第二个实例)
     */
    private function __wakeup()
    {
    }
}

#Tests/SingletonTest.php
<?php declare(strict_types=1);
namespace DesignPattern\Singleton\Tests;

use DesignPattern\Singleton\Singleton;
use PHPUnit\Framework\TestCase;

class SingletonTest extends TestCase {

    public function testUniqueness()
    {
        $firstCall = Singleton::getInstance();
        $secondCall = Singleton::getInstance();

        $this->assertInstanceOf(Singleton::class, $firstCall);
        $this->assertEquals($firstCall, $secondCall);
    }

}

工厂模式

使用一个工厂类来统一创建其他类的实例(避免每次使用都使用new来实例化),这些类通常继承同一个抽象类,或实现同一个接口。

  • 工厂模式有利于代码解耦:
    如果将来要修改某个类名,参数的时候,只需要修改工厂类实现即可,不用修改每个使用该类的地方;
    如果类的实例化比较复杂,使用工厂类还可以使类的实例化更简便
    示例:
#文件结构
code
   |-- Factory
          |-- Logger.php
          |-- FileLogger.php
          |-- StdoutLogger.php
          |-- LoggerFactory.php
          |-- FileLoggerFactory.php
          |-- StdoutLoggerFactory.php
          |--Tests
              |-- FactoryMethodTest.php
          |-- phpunit.xml
          |-- composer.json

#Logger.php
<?php declare(strict_types=1);
namespace DesignPattern\Factory;


interface Logger {
    public function log(string $message);
}

#FileLogger.php
<?php declare(strict_types=1);
namespace DesignPattern\Factory;


class FileLogger implements Logger {
    private string $filePath;

    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
    }

    public function log(string $message)
    {
        file_put_contents($this->filePath, $message . PHP_EOL, FILE_APPEND);
    }
}

#StdoutLogger.php
<?php declare(strict_types=1);
namespace DesignPattern\Factory;

class StdoutLogger implements Logger {
    public function log(string $message){
        echo $message;
    }
}

#LoggerFactory.php
<?php declare(strict_types=1);
namespace DesignPattern\Factory;


interface LoggerFactory {
    public function createLogger(): Logger;
}

#FileLoggerFactory.php
<?php declare(strict_types=1);
namespace DesignPattern\Factory;


class FileLoggerFactory implements LoggerFactory {
    private string $filePath;

    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
    }

    public function createLogger(): Logger
    {
        return new FileLogger($this->filePath);
    }
}

#StdoutLoggerFactory.php
<?php declare(strict_types=1);
namespace DesignPattern\Factory;

class StdoutLoggerFactory implements LoggerFactory {
    public function createLogger(): Logger
    {
        return new StdoutLogger();
    }
}

#Tests/FactoryMethodTest.php
<?php declare(strict_types=1);

namespace DesignPattern\Factory;

use DesignPattern\Factory\FileLogger;
use DesignPattern\Factory\FileLoggerFactory;
use DesignPattern\Factory\StdoutLogger;
use DesignPattern\Factory\StdoutLoggerFactory;
use PHPUnit\Framework\TestCase;

class FactoryMethodTest extends TestCase
{
    public function testCanCreateStdoutLogging()
    {
        $loggerFactory = new StdoutLoggerFactory();
        $logger = $loggerFactory->createLogger();

        $this->assertInstanceOf(StdoutLogger::class, $logger);
    }

    public function testCanCreateFileLogging()
    {
        $loggerFactory = new FileLoggerFactory(sys_get_temp_dir());
        $logger = $loggerFactory->createLogger();

        $this->assertInstanceOf(FileLogger::class, $logger);
    }
}

观察者模式

也可以看作发布/订阅模式,实现方式很简单:一个对象提供一个方法,让另一个对象(观察者)注册自己,当对象发生变化时,调用注册的观察者的方法通知观察者。
示例:

#文件结构
code
   |-- Factory
          |-- User.php
          |-- UserObserver.php
          |--Tests
              |--ObserverTest.php
          |-- phpunit.xml
          |-- composer.json

#User.php
<?php declare(strict_types=1);
namespace DesignPattern\Observer;

use SplSubject;
use SplObserver;
use SplObjectStorage;

class User implements SplSubject {
    private string $email;
    private SplObjectStorage $observers;

    public function __construct()
    {
        $this->observers = new SplObjectStorage();
    }

    public function attach(SplObserver $observer)
    {
        $this->observers->attach($observer);
    }

    public function detach(SplObserver $observer)
    {
        $this->observers->detach($observer);
    }

    public function changeEmail(string $email)
    {
        $this->email = $email;
        $this->notify();
    }

    public function notify()
    {
        /** @var SplObserver $observer */
        foreach ($this->observers as $observer)
        {
            $observer->update($this);
        }
    }
}

#UserObserver.php
<?php declare(strict_types=1);
namespace DesignPattern\Observer;

use SplObserver;
use SplSubject;

class UserObserver implements SplObserver
{
    /**
     * @var SplSubject[]
     */
    private array $changedUsers = [];

    /**
     * It is called by the Subject, usually by SplSubject::notify()
     */
    public function update(SplSubject $subject)
    {
        $this->changedUsers[] = clone $subject;
    }

    /**
     * @return SplSubject[]
     */
    public function getChangedUsers(): array
    {
        return $this->changedUsers;
    }
}

#Tests/ObserverTest.php
<?php declare(strict_types=1);
namespace DesignPattern\Observer\Tests;

use DesignPattern\Observer\User;
use DesignPattern\Observer\UserObserver;
use PHPUnit\Framework\TestCase;

class ObserverTest extends TestCase {
    public function testChangeInUserLeadsToUserObserverBeingNotified()
    {
        $observer = new UserObserver();

        $user = new User();
        $user->attach($observer);

        $user->changeEmail('foo@bar.com');
        $this->assertCount(1, $observer->getChangedUsers());
    }
}

策略模式

主要用于在多种策略间快速切换,利用扩展:多种策略实现同一个接口,功能相似,但相互独立
示例:

#文件结构
code
   |--Stragy
          |--Context.php
          |--Comparator.php
          |--IdComparator.php
          |--DateComparator.php
          |--Tests
              |--StrategyTest.php
          |-- phpunit.xml
          |-- composer.json

#Context.php
<?php declare(strict_types=1);
namespace DesignPattern\Strategy;

class Context {
    private Comparator $comparator;

    public function __construct(Comparator $comparator)
    {
        $this->comparator = $comparator;
    }

    public function executeStrategy(array $elements): array
    {
        uasort($elements, [$this->comparator, 'compare']);

        return $elements;
    }
}

#Comparator.php
<?php declare(strict_types=1);
namespace DesignPattern\Strategy;

interface Comparator {
    /**
     * @param mixed $a
     * @param mixed $b
     *
     * @return int
     */
    public function compare($a, $b): int;
}

#IdComparator.php
<?php declare(strict_types=1);
namespace DesignPattern\Strategy;

class IdComparator implements Comparator {
    public function compare($a, $b): int
    {
        return $a['id'] <=> $b['id'];
    }
}

#DateComparator.php
<?php declare(strict_types=1);
namespace DesignPattern\Strategy;

use DateTime;

class DateComparator implements Comparator {
    public function compare($a, $b): int
    {
        $aDate = new DateTime($a['date']);
        $bDate = new DateTime($b['date']);
        return $aDate <=> $bDate;
    }
}

#Tests/StrategyTest.php
<?php declare(strict_types=1);
namespace DesignPattern\Strategy\Tests;

use DesignPattern\Strategy\Context;
use DesignPattern\Strategy\DateComparator;
use DesignPattern\Strategy\IdComparator;
use PHPUnit\Framework\TestCase;

class StrategyTest extends TestCase {
    public function provideIntegers()
    {
        return [
            [
                [['id' => 2], ['id' => 1], ['id' => 3]],
                ['id' => 1],
            ],
            [
                [['id' => 3], ['id' => 2], ['id' => 1]],
                ['id' => 1],
            ],
        ];
    }

    public function provideDates()
    {
        return [
            [
                [['date' => '2014-03-03'], ['date' => '2015-03-02'], ['date' => '2013-03-01']],
                ['date' => '2013-03-01'],
            ],
            [
                [['date' => '2014-02-03'], ['date' => '2013-02-01'], ['date' => '2015-02-02']],
                ['date' => '2013-02-01'],
            ],
        ];
    }

    /**
     * @dataProvider provideIntegers
     *
     * @param array $collection
     * @param array $expected
     */
    public function testIdComparator($collection, $expected)
    {
        $obj = new Context(new IdComparator());
        $elements = $obj->executeStrategy($collection);

        $firstElement = array_shift($elements);
        $this->assertSame($expected, $firstElement);
    }

    /**
     * @dataProvider provideDates
     *
     * @param array $collection
     * @param array $expected
     */
    public function testDateComparator($collection, $expected)
    {
        $obj = new Context(new DateComparator());
        $elements = $obj->executeStrategy($collection);

        $firstElement = array_shift($elements);
        $this->assertSame($expected, $firstElement);
    }

}

控制链模式

多个对象按顺序执行,如果某个对象不能处理,则交由下个对象处理。比如缓存,首先缓存实例处理,如果不能处理,则使用数据库实例。
示例:

#文件结构
code
   |--Stragy
          |--Handler.php
          |--FastStorage.php
          |--SlowStorage.php
          |--Tests
              |--ChainTest.php
          |-- phpunit.xml
          |-- composer.json

#Handler.php
<?php declare(strict_types=1);
namespace DesignPattern\Chain;

use Psr\Http\Message\RequestInterface;

abstract class Handler
{
    private ?Handler $successor = null;

    public function __construct(Handler $handler = null)
    {
        $this->successor = $handler;
    }

    /**
     * This approach by using a template method pattern ensures you that
     * each subclass will not forget to call the successor
     */
    final public function handle(RequestInterface $request): ?string
    {
        $processed = $this->processing($request);

        if ($processed === null && $this->successor !== null) {
            // the request has not been processed by this handler => see the next
            $processed = $this->successor->handle($request);
        }

        return $processed;
    }

    abstract protected function processing(RequestInterface $request): ?string;
}

#FastStorage.php
<?php declare(strict_types=1);
namespace DesignPattern\Chain;

use Psr\Http\Message\RequestInterface;

class HttpInMemoryCacheHandler extends Handler
{
    private array $data;

    public function __construct(array $data, ?Handler $successor = null)
    {
        parent::__construct($successor);

        $this->data = $data;
    }

    protected function processing(RequestInterface $request): ?string
    {
        $key = sprintf(
            '%s?%s',
            $request->getUri()->getPath(),
            $request->getUri()->getQuery()
        );

        if ($request->getMethod() == 'GET' && isset($this->data[$key])) {
            return $this->data[$key];
        }

        return null;
    }
}

#SlowStorage.php
<?php declare(strict_types=1);
namespace DesignPattern\Chain;

use Psr\Http\Message\RequestInterface;

class SlowDatabaseHandler extends Handler
{
    protected function processing(RequestInterface $request): ?string
    {
        // this is a mockup, in production code you would ask a slow (compared to in-memory) DB for the results

        return 'Hello World!';
    }
}

# ChainTest.php
<?php declare(strict_types=1);
namespace DesignPattern\Chain\Tests;

use DesignPattern\Chain\Handler;
use DesignPattern\Chain\HttpInMemoryCacheHandler;
use DesignPattern\Chain\SlowDatabaseHandler;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;

class ChainTest extends TestCase
{
    private Handler $chain;

    protected function setUp(): void
    {
        $this->chain = new HttpInMemoryCacheHandler(
            ['/foo/bar?index=1' => 'Hello In Memory!'],
            new SlowDatabaseHandler()
        );
    }

    public function testCanRequestKeyInFastStorage()
    {
        $uri = $this->createMock(UriInterface::class);
        $uri->method('getPath')->willReturn('/foo/bar');
        $uri->method('getQuery')->willReturn('index=1');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')
            ->willReturn('GET');
        $request->method('getUri')->willReturn($uri);

        $this->assertSame('Hello In Memory!', $this->chain->handle($request));
    }

    public function testCanRequestKeyInSlowStorage()
    {
        $uri = $this->createMock(UriInterface::class);
        $uri->method('getPath')->willReturn('/foo/baz');
        $uri->method('getQuery')->willReturn('');

        $request = $this->createMock(RequestInterface::class);
        $request->method('getMethod')
            ->willReturn('GET');
        $request->method('getUri')->willReturn($uri);

        $this->assertSame('Hello World!', $this->chain->handle($request));
    }
}


测试

如果要运行以上测试文件,需使用composer自动加载,和PHPUnit来测试:

  1. 创建以下文件
    composer.json 放在code目录下
{
  "autoload": {
    "psr-4": {
      "DesignPattern\\": "./"
    }
  },
  "require": {
    "phpunit/phpunit": "^9.0",
    "psr/http-message": "^1.0"
  }
}

phpunit.xml 编排测试

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./vendor/autoload.php">
    <testsuites>
        <testsuite name="Design Pattern">
            <directory suffix="Test.php">*/Tests</directory>
        </testsuite>
    </testsuites>
</phpunit>
  1. code目录下执行composer install
  2. code目录下执行: ./vendor/bin/phpunit 运行所有测试文件, ./vendor/bin/phpunit ./Factory 运行Factory目录下的文件
$  cv ./vendor/bin/phpunit
PHPUnit 9.0.1 by Sebastian Bergmann and contributors.

..........                                                        10 / 10 (100%)

Time: 41 ms, Memory: 6.00 MB

OK (10 tests, 11 assertions)

如果出现类找不到的异常,请执行composer dump-autoload -o更新自动加载


参考:designpatternsphp

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 一、安装 composer require --dev phpunit/phpunit ^6.5 composer...
    ZyBlog阅读 2,710评论 0 4
  • 介绍 PHPUnit 测试的基础知识,使用基本的 PHPUnit 断言和 Laravel 测试助手。 介绍 PHP...
    summerbluet阅读 4,926评论 0 7
  • 根目录 App目录 app 目录包含应用程序的核心代码。 Bootstrap目录 bootstrap 目录包含了一...
    伊Summer阅读 2,700评论 0 1
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,423评论 0 4
  • 【文字模版在此】 觉察曰记(模板) 受 今天发生的事件,我对这件事的感受(事实、感受) 今天好开心呀,和学生好好吃...
    周周是个妞阅读 456评论 4 1