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

2020-03-09  本文已影响0人  马蹄哒

单例模式

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

#文件结构
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

上一篇下一篇

猜你喜欢

热点阅读