PHP 行为型模式


PHP 行为型模式


正文

行为型设计模式用于处理类的对象间通信。

责任链模式(Chain Of Responsibilities)

  • 模式定义

责任链模式将处理请求的对象连成一条链,沿着这条链传递该请求,直到有一个对象处理请求为止,这使得多个对象都有机会处理请求, 从而避免请求的发送者和接受者之间的耦合关系。 责任链模式在现实中使用的很多,常见的就是 OA 系统中的工作流。

  • UML类图

  • 示例代码

Request.php

<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities;

/**
 * 经过责任链的Request类
 *
 * 关于请求: 有时候,不需要一个请求对象,只需一个整型数据或者一个数组即可。
 * 但是作为一个完整示例,这里我们生成了一个请求类。
 * 在实际项目中,也推荐使用请求类,即是是一个标准类\stdClass,
 * 因为这样的话代码更具扩展性,因为责任链的处理器并不了解外部世界,
 * 如果某天你想要添加其它复杂处理时不使用请求类会很麻烦
 */
class Request
{
    // getter and setter but I don't want to generate too much noise in handlers
}

Handler.php

<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities;

/**
 * 责任链的通用处理器类Handler(通常是一个接口或抽象类)
 *
 * 当然你可以通过一个更简单的处理器实现更加轻量级的责任链,
 * 但是如果你想让你的责任链拥有更好的扩展性和松耦合,
 * 那么就需要模拟一个更加真实的场景:通常一个责任链每时每刻都会被修改,
 * 这也是为什么我们在这里将其切分成好几个部分来完成。
 */
abstract class Handler
{
    /**
     * @var Handler
     */
    private $successor = null;

    /**
     * 追加处理类到责任链
     * 通过这个方法可以追加多个处理类到责任链
     *
     * @param Handler $handler
     */
    final public function append(Handler $handler)
    {
        if (is_null($this->successor)) {
            $this->successor = $handler;
        } else {
            $this->successor->append($handler);
        }
    }

    /**
     * 处理请求
     *
     * 这里我们使用模板方法模式以确保每个子类都不会忘记调用successor
     * 此外,返回的布尔值表明请求是否被处理
     * 
     * @param Request $req
     *
     * @return bool
     */
    final public function handle(Request $req)
    {
        $req->forDebugOnly = get_called_class();
        $processed = $this->processing($req);
        if (!$processed) {
            // the request has not been processed by this handler => see the next
            if (!is_null($this->successor)) {
                $processed = $this->successor->handle($req);
            }
        }

        return $processed;
    }

    /**
     * 每个处理器具体实现类都要实现这个方法对请求进行处理
     *
     * @param Request $req
     *
     * @return bool true if the request has been processed
     */
    abstract protected function processing(Request $req);
}

Responsible/SlowStorage.php

<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;

use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Request;

/**
 * 该类和FastStorage基本相同,但也有所不同
 *
 * 责任链模式的一个重要特性是: 责任链中的每个处理器都不知道自己在责任链中的位置,
 * 如果请求没有被处理,那么责任链也就不能被称作责任链,除非在请求到达的时候抛出异常
 *
 * 为了实现真正的扩展性,每一个处理器都不知道后面是否还有处理器
 *
 */
class SlowStorage extends Handler
{
    /**
     * @var array
     */
    protected $data = array();

    /**
     * @param array $data
     */
    public function __construct($data = array())
    {
        $this->data = $data;
    }

    protected function processing(Request $req)
    {
        if ('get' === $req->verb) {
            if (array_key_exists($req->key, $this->data)) {
                $req->response = $this->data[$req->key];
                return true;
            }
        }

        return false;
    }
}

Responsible/FastStorage.php

<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;

use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Request;

/**
 * FastStorage类
 */
class FastStorage extends Handler
{
    /**
     * @var array
     */
    protected $data = array();

    /**
     * @param array $data
     */
    public function __construct($data = array())
    {
        $this->data = $data;
    }

    protected function processing(Request $req)
    {
        if ('get' === $req->verb) {
            if (array_key_exists($req->key, $this->data)) {
                $req->response = $this->data[$req->key];
                return true;
            }
        }

        return false;
    }
}
  • 测试代码

Tests/ChainTest.php

<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Tests;

use DesignPatterns\Behavioral\ChainOfResponsibilities\Request;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\FastStorage;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowStorage;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;

/**
 * ChainTest用于测试责任链模式
 */
class ChainTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @var FastStorage
     */
    protected $chain;

    protected function setUp()
    {
        $this->chain = new FastStorage(array('bar' => 'baz'));
        $this->chain->append(new SlowStorage(array('bar' => 'baz', 'foo' => 'bar')));
    }

    public function makeRequest()
    {
        $request = new Request();
        $request->verb = 'get';

        return array(
            array($request)
        );
    }

    /**
     * @dataProvider makeRequest
     */
    public function testFastStorage($request)
    {
        $request->key = 'bar';
        $ret = $this->chain->handle($request);

        $this->assertTrue($ret);
        $this->assertObjectHasAttribute('response', $request);
        $this->assertEquals('baz', $request->response);
        // despite both handle owns the 'bar' key, the FastStorage is responding first
        $className = 'DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\FastStorage';
        $this->assertEquals($className, $request->forDebugOnly);
    }

    /**
     * @dataProvider makeRequest
     */
    public function testSlowStorage($request)
    {
        $request->key = 'foo';
        $ret = $this->chain->handle($request);

        $this->assertTrue($ret);
        $this->assertObjectHasAttribute('response', $request);
        $this->assertEquals('bar', $request->response);
        // FastStorage has no 'foo' key, the SlowStorage is responding
        $className = 'DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowStorage';
        $this->assertEquals($className, $request->forDebugOnly);
    }

    /**
     * @dataProvider makeRequest
     */
    public function testFailure($request)
    {
        $request->key = 'kurukuku';
        $ret = $this->chain->handle($request);

        $this->assertFalse($ret);
        // the last responsible :
        $className = 'DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowStorage';
        $this->assertEquals($className, $request->forDebugOnly);
    }
}
  • 总结

责任链模式的主要优点在于可以降低系统的耦合度,简化对象的相互连接,同时增强给对象指派职责的灵活性,增加新的请求处理类也很方便; 其主要缺点在于不能保证请求一定被接收,且对于比较长的职责链,请求的处理可能涉及到多个处理对象,系统性能将受到一定影响,而且在进行代码调试时不太方便。

命令模式(Command)

  • 模式定义

命令模式(Command)将请求封装成对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。 这么说很抽象,我们举个例子: 假设我们有一个调用者类 Invoker 和一个接收调用请求的类 Receiver, 在两者之间我们使用命令类 Command 的 execute 方法来托管请求调用方法, 这样,调用者 Invoker 只知道调用命令类的 execute 方法来处理客户端请求,从而实现接收者 Receiver 与调用者 Invoker 的解耦。 Laravel 中的 Artisan 命令就使用了命令模式。

  • UML类图

  • 示例代码

CommandInterface.php

<?php
namespace DesignPatterns\Behavioral\Command;

/**
 * CommandInterface
 */
interface CommandInterface
{
    /**
     * 在命令模式中这是最重要的方法,
     * Receiver在构造函数中传入.
     */
    public function execute();
}

HelloCommand.php

<?php
namespace DesignPatterns\Behavioral\Command;

/**
 * 这是一个调用Receiver的print方法的命令实现类,
 * 但是对于调用者而言,只知道调用命令类的execute方法
 */
class HelloCommand implements CommandInterface
{
    /**
     * @var Receiver
     */
    protected $output;

    /**
     * 每一个具体的命令基于不同的Receiver
     * 它们可以是一个、多个,甚至完全没有Receiver
     *
     * @param Receiver $console
     */
    public function __construct(Receiver $console)
    {
        $this->output = $console;
    }

    /**
     * 执行并输出 "Hello World"
     */
    public function execute()
    {
        // 没有Receiver的时候完全通过命令类来实现功能
        $this->output->write('Hello World');
    }
}

Receiver.php

<?php
namespace DesignPatterns\Behavioral\Command;

/**
 * Receiver类
 */
class Receiver
{
    /**
     * @param string $str
     */
    public function write($str)
    {
        echo $str;
    }
}

Invoker.php

<?php
namespace DesignPatterns\Behavioral\Command;

/**
 * Invoker类
 */
class Invoker
{
    /**
     * @var CommandInterface
     */
    protected $command;

    /**
     * 在调用者中我们通常可以找到这种订阅命令的方法
     *
     * @param CommandInterface $cmd
     */
    public function setCommand(CommandInterface $cmd)
    {
        $this->command = $cmd;
    }

    /**
     * 执行命令
     */
    public function run()
    {
        $this->command->execute();
    }
}
  • 测试代码

Tests/CommandTest.php

<?php
namespace DesignPatterns\Behavioral\Command\Tests;

use DesignPatterns\Behavioral\Command\Invoker;
use DesignPatterns\Behavioral\Command\Receiver;
use DesignPatterns\Behavioral\Command\HelloCommand;

/**
 * CommandTest在命令模式中扮演客户端角色
 */
class CommandTest extends \PHPUnit_Framework_TestCase
{

    /**
     * @var Invoker
     */
    protected $invoker;

    /**
     * @var Receiver
     */
    protected $receiver;

    protected function setUp()
    {
        $this->invoker = new Invoker();
        $this->receiver = new Receiver();
    }

    public function testInvocation()
    {
        $this->invoker->setCommand(new HelloCommand($this->receiver));
        $this->expectOutputString('Hello World');
        $this->invoker->run();
    }
}
  • 总结

命令模式就是将一组对象的相似行为,进行了抽象,将调用者与被调用者之间进行解耦,提高了应用的灵活性。 命令模式将调用的目标对象的一些异构性给封装起来,通过统一的方式来为调用者提供服务。

与类似接口的区别:

  1. 命令模式 —— 将调用者与被调用者之间进行解耦,调用者中注入命令对象,命令对象中注入被调用者并进行相应命令调用。
  2. 中介者模式 —— 用一个中介对象来封装一系列的对象交互,每一个对象中都注入中介,对象只与中介交互。
  3. 门面模式 —— 为一组接口提供一个一致的界面,用户只需要直接与门面角色交互,从而降低了系统的耦合度。

迭代器模式(Iterator)

  • 模式定义

迭代器模式(Iterator),又叫做游标(Cursor)模式。提供一种方法访问一个容器(Container)对象中各个元素,而又不需暴露该对象的内部细节。 当你需要访问一个聚合对象,而且不管这些对象是什么都需要遍历的时候,就应该考虑使用迭代器模式。 另外,当需要对聚集有多种方式遍历时,可以考虑去使用迭代器模式。 迭代器模式为遍历不同的聚集结构提供如开始、下一个、是否结束、当前哪一项等统一的接口。 PHP标准库(SPL)中提供了迭代器接口 Iterator,要实现迭代器模式,实现该接口即可。

  • UML类图

  • 示例代码

Book.php

<?php
namespace DesignPatterns\Behavioral\Iterator;

class Book
{
    private $author;

    private $title;

    public function __construct($title, $author)
    {
        $this->author = $author;
        $this->title = $title;
    }

    public function getAuthor()
    {
        return $this->author;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function getAuthorAndTitle()
    {
        return $this->getTitle() . ' by ' . $this->getAuthor();
    }
}

BookList.php

<?php
namespace DesignPatterns\Behavioral\Iterator;

class BookList implements \Countable
{
    private $books;

    public function getBook($bookNumberToGet)
    {
        if (isset($this->books[$bookNumberToGet])) {
            return $this->books[$bookNumberToGet];
        }

        return null;
    }

    public function addBook(Book $book)
    {
        $this->books[] = $book;
    }

    public function removeBook(Book $bookToRemove)
    {
        foreach ($this->books as $key => $book) {
            /** @var Book $book */
            if ($book->getAuthorAndTitle() === $bookToRemove->getAuthorAndTitle()) {
                unset($this->books[$key]);
            }
        }
    }

    public function count()
    {
        return count($this->books);
    }
}

BookListIterator.php

<?php
namespace DesignPatterns\Behavioral\Iterator;

class BookListIterator implements \Iterator
{
    /**
     * @var BookList
     */
    private $bookList;

    /**
     * @var int
     */
    protected $currentBook = 0;

    public function __construct(BookList $bookList)
    {
        $this->bookList = $bookList;
    }

    /**
     * Return the current book
     * @link http://php.net/manual/en/iterator.current.php
     * @return Book Can return any type.
     */
    public function current()
    {
        return $this->bookList->getBook($this->currentBook);
    }

    /**
     * (PHP 5 >= 5.0.0)
     *
     * Move forward to next element
     * @link http://php.net/manual/en/iterator.next.php
     * @return void Any returned value is ignored.
     */
    public function next()
    {
        $this->currentBook++;
    }

    /**
     * (PHP 5 >= 5.0.0)
     *
     * Return the key of the current element
     * @link http://php.net/manual/en/iterator.key.php
     * @return mixed scalar on success, or null on failure.
     */
    public function key()
    {
        return $this->currentBook;
    }

    /**
     * (PHP 5 >= 5.0.0)
     *
     * Checks if current position is valid
     * @link http://php.net/manual/en/iterator.valid.php
     * @return boolean The return value will be casted to boolean and then evaluated.
     *       Returns true on success or false on failure.
     */
    public function valid()
    {
        return null !== $this->bookList->getBook($this->currentBook);
    }

    /**
     * (PHP 5 >= 5.0.0)
     *
     * Rewind the Iterator to the first element
     * @link http://php.net/manual/en/iterator.rewind.php
     * @return void Any returned value is ignored.
     */
    public function rewind()
    {
        $this->currentBook = 0;
    }
}

BookListReverseIterator.php

<?php
namespace DesignPatterns\Behavioral\Iterator;

class BookListReverseIterator implements \Iterator
{
    /**
     * @var BookList
     */
    private $bookList;

    /**
     * @var int
     */
    protected $currentBook = 0;

    public function __construct(BookList $bookList)
    {
        $this->bookList = $bookList;
        $this->currentBook = $this->bookList->count() - 1;
    }

    /**
     * Return the current book
     * @link http://php.net/manual/en/iterator.current.php
     * @return Book Can return any type.
     */
    public function current()
    {
        return $this->bookList->getBook($this->currentBook);
    }

    /**
     * (PHP 5 >= 5.0.0)
     *
     * Move forward to next element
     * @link http://php.net/manual/en/iterator.next.php
     * @return void Any returned value is ignored.
     */
    public function next()
    {
        $this->currentBook--;
    }

    /**
     * (PHP 5 >= 5.0.0)
     *
     * Return the key of the current element
     * @link http://php.net/manual/en/iterator.key.php
     * @return mixed scalar on success, or null on failure.
     */
    public function key()
    {
        return $this->currentBook;
    }

    /**
     * (PHP 5 >= 5.0.0)
     *
     * Checks if current position is valid
     * @link http://php.net/manual/en/iterator.valid.php
     * @return boolean The return value will be casted to boolean and then evaluated.
     *       Returns true on success or false on failure.
     */
    public function valid()
    {
        return null !== $this->bookList->getBook($this->currentBook);
    }

    /**
     * (PHP 5 >= 5.0.0)
     *
     * Rewind the Iterator to the first element
     * @link http://php.net/manual/en/iterator.rewind.php
     * @return void Any returned value is ignored.
     */
    public function rewind()
    {
        $this->currentBook = $this->bookList->count() - 1;
    }
}
  • 测试代码

Tests/IteratorTest.php

<?php
namespace DesignPatterns\Behavioral\Iterator\Tests;

use DesignPatterns\Behavioral\Iterator\Book;
use DesignPatterns\Behavioral\Iterator\BookList;
use DesignPatterns\Behavioral\Iterator\BookListIterator;
use DesignPatterns\Behavioral\Iterator\BookListReverseIterator;

class IteratorTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @var BookList
     */
    protected $bookList;

    protected function setUp()
    {
        $this->bookList = new BookList();
        $this->bookList->addBook(new Book('Learning PHP Design Patterns', 'William Sanders'));
        $this->bookList->addBook(new Book('Professional Php Design Patterns', 'Aaron Saray'));
        $this->bookList->addBook(new Book('Clean Code', 'Robert C. Martin'));
    }

    public function expectedAuthors()
    {
        return array(
            array(
                array(
                    'Learning PHP Design Patterns by William Sanders',
                    'Professional Php Design Patterns by Aaron Saray',
                    'Clean Code by Robert C. Martin'
                )
            ),
        );
    }

    /**
     * @dataProvider expectedAuthors
     */
    public function testUseAIteratorAndValidateAuthors($expected)
    {
        $iterator = new BookListIterator($this->bookList);

        while ($iterator->valid()) {
            $expectedBook = array_shift($expected);
            $this->assertEquals($expectedBook, $iterator->current()->getAuthorAndTitle());
            $iterator->next();
        }
    }

    /**
     * @dataProvider expectedAuthors
     */
    public function testUseAReverseIteratorAndValidateAuthors($expected)
    {
        $iterator = new BookListReverseIterator($this->bookList);

        while ($iterator->valid()) {
            $expectedBook = array_pop($expected);
            $this->assertEquals($expectedBook, $iterator->current()->getAuthorAndTitle());
            $iterator->next();
        }
    }

    /**
     * Test BookList Remove
     */
    public function testBookRemove()
    {
        $this->bookList->removeBook($this->bookList->getBook(0));
        $this->assertEquals($this->bookList->count(), 2);
    }
}

中介者模式(Mediator)

  • 模式定义

中介者模式(Mediator)就是用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用, 从而使其耦合松散,而且可以独立地改变它们之间的交互。

对于中介对象而言,所有相互交互的对象,都视为同事类,中介对象就是用来维护各个同事对象之间的关系, 所有的同事类都只和中介对象交互,也就是说,中介对象是需要知道所有的同事对象的。当一个同事对象自身发生变化时, 它是不知道会对其他同事对象产生什么影响,它只需要通知中介对象,“我发生变化了”, 中介对象会去和其他同事对象进行交互的。这样一来,同事对象之间的依赖就没有了。 有了中介者之后,所有的交互都封装在了中介对象里面,各个对象只需要关心自己能做什么就行, 不需要再关心做了之后会对其他对象产生什么影响,也就是无需再维护这些关系了。

  • UML类图

  • 示例代码

MediatorInterface.php

<?php
namespace DesignPatterns\Behavioral\Mediator;

/**
 * MediatorInterface是一个中介者契约
 * 该接口不是强制的,但是使用它更加符合里氏替换原则
 */
interface MediatorInterface
{
    /**
     * 发送响应
     *
     * @param string $content
     */
    public function sendResponse($content);

    /**
     * 发起请求
     */
    public function makeRequest();

    /**
     * 查询数据库
     */
    public function queryDb();
}

Mediator.php

<?php
namespace DesignPatterns\Behavioral\Mediator;

use DesignPatterns\Behavioral\Mediator\Subsystem;

/**
 * Mediator是中介者模式的具体实现类
 * In this example, I have made a "Hello World" with the Mediator Pattern.
 */
class Mediator implements MediatorInterface
{

    /**
     * @var Subsystem\Server
     */
    protected $server;

    /**
     * @var Subsystem\Database
     */
    protected $database;

    /**
     * @var Subsystem\Client
     */
    protected $client;

    /**
     * @param Subsystem\Database $db
     * @param Subsystem\Client   $cl
     * @param Subsystem\Server   $srv
     */
    public function setColleague(Subsystem\Database $db, Subsystem\Client $cl, Subsystem\Server $srv)
    {
        $this->database = $db;
        $this->server = $srv;
        $this->client = $cl;
    }

    /**
     * 发起请求
     */
    public function makeRequest()
    {
        $this->server->process();
    }

    /**
     * 查询数据库
     * @return mixed
     */
    public function queryDb()
    {
        return $this->database->getData();
    }

    /**
     * 发送响应
     *
     * @param string $content
     */
    public function sendResponse($content)
    {
        $this->client->output($content);
    }
}

Colleague.php

<?php
namespace DesignPatterns\Behavioral\Mediator;

/**
 * Colleague是一个抽象的同事类,但是它只知道中介者Mediator,而不知道其他同事
 */
abstract class Colleague
{
    /**
     * this ensures no change in subclasses
     *
     * @var MediatorInterface
     */
    private $mediator;
    
    /**
     * @param MediatorInterface $medium
     */
    public function __construct(MediatorInterface $medium)
    {
        $this->mediator = $medium;
    }

    // for subclasses
    protected function getMediator()
    {
        return $this->mediator;
    }
}

Subsystem/Client.php

<?php
namespace DesignPatterns\Behavioral\Mediator\Subsystem;

use DesignPatterns\Behavioral\Mediator\Colleague;

/**
 * Client是发起请求&获取响应的客户端
 */
class Client extends Colleague
{
    /**
     * request
     */
    public function request()
    {
        $this->getMediator()->makeRequest();
    }

    /**
     * output content
     *
     * @param string $content
     */
    public function output($content)
    {
        echo $content;
    }
}

Subsystem/Database.php

<?php
namespace DesignPatterns\Behavioral\Mediator\Subsystem;

use DesignPatterns\Behavioral\Mediator\Colleague;

/**
 * Database提供数据库服务
 */
class Database extends Colleague
{
    /**
     * @return string
     */
    public function getData()
    {
        return "World";
    }
}

Subsystem/Server.php

<?php
namespace DesignPatterns\Behavioral\Mediator\Subsystem;

use DesignPatterns\Behavioral\Mediator\Colleague;

/**
 * Server用于发送响应
 */
class Server extends Colleague
{
    /**
     * process on server
     */
    public function process()
    {
        $data = $this->getMediator()->queryDb();
        $this->getMediator()->sendResponse("Hello $data");
    }
}
  • 测试代码

Tests/MediatorTest.php

<?php
namespace DesignPatterns\Tests\Mediator\Tests;

use DesignPatterns\Behavioral\Mediator\Mediator;
use DesignPatterns\Behavioral\Mediator\Subsystem\Database;
use DesignPatterns\Behavioral\Mediator\Subsystem\Client;
use DesignPatterns\Behavioral\Mediator\Subsystem\Server;

/**
 * MediatorTest tests hello world
 */
class MediatorTest extends \PHPUnit_Framework_TestCase
{

    protected $client;

    protected function setUp()
    {
        $media = new Mediator();
        $this->client = new Client($media);
        $media->setColleague(new Database($media), $this->client, new Server($media));
    }

    public function testOutputHelloWorld()
    {
        // 测试是否输出 Hello World :
        $this->expectOutputString('Hello World');
        // 正如你所看到的, Client, Server 和 Database 是完全解耦的
        $this->client->request();
    }
}
  • 总结

中介者主要是通过中介对象来封装对象之间的关系,使之各个对象在不需要知道其他对象的具体信息情况下通过中介者对象来与之通信。 同时通过引用中介者对象来减少系统对象之间关系,提高了对象的可复用和系统的可扩展性。 但是就是因为中介者对象封装了对象之间的关联关系,导致中介者对象变得比较庞大,所承担的责任也比较多。 它需要知道每个对象和他们之间的交互细节,如果它出问题,将会导致整个系统都会出问题。

与类似接口的区别:

  1. 中介者模式 —— 用一个中介对象来封装一系列的对象交互,每一个对象中都注入中介,对象只与中介交互。
  2. 命令模式 —— 将调用者与被调用者之间进行解耦,调用者中注入命令对象,命令对象中注入被调用者并进行相应命令调用。
  3. 门面模式 —— 为一组接口提供一个一致的界面,用户只需要直接与门面角色交互,从而降低了系统的耦合度。

备忘录模式(Memento)

  • 模式定义

备忘录模式又叫做快照模式(Snapshot)或 Token 模式,备忘录模式的用意是在不破坏封装性的前提下, 捕获一个对象的内部状态,并在该对象之外保存这个状态,这样就可以在合适的时候将该对象恢复到原先保存的状态。 我们在编程的时候,经常需要保存对象的中间状态,当需要的时候,可以恢复到这个状态。 比如,我们使用Eclipse进行编程时,假如编写失误(例如不小心误删除了几行代码),我们希望返回删除前的状态, 便可以使用Ctrl+Z来进行返回。这时我们便可以使用备忘录模式来实现。

  • UML类图

备忘录模式所涉及的角色有三个:备忘录(Memento)角色、发起人(Originator)角色、负责人(Caretaker)角色。 这三个角色的职责分别是:

  1. 发起人:记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据。
  2. 备忘录:负责存储发起人对象的内部状态,在需要的时候提供发起人需要的内部状态。
  3. 管理角色:对备忘录进行管理,保存和提供备忘录。

  • 示例代码

Memento.php

<?php
namespace DesignPatterns\Behavioral\Memento;

class Memento
{
    /* @var mixed */
    private $state;

    /**
     * @param mixed $stateToSave
     */
    public function __construct($stateToSave)
    {
        $this->state = $stateToSave;
    }

    /**
     * @return mixed
     */
    public function getState()
    {
        return $this->state;
    }
}

Originator.php

<?php
namespace DesignPatterns\Behavioral\Memento;

class Originator
{
    /* @var mixed */
    private $state;

    // 这个类还可以包含不属于备忘录状态的额外数据

    /**
     * @param mixed $state
     */
    public function setState($state)
    {
        // 必须检查该类子类内部的状态类型或者使用依赖注入
        $this->state = $state;
    }

    /**
     * @return Memento
     */
    public function getStateAsMemento()
    {
        // 在Memento中必须保存一份隔离的备份
        $state = is_object($this->state) ? clone $this->state : $this->state;

        return new Memento($state);
    }

    public function restoreFromMemento(Memento $memento)
    {
        $this->state = $memento->getState();
    }
}

Caretaker.php

<?php
namespace DesignPatterns\Behavioral\Memento;

class Caretaker
{
    protected $history = array();

    /**
     * @return Memento
     */
    public function getFromHistory($id)
    {
        return $this->history[$id];
    }

    /**
     * @param Memento $state
     */
    public function saveToHistory(Memento $state)
    {
        $this->history[] = $state;
    }

    public function runCustomLogic()
    {
        $originator = new Originator();

        //设置状态为State1
        $originator->setState("State1");
        //设置状态为State2
        $originator->setState("State2");
        //将State2保存到Memento
        $this->saveToHistory($originator->getStateAsMemento());
        //设置状态为State3
        $originator->setState("State3");

        //我们可以请求多个备忘录, 然后选择其中一个进行回滚
        
        //保存State3到Memento
        $this->saveToHistory($originator->getStateAsMemento());
        //设置状态为State4
        $originator->setState("State4");

        $originator->restoreFromMemento($this->getFromHistory(1));
        //从备忘录恢复后的状态: State3

        return $originator->getStateAsMemento()->getState();
    }
}
  • 测试代码

Tests/MementoTest.php

<?php
namespace DesignPatterns\Behavioral\Memento\Tests;

use DesignPatterns\Behavioral\Memento\Caretaker;
use DesignPatterns\Behavioral\Memento\Memento;
use DesignPatterns\Behavioral\Memento\Originator;

/**
 * MementoTest用于测试备忘录模式
 */
class MementoTest extends \PHPUnit_Framework_TestCase
{

    public function testUsageExample()
    {
        $originator = new Originator();
        $caretaker = new Caretaker();

        $character = new \stdClass();
        // new object
        $character->name = "Gandalf";
        // connect Originator to character object
        $originator->setState($character);

        // work on the object
        $character->name = "Gandalf the Grey";
        // still change something
        $character->race = "Maia";
        // time to save state
        $snapshot = $originator->getStateAsMemento();
        // put state to log
        $caretaker->saveToHistory($snapshot);

        // change something
        $character->name = "Sauron";
        // and again
        $character->race = "Ainur";
        // state inside the Originator was equally changed
        $this->assertAttributeEquals($character, "state", $originator);

        // time to save another state
        $snapshot = $originator->getStateAsMemento();
        // put state to log
        $caretaker->saveToHistory($snapshot);

        $rollback = $caretaker->getFromHistory(0);
        // return to first state
        $originator->restoreFromMemento($rollback);
        // use character from old state
        $character = $rollback->getState();

        // yes, that what we need
        $this->assertEquals("Gandalf the Grey", $character->name);
        // make new changes
        $character->name = "Gandalf the White";

        // and Originator linked to actual object again
        $this->assertAttributeEquals($character, "state", $originator);
    }

    public function testStringState()
    {
        $originator = new Originator();
        $originator->setState("State1");

        $this->assertAttributeEquals("State1", "state", $originator);

        $originator->setState("State2");
        $this->assertAttributeEquals("State2", "state", $originator);

        $snapshot = $originator->getStateAsMemento();
        $this->assertAttributeEquals("State2", "state", $snapshot);

        $originator->setState("State3");
        $this->assertAttributeEquals("State3", "state", $originator);

        $originator->restoreFromMemento($snapshot);
        $this->assertAttributeEquals("State2", "state", $originator);
    }

    public function testSnapshotIsClone()
    {
        $originator = new Originator();
        $object = new \stdClass();

        $originator->setState($object);
        $snapshot = $originator->getStateAsMemento();
        $object->new_property = 1;

        $this->assertAttributeEquals($object, "state", $originator);
        $this->assertAttributeNotEquals($object, "state", $snapshot);

        $originator->restoreFromMemento($snapshot);
        $this->assertAttributeNotEquals($object, "state", $originator);
    }

    public function testCanChangeActualState()
    {
        $originator = new Originator();
        $first_state = new \stdClass();

        $originator->setState($first_state);
        $snapshot = $originator->getStateAsMemento();
        $second_state = $snapshot->getState();

        // still actual
        $first_state->first_property = 1;
        // just history
        $second_state->second_property = 2;
        $this->assertAttributeEquals($first_state, "state", $originator);
        $this->assertAttributeNotEquals($second_state, "state", $originator);

        $originator->restoreFromMemento($snapshot);
        // now it lost state
        $first_state->first_property = 11;
        // must be actual
        $second_state->second_property = 22;
        $this->assertAttributeEquals($second_state, "state", $originator);
        $this->assertAttributeNotEquals($first_state, "state", $originator);
    }

    public function testStateWithDifferentObjects()
    {
        $originator = new Originator();

        $first = new \stdClass();
        $first->data = "foo";

        $originator->setState($first);
        $this->assertAttributeEquals($first, "state", $originator);

        $first_snapshot = $originator->getStateAsMemento();
        $this->assertAttributeEquals($first, "state", $first_snapshot);

        $second       = new \stdClass();
        $second->data = "bar";
        $originator->setState($second);
        $this->assertAttributeEquals($second, "state", $originator);

        $originator->restoreFromMemento($first_snapshot);
        $this->assertAttributeEquals($first, "state", $originator);
    }

    public function testCaretaker()
    {
        $caretaker = new Caretaker();
        $memento1 = new Memento("foo");
        $memento2 = new Memento("bar");
        $caretaker->saveToHistory($memento1);
        $caretaker->saveToHistory($memento2);
        $this->assertAttributeEquals(array($memento1, $memento2), "history", $caretaker);
        $this->assertEquals($memento1, $caretaker->getFromHistory(0));
        $this->assertEquals($memento2, $caretaker->getFromHistory(1));

    }

    public function testCaretakerCustomLogic()
    {
        $caretaker = new Caretaker();
        $result = $caretaker->runCustomLogic();
        $this->assertEquals("State3", $result);
    }
}
  • 总结

如果有需要提供回滚操作的需求,使用备忘录模式非常适合,比如数据库的事务操作,文本编辑器的 Ctrl+Z 恢复等。

空对象模式(Null Object)

  • 模式定义

空对象模式并不是 GoF 那本《设计模式》中提到的 23 种经典设计模式之一,但却是一个经常出现以致我们不能忽略的模式。 该模式有以下优点:

  1. 简化客户端代码
  2. 减少空指针异常风险
  3. 更少的条件控制语句以减少测试用例

在空对象模式中,以前返回对象或 null 的方法现在返回对象或空对象 NullObject,这样会减少代码中的条件判断, 比如之前调用返回对象方法要这么写:

if (!is_null($obj)) { 
    $obj->callSomething(); 
}

现在因为即使对象为空也会返回空对象,所以可以直接这样调用返回对象上的方法:

$obj->callSomething();

从而消除客户端的检查代码。 当然,你可能已经意识到了,要实现这种调用的前提是返回对象和空对象需要实现同一个接口,具备一致的代码结构。

  • UML类图

  • 示例代码

Service.php

<?php
namespace DesignPatterns\Behavioral\NullObject;

/**
 * Service 是使用 logger 的模拟服务
 */
class Service
{
    /**
     * @var LoggerInterface
     */
    protected $logger;

    /**
     * 我们在构造函数中注入logger
     *
     * @param LoggerInterface $log
     */
    public function __construct(LoggerInterface $log)
    {
        $this->logger = $log;
    }

    /**
     * do something ...
     */
    public function doSomething()
    {
        // 在空对象模式中不再需要这样判断 "if (!is_null($this->logger))..."
        $this->logger->log('We are in ' . __METHOD__);
        // something to do...
    }
}

LoggerInterface.php

<?php
namespace DesignPatterns\Behavioral\NullObject;

/**
 * LoggerInterface 是 logger 接口
 *
 * 核心特性: NullLogger必须和其它Logger一样实现这个接口
 */
interface LoggerInterface
{
    /**
     * @param string $str
     *
     * @return mixed
     */
    public function log($str);
}

PrintLogger.php

<?php
namespace DesignPatterns\Behavioral\NullObject;

/**
 * PrintLogger是用于打印Logger实体到标准输出的Logger
 */
class PrintLogger implements LoggerInterface
{
    /**
     * @param string $str
     */
    public function log($str)
    {
        echo $str;
    }
}

NullLogger.php

<?php
namespace DesignPatterns\Behavioral\NullObject;

/**
 * 核心特性 : 必须实现LoggerInterface接口
 */
class NullLogger implements LoggerInterface
{
    /**
     * {@inheritdoc}
     */
    public function log($str)
    {
        // do nothing
    }
}
  • 测试代码

Tests/LoggerTest.php

<?php
namespace DesignPatterns\Behavioral\NullObject\Tests;

use DesignPatterns\Behavioral\NullObject\NullLogger;
use DesignPatterns\Behavioral\NullObject\Service;
use DesignPatterns\Behavioral\NullObject\PrintLogger;

/**
 * LoggerTest 用于测试不同的Logger
 */
class LoggerTest extends \PHPUnit_Framework_TestCase
{

    public function testNullObject()
    {
        $service = new Service(new NullLogger());
        $this->expectOutputString(null);  // 没有输出
        $service->doSomething();
    }

    public function testStandardLogger()
    {
        $service = new Service(new PrintLogger());
        $this->expectOutputString('We are in DesignPatterns\Behavioral\NullObject\Service::doSomething');
        $service->doSomething();
    }
}

观察者模式(Observer)

  • 模式定义

观察者模式有时也被称作发布/订阅模式,该模式用于为对象实现发布/订阅功能: 一旦主体对象状态发生改变,与之关联的观察者对象会收到通知,并进行相应操作。 将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。 我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。观察者就是解决这类的耦合关系的。 消息队列系统、事件都使用了观察者模式。 PHP 为观察者模式定义了两个接口:SplSubject 和 SplObserver。 SplSubject 可以看做主体对象的抽象,SplObserver 可以看做观察者对象的抽象, 要实现观察者模式,只需让主体对象实现 SplSubject ,观察者对象实现 SplObserver,并实现相应方法即可。

  • UML类图

  • 示例代码

User.php

<?php
namespace DesignPatterns\Behavioral\Observer;

/**
 * 观察者模式 : 被观察对象 (主体对象)
 *
 * 主体对象维护观察者列表并发送通知
 *
 */
class User implements \SplSubject
{
    /**
     * user data
     *
     * @var array
     */
    protected $data = array();

    /**
     * observers
     *
     * @var \SplObjectStorage
     */
    protected $observers;
    
    public function __construct()
    {
        $this->observers = new \SplObjectStorage();
    }

    /**
     * 附加观察者
     *
     * @param \SplObserver $observer
     *
     * @return void
     */
    public function attach(\SplObserver $observer)
    {
        $this->observers->attach($observer);
    }

    /**
     * 取消观察者
     *
     * @param \SplObserver $observer
     *
     * @return void
     */
    public function detach(\SplObserver $observer)
    {
        $this->observers->detach($observer);
    }

    /**
     * 通知观察者方法
     *
     * @return void
     */
    public function notify()
    {
        /** @var \SplObserver $observer */
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    /**
     *
     * @param string $name
     * @param mixed  $value
     *
     * @return void
     */
    public function __set($name, $value)
    {
        $this->data[$name] = $value;

        // 通知观察者用户被改变
        $this->notify();
    }
}

UserObserver.php

<?php
namespace DesignPatterns\Behavioral\Observer;

/**
 * UserObserver 类(观察者对象)
 */
class UserObserver implements \SplObserver
{
    /**
     * 观察者要实现的唯一方法
     * 也是被 Subject 调用的方法
     *
     * @param \SplSubject $subject
     */
    public function update(\SplSubject $subject)
    {
        echo get_class($subject) . ' has been updated';
    }
}
  • 测试代码

Tests/ObserverTest.php

<?php

namespace DesignPatterns\Behavioral\Observer\Tests;

use DesignPatterns\Behavioral\Observer\UserObserver;
use DesignPatterns\Behavioral\Observer\User;

/**
 * ObserverTest 测试观察者模式
 */
class ObserverTest extends \PHPUnit_Framework_TestCase
{
    protected $observer;

    protected function setUp()
    {
        $this->observer = new UserObserver();
    }

    /**
     * 测试通知
     */
    public function testNotify()
    {
        $this->expectOutputString('DesignPatterns\Behavioral\Observer\User has been updated');
        $subject = new User();

        $subject->attach($this->observer);
        $subject->property = 123;
    }

    /**
     * 测试订阅
     */
    public function testAttachDetach()
    {
        $subject = new User();
        $reflection = new \ReflectionProperty($subject, 'observers');

        $reflection->setAccessible(true);
        /** @var \SplObjectStorage $observers */
        $observers = $reflection->getValue($subject);

        $this->assertInstanceOf('SplObjectStorage', $observers);
        $this->assertFalse($observers->contains($this->observer));

        $subject->attach($this->observer);
        $this->assertTrue($observers->contains($this->observer));

        $subject->detach($this->observer);
        $this->assertFalse($observers->contains($this->observer));
    }

    /**
     * 测试 update() 调用
     */
    public function testUpdateCalling()
    {
        $subject = new User();
        $observer = $this->getMock('SplObserver');
        $subject->attach($observer);

        $observer->expects($this->once())
            ->method('update')
            ->with($subject);

        $subject->notify();
    }
}
  • 总结

观察者模式解除了主体和具体观察者的耦合,让耦合的双方都依赖于抽象,而不是依赖具体。从而使得各自的变化都不会影响另一边的变化。

  • 拓展

Spl标准类

/**
 * The <b>SplObserver</b> interface is used alongside
 * <b>SplSubject</b> to implement the Observer Design Pattern.
 * @link https://php.net/manual/en/class.splobserver.php
 */
interface 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
         */
        public function update (SplSubject $subject);

}

/**
 * The <b>SplSubject</b> interface is used alongside
 * <b>SplObserver</b> to implement the Observer Design Pattern.
 * @link https://php.net/manual/en/class.splsubject.php
 */
interface SplSubject  {

        /**
         * 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
         */
        public function attach (SplObserver $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
         */
        public function detach (SplObserver $observer);

        /**
         * Notify an observer
         * @link https://php.net/manual/en/splsubject.notify.php
         * @return void
         */
        public function notify ();

}

/**
 * The SplObjectStorage class provides a map from objects to data or, by
 * ignoring data, an object set. This dual purpose can be useful in many
 * cases involving the need to uniquely identify objects.
 * @link https://php.net/manual/en/class.splobjectstorage.php
 */
class SplObjectStorage implements Countable, Iterator, Serializable, ArrayAccess {

        /**
         * Adds an object in the storage
         * @link https://php.net/manual/en/splobjectstorage.attach.php
         * @param object $object <p>
         * The object to add.
         * </p>
         * @param mixed $info [optional] <p>
         * The data to associate with the object.
         * </p>
         * @return void
         */
        public function attach ($object, $info = null) {}

        /**
     * Removes an object from the storage
         * @link https://php.net/manual/en/splobjectstorage.detach.php
         * @param object $object <p>
         * The object to remove.
         * </p>
         * @return void
         */
        public function detach ($object) {}

        /**
         * Checks if the storage contains a specific object
         * @link https://php.net/manual/en/splobjectstorage.contains.php
         * @param object $object <p>
         * The object to look for.
         * </p>
     * @return bool true if the object is in the storage, false otherwise.
         */
        public function contains ($object) {}

        /**
         * Adds all objects from another storage
         * @link https://php.net/manual/en/splobjectstorage.addall.php
         * @param SplObjectStorage $storage <p>
         * The storage you want to import.
         * </p>
         * @return void
         */
    public function addAll ($storage) {}

        /**
         * Removes objects contained in another storage from the current storage
         * @link https://php.net/manual/en/splobjectstorage.removeall.php
         * @param SplObjectStorage $storage <p>
         * The storage containing the elements to remove.
         * </p>
         * @return void
         */
    public function removeAll ($storage) {}

        /**
     * Removes all objects except for those contained in another storage from the current storage
     * @link https://php.net/manual/en/splobjectstorage.removeallexcept.php
     * @param SplObjectStorage $storage <p>
     * The storage containing the elements to retain in the current storage.
     * </p>
     * @return void
     * @since 5.3.6
     */
    public function removeAllExcept ($storage) {}

    /**
         * Returns the data associated with the current iterator entry
         * @link https://php.net/manual/en/splobjectstorage.getinfo.php
         * @return mixed The data associated with the current iterator position.
         */
        public function getInfo () {}

        /**
         * Sets the data associated with the current iterator entry
         * @link https://php.net/manual/en/splobjectstorage.setinfo.php
         * @param mixed $info <p>
         * The data to associate with the current iterator entry.
         * </p>
         * @return void
         */
        public function setInfo ($info) {}

        /**
         * Returns the number of objects in the storage
         * @link https://php.net/manual/en/splobjectstorage.count.php
         * @param int $mode [optional]
         * @return int The number of objects in the storage.
         */
        public function count ($mode = COUNT_NORMAL) {}

        /**
         * Rewind the iterator to the first storage element
         * @link https://php.net/manual/en/splobjectstorage.rewind.php
         * @return void
         */
        public function rewind () {}

        /**
         * Returns if the current iterator entry is valid
         * @link https://php.net/manual/en/splobjectstorage.valid.php
     * @return bool true if the iterator entry is valid, false otherwise.
         */
        public function valid () {}

        /**
         * Returns the index at which the iterator currently is
         * @link https://php.net/manual/en/splobjectstorage.key.php
         * @return int The index corresponding to the position of the iterator.
         */
        public function key () {}

        /**
         * Returns the current storage entry
         * @link https://php.net/manual/en/splobjectstorage.current.php
         * @return object The object at the current iterator position.
         */
        public function current () {}

        /**
         * Move to the next entry
         * @link https://php.net/manual/en/splobjectstorage.next.php
         * @return void
         */
        public function next () {}

        /**
         * Unserializes a storage from its string representation
         * @link https://php.net/manual/en/splobjectstorage.unserialize.php
         * @param string $data <p>
         * The serialized representation of a storage.
         * </p>
         * @return void
         * @since 5.2.2
         */
        public function unserialize ($data) {}

        /**
         * Serializes the storage
         * @link https://php.net/manual/en/splobjectstorage.serialize.php
         * @return string A string representing the storage.
         * @since 5.2.2
         */
        public function serialize () {}

        /**
         * Checks whether an object exists in the storage
         * @link https://php.net/manual/en/splobjectstorage.offsetexists.php
         * @param object $object <p>
         * The object to look for.
         * </p>
     * @return bool true if the object exists in the storage,
         * and false otherwise.
         */
        public function offsetExists ($object) {}

        /**
         * Associates data to an object in the storage
         * @link https://php.net/manual/en/splobjectstorage.offsetset.php
         * @param object $object <p>
         * The object to associate data with.
         * </p>
     * @param mixed $info [optional] <p>
         * The data to associate with the object.
         * </p>
         * @return void
         */
    public function offsetSet ($object, $info = null) {}

        /**
         * Removes an object from the storage
         * @link https://php.net/manual/en/splobjectstorage.offsetunset.php
         * @param object $object <p>
         * The object to remove.
         * </p>
         * @return void
         */
        public function offsetUnset ($object) {}

        /**
         * Returns the data associated with an <type>object</type>
         * @link https://php.net/manual/en/splobjectstorage.offsetget.php
         * @param object $object <p>
         * The object to look for.
         * </p>
         * @return mixed The data previously associated with the object in the storage.
         */
        public function offsetGet ($object) {}

        /**
         * Calculate a unique identifier for the contained objects
         * @link https://php.net/manual/en/splobjectstorage.gethash.php
         * @param object $object  <p>
         * object whose identifier is to be calculated.
         * @return string A string with the calculated identifier.
         * @since 5.4
        */
        public function getHash($object) {}

        /**
         * @return array
         * @since 7.4
         */
        public function __serialize(): array {}

        /**
         * @param array $data
         * @since 7.4
         */
        public function __unserialize(array $data): void {}

        /**
         * @return array
         * @since 7.4
         */
        public function __debugInfo(){}

}

关于 观察者模式 与 发布/订阅模式 。 1.观察者模式是松耦合,而发布订阅模式是完全解耦的。 2.观察者模式里,只有两个角色:观察者、被观察者,而发布的订阅模式却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的:经纪人Broker。

在《大话设计模式》一书,把 subject 看作发布者,observer 看作订阅者,反过来,可以把发布订阅模式看作松耦合的观察者模式实现; 在 GoF 的《设计模式》一书中,对观察者模式的定义只是一个基本概念,没有对这些细节做定义, 在 23 种设计模式中没有发布订阅模式,发布/订阅模式其实是从消息系统中作为架构模式迁移而来, 在 Windows 系统中也将这两个模式视作同义词,可以参考维基百科 中的介绍, 从宽松的角度来说,把它们看作一个模式没什么问题,发布/订阅模式只是观察者模式的一种松耦合实现, 从严格角度来说,也可以把它们看作不同的模式,比如 《JavaScript 设计模式》一书中就做了这样的区分 , 看具体的理解角度,比如微服务和服务化,有些人认为它们就是一个东西,有些人则要严格区分它们。

规格模式(Specification)

  • 模式定义

规格模式(Specification)可以认为是组合模式的一种扩展。有时项目中某些条件决定了业务逻辑, 这些条件就可以抽离出来以某种关系(与、或、非)进行组合,从而灵活地对业务逻辑进行定制。另外,在查询、过滤等应用场合中, 通过预定义多个条件,然后使用这些条件的组合来处理查询或过滤,而不是使用逻辑判断语句来处理,可以简化整个实现逻辑。 这里的每个条件就是一个规格,多个规格/条件通过串联的方式以某种逻辑关系形成一个组合式的规格。

  • UML类图

  • 示例代码

Item.php

<?php
namespace DesignPatterns\Behavioral\Specification;

class Item
{
    protected $price;

    /**
     * An item must have a price
     *
     * @param int $price
     */
    public function __construct($price)
    {
        $this->price = $price;
    }

    /**
     * Get the items price
     *
     * @return int
     */
    public function getPrice()
    {
        return $this->price;
    }
}

SpecificationInterface.php

<?php
namespace DesignPatterns\Behavioral\Specification;

/**
 * 规格接口
 */
interface SpecificationInterface
{
    /**
     * 判断对象是否满足规格
     *
     * @param Item $item
     *
     * @return bool
     */
    public function isSatisfiedBy(Item $item);

    /**
     * 创建一个逻辑与规格(AND)
     *
     * @param SpecificationInterface $spec
     */
    public function plus(SpecificationInterface $spec);

    /**
     * 创建一个逻辑或规格(OR)
     *
     * @param SpecificationInterface $spec
     */
    public function either(SpecificationInterface $spec);

    /**
     * 创建一个逻辑非规格(NOT)
     */
    public function not();
}

AbstractSpecification.php

<?php
namespace DesignPatterns\Behavioral\Specification;

/**
 * 规格抽象类
 */
abstract class AbstractSpecification implements SpecificationInterface
{
    /**
     * 检查给定Item是否满足所有规则
     *
     * @param Item $item
     *
     * @return bool
     */
    abstract public function isSatisfiedBy(Item $item);

    /**
     * 创建一个新的逻辑与规格(AND)
     *
     * @param SpecificationInterface $spec
     *
     * @return SpecificationInterface
     */
    public function plus(SpecificationInterface $spec)
    {
        return new Plus($this, $spec);
    }

    /**
     * 创建一个新的逻辑或组合规格(OR)
     *
     * @param SpecificationInterface $spec
     *
     * @return SpecificationInterface
     */
    public function either(SpecificationInterface $spec)
    {
        return new Either($this, $spec);
    }

    /**
     * 创建一个新的逻辑非规格(NOT)
     *
     * @return SpecificationInterface
     */
    public function not()
    {
        return new Not($this);
    }
}

Plus.php

<?php
namespace DesignPatterns\Behavioral\Specification;

/**
 * 逻辑与规格(AND)
 */
class Plus extends AbstractSpecification
{
    protected $left;
    protected $right;

    /**
     * 在构造函数中传入两种规格
     *
     * @param SpecificationInterface $left
     * @param SpecificationInterface $right
     */
    public function __construct(SpecificationInterface $left, SpecificationInterface $right)
    {
        $this->left = $left;
        $this->right = $right;
    }

    /**
     * 返回两种规格的逻辑与评估
     *
     * @param Item $item
     *
     * @return bool
     */
    public function isSatisfiedBy(Item $item)
    {
        return $this->left->isSatisfiedBy($item) && $this->right->isSatisfiedBy($item);
    }
}

Either.php

<?php
namespace DesignPatterns\Behavioral\Specification;

/**
 * 逻辑或规格
 */
class Either extends AbstractSpecification
{
    protected $left;
    protected $right;

    /**
     * 两种规格的组合
     *
     * @param SpecificationInterface $left
     * @param SpecificationInterface $right
     */
    public function __construct(SpecificationInterface $left, SpecificationInterface $right)
    {
        $this->left = $left;
        $this->right = $right;
    }

    /**
     * 返回两种规格的逻辑或评估
     *
     * @param Item $item
     *
     * @return bool
     */
    public function isSatisfiedBy(Item $item)
    {
        return $this->left->isSatisfiedBy($item) || $this->right->isSatisfiedBy($item);
    }
}

Not.php

<?php
namespace DesignPatterns\Behavioral\Specification;

/**
 * 逻辑非规格
 */
class Not extends AbstractSpecification
{
    protected $spec;

    /**
     * 在构造函数中传入指定规格
     *
     * @param SpecificationInterface $spec
     */
    public function __construct(SpecificationInterface $spec)
    {
        $this->spec = $spec;
    }

    /**
     * 返回规格的相反结果
     *
     * @param Item $item
     *
     * @return bool
     */
    public function isSatisfiedBy(Item $item)
    {
        return !$this->spec->isSatisfiedBy($item);
    }
}

PriceSpecification.php

<?php
namespace DesignPatterns\Behavioral\Specification;

/**
 * 判断给定Item的价格是否介于最小值和最大值之间的规格
 */
class PriceSpecification extends AbstractSpecification
{
    protected $maxPrice;
    protected $minPrice;

    /**
     * 设置最大值
     *
     * @param int $maxPrice
     */
    public function setMaxPrice($maxPrice)
    {
        $this->maxPrice = $maxPrice;
    }

    /**
     * 设置最小值
     *
     * @param int $minPrice
     */
    public function setMinPrice($minPrice)
    {
        $this->minPrice = $minPrice;
    }

    /**
     * 判断给定Item的定价是否在最小值和最大值之间
     *
     * @param Item $item
     *
     * @return bool
     */
    public function isSatisfiedBy(Item $item)
    {
        if (!empty($this->maxPrice) && $item->getPrice() > $this->maxPrice) {
            return false;
        }
        if (!empty($this->minPrice) && $item->getPrice() < $this->minPrice) {
            return false;
        }

        return true;
    }
}
  • 测试代码

Tests/SpecificationTest.php

<?php
namespace DesignPatterns\Behavioral\Specification\Tests;

use DesignPatterns\Behavioral\Specification\PriceSpecification;
use DesignPatterns\Behavioral\Specification\Item;

/**
 * SpecificationTest 用于测试规格模式
 */
class SpecificationTest extends \PHPUnit_Framework_TestCase
{
    public function testSimpleSpecification()
    {
        $item = new Item(100);
        $spec = new PriceSpecification();

        $this->assertTrue($spec->isSatisfiedBy($item));

        $spec->setMaxPrice(50);
        $this->assertFalse($spec->isSatisfiedBy($item));

        $spec->setMaxPrice(150);
        $this->assertTrue($spec->isSatisfiedBy($item));

        $spec->setMinPrice(101);
        $this->assertFalse($spec->isSatisfiedBy($item));

        $spec->setMinPrice(100);
        $this->assertTrue($spec->isSatisfiedBy($item));
    }

    public function testNotSpecification()
    {
        $item = new Item(100);
        $spec = new PriceSpecification();
        $not = $spec->not();

        $this->assertFalse($not->isSatisfiedBy($item));

        $spec->setMaxPrice(50);
        $this->assertTrue($not->isSatisfiedBy($item));

        $spec->setMaxPrice(150);
        $this->assertFalse($not->isSatisfiedBy($item));

        $spec->setMinPrice(101);
        $this->assertTrue($not->isSatisfiedBy($item));

        $spec->setMinPrice(100);
        $this->assertFalse($not->isSatisfiedBy($item));
    }

    public function testPlusSpecification()
    {
        $spec1 = new PriceSpecification();
        $spec2 = new PriceSpecification();
        $plus = $spec1->plus($spec2);

        $item = new Item(100);

        $this->assertTrue($plus->isSatisfiedBy($item));

        $spec1->setMaxPrice(150);
        $spec2->setMinPrice(50);
        $this->assertTrue($plus->isSatisfiedBy($item));

        $spec1->setMaxPrice(150);
        $spec2->setMinPrice(101);
        $this->assertFalse($plus->isSatisfiedBy($item));

        $spec1->setMaxPrice(99);
        $spec2->setMinPrice(50);
        $this->assertFalse($plus->isSatisfiedBy($item));
    }

    public function testEitherSpecification()
    {
        $spec1 = new PriceSpecification();
        $spec2 = new PriceSpecification();
        $either = $spec1->either($spec2);

        $item = new Item(100);

        $this->assertTrue($either->isSatisfiedBy($item));

        $spec1->setMaxPrice(150);
        $spec2->setMaxPrice(150);
        $this->assertTrue($either->isSatisfiedBy($item));

        $spec1->setMaxPrice(150);
        $spec2->setMaxPrice(0);
        $this->assertTrue($either->isSatisfiedBy($item));

        $spec1->setMaxPrice(0);
        $spec2->setMaxPrice(150);
        $this->assertTrue($either->isSatisfiedBy($item));

        $spec1->setMaxPrice(99);
        $spec2->setMaxPrice(99);
        $this->assertFalse($either->isSatisfiedBy($item));
    }
}

状态模式(State)

  • 模式定义

状态模式(State)又称状态对象模式,主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。 状态模式允许一个对象在其内部状态改变的时候改变其行为,把状态的判断逻辑转移到表示不同的一系列类当中, 从而把复杂的逻辑判断简单化。 用一句话来表述,状态模式把所研究的对象的行为包装在不同的状态对象里, 每一个状态对象都属于一个抽象状态类的一个子类。状态模式的意图是让一个对象在其内部状态改变的时候,其行为也随之改变。

  • UML类图

  • 示例代码

OrderController.php

<?php
namespace DesignPatterns\Behavioral\State;

/**
 * OrderController类
 */
class OrderController
{
    /**
     * @param int $id
     */
    public function shipAction($id)
    {
        $order = OrderFactory::getOrder($id);
        try {
            $order->shipOrder();
        } catch (Exception $e) {
            //处理错误!
        }
        // 发送响应到浏览器
    }

    /**
     * @param int $id
     */
    public function completeAction($id)
    {
        $order = OrderFactory::getOrder($id);
        try {
            $order->completeOrder();
        } catch (Exception $e) {
            //处理错误!
        }
        // 发送响应到浏览器
    }
}

OrderFactory.php

<?php
namespace DesignPatterns\Behavioral\State;

/**
 * OrderFactory类
 */
class OrderFactory
{
    private function __construct()
    {
        throw new \Exception('Can not instance the OrderFactory class!');
    }

    /**
     * @param int $id
     *
     * @return CreateOrder|ShippingOrder
     * @throws \Exception
     */
    public static function getOrder($id)
    {
        //从数据库获取订单伪代码
        $order = 'Get Order From Database';

        switch ($order['status']) {
            case 'created':
                return new CreateOrder($order);
            case 'shipping':
                return new ShippingOrder($order);
            default:
                throw new \Exception('Order status error!');
                break;
        }
    }
}

OrderInterface.php

<?php
namespace DesignPatterns\Behavioral\State;

/**
 * OrderInterface接口
 */
interface OrderInterface
{
    /**
     * @return mixed
     */
    public function shipOrder();

    /**
     * @return mixed
     */
    public function completeOrder();
}

ShippingOrder.php

<?php
namespace DesignPatterns\Behavioral\State;

/**
 * ShippingOrder类
 */
class ShippingOrder implements OrderInterface
{
    /**
     * @var array
     */
    private $order;

    /**
     * @param array $order
     *
     * @throws \Exception
     */
    public function __construct(array $order)
    {
        if (empty($order)) {
            throw new \Exception('Order can not be empty!');
        }
        $this->order = $order;
    }

    /**
     * @return mixed|void
     * @throws \Exception
     */
    public function shipOrder()
    {
        //当订单发货过程中不能对该订单进行发货处理
        throw new \Exception('Can not ship the order which status is shipping!');
    }

    /**
     * @return mixed
     */
    public function completeOrder()
    {
        $this->order['status'] = 'completed';
        $this->order['updatedTime'] = time();

        // 将订单状态保存到数据库
        return $this->updateOrder($this->order);
    }
}

CreateOrder.php

<?php
namespace DesignPatterns\Behavioral\State;

/**
 * CreateOrder类
 */
class CreateOrder implements OrderInterface
{
    /**
     * @var array
     */
    private $order;

    /**
     * @param array $order
     *
     * @throws \Exception
     */
    public function __construct(array $order)
    {
        if (empty($order)) {
            throw new \Exception('Order can not be empty!');
        }
        $this->order = $order;
    }

    /**
     * @return mixed
     */
    public function shipOrder()
    {
        $this->order['status'] = 'shipping';
        $this->order['updatedTime'] = time();

        // 将订单状态保存到数据库
        return $this->updateOrder($this->order);
    }

    /**
     * @return mixed|void
     * @throws \Exception
     */
    public function completeOrder()
    {
        // 还未发货的订单不能设置为完成状态
        throw new \Exception('Can not complete the order which status is created!');
    }
}
  • 测试代码

  • 总结

在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到。 然后使用if… ellse语句来做状态判断来进行不同情况的处理。但是对复杂状态的判断就显得“力不从心了”。 随着增加新的状态或者修改一个状态(if else(或switch case)语句的增多或者修改)可能会引起很大的修改, 而程序的可读性,扩展性也会变得很弱。维护也会很麻烦。那么就要考虑使用状态模式。 状态模式的主要优点在于封装了转换规则,并枚举可能的状态,它将所有与某个状态有关的行为放到一个类中, 并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象, 从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂, 如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求。

策略模式(Strategy)

  • 模式定义

在软件开发中也常常遇到类似的情况,实现某一个功能有多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能。 如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中, 如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法; 当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…或者case等条件判断语句来进行选择。 这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码; 更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。 如果我们将这些策略包含在客户端,这种做法更不可取,将导致客户端程序庞大而且难以维护,如果存在大量可供选择的算法时问题将变得更加严重。

如何让算法和对象分开来,使得算法可以独立于使用它的客户而变化?为此我们引入策略模式。

策略模式(Strategy),又叫算法簇模式,就是定义了不同的算法族,并且之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

常见的使用场景比如对象筛选,可以根据日期筛选,也可以根据 ID 筛选;又比如在单元测试中,我们可以在文件和内存存储之间进行切换。

  • UML类图

  • 示例代码

ObjectCollection.php

<?php
namespace DesignPatterns\Behavioral\Strategy;

/**
 * ObjectCollection类
 */
class ObjectCollection
{
    /**
     * @var array
     */
    private $elements;

    /**
     * @var ComparatorInterface
     */
    private $comparator;

    /**
     * @param array $elements
     */
    public function __construct(array $elements = array())
    {
        $this->elements = $elements;
    }

    /**
     * @return array
     */
    public function sort()
    {
        if (!$this->comparator) {
            throw new \LogicException("Comparator is not set");
        }

        $callback = array($this->comparator, 'compare');
        uasort($this->elements, $callback);

        return $this->elements;
    }

    /**
     * @param ComparatorInterface $comparator
     *
     * @return void
     */
    public function setComparator(ComparatorInterface $comparator)
    {
        $this->comparator = $comparator;
    }
}

ComparatorInterface.php

<?php
namespace DesignPatterns\Behavioral\Strategy;

/**
 * ComparatorInterface类
 */
interface ComparatorInterface
{
    /**
     * @param mixed $a
     * @param mixed $b
     *
     * @return bool
     */
    public function compare($a, $b);
}

DateComparator.php

<?php
namespace DesignPatterns\Behavioral\Strategy;

/**
 * DateComparator类
 */
class DateComparator implements ComparatorInterface
{
    /**
     * {@inheritdoc}
     */
    public function compare($a, $b)
    {
        $aDate = new \DateTime($a['date']);
        $bDate = new \DateTime($b['date']);

        if ($aDate == $bDate) {
            return 0;
        } else {
            return $aDate < $bDate ? -1 : 1;
        }
    }
}

IdComparator.php

<?php
namespace DesignPatterns\Behavioral\Strategy;

/**
 * IdComparator类
 */
class IdComparator implements ComparatorInterface
{
    /**
     * {@inheritdoc}
     */
    public function compare($a, $b)
    {
        if ($a['id'] == $b['id']) {
            return 0;
        } else {
            return $a['id'] < $b['id'] ? -1 : 1;
        }
    }
}
  • 测试代码

Tests/StrategyTest.php

<?php
namespace DesignPatterns\Behavioral\Strategy\Tests;

use DesignPatterns\Behavioral\Strategy\DateComparator;
use DesignPatterns\Behavioral\Strategy\IdComparator;
use DesignPatterns\Behavioral\Strategy\ObjectCollection;
use DesignPatterns\Behavioral\Strategy\Strategy;

/**
 * 策略模式测试
 */
class StrategyTest extends \PHPUnit_Framework_TestCase
{

    public function getIdCollection()
    {
        return array(
            array(
                array(array('id' => 2), array('id' => 1), array('id' => 3)),
                array('id' => 1)
            ),
            array(
                array(array('id' => 3), array('id' => 2), array('id' => 1)),
                array('id' => 1)
            ),
        );
    }

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

    /**
     * @dataProvider getIdCollection
     */
    public function testIdComparator($collection, $expected)
    {
        $obj = new ObjectCollection($collection);
        $obj->setComparator(new IdComparator());
        $elements = $obj->sort();

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

    /**
     * @dataProvider getDateCollection
     */
    public function testDateComparator($collection, $expected)
    {
        $obj = new ObjectCollection($collection);
        $obj->setComparator(new DateComparator());
        $elements = $obj->sort();

        $firstElement = array_shift($elements);
        $this->assertEquals($expected, $firstElement);
    }
}
  • 总结

策略模式属于对象行为型模式,主要针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。 策略模式使得算法可以在不影响到客户端的情况下发生变化。 通常,策略模式适用于当一个应用程序需要实现一种特定的服务或者功能,而且该程序有多种实现方式时使用。

  • 拓展

使用策略模式,你可以把一族不同的算法(业务)封装到不同的类中,使 client 类可以在不知道具体实现的情况下选择实例化其中一个算法。 策略模式有几种不同的变体,最简单的是下面这种:

第一段代码展示了一族输出算法,分别具体实现了 OutputInterface 的 load 方法,返回序列化结果,json 和数组:

<?php
interface OutputInterface
{
    public function load();
}

class SerializedArrayOutput implements OutputInterface
{
    public function load()
    {
        return serialize($arrayOfData);
    }
}

class JsonStringOutput implements OutputInterface
{
    public function load()
    {
        return json_encode($arrayOfData);
    }
}

class ArrayOutput implements OutputInterface
{
    public function load()
    {
        return $arrayOfData;
    }
}

通过像上面这样把不同类型的输出算法封装起来,其他的开发者可以很容易地在不影响 client 代码的情况下添加新的输出类型。

每个具体的输出类实现了 OutputInterface —— 这有两个目的,第一是它提供了一个所有输出类都必须遵守的契约, 第二,你将会在本文后面的部分看到,通过实现公共的接口,你可以利用类型约束保证 client 中使用的输出类必须是实现了 OutputInterface 的类。

接下来的一小段代码展示了一个 client 类如何使用其中一个输出算法,并可以在运行时根据需要选用不同的算法。

<?php
class SomeClient
{
    private $output;

    public function setOutput(OutputInterface $outputType)
    {
        $this->output = $outputType;
    }

    public function loadOutput()
    {
        return $this->output->load();
    }
}

上面的 client类有一个必须在运行时设置的私有属性,并且是“OutputInterface”类型的。 一旦这个属性被设置为具体的实例(三个输出类中之一的实例), 并且 loadOutput 方法被调用,那么它的 load 方法就会被调用,返回回序列化结果或 json 或数组。

<?php
$client = new SomeClient();

// Want an array?
$client->setOutput(new ArrayOutput());
$data = $client->loadOutput();

// Want some JSON?
$client->setOutput(new JsonStringOutput());
$data = $client->loadOutput();

模板方法模式(Template Method)

  • 模式定义

模板方法模式又叫模板模式,该模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。 模板方法模式将主要的方法定义为 final,防止子类修改算法骨架,将子类必须实现的方法定义为 abstract。 而普通的方法(无 final 或 abstract 修饰)则称之为钩子(hook)。

  • UML类图

  • 示例代码

Journey.php

<?php
namespace DesignPatterns\Behavioral\TemplateMethod;

abstract class Journey
{
    /**
     * 该方法是父类和子类提供的公共服务
     * 注意到方法前加了final,意味着子类不能重写该方法
     */
    final public function takeATrip()
    {
        $this->buyAFlight();
        $this->takePlane();
        $this->enjoyVacation();
        $this->buyGift();
        $this->takePlane();
    }

    /**
     * 该方法必须被子类实现, 这是模板方法模式的核心特性
     */
    abstract protected function enjoyVacation();

    /**
     * 这个方法也是算法的一部分,但是是可选的,只有在需要的时候才去重写它
     */
    protected function buyGift()
    {
    }

    /**
     * 子类不能访问该方法
     */
    private function buyAFlight()
    {
        echo "Buying a flight\n";
    }

    /**
     * 这也是个final方法
     */
    final protected function takePlane()
    {
        echo "Taking the plane\n";
    }
}

BeachJourney.php

<?php
namespace DesignPatterns\Behavioral\TemplateMethod;

/**
 * BeachJourney类(在海滩度假)
 */
class BeachJourney extends Journey
{
    protected function enjoyVacation()
    {
        echo "Swimming and sun-bathing\n";
    }
}

CityJourney.php

<?php
namespace DesignPatterns\Behavioral\TemplateMethod;

/**
 * CityJourney类(在城市中度假)
 */
class CityJourney extends Journey
{
    protected function enjoyVacation()
    {
        echo "Eat, drink, take photos and sleep\n";
    }
}
  • 测试代码

Tests/JourneyTest.php

<?php
namespace DesignPatterns\Behavioral\TemplateMethod\Tests;

use DesignPatterns\Behavioral\TemplateMethod;

/**
 * JourneyTest测试所有的度假
 */
class JourneyTest extends \PHPUnit_Framework_TestCase
{

    public function testBeach()
    {
        $journey = new TemplateMethod\BeachJourney();
        $this->expectOutputRegex('#sun-bathing#');
        $journey->takeATrip();
    }

    public function testCity()
    {
        $journey = new TemplateMethod\CityJourney();
        $this->expectOutputRegex('#drink#');
        $journey->takeATrip();
    }

    /**
     * 在PHPUnit中如何测试抽象模板方法
     */
    public function testLasVegas()
    {
        $journey = $this->getMockForAbstractClass('DesignPatterns\Behavioral\TemplateMethod\Journey');
        $journey->expects($this->once())
            ->method('enjoyVacation')
            ->will($this->returnCallback(array($this, 'mockUpVacation')));
        $this->expectOutputRegex('#Las Vegas#');
        $journey->takeATrip();
    }

    public function mockUpVacation()
    {
        echo "Fear and loathing in Las Vegas\n";
    }
}
  • 总结

模板方法模式是基于继承的代码复用技术,模板方法模式的结构和用法也是面向对象设计的核心之一。 在模板方法模式中,可以将相同的代码放在父类中,而将不同的方法实现放在不同的子类中。 在模板方法模式中,我们需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现, 然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法, 从而对剩余的逻辑有不同的实现,这就是模板方法模式的用意。 模板方法模式体现了面向对象的诸多重要思想,是一种使用频率较高的模式。

访问者模式(Visitor)

  • 模式定义

我们去银行柜台办业务,一般情况下会开几个个人业务柜台的,你去其中任何一个柜台办理都是可以的。 我们的访问者模式可以很好付诸在这个场景中:对于银行柜台来说,他们是不用变化的, 就是说今天和明天提供个人业务的柜台是不需要有变化的。而我们作为访问者,今天来银行可能是取消费流水, 明天来银行可能是去办理手机银行业务,这些是我们访问者的操作,一直是在变化的。

访问者模式就是表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

  • UML类图

  • 示例代码

RoleVisitorInterface.php

<?php
namespace DesignPatterns\Behavioral\Visitor;

/**
 * 访问者接口
 */
interface RoleVisitorInterface
{
    /**
     * 访问 User 对象
     *
     * @param \DesignPatterns\Behavioral\Visitor\User $role
     */
    public function visitUser(User $role);

    /**
     * 访问 Group 对象
     *
     * @param \DesignPatterns\Behavioral\Visitor\Group $role
     */
    public function visitGroup(Group $role);
}

RolePrintVisitor.php

<?php
namespace DesignPatterns\Behavioral\Visitor;

/**
 * Visitor接口的具体实现
 */
class RolePrintVisitor implements RoleVisitorInterface
{
    /**
     * {@inheritdoc}
     */
    public function visitGroup(Group $role)
    {
        echo "Role: " . $role->getName();
    }

    /**
     * {@inheritdoc}
     */
    public function visitUser(User $role)
    {
        echo "Role: " . $role->getName();
    }
}

Role.php

<?php
namespace DesignPatterns\Behavioral\Visitor;

/**
 * Role 类
 */
abstract class Role
{
    /**
     * 该方法基于Visitor的类名判断调用Visitor的方法
     *
     * 如果必须调用其它方法,重写本方法即可
     *
     * @param \DesignPatterns\Behavioral\Visitor\RoleVisitorInterface $visitor
     *
     * @throws \InvalidArgumentException
     */
    public function accept(RoleVisitorInterface $visitor)
    {
        $klass = get_called_class();
        preg_match('#([^\\\\]+)$#', $klass, $extract);
        $visitingMethod = 'visit' . $extract[1];

        if (!method_exists(__NAMESPACE__ . '\RoleVisitorInterface', $visitingMethod)) {
            throw new \InvalidArgumentException("The visitor you provide cannot visit a $klass instance");
        }

        call_user_func(array($visitor, $visitingMethod), $this);
    }
}

User.php

<?php
namespace DesignPatterns\Behavioral\Visitor;

class User extends Role
{
    /**
     * @var string
     */
    protected $name;

    /**
     * @param string $name
     */
    public function __construct($name)
    {
        $this->name = (string) $name;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return "User " . $this->name;
    }
}

Group.php

<?php
namespace DesignPatterns\Behavioral\Visitor;

class Group extends Role
{
    /**
     * @var string
     */
    protected $name;

    /**
     * @param string $name
     */
    public function __construct($name)
    {
        $this->name = (string) $name;
    }

    /**
     * @return string
     */
    public function getName()
    {
        return "Group: " . $this->name;
    }
}
  • 测试代码

Tests/VisitorTest.php

<?php
namespace DesignPatterns\Tests\Visitor\Tests;

use DesignPatterns\Behavioral\Visitor;

/**
 * VisitorTest 用于测试访问者模式
 */
class VisitorTest extends \PHPUnit_Framework_TestCase
{

    protected $visitor;

    protected function setUp()
    {
        $this->visitor = new Visitor\RolePrintVisitor();
    }

    public function getRole()
    {
        return array(
            array(new Visitor\User("Dominik"), 'Role: User Dominik'),
            array(new Visitor\Group("Administrators"), 'Role: Group: Administrators')
        );
    }

    /**
     * @dataProvider getRole
     */
    public function testVisitSomeRole(Visitor\Role $role, $expect)
    {
        $this->expectOutputString($expect);
        $role->accept($this->visitor);
    }

    /**
     * @expectedException \InvalidArgumentException
     * @expectedExceptionMessage Mock
     */
    public function testUnknownObject()
    {
        $mock = $this->getMockForAbstractClass('DesignPatterns\Behavioral\Visitor\Role');
        $mock->accept($this->visitor);
    }
}
  • 总结

访问者模式适用于数据结构相对稳定的系统,它把数据结构和作用于结构之上的操作之间的耦合解脱开, 使得操作集合可以相对自由的演化。在本例中,User、Group 是数据结构,而 RolePrintVisitor 是访问者(用于结构之上的操作)。

当实现访问者模式时,要将尽可能多的将对象浏览逻辑放在 Visitor 类中,而不是放在它的子类中, 这样的话,ConcreteVisitor 类所访问的对象结构依赖较少,从而使维护较为容易。

解释器模式(Interpreter)

  • 模式定义

  • UML类图

  • 示例代码

  • 测试代码






参考资料

PHP 常用设计模式 https://ibaiyang.github.io/blog/php/2019/07/30/PHP-常用设计模式.html

常见软件架构模式 https://ibaiyang.github.io/blog/架构/2019/07/30/常见软件架构模式.html

DesignPatternsPHP https://github.com/domnikl/DesignPatternsPHP

PHP 设计模式系列 https://laravelacademy.org/books/php-design-pattern

PHP 设计模式(Design Pattern For PHP) https://www.cnblogs.com/wilburxu/category/910011.html

php-the-right-way 设计模式 http://laravel-china.github.io/php-the-right-way/pages/Design-Patterns.html

DesignPatternsPHP https://designpatternsphp.readthedocs.io/en/latest/

DesignPatternsPHP(PHP设计模式范例) https://designpatternsphp.readthedocs.io/zh_CN/latest/README.html

PHP 设计模式概述 https://segmentfault.com/a/1190000016629282

PHP八大设计模式 https://blog.csdn.net/flitrue/article/details/52614599

php各种设计模式简单实践思考 https://www.cnblogs.com/aksir/p/6777595.html

漫画设计模式:什么是 “职责链模式” https://mp.weixin.qq.com/s?__biz=MzIxMjE5MTE1Nw==&mid=2653216646&idx=1&sn=223efe5b30bb64a9fb697690589325f2&chksm=8c99ad5cbbee244ab4bed34d7d514d80f3db97c6cbe7e41d9124b0b487fabd0caa86bdb41ade&scene=21#wechat_redirect

PHP设计模式之迭代器模式 https://blog.csdn.net/bujidexinq/article/details/104956869


返回