Merged branch master into translate-template

This commit is contained in:
Axel Pardemann
2016-09-23 11:44:18 -05:00
288 changed files with 9359 additions and 9024 deletions

View File

@@ -2,77 +2,42 @@
namespace DesignPatterns\Behavioral\ChainOfResponsibilities; namespace DesignPatterns\Behavioral\ChainOfResponsibilities;
/** use Psr\Http\Message\RequestInterface;
* Handler is a generic handler in the chain of responsibilities. use Psr\Http\Message\ResponseInterface;
*
* Yes you could have a lighter CoR with a simpler handler but if you want your CoR
* to be extendable and decoupled, it's a better idea to do things like that in real
* situations. Usually, a CoR is meant to be changed everytime and evolves, that's
* why we slice the workflow in little bits of code.
*/
abstract class Handler abstract class Handler
{ {
/** /**
* @var Handler * @var Handler|null
*/ */
private $successor = null; private $successor = null;
/** public function __construct(Handler $handler = null)
* Append a responsibility to the end of chain.
*
* A prepend method could be done with the same spirit
*
* You could also send the successor in the constructor but in PHP that is a
* bad idea because you have to remove the type-hint of the parameter because
* the last handler has a null successor.
*
* And if you override the constructor, that Handler can no longer have a
* successor. One solution is to provide a NullObject (see pattern).
* It is more preferable to keep the constructor "free" to inject services
* you need with the DiC of symfony2 for example.
*
* @param Handler $handler
*/
final public function append(Handler $handler)
{ {
if (is_null($this->successor)) { $this->successor = $handler;
$this->successor = $handler;
} else {
$this->successor->append($handler);
}
} }
/** /**
* Handle the request.
*
* This approach by using a template method pattern ensures you that * This approach by using a template method pattern ensures you that
* each subclass will not forget to call the successor. Besides, the returned * each subclass will not forget to call the successor
* boolean value indicates you if the request have been processed or not.
* *
* @param Request $req * @param RequestInterface $request
* *
* @return bool * @return string|null
*/ */
final public function handle(Request $req) final public function handle(RequestInterface $request)
{ {
$req->forDebugOnly = get_called_class(); $processed = $this->processing($request);
$processed = $this->processing($req);
if (!$processed) { if ($processed === null) {
// the request has not been processed by this handler => see the next // the request has not been processed by this handler => see the next
if (!is_null($this->successor)) { if ($this->successor !== null) {
$processed = $this->successor->handle($req); $processed = $this->successor->handle($request);
} }
} }
return $processed; return $processed;
} }
/** abstract protected function processing(RequestInterface $request);
* Each concrete handler has to implement the processing of the request.
*
* @param Request $req
*
* @return bool true if the request has been processed
*/
abstract protected function processing(Request $req);
} }

View File

@@ -33,12 +33,6 @@ Code
You can also find these code on `GitHub`_ You can also find these code on `GitHub`_
Request.php
.. literalinclude:: Request.php
:language: php
:linenos:
Handler.php Handler.php
.. literalinclude:: Handler.php .. literalinclude:: Handler.php

View File

@@ -1,19 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities;
/**
* Request is a request which goes through the chain of responsibilities.
*
* About the request: Sometimes, you don't need an object, just an integer or
* an array. But in this case of a full example, I've made a class to illustrate
* this important idea in the CoR (Chain of Responsibilities). In the real world,
* I recommend to always use a class, even a \stdClass if you want, it proves
* to be more adaptive because a single handler doesn't know much about the
* outside world and it is more difficult if, one day, you want to add some
* criterion in a decision process.
*/
class Request
{
// getter and setter but I don't want to generate too much noise in handlers
}

View File

@@ -1,40 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Request;
/**
* Class 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)) {
// the handler IS responsible and then processes the request
$req->response = $this->data[$req->key];
// instead of returning true, I could return the value but it proves
// to be a bad idea. What if the value IS "false" ?
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use Psr\Http\Message\RequestInterface;
class HttpInMemoryCacheHandler extends Handler
{
/**
* @var array
*/
private $data;
/**
* @param array $data
* @param Handler|null $successor
*/
public function __construct(array $data, Handler $successor = null)
{
parent::__construct($successor);
$this->data = $data;
}
/**
* @param RequestInterface $request
*
* @return string|null
*/
protected function processing(RequestInterface $request)
{
$key = sprintf(
'%s?%s',
$request->getUri()->getPath(),
$request->getUri()->getQuery()
);
if ($request->getMethod() == 'GET' && isset($this->data[$key])) {
return $this->data[$key];
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use Psr\Http\Message\RequestInterface;
class SlowDatabaseHandler extends Handler
{
/**
* @param RequestInterface $request
*
* @return string|null
*/
protected function processing(RequestInterface $request)
{
// this is a mockup, in production code you would ask a slow (compared to in-memory) DB for the results
return 'Hello World!';
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Request;
/**
* This is mostly the same code as FastStorage but in fact, it may greatly differs.
*
* One important fact about CoR: each item in the chain MUST NOT assume its position
* in the chain. A CoR is not responsible if the request is not handled UNLESS
* you make an "ExceptionHandler" which throws exception if the request goes there.
*
* To be really extendable, each handler doesn't know if there is something after it.
*/
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;
}
}

View File

@@ -2,80 +2,50 @@
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Tests; namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Tests;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Request; use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible; use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\HttpInMemoryCacheHandler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\FastStorage; use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowDatabaseHandler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowStorage;
/**
* ChainTest tests the CoR.
*/
class ChainTest extends \PHPUnit_Framework_TestCase class ChainTest extends \PHPUnit_Framework_TestCase
{ {
/** /**
* @var FastStorage * @var Handler
*/ */
protected $chain; private $chain;
protected function setUp() protected function setUp()
{ {
$this->chain = new FastStorage(array('bar' => 'baz')); $this->chain = new HttpInMemoryCacheHandler(
$this->chain->append(new SlowStorage(array('bar' => 'baz', 'foo' => 'bar'))); ['/foo/bar?index=1' => 'Hello In Memory!'],
} new SlowDatabaseHandler()
public function makeRequest()
{
$request = new Request();
$request->verb = 'get';
return array(
array($request),
); );
} }
/** public function testCanRequestKeyInFastStorage()
* @dataProvider makeRequest
*/
public function testFastStorage($request)
{ {
$request->key = 'bar'; $uri = $this->createMock('Psr\Http\Message\UriInterface');
$ret = $this->chain->handle($request); $uri->method('getPath')->willReturn('/foo/bar');
$uri->method('getQuery')->willReturn('index=1');
$this->assertTrue($ret); $request = $this->createMock('Psr\Http\Message\RequestInterface');
$this->assertObjectHasAttribute('response', $request); $request->method('getMethod')
$this->assertEquals('baz', $request->response); ->willReturn('GET');
// despite both handle owns the 'bar' key, the FastStorage is responding first $request->method('getUri')->willReturn($uri);
$className = 'DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\FastStorage';
$this->assertEquals($className, $request->forDebugOnly); $this->assertEquals('Hello In Memory!', $this->chain->handle($request));
} }
/** public function testCanRequestKeyInSlowStorage()
* @dataProvider makeRequest
*/
public function testSlowStorage($request)
{ {
$request->key = 'foo'; $uri = $this->createMock('Psr\Http\Message\UriInterface');
$ret = $this->chain->handle($request); $uri->method('getPath')->willReturn('/foo/baz');
$uri->method('getQuery')->willReturn('');
$this->assertTrue($ret); $request = $this->createMock('Psr\Http\Message\RequestInterface');
$this->assertObjectHasAttribute('response', $request); $request->method('getMethod')
$this->assertEquals('bar', $request->response); ->willReturn('GET');
// FastStorage has no 'foo' key, the SlowStorage is responding $request->method('getUri')->willReturn($uri);
$className = 'DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowStorage';
$this->assertEquals($className, $request->forDebugOnly);
}
/** $this->assertEquals('Hello World!', $this->chain->handle($request));
* @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);
} }
} }

View File

@@ -4,14 +4,14 @@ namespace DesignPatterns\Behavioral\Command;
/** /**
* This concrete command tweaks receiver to add current date to messages * This concrete command tweaks receiver to add current date to messages
* invoker just knows that it can call "execute". * invoker just knows that it can call "execute"
*/ */
class AddMessageDateCommand implements UndoableCommandInterface class AddMessageDateCommand implements UndoableCommandInterface
{ {
/** /**
* @var Receiver * @var Receiver
*/ */
protected $output; private $output;
/** /**
* Each concrete command is built with different receivers. * Each concrete command is built with different receivers.

View File

@@ -2,9 +2,6 @@
namespace DesignPatterns\Behavioral\Command; namespace DesignPatterns\Behavioral\Command;
/**
* class CommandInterface.
*/
interface CommandInterface interface CommandInterface
{ {
/** /**

View File

@@ -4,18 +4,18 @@ namespace DesignPatterns\Behavioral\Command;
/** /**
* This concrete command calls "print" on the Receiver, but an external * This concrete command calls "print" on the Receiver, but an external
* invoker just knows that it can call "execute". * invoker just knows that it can call "execute"
*/ */
class HelloCommand implements CommandInterface class HelloCommand implements CommandInterface
{ {
/** /**
* @var Receiver * @var Receiver
*/ */
protected $output; private $output;
/** /**
* Each concrete command is built with different receivers. * Each concrete command is built with different receivers.
* There can be one, many or completely no receivers, but there can be other commands in the parameters. * There can be one, many or completely no receivers, but there can be other commands in the parameters
* *
* @param Receiver $console * @param Receiver $console
*/ */
@@ -29,8 +29,7 @@ class HelloCommand implements CommandInterface
*/ */
public function execute() public function execute()
{ {
// sometimes, there is no receiver and this is the command which // sometimes, there is no receiver and this is the command which does all the work
// does all the work
$this->output->write('Hello World'); $this->output->write('Hello World');
} }
} }

View File

@@ -11,11 +11,11 @@ class Invoker
/** /**
* @var CommandInterface * @var CommandInterface
*/ */
protected $command; private $command;
/** /**
* In the invoker we find this kind of method for subscribing the command. * in the invoker we find this kind of method for subscribing the command
* There can be also a stack, a list, a fixed set... * There can be also a stack, a list, a fixed set ...
* *
* @param CommandInterface $cmd * @param CommandInterface $cmd
*/ */
@@ -25,12 +25,10 @@ class Invoker
} }
/** /**
* executes the command. * executes the command; the invoker is the same whatever is the command
*/ */
public function run() public function run()
{ {
// here is a key feature of the invoker
// the invoker is the same whatever is the command
$this->command->execute(); $this->command->execute();
} }
} }

View File

@@ -7,14 +7,20 @@ namespace DesignPatterns\Behavioral\Command;
*/ */
class Receiver class Receiver
{ {
/**
* @var bool
*/
private $enableDate = false; private $enableDate = false;
private $output = array(); /**
* @var string[]
*/
private $output = [];
/** /**
* @param string $str * @param string $str
*/ */
public function write($str) public function write(string $str)
{ {
if ($this->enableDate) { if ($this->enableDate) {
$str .= ' ['.date('Y-m-d').']'; $str .= ' ['.date('Y-m-d').']';
@@ -23,13 +29,13 @@ class Receiver
$this->output[] = $str; $this->output[] = $str;
} }
public function getOutput() public function getOutput(): string
{ {
return implode("\n", $this->output); return join("\n", $this->output);
} }
/** /**
* Enable receiver to display message date. * Enable receiver to display message date
*/ */
public function enableDate() public function enableDate()
{ {
@@ -37,7 +43,7 @@ class Receiver
} }
/** /**
* Disable receiver to display message date. * Disable receiver to display message date
*/ */
public function disableDate() public function disableDate()
{ {

View File

@@ -6,31 +6,15 @@ use DesignPatterns\Behavioral\Command\HelloCommand;
use DesignPatterns\Behavioral\Command\Invoker; use DesignPatterns\Behavioral\Command\Invoker;
use DesignPatterns\Behavioral\Command\Receiver; use DesignPatterns\Behavioral\Command\Receiver;
/**
* CommandTest has the role of the Client in the Command Pattern.
*/
class CommandTest extends \PHPUnit_Framework_TestCase 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() public function testInvocation()
{ {
$this->invoker->setCommand(new HelloCommand($this->receiver)); $invoker = new Invoker();
$this->invoker->run(); $receiver = new Receiver();
$this->assertEquals($this->receiver->getOutput(), 'Hello World');
$invoker->setCommand(new HelloCommand($receiver));
$invoker->run();
$this->assertEquals($receiver->getOutput(), 'Hello World');
} }
} }

View File

@@ -6,44 +6,27 @@ use DesignPatterns\Behavioral\Command\AddMessageDateCommand;
use DesignPatterns\Behavioral\Command\HelloCommand; use DesignPatterns\Behavioral\Command\HelloCommand;
use DesignPatterns\Behavioral\Command\Invoker; use DesignPatterns\Behavioral\Command\Invoker;
use DesignPatterns\Behavioral\Command\Receiver; use DesignPatterns\Behavioral\Command\Receiver;
use PHPUnit_Framework_TestCase;
/** class UndoableCommandTest extends \PHPUnit_Framework_TestCase
* UndoableCommandTest has the role of the Client in the Command Pattern.
*/
class UndoableCommandTest 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() public function testInvocation()
{ {
$this->invoker->setCommand(new HelloCommand($this->receiver)); $invoker = new Invoker();
$this->invoker->run(); $receiver = new Receiver();
$this->assertEquals($this->receiver->getOutput(), 'Hello World');
$messageDateCommand = new AddMessageDateCommand($this->receiver); $invoker->setCommand(new HelloCommand($receiver));
$invoker->run();
$this->assertEquals($receiver->getOutput(), 'Hello World');
$messageDateCommand = new AddMessageDateCommand($receiver);
$messageDateCommand->execute(); $messageDateCommand->execute();
$this->invoker->run(); $invoker->run();
$this->assertEquals($this->receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d').']'); $this->assertEquals($receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d').']');
$messageDateCommand->undo(); $messageDateCommand->undo();
$this->invoker->run(); $invoker->run();
$this->assertEquals($this->receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d')."]\nHello World"); $this->assertEquals($receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d')."]\nHello World");
} }
} }

View File

@@ -2,14 +2,10 @@
namespace DesignPatterns\Behavioral\Command; namespace DesignPatterns\Behavioral\Command;
/**
* Interface UndoableCommandInterface.
*/
interface UndoableCommandInterface extends CommandInterface interface UndoableCommandInterface extends CommandInterface
{ {
/** /**
* This method is used to undo change made by command execution * This method is used to undo change made by command execution
* The Receiver goes in the constructor.
*/ */
public function undo(); public function undo();
} }

View File

@@ -4,7 +4,7 @@ namespace DesignPatterns\Behavioral\Mediator;
/** /**
* Colleague is an abstract colleague who works together but he only knows * Colleague is an abstract colleague who works together but he only knows
* the Mediator, not other colleague. * the Mediator, not other colleagues
*/ */
abstract class Colleague abstract class Colleague
{ {
@@ -13,21 +13,13 @@ abstract class Colleague
* *
* @var MediatorInterface * @var MediatorInterface
*/ */
private $mediator; protected $mediator;
/** /**
* @param MediatorInterface $medium * @param MediatorInterface $mediator
*/ */
public function __construct(MediatorInterface $medium) public function setMediator(MediatorInterface $mediator)
{ {
// in this way, we are sure the concrete colleague knows the mediator $this->mediator = $mediator;
$this->mediator = $medium;
}
// for subclasses
protected function getMediator()
{
return $this->mediator;
} }
} }

View File

@@ -3,59 +3,54 @@
namespace DesignPatterns\Behavioral\Mediator; namespace DesignPatterns\Behavioral\Mediator;
/** /**
* Mediator is the concrete Mediator for this design pattern. * Mediator is the concrete Mediator for this design pattern
* In this example, I have made a "Hello World" with the Mediator Pattern. *
* In this example, I have made a "Hello World" with the Mediator Pattern
*/ */
class Mediator implements MediatorInterface class Mediator implements MediatorInterface
{ {
/** /**
* @var Subsystem\Server * @var Subsystem\Server
*/ */
protected $server; private $server;
/** /**
* @var Subsystem\Database * @var Subsystem\Database
*/ */
protected $database; private $database;
/** /**
* @var Subsystem\Client * @var Subsystem\Client
*/ */
protected $client; private $client;
/** /**
* @param Subsystem\Database $db * @param Subsystem\Database $database
* @param Subsystem\Client $cl * @param Subsystem\Client $client
* @param Subsystem\Server $srv * @param Subsystem\Server $server
*/ */
public function setColleague(Subsystem\Database $db, Subsystem\Client $cl, Subsystem\Server $srv) public function __construct(Subsystem\Database $database, Subsystem\Client $client, Subsystem\Server $server)
{ {
$this->database = $db; $this->database = $database;
$this->server = $srv; $this->server = $server;
$this->client = $cl; $this->client = $client;
$this->database->setMediator($this);
$this->server->setMediator($this);
$this->client->setMediator($this);
} }
/**
* make request.
*/
public function makeRequest() public function makeRequest()
{ {
$this->server->process(); $this->server->process();
} }
/** public function queryDb(): string
* query db.
*
* @return mixed
*/
public function queryDb()
{ {
return $this->database->getData(); return $this->database->getData();
} }
/** /**
* send response.
*
* @param string $content * @param string $content
*/ */
public function sendResponse($content) public function sendResponse($content)

View File

@@ -4,7 +4,7 @@ namespace DesignPatterns\Behavioral\Mediator;
/** /**
* MediatorInterface is a contract for the Mediator * MediatorInterface is a contract for the Mediator
* This interface is not mandatory but it is better for LSP concerns. * This interface is not mandatory but it is better for Liskov substitution principle concerns.
*/ */
interface MediatorInterface interface MediatorInterface
{ {
@@ -16,12 +16,12 @@ interface MediatorInterface
public function sendResponse($content); public function sendResponse($content);
/** /**
* makes a request. * makes a request
*/ */
public function makeRequest(); public function makeRequest();
/** /**
* queries the DB. * queries the DB
*/ */
public function queryDb(); public function queryDb();
} }

View File

@@ -5,24 +5,16 @@ namespace DesignPatterns\Behavioral\Mediator\Subsystem;
use DesignPatterns\Behavioral\Mediator\Colleague; use DesignPatterns\Behavioral\Mediator\Colleague;
/** /**
* Client is a client that make request et get response. * Client is a client that makes requests and gets the response response.
*/ */
class Client extends Colleague class Client extends Colleague
{ {
/**
* request.
*/
public function request() public function request()
{ {
$this->getMediator()->makeRequest(); $this->mediator->makeRequest();
} }
/** public function output(string $content)
* output content.
*
* @param string $content
*/
public function output($content)
{ {
echo $content; echo $content;
} }

View File

@@ -4,15 +4,9 @@ namespace DesignPatterns\Behavioral\Mediator\Subsystem;
use DesignPatterns\Behavioral\Mediator\Colleague; use DesignPatterns\Behavioral\Mediator\Colleague;
/**
* Database is a database service.
*/
class Database extends Colleague class Database extends Colleague
{ {
/** public function getData(): string
* @return string
*/
public function getData()
{ {
return 'World'; return 'World';
} }

View File

@@ -4,17 +4,11 @@ namespace DesignPatterns\Behavioral\Mediator\Subsystem;
use DesignPatterns\Behavioral\Mediator\Colleague; use DesignPatterns\Behavioral\Mediator\Colleague;
/**
* Server serves responses.
*/
class Server extends Colleague class Server extends Colleague
{ {
/**
* process on server.
*/
public function process() public function process()
{ {
$data = $this->getMediator()->queryDb(); $data = $this->mediator->queryDb();
$this->getMediator()->sendResponse("Hello $data"); $this->mediator->sendResponse(sprintf("Hello %s", $data));
} }
} }

View File

@@ -7,27 +7,14 @@ use DesignPatterns\Behavioral\Mediator\Subsystem\Client;
use DesignPatterns\Behavioral\Mediator\Subsystem\Database; use DesignPatterns\Behavioral\Mediator\Subsystem\Database;
use DesignPatterns\Behavioral\Mediator\Subsystem\Server; use DesignPatterns\Behavioral\Mediator\Subsystem\Server;
/**
* MediatorTest tests hello world.
*/
class MediatorTest extends \PHPUnit_Framework_TestCase 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() public function testOutputHelloWorld()
{ {
// testing if Hello World is output : $client = new Client();
new Mediator(new Database(), $client, new Server());
$this->expectOutputString('Hello World'); $this->expectOutputString('Hello World');
// as you see, the 3 components Client, Server and Database are totally decoupled $client->request();
$this->client->request();
// Anyway, it remains complexity in the Mediator that's why the pattern
// Observer is preferable in mnay situations.
} }
} }

View File

@@ -1,49 +0,0 @@
<?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();
//Setting state to State1
$originator->setState('State1');
//Setting state to State2
$originator->setState('State2');
//Saving State2 to Memento
$this->saveToHistory($originator->getStateAsMemento());
//Setting state to State3
$originator->setState('State3');
// We can request multiple mementos, and choose which one to roll back to.
// Saving State3 to Memento
$this->saveToHistory($originator->getStateAsMemento());
//Setting state to State4
$originator->setState('State4');
$originator->restoreFromMemento($this->getFromHistory(1));
//State after restoring from Memento: State3
return $originator->getStateAsMemento()->getState();
}
}

View File

@@ -4,19 +4,21 @@ namespace DesignPatterns\Behavioral\Memento;
class Memento class Memento
{ {
/* @var mixed */ /**
* @var State
*/
private $state; private $state;
/** /**
* @param mixed $stateToSave * @param State $stateToSave
*/ */
public function __construct($stateToSave) public function __construct(State $stateToSave)
{ {
$this->state = $stateToSave; $this->state = $stateToSave;
} }
/** /**
* @return mixed * @return State
*/ */
public function getState() public function getState()
{ {

View File

@@ -1,38 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Memento;
class Originator
{
/* @var mixed */
private $state;
// The class could also contain additional data that is not part of the
// state saved in the memento..
/**
* @param mixed $state
*/
public function setState($state)
{
// you must check type of state inside child of this class
// or use type-hinting for full pattern implementation
$this->state = $state;
}
/**
* @return Memento
*/
public function getStateAsMemento()
{
// you must save a separate copy in Memento
$state = is_object($this->state) ? clone $this->state : $this->state;
return new Memento($state);
}
public function restoreFromMemento(Memento $memento)
{
$this->state = $memento->getState();
}
}

View File

@@ -6,8 +6,8 @@ Purpose
It provides the ability to restore an object to it's previous state (undo It provides the ability to restore an object to it's previous state (undo
via rollback) or to gain access to state of the object, without revealing via rollback) or to gain access to state of the object, without revealing
it's implementation (i.e., the object is not required to have a functional it's implementation (i.e., the object is not required to have a function
for return the current state). to return the current state).
The memento pattern is implemented with three objects: the Originator, a The memento pattern is implemented with three objects: the Originator, a
Caretaker and a Memento. Caretaker and a Memento.
@@ -66,9 +66,9 @@ Originator.php
:language: php :language: php
:linenos: :linenos:
Caretaker.php Ticket.php
.. literalinclude:: Caretaker.php .. literalinclude:: Ticket.php
:language: php :language: php
:linenos: :linenos:

View File

@@ -0,0 +1,48 @@
<?php
namespace DesignPatterns\Behavioral\Memento;
class State
{
const STATE_CREATED = 'created';
const STATE_OPENED = 'opened';
const STATE_ASSIGNED = 'assigned';
const STATE_CLOSED = 'closed';
/**
* @var string
*/
private $state;
/**
* @var string[]
*/
private static $validStates = [
self::STATE_CREATED,
self::STATE_OPENED,
self::STATE_ASSIGNED,
self::STATE_CLOSED,
];
/**
* @param string $state
*/
public function __construct(string $state)
{
self::ensureIsValidState($state);
$this->state = $state;
}
private static function ensureIsValidState(string $state)
{
if (!in_array($state, self::$validStates)) {
throw new \InvalidArgumentException('Invalid state given');
}
}
public function __toString(): string
{
return $this->state;
}
}

View File

@@ -2,161 +2,30 @@
namespace DesignPatterns\Behavioral\Memento\Tests; namespace DesignPatterns\Behavioral\Memento\Tests;
use DesignPatterns\Behavioral\Memento\Caretaker; use DesignPatterns\Behavioral\Memento\State;
use DesignPatterns\Behavioral\Memento\Memento; use DesignPatterns\Behavioral\Memento\Ticket;
use DesignPatterns\Behavioral\Memento\Originator;
/**
* MementoTest tests the memento pattern.
*/
class MementoTest extends \PHPUnit_Framework_TestCase class MementoTest extends \PHPUnit_Framework_TestCase
{ {
public function testUsageExample() public function testOpenTicketAssignAndSetBackToOpen()
{ {
$originator = new Originator(); $ticket = new Ticket();
$caretaker = new Caretaker();
$character = new \stdClass(); // open the ticket
// new object $ticket->open();
$character->name = 'Gandalf'; $openedState = $ticket->getState();
// connect Originator to character object $this->assertEquals(State::STATE_OPENED, (string) $ticket->getState());
$originator->setState($character);
// work on the object $memento = $ticket->saveToMemento();
$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 // assign the ticket
$character->name = 'Sauron'; $ticket->assign();
// and again $this->assertEquals(State::STATE_ASSIGNED, (string) $ticket->getState());
$character->race = 'Ainur';
// state inside the Originator was equally changed
$this->assertAttributeEquals($character, 'state', $originator);
// time to save another state // no restore to the opened state, but verify that the state object has been cloned for the memento
$snapshot = $originator->getStateAsMemento(); $ticket->restoreFromMemento($memento);
// put state to log
$caretaker->saveToHistory($snapshot);
$rollback = $caretaker->getFromHistory(0); $this->assertEquals(State::STATE_OPENED, (string) $ticket->getState());
// return to first state $this->assertNotSame($openedState, $ticket->getState());
$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);
} }
} }

View File

@@ -0,0 +1,49 @@
<?php
namespace DesignPatterns\Behavioral\Memento;
/**
* Ticket is the "Originator" in this implementation
*/
class Ticket
{
/**
* @var State
*/
private $currentState;
public function __construct()
{
$this->currentState = new State(State::STATE_CREATED);
}
public function open()
{
$this->currentState = new State(State::STATE_OPENED);
}
public function assign()
{
$this->currentState = new State(State::STATE_ASSIGNED);
}
public function close()
{
$this->currentState = new State(State::STATE_CLOSED);
}
public function saveToMemento(): Memento
{
return new Memento(clone $this->currentState);
}
public function restoreFromMemento(Memento $memento)
{
$this->currentState = $memento->getState();
}
public function getState(): State
{
return $this->currentState;
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\Memento\Memento</OriginalElement>
<nodes>
<node x="0.0" y="0.0">\DesignPatterns\Behavioral\Memento\State</node>
<node x="313.0" y="252.0">\DesignPatterns\Behavioral\Memento\Memento</node>
<node x="0.0" y="252.0">\DesignPatterns\Behavioral\Memento\Ticket</node>
</nodes>
<notes />
<edges />
<settings layout="Hierarchic Group" zoom="1.0" x="150.0" y="130.0" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\Memento\Caretaker</OriginalElement>
<nodes>
<node x="182.0" y="153.0">\DesignPatterns\Behavioral\Memento\Caretaker</node>
<node x="0.0" y="0.0">\DesignPatterns\Behavioral\Memento\Originator</node>
<node x="0.0" y="153.0">\DesignPatterns\Behavioral\Memento\Memento</node>
</nodes>
<notes />
<edges />
<settings layout="Hierarchic Group" zoom="1.0" x="131.0" y="121.0" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Constructors</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

@@ -3,16 +3,9 @@
namespace DesignPatterns\Behavioral\NullObject; namespace DesignPatterns\Behavioral\NullObject;
/** /**
* LoggerInterface is a contract for logging something. * Key feature: NullLogger must inherit from this interface like any other loggers
*
* Key feature: NullLogger MUST inherit from this interface like any other Loggers
*/ */
interface LoggerInterface interface LoggerInterface
{ {
/** public function log(string $str);
* @param string $str
*
* @return mixed
*/
public function log($str);
} }

View File

@@ -2,19 +2,9 @@
namespace DesignPatterns\Behavioral\NullObject; namespace DesignPatterns\Behavioral\NullObject;
/**
* Performance concerns : ok there is a call for nothing but we spare an "if is_null"
* I didn't run a benchmark but I think it's equivalent.
*
* Key feature : of course this logger MUST implement the same interface (or abstract)
* like the other loggers.
*/
class NullLogger implements LoggerInterface class NullLogger implements LoggerInterface
{ {
/** public function log(string $str)
* {@inheritdoc}
*/
public function log($str)
{ {
// do nothing // do nothing
} }

View File

@@ -2,15 +2,9 @@
namespace DesignPatterns\Behavioral\NullObject; namespace DesignPatterns\Behavioral\NullObject;
/**
* PrintLogger is a logger that prints the log entry to standard output.
*/
class PrintLogger implements LoggerInterface class PrintLogger implements LoggerInterface
{ {
/** public function log(string $str)
* @param string $str
*/
public function log($str)
{ {
echo $str; echo $str;
} }

View File

@@ -2,24 +2,19 @@
namespace DesignPatterns\Behavioral\NullObject; namespace DesignPatterns\Behavioral\NullObject;
/**
* Service is dummy service that uses a logger.
*/
class Service class Service
{ {
/** /**
* @var LoggerInterface * @var LoggerInterface
*/ */
protected $logger; private $logger;
/** /**
* we inject the logger in ctor and it is mandatory. * @param LoggerInterface $logger
*
* @param LoggerInterface $log
*/ */
public function __construct(LoggerInterface $log) public function __construct(LoggerInterface $logger)
{ {
$this->logger = $log; $this->logger = $logger;
} }
/** /**
@@ -27,8 +22,7 @@ class Service
*/ */
public function doSomething() public function doSomething()
{ {
// no more check "if (!is_null($this->logger))..." with the NullObject pattern // notice here that you don't have to check if the logger is set with eg. is_null(), instead just use it
$this->logger->log('We are in '.__METHOD__); $this->logger->log('We are in '.__METHOD__);
// something to do...
} }
} }

View File

@@ -6,17 +6,12 @@ use DesignPatterns\Behavioral\NullObject\NullLogger;
use DesignPatterns\Behavioral\NullObject\PrintLogger; use DesignPatterns\Behavioral\NullObject\PrintLogger;
use DesignPatterns\Behavioral\NullObject\Service; use DesignPatterns\Behavioral\NullObject\Service;
/**
* LoggerTest tests for different loggers.
*/
class LoggerTest extends \PHPUnit_Framework_TestCase class LoggerTest extends \PHPUnit_Framework_TestCase
{ {
public function testNullObject() public function testNullObject()
{ {
// one can use a singleton for NullObjet : I don't think it's a good idea
// because the purpose behind null object is to "avoid special case".
$service = new Service(new NullLogger()); $service = new Service(new NullLogger());
$this->expectOutputString(null); // no output $this->expectOutputString(null);
$service->doSomething(); $service->doSomething();
} }

View File

@@ -5,65 +5,16 @@ namespace DesignPatterns\Behavioral\Observer\Tests;
use DesignPatterns\Behavioral\Observer\User; use DesignPatterns\Behavioral\Observer\User;
use DesignPatterns\Behavioral\Observer\UserObserver; use DesignPatterns\Behavioral\Observer\UserObserver;
/**
* ObserverTest tests the Observer pattern.
*/
class ObserverTest extends \PHPUnit_Framework_TestCase class ObserverTest extends \PHPUnit_Framework_TestCase
{ {
protected $observer; public function testChangeInUserLeadsToUserObserverBeingNotified()
protected function setUp()
{ {
$this->observer = new UserObserver(); $observer = new UserObserver();
}
/** $user = new User();
* Tests the notification. $user->attach($observer);
*/
public function testNotify()
{
$this->expectOutputString('DesignPatterns\Behavioral\Observer\User has been updated');
$subject = new User();
$subject->attach($this->observer); $user->changeEmail('foo@bar.com');
$subject->property = 123; $this->assertCount(1, $observer->getChangedUsers());
}
/**
* Tests the subscribing.
*/
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));
}
/**
* Tests the update() invocation on a mockup.
*/
public function testUpdateCalling()
{
$subject = new User();
$observer = $this->getMock('SplObserver');
$subject->attach($observer);
$observer->expects($this->once())
->method('update')
->with($subject);
$subject->notify();
} }
} }

View File

@@ -3,60 +3,42 @@
namespace DesignPatterns\Behavioral\Observer; namespace DesignPatterns\Behavioral\Observer;
/** /**
* Observer pattern : The observed object (the subject). * User implements the observed object (called Subject), it maintains a list of observers and sends notifications to
* * them in case changes are made on the User object
* The subject maintains a list of Observers and sends notifications.
*/ */
class User implements \SplSubject class User implements \SplSubject
{ {
/** /**
* user data. * @var string
*
* @var array
*/ */
protected $data = array(); private $email;
/** /**
* observers.
*
* @var \SplObjectStorage * @var \SplObjectStorage
*/ */
protected $observers; private $observers;
public function __construct() public function __construct()
{ {
$this->observers = new \SplObjectStorage(); $this->observers = new \SplObjectStorage();
} }
/**
* attach a new observer.
*
* @param \SplObserver $observer
*
* @return void
*/
public function attach(\SplObserver $observer) public function attach(\SplObserver $observer)
{ {
$this->observers->attach($observer); $this->observers->attach($observer);
} }
/**
* detach an observer.
*
* @param \SplObserver $observer
*
* @return void
*/
public function detach(\SplObserver $observer) public function detach(\SplObserver $observer)
{ {
$this->observers->detach($observer); $this->observers->detach($observer);
} }
/** public function changeEmail(string $email)
* notify observers. {
* $this->email = $email;
* @return void $this->notify();
*/ }
public function notify() public function notify()
{ {
/** @var \SplObserver $observer */ /** @var \SplObserver $observer */
@@ -64,21 +46,4 @@ class User implements \SplSubject
$observer->update($this); $observer->update($this);
} }
} }
/**
* Ideally one would better write setter/getter for all valid attributes and only call notify()
* on attributes that matter when changed.
*
* @param string $name
* @param mixed $value
*
* @return void
*/
public function __set($name, $value)
{
$this->data[$name] = $value;
// notify the observers, that user has been updated
$this->notify();
}
} }

View File

@@ -2,19 +2,28 @@
namespace DesignPatterns\Behavioral\Observer; namespace DesignPatterns\Behavioral\Observer;
/**
* class UserObserver.
*/
class UserObserver implements \SplObserver class UserObserver implements \SplObserver
{ {
/** /**
* This is the only method to implement as an observer. * @var User[]
* It is called by the Subject (usually by SplSubject::notify() ). */
private $changedUsers = [];
/**
* It is called by the Subject, usually by SplSubject::notify()
* *
* @param \SplSubject $subject * @param \SplSubject $subject
*/ */
public function update(\SplSubject $subject) public function update(\SplSubject $subject)
{ {
echo get_class($subject).' has been updated'; $this->changedUsers[] = clone $subject;
}
/**
* @return User[]
*/
public function getChangedUsers(): array
{
return $this->changedUsers;
} }
} }

View File

@@ -1,27 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Diagram> <Diagram>
<ID>PHP</ID> <ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\Observer\User</OriginalElement> <OriginalElement>\DesignPatterns\Behavioral\Observer\UserObserver</OriginalElement>
<nodes> <nodes>
<node x="239.0" y="0.0">\DesignPatterns\Behavioral\Observer\UserObserver</node> <node x="215.0" y="112.0">\DesignPatterns\Behavioral\Observer\UserObserver</node>
<node x="0.0" y="137.0">\DesignPatterns\Behavioral\Observer\User</node> <node x="229.5" y="7.0">\SplObserver</node>
<node x="8.0" y="0.0">\SplSubject</node> <node x="-29.0" y="25.0">\DesignPatterns\Behavioral\Observer\User</node>
</nodes> </nodes>
<notes /> <notes />
<edges> <edges>
<edge source="\DesignPatterns\Behavioral\Observer\User" target="\SplSubject"> <edge source="\DesignPatterns\Behavioral\Observer\UserObserver" target="\SplObserver">
<point x="0.0" y="-74.0" /> <point x="0.0" y="-48.0" />
<point x="0.0" y="43.5" /> <point x="0.0" y="25.5" />
</edge> </edge>
</edges> </edges>
<settings layout="Hierarchic Group" zoom="1.0" x="186.0" y="142.5" /> <settings layout="Hierarchic Group" zoom="1.0" x="58.0" y="91.0" />
<SelectedNodes /> <SelectedNodes />
<Categories> <Categories>
<Category>Fields</Category> <Category>Fields</Category>
<Category>Constants</Category> <Category>Constants</Category>
<Category>Constructors</Category> <Category>Methods</Category>
<Category>Methods</Category> </Categories>
</Categories> <VISIBILITY>private</VISIBILITY>
<VISIBILITY>private</VISIBILITY> </Diagram>
</Diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1,52 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
/**
* An abstract specification allows the creation of wrapped specifications.
*/
abstract class AbstractSpecification implements SpecificationInterface
{
/**
* Checks if given item meets all criteria.
*
* @param Item $item
*
* @return bool
*/
abstract public function isSatisfiedBy(Item $item);
/**
* Creates a new logical AND specification.
*
* @param SpecificationInterface $spec
*
* @return SpecificationInterface
*/
public function plus(SpecificationInterface $spec)
{
return new Plus($this, $spec);
}
/**
* Creates a new logical OR composite specification.
*
* @param SpecificationInterface $spec
*
* @return SpecificationInterface
*/
public function either(SpecificationInterface $spec)
{
return new Either($this, $spec);
}
/**
* Creates a new logical NOT specification.
*
* @return SpecificationInterface
*/
public function not()
{
return new Not($this);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
class AndSpecification implements SpecificationInterface
{
/**
* @var SpecificationInterface[]
*/
private $specifications;
/**
* @param SpecificationInterface[] ...$specifications
*/
public function __construct(SpecificationInterface ...$specifications)
{
$this->specifications = $specifications;
}
public function isSatisfiedBy(Item $item): bool
{
$satisfied = [];
foreach ($this->specifications as $specification) {
$satisfied[] = $specification->isSatisfiedBy($item);
}
return !in_array(false, $satisfied);
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
/**
* A logical OR specification.
*/
class Either extends AbstractSpecification
{
protected $left;
protected $right;
/**
* A composite wrapper of two specifications.
*
* @param SpecificationInterface $left
* @param SpecificationInterface $right
*/
public function __construct(SpecificationInterface $left, SpecificationInterface $right)
{
$this->left = $left;
$this->right = $right;
}
/**
* Returns the evaluation of both wrapped specifications as a logical OR.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item)
{
return $this->left->isSatisfiedBy($item) || $this->right->isSatisfiedBy($item);
}
}

View File

@@ -2,29 +2,19 @@
namespace DesignPatterns\Behavioral\Specification; namespace DesignPatterns\Behavioral\Specification;
/**
* An trivial item.
*/
class Item class Item
{ {
protected $price;
/** /**
* An item must have a price. * @var float
*
* @param int $price
*/ */
public function __construct($price) private $price;
public function __construct(float $price)
{ {
$this->price = $price; $this->price = $price;
} }
/** public function getPrice(): float
* Get the items price.
*
* @return int
*/
public function getPrice()
{ {
return $this->price; return $this->price;
} }

View File

@@ -1,33 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
/**
* A logical Not specification.
*/
class Not extends AbstractSpecification
{
protected $spec;
/**
* Creates a new specification wrapping another.
*
* @param SpecificationInterface $spec
*/
public function __construct(SpecificationInterface $spec)
{
$this->spec = $spec;
}
/**
* Returns the negated result of the wrapped specification.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item)
{
return !$this->spec->isSatisfiedBy($item);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
class NotSpecification implements SpecificationInterface
{
/**
* @var SpecificationInterface
*/
private $specification;
public function __construct(SpecificationInterface $specification)
{
$this->specification = $specification;
}
public function isSatisfiedBy(Item $item): bool
{
return !$this->specification->isSatisfiedBy($item);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
class OrSpecification implements SpecificationInterface
{
/**
* @var SpecificationInterface[]
*/
private $specifications;
/**
* @param SpecificationInterface[] ...$specifications
*/
public function __construct(SpecificationInterface ...$specifications)
{
$this->specifications = $specifications;
}
public function isSatisfiedBy(Item $item): bool
{
$satisfied = [];
foreach ($this->specifications as $specification) {
$satisfied[] = $specification->isSatisfiedBy($item);
}
return in_array(true, $satisfied);
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
/**
* A logical AND specification.
*/
class Plus extends AbstractSpecification
{
protected $left;
protected $right;
/**
* Creation of a logical AND of two specifications.
*
* @param SpecificationInterface $left
* @param SpecificationInterface $right
*/
public function __construct(SpecificationInterface $left, SpecificationInterface $right)
{
$this->left = $left;
$this->right = $right;
}
/**
* Checks if the composite AND of specifications passes.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item)
{
return $this->left->isSatisfiedBy($item) && $this->right->isSatisfiedBy($item);
}
}

View File

@@ -2,47 +2,35 @@
namespace DesignPatterns\Behavioral\Specification; namespace DesignPatterns\Behavioral\Specification;
/** class PriceSpecification implements SpecificationInterface
* A specification to check an Item is priced between min and max.
*/
class PriceSpecification extends AbstractSpecification
{ {
protected $maxPrice; /**
protected $minPrice; * @var float|null
*/
private $maxPrice;
/** /**
* Sets the optional maximum price. * @var float|null
*
* @param int $maxPrice
*/ */
public function setMaxPrice($maxPrice) private $minPrice;
/**
* @param float $minPrice
* @param float $maxPrice
*/
public function __construct($minPrice, $maxPrice)
{ {
$this->minPrice = $minPrice;
$this->maxPrice = $maxPrice; $this->maxPrice = $maxPrice;
} }
/** public function isSatisfiedBy(Item $item): bool
* Sets the optional minimum price.
*
* @param int $minPrice
*/
public function setMinPrice($minPrice)
{ {
$this->minPrice = $minPrice; if ($this->maxPrice !== null && $item->getPrice() > $this->maxPrice) {
}
/**
* Checks if Item price falls between bounds.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item)
{
if (!empty($this->maxPrice) && $item->getPrice() > $this->maxPrice) {
return false; return false;
} }
if (!empty($this->minPrice) && $item->getPrice() < $this->minPrice) {
if ($this->minPrice !== null && $item->getPrice() < $this->minPrice) {
return false; return false;
} }

View File

@@ -38,15 +38,9 @@ SpecificationInterface.php
:language: php :language: php
:linenos: :linenos:
AbstractSpecification.php OrSpecification.php
.. literalinclude:: AbstractSpecification.php .. literalinclude:: OrSpecification.php
:language: php
:linenos:
Either.php
.. literalinclude:: Either.php
:language: php :language: php
:linenos: :linenos:
@@ -56,15 +50,15 @@ PriceSpecification.php
:language: php :language: php
:linenos: :linenos:
Plus.php AndSpecification.php
.. literalinclude:: Plus.php .. literalinclude:: AndSpecification.php
:language: php :language: php
:linenos: :linenos:
Not.php NotSpecification.php
.. literalinclude:: Not.php .. literalinclude:: NotSpecification.php
:language: php :language: php
:linenos: :linenos:

View File

@@ -2,36 +2,7 @@
namespace DesignPatterns\Behavioral\Specification; namespace DesignPatterns\Behavioral\Specification;
/**
* An interface for a specification.
*/
interface SpecificationInterface interface SpecificationInterface
{ {
/** public function isSatisfiedBy(Item $item): bool;
* A boolean evaluation indicating if the object meets the specification.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item);
/**
* Creates a logical AND specification.
*
* @param SpecificationInterface $spec
*/
public function plus(SpecificationInterface $spec);
/**
* Creates a logical OR specification.
*
* @param SpecificationInterface $spec
*/
public function either(SpecificationInterface $spec);
/**
* Creates a logical not specification.
*/
public function not();
} }

View File

@@ -3,101 +3,44 @@
namespace DesignPatterns\Behavioral\Specification\Tests; namespace DesignPatterns\Behavioral\Specification\Tests;
use DesignPatterns\Behavioral\Specification\Item; use DesignPatterns\Behavioral\Specification\Item;
use DesignPatterns\Behavioral\Specification\NotSpecification;
use DesignPatterns\Behavioral\Specification\OrSpecification;
use DesignPatterns\Behavioral\Specification\AndSpecification;
use DesignPatterns\Behavioral\Specification\PriceSpecification; use DesignPatterns\Behavioral\Specification\PriceSpecification;
/**
* SpecificationTest tests the specification pattern.
*/
class SpecificationTest extends \PHPUnit_Framework_TestCase class SpecificationTest extends \PHPUnit_Framework_TestCase
{ {
public function testSimpleSpecification() public function testCanOr()
{ {
$item = new Item(100); $spec1 = new PriceSpecification(50, 99);
$spec = new PriceSpecification(); $spec2 = new PriceSpecification(101, 200);
$this->assertTrue($spec->isSatisfiedBy($item)); $orSpec = new OrSpecification($spec1, $spec2);
$spec->setMaxPrice(50); $this->assertFalse($orSpec->isSatisfiedBy(new Item(100)));
$this->assertFalse($spec->isSatisfiedBy($item)); $this->assertTrue($orSpec->isSatisfiedBy(new Item(51)));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(150)));
$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() public function testCanAnd()
{ {
$item = new Item(100); $spec1 = new PriceSpecification(50, 100);
$spec = new PriceSpecification(); $spec2 = new PriceSpecification(80, 200);
$not = $spec->not();
$this->assertFalse($not->isSatisfiedBy($item)); $orSpec = new AndSpecification($spec1, $spec2);
$spec->setMaxPrice(50); $this->assertFalse($orSpec->isSatisfiedBy(new Item(150)));
$this->assertTrue($not->isSatisfiedBy($item)); $this->assertFalse($orSpec->isSatisfiedBy(new Item(1)));
$this->assertFalse($orSpec->isSatisfiedBy(new Item(51)));
$spec->setMaxPrice(150); $this->assertTrue($orSpec->isSatisfiedBy(new Item(100)));
$this->assertFalse($not->isSatisfiedBy($item));
$spec->setMinPrice(101);
$this->assertTrue($not->isSatisfiedBy($item));
$spec->setMinPrice(100);
$this->assertFalse($not->isSatisfiedBy($item));
} }
public function testPlusSpecification() public function testCanNot()
{ {
$spec1 = new PriceSpecification(); $spec1 = new PriceSpecification(50, 100);
$spec2 = new PriceSpecification(); $orSpec = new NotSpecification($spec1);
$plus = $spec1->plus($spec2);
$item = new Item(100); $this->assertTrue($orSpec->isSatisfiedBy(new Item(150)));
$this->assertFalse($orSpec->isSatisfiedBy(new Item(50)));
$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));
} }
} }

View File

@@ -2,49 +2,34 @@
namespace DesignPatterns\Behavioral\State; namespace DesignPatterns\Behavioral\State;
/** class CreateOrder implements Order
* Class CreateOrder.
*/
class CreateOrder implements OrderInterface
{ {
/** /**
* @var array * @var array
*/ */
private $order; private $details;
/** /**
* @param array $order * @param array $details
*
* @throws \Exception
*/ */
public function __construct(array $order) public function __construct(array $details)
{ {
if (empty($order)) { $this->details = $details;
throw new \Exception('Order can not be empty!');
}
$this->order = $order;
} }
/**
* @return mixed
*/
public function shipOrder() public function shipOrder()
{ {
$this->order['status'] = 'shipping'; $this->details['status'] = 'shipping';
$this->order['updatedTime'] = time(); $this->details['updatedTime'] = time();
// Setting the new order status into database;
return $this->updateOrder($this->order);
} }
/**
* @throws \Exception
*
* @return mixed|void
*/
public function completeOrder() public function completeOrder()
{ {
//Can not complete the order which status is created, throw exception; throw new \Exception('Can not complete the order which status is created');
throw new \Exception('Can not complete the order which status is created!'); }
public function getStatus(): string
{
return $this->details['status'];
} }
} }

View File

@@ -2,10 +2,7 @@
namespace DesignPatterns\Behavioral\State; namespace DesignPatterns\Behavioral\State;
/** interface Order
* Class OrderInterface.
*/
interface OrderInterface
{ {
/** /**
* @return mixed * @return mixed
@@ -16,4 +13,6 @@ interface OrderInterface
* @return mixed * @return mixed
*/ */
public function completeOrder(); public function completeOrder();
public function getStatus(): string;
} }

View File

@@ -1,37 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\State;
/**
* Class OrderController.
*/
class OrderController
{
/**
* @param int $id
*/
public function shipAction($id)
{
$order = OrderFactory::getOrder($id);
try {
$order->shipOrder();
} catch (Exception $e) {
//handle error!
}
// response to browser
}
/**
* @param int $id
*/
public function completeAction($id)
{
$order = OrderFactory::getOrder($id);
try {
$order->completeOrder();
} catch (Exception $e) {
//handle error!
}
// response to browser
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\State;
/**
* Class OrderFactory.
*/
class OrderFactory
{
private function __construct()
{
throw new \Exception('Can not instance the OrderFactory class!');
}
/**
* @param int $id
*
* @throws \Exception
*
* @return CreateOrder|ShippingOrder
*/
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;
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace DesignPatterns\Behavioral\State;
class OrderRepository
{
/**
* @var array
*/
private static $orders = [
1 => ['status' => 'created'],
2 => ['status' => 'shipping'],
3 => ['status' => 'completed'],
];
public static function findById(int $id): Order
{
if (!isset(self::$orders[$id])) {
throw new \InvalidArgumentException(sprintf('Order with id %d does not exist', $id));
}
$order = self::$orders[$id];
switch ($order['status']) {
case 'created':
return new CreateOrder($order);
case 'shipping':
return new ShippingOrder($order);
default:
throw new \InvalidArgumentException('Invalid order status given');
break;
}
}
}

View File

@@ -20,21 +20,15 @@ Code
You can also find these code on `GitHub`_ You can also find these code on `GitHub`_
OrderController.php OrderRepository.php
.. literalinclude:: OrderController.php .. literalinclude:: OrderRepository.php
:language: php :language: php
:linenos: :linenos:
OrderFactory.php Order.php
.. literalinclude:: OrderFactory.php .. literalinclude:: Order.php
:language: php
:linenos:
OrderInterface.php
.. literalinclude:: OrderInterface.php
:language: php :language: php
:linenos: :linenos:

View File

@@ -2,49 +2,34 @@
namespace DesignPatterns\Behavioral\State; namespace DesignPatterns\Behavioral\State;
/** class ShippingOrder implements Order
* Class ShippingOrder.
*/
class ShippingOrder implements OrderInterface
{ {
/** /**
* @var array * @var array
*/ */
private $order; private $details;
/** /**
* @param array $order * @param array $details
*
* @throws \Exception
*/ */
public function __construct(array $order) public function __construct(array $details)
{ {
if (empty($order)) { $this->details = $details;
throw new \Exception('Order can not be empty!');
}
$this->order = $order;
} }
/**
* @throws \Exception
*
* @return mixed|void
*/
public function shipOrder() public function shipOrder()
{ {
//Can not ship the order which status is shipping, throw exception;
throw new \Exception('Can not ship the order which status is shipping!'); throw new \Exception('Can not ship the order which status is shipping!');
} }
/**
* @return mixed
*/
public function completeOrder() public function completeOrder()
{ {
$this->order['status'] = 'completed'; $this->details['status'] = 'completed';
$this->order['updatedTime'] = time(); $this->details['updatedTime'] = time();
}
// Setting the new order status into database; public function getStatus(): string
return $this->updateOrder($this->order); {
return $this->details['status'];
} }
} }

View File

@@ -0,0 +1,33 @@
<?php
namespace DesignPatterns\Behavioral\State\Tests;
use DesignPatterns\Behavioral\State\OrderRepository;
class StateTest extends \PHPUnit_Framework_TestCase
{
public function testCanShipCreatedOrder()
{
$order = (new OrderRepository())->findById(1);
$order->shipOrder();
$this->assertEquals('shipping', $order->getStatus());
}
public function testCanCompleteShippedOrder()
{
$order = (new OrderRepository())->findById(2);
$order->completeOrder();
$this->assertEquals('completed', $order->getStatus());
}
/**
* @expectedException \Exception
*/
public function testThrowsExceptionWhenTryingToCompleteCreatedOrder()
{
$order = (new OrderRepository())->findById(1);
$order->completeOrder();
}
}

View File

@@ -2,16 +2,13 @@
namespace DesignPatterns\Behavioral\Strategy; namespace DesignPatterns\Behavioral\Strategy;
/**
* Class ComparatorInterface.
*/
interface ComparatorInterface interface ComparatorInterface
{ {
/** /**
* @param mixed $a * @param mixed $a
* @param mixed $b * @param mixed $b
* *
* @return bool * @return int
*/ */
public function compare($a, $b); public function compare($a, $b): int;
} }

View File

@@ -2,23 +2,19 @@
namespace DesignPatterns\Behavioral\Strategy; namespace DesignPatterns\Behavioral\Strategy;
/**
* Class DateComparator.
*/
class DateComparator implements ComparatorInterface class DateComparator implements ComparatorInterface
{ {
/** /**
* {@inheritdoc} * @param mixed $a
* @param mixed $b
*
* @return int
*/ */
public function compare($a, $b) public function compare($a, $b): int
{ {
$aDate = new \DateTime($a['date']); $aDate = new \DateTime($a['date']);
$bDate = new \DateTime($b['date']); $bDate = new \DateTime($b['date']);
if ($aDate == $bDate) { return $aDate <=> $bDate;
return 0;
}
return $aDate < $bDate ? -1 : 1;
} }
} }

View File

@@ -2,20 +2,16 @@
namespace DesignPatterns\Behavioral\Strategy; namespace DesignPatterns\Behavioral\Strategy;
/**
* Class IdComparator.
*/
class IdComparator implements ComparatorInterface class IdComparator implements ComparatorInterface
{ {
/** /**
* {@inheritdoc} * @param mixed $a
* @param mixed $b
*
* @return int
*/ */
public function compare($a, $b) public function compare($a, $b): int
{ {
if ($a['id'] == $b['id']) { return $a['id'] <=> $b['id'];
return 0;
} else {
return $a['id'] < $b['id'] ? -1 : 1;
}
} }
} }

View File

@@ -2,9 +2,6 @@
namespace DesignPatterns\Behavioral\Strategy; namespace DesignPatterns\Behavioral\Strategy;
/**
* Class ObjectCollection.
*/
class ObjectCollection class ObjectCollection
{ {
/** /**
@@ -20,30 +17,24 @@ class ObjectCollection
/** /**
* @param array $elements * @param array $elements
*/ */
public function __construct(array $elements = array()) public function __construct(array $elements = [])
{ {
$this->elements = $elements; $this->elements = $elements;
} }
/** public function sort(): array
* @return array
*/
public function sort()
{ {
if (!$this->comparator) { if (!$this->comparator) {
throw new \LogicException('Comparator is not set'); throw new \LogicException('Comparator is not set');
} }
$callback = array($this->comparator, 'compare'); uasort($this->elements, [$this->comparator, 'compare']);
uasort($this->elements, $callback);
return $this->elements; return $this->elements;
} }
/** /**
* @param ComparatorInterface $comparator * @param ComparatorInterface $comparator
*
* @return void
*/ */
public function setComparator(ComparatorInterface $comparator) public function setComparator(ComparatorInterface $comparator)
{ {

View File

@@ -5,43 +5,42 @@ namespace DesignPatterns\Behavioral\Strategy\Tests;
use DesignPatterns\Behavioral\Strategy\DateComparator; use DesignPatterns\Behavioral\Strategy\DateComparator;
use DesignPatterns\Behavioral\Strategy\IdComparator; use DesignPatterns\Behavioral\Strategy\IdComparator;
use DesignPatterns\Behavioral\Strategy\ObjectCollection; use DesignPatterns\Behavioral\Strategy\ObjectCollection;
use DesignPatterns\Behavioral\Strategy\Strategy;
/**
* Tests for Strategy pattern.
*/
class StrategyTest extends \PHPUnit_Framework_TestCase class StrategyTest extends \PHPUnit_Framework_TestCase
{ {
public function getIdCollection() public function provideIntegers()
{ {
return array( return [
array( [
array(array('id' => 2), array('id' => 1), array('id' => 3)), [['id' => 2], ['id' => 1], ['id' => 3]],
array('id' => 1), ['id' => 1],
), ],
array( [
array(array('id' => 3), array('id' => 2), array('id' => 1)), [['id' => 3], ['id' => 2], ['id' => 1]],
array('id' => 1), ['id' => 1],
), ],
); ];
} }
public function getDateCollection() public function providateDates()
{ {
return array( return [
array( [
array(array('date' => '2014-03-03'), array('date' => '2015-03-02'), array('date' => '2013-03-01')), [['date' => '2014-03-03'], ['date' => '2015-03-02'], ['date' => '2013-03-01']],
array('date' => '2013-03-01'), ['date' => '2013-03-01'],
), ],
array( [
array(array('date' => '2014-02-03'), array('date' => '2013-02-01'), array('date' => '2015-02-02')), [['date' => '2014-02-03'], ['date' => '2013-02-01'], ['date' => '2015-02-02']],
array('date' => '2013-02-01'), ['date' => '2013-02-01'],
), ],
); ];
} }
/** /**
* @dataProvider getIdCollection * @dataProvider provideIntegers
*
* @param array $collection
* @param array $expected
*/ */
public function testIdComparator($collection, $expected) public function testIdComparator($collection, $expected)
{ {
@@ -54,7 +53,10 @@ class StrategyTest extends \PHPUnit_Framework_TestCase
} }
/** /**
* @dataProvider getDateCollection * @dataProvider providateDates
*
* @param array $collection
* @param array $expected
*/ */
public function testDateComparator($collection, $expected) public function testDateComparator($collection, $expected)
{ {

View File

@@ -2,16 +2,10 @@
namespace DesignPatterns\Behavioral\TemplateMethod; namespace DesignPatterns\Behavioral\TemplateMethod;
/**
* BeachJourney is vacation at the beach.
*/
class BeachJourney extends Journey class BeachJourney extends Journey
{ {
/** protected function enjoyVacation(): string
* prints what to do to enjoy your vacation.
*/
protected function enjoyVacation()
{ {
echo "Swimming and sun-bathing\n"; return "Swimming and sun-bathing";
} }
} }

View File

@@ -2,16 +2,15 @@
namespace DesignPatterns\Behavioral\TemplateMethod; namespace DesignPatterns\Behavioral\TemplateMethod;
/**
* CityJourney is a journey in a city.
*/
class CityJourney extends Journey class CityJourney extends Journey
{ {
/** protected function enjoyVacation(): string
* prints what to do in your journey to enjoy vacation.
*/
protected function enjoyVacation()
{ {
echo "Eat, drink, take photos and sleep\n"; return "Eat, drink, take photos and sleep";
}
protected function buyGift(): string
{
return "Buy a gift";
} }
} }

View File

@@ -4,6 +4,11 @@ namespace DesignPatterns\Behavioral\TemplateMethod;
abstract class Journey abstract class Journey
{ {
/**
* @var string[]
*/
private $thingsToDo = [];
/** /**
* This is the public service provided by this class and its subclasses. * This is the public service provided by this class and its subclasses.
* Notice it is final to "freeze" the global behavior of algorithm. * Notice it is final to "freeze" the global behavior of algorithm.
@@ -12,46 +17,49 @@ abstract class Journey
*/ */
final public function takeATrip() final public function takeATrip()
{ {
$this->buyAFlight(); $this->thingsToDo[] = $this->buyAFlight();
$this->takePlane(); $this->thingsToDo[] = $this->takePlane();
$this->enjoyVacation(); $this->thingsToDo[] = $this->enjoyVacation();
$this->buyGift(); $buyGift = $this->buyGift();
$this->takePlane();
if ($buyGift !== null) {
$this->thingsToDo[] = $buyGift;
}
$this->thingsToDo[] = $this->takePlane();
} }
/** /**
* This method must be implemented, this is the key-feature of this pattern. * This method must be implemented, this is the key-feature of this pattern.
*/ */
abstract protected function enjoyVacation(); abstract protected function enjoyVacation(): string;
/** /**
* This method is also part of the algorithm but it is optional. * This method is also part of the algorithm but it is optional.
* This is an "adapter" (do not confuse with the Adapter pattern, not related) * You can override it only if you need to
* You can override it only if you need to. *
* @return null|string
*/ */
protected function buyGift() protected function buyGift()
{ {
return null;
}
private function buyAFlight(): string
{
return 'Buy a flight ticket';
}
private function takePlane(): string
{
return 'Taking the plane';
} }
/** /**
* This method will be unknown by subclasses (better). * @return string[]
*/ */
private function buyAFlight() public function getThingsToDo(): array
{ {
echo "Buying a flight\n"; return $this->thingsToDo;
} }
/**
* Subclasses will get access to this method but cannot override it and
* compromise this algorithm (warning : cause of cyclic dependencies).
*/
final protected function takePlane()
{
echo "Taking the plane\n";
}
// A note regarding the keyword "final" : don't use it when you start coding :
// add it after you narrow and know exactly what change and what remain unchanged
// in this algorithm.
// [abstract] x [3 access] x [final] = 12 combinations, it can be hard !
} }

View File

@@ -4,40 +4,33 @@ namespace DesignPatterns\Behavioral\TemplateMethod\Tests;
use DesignPatterns\Behavioral\TemplateMethod; use DesignPatterns\Behavioral\TemplateMethod;
/**
* JourneyTest tests all journeys.
*/
class JourneyTest extends \PHPUnit_Framework_TestCase class JourneyTest extends \PHPUnit_Framework_TestCase
{ {
public function testBeach() public function testCanGetOnVacationOnTheBeach()
{ {
$journey = new TemplateMethod\BeachJourney(); $beachJourney = new TemplateMethod\BeachJourney();
$this->expectOutputRegex('#sun-bathing#'); $beachJourney->takeATrip();
$journey->takeATrip();
$this->assertEquals(
['Buy a flight ticket', 'Taking the plane', 'Swimming and sun-bathing', 'Taking the plane'],
$beachJourney->getThingsToDo()
);
} }
public function testCity() public function testCanGetOnAJourneyToACity()
{ {
$journey = new TemplateMethod\CityJourney(); $beachJourney = new TemplateMethod\CityJourney();
$this->expectOutputRegex('#drink#'); $beachJourney->takeATrip();
$journey->takeATrip();
}
/** $this->assertEquals(
* How to test an abstract template method with PHPUnit. [
*/ 'Buy a flight ticket',
public function testLasVegas() 'Taking the plane',
{ 'Eat, drink, take photos and sleep',
$journey = $this->getMockForAbstractClass('DesignPatterns\Behavioral\TemplateMethod\Journey'); 'Buy a gift',
$journey->expects($this->once()) 'Taking the plane'
->method('enjoyVacation') ],
->will($this->returnCallback(array($this, 'mockUpVacation'))); $beachJourney->getThingsToDo()
$this->expectOutputRegex('#Las Vegas#'); );
$journey->takeATrip();
}
public function mockUpVacation()
{
echo "Fear and loathing in Las Vegas\n";
} }
} }

View File

@@ -1,35 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Diagram> <Diagram>
<ID>PHP</ID> <ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\TemplateMethod\BeachJourney</OriginalElement> <OriginalElement>\DesignPatterns\Behavioral\TemplateMethod\BeachJourney</OriginalElement>
<nodes> <nodes>
<node x="152.0" y="177.0">\DesignPatterns\Behavioral\TemplateMethod\CityJourney</node> <node x="-18.0" y="234.0">\DesignPatterns\Behavioral\TemplateMethod\BeachJourney</node>
<node x="47.0" y="0.0">\DesignPatterns\Behavioral\TemplateMethod\Journey</node> <node x="13.0" y="-3.0">\DesignPatterns\Behavioral\TemplateMethod\Journey</node>
<node x="0.0" y="177.0">\DesignPatterns\Behavioral\TemplateMethod\BeachJourney</node> <node x="151.0" y="234.0">\DesignPatterns\Behavioral\TemplateMethod\CityJourney</node>
</nodes> </nodes>
<notes /> <notes />
<edges> <edges>
<edge source="\DesignPatterns\Behavioral\TemplateMethod\CityJourney" target="\DesignPatterns\Behavioral\TemplateMethod\Journey"> <edge source="\DesignPatterns\Behavioral\TemplateMethod\BeachJourney" target="\DesignPatterns\Behavioral\TemplateMethod\Journey">
<point x="0.0" y="-23.5" /> <point x="0.0" y="-23.5" />
<point x="218.0" y="152.0" /> <point x="66.0" y="152.0" />
<point x="189.5" y="152.0" /> <point x="94.5" y="152.0" />
<point x="47.5" y="63.5" /> <point x="-47.5" y="63.5" />
</edge> </edge>
<edge source="\DesignPatterns\Behavioral\TemplateMethod\BeachJourney" target="\DesignPatterns\Behavioral\TemplateMethod\Journey"> <edge source="\DesignPatterns\Behavioral\TemplateMethod\CityJourney" target="\DesignPatterns\Behavioral\TemplateMethod\Journey">
<point x="0.0" y="-23.5" /> <point x="0.0" y="-23.5" />
<point x="66.0" y="152.0" /> <point x="218.0" y="152.0" />
<point x="94.5" y="152.0" /> <point x="189.5" y="152.0" />
<point x="-47.5" y="63.5" /> <point x="47.5" y="63.5" />
</edge> </edge>
</edges> </edges>
<settings layout="Hierarchic Group" zoom="1.0" x="142.0" y="112.0" /> <settings layout="Hierarchic Group" zoom="1.0" x="142.0" y="112.0" />
<SelectedNodes /> <SelectedNodes />
<Categories> <Categories>
<Category>Fields</Category> <Category>Fields</Category>
<Category>Constants</Category> <Category>Constants</Category>
<Category>Constructors</Category> <Category>Constructors</Category>
<Category>Methods</Category> <Category>Methods</Category>
</Categories> </Categories>
<VISIBILITY>private</VISIBILITY> <VISIBILITY>private</VISIBILITY>
</Diagram> </Diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -2,29 +2,25 @@
namespace DesignPatterns\Behavioral\Visitor; namespace DesignPatterns\Behavioral\Visitor;
/** class Group implements Role
* An example of a Visitor: Group.
*/
class Group extends Role
{ {
/** /**
* @var string * @var string
*/ */
protected $name; private $name;
/** public function __construct(string $name)
* @param string $name
*/
public function __construct($name)
{ {
$this->name = (string) $name; $this->name = $name;
} }
/** public function getName(): string
* @return string
*/
public function getName()
{ {
return 'Group: '.$this->name; return sprintf('Group: %s', $this->name);
}
public function accept(RoleVisitorInterface $visitor)
{
$visitor->visitGroup($this);
} }
} }

View File

@@ -31,9 +31,9 @@ RoleVisitorInterface.php
:language: php :language: php
:linenos: :linenos:
RolePrintVisitor.php RoleVisitor.php
.. literalinclude:: RolePrintVisitor.php .. literalinclude:: RoleVisitor.php
:language: php :language: php
:linenos: :linenos:

View File

@@ -2,32 +2,7 @@
namespace DesignPatterns\Behavioral\Visitor; namespace DesignPatterns\Behavioral\Visitor;
/** interface Role
* class Role.
*/
abstract class Role
{ {
/** public function accept(RoleVisitorInterface $visitor);
* This method handles a double dispatch based on the short name of the Visitor.
*
* Feel free to override it if your object must call another visiting behavior
*
* @param \DesignPatterns\Behavioral\Visitor\RoleVisitorInterface $visitor
*
* @throws \InvalidArgumentException
*/
public function accept(RoleVisitorInterface $visitor)
{
// this trick to simulate double-dispatch based on type-hinting
$klass = get_called_class();
preg_match('#([^\\\\]+)$#', $klass, $extract);
$visitingMethod = 'visit'.$extract[1];
// this ensures strong typing with visitor interface, not some visitor objects
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);
}
} }

View File

@@ -1,27 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Visitor;
/**
* Visitor Pattern.
*
* An implementation of a concrete 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();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace DesignPatterns\Behavioral\Visitor;
class RoleVisitor implements RoleVisitorInterface
{
/**
* @var Role[]
*/
private $visited = [];
public function visitGroup(Group $role)
{
$this->visited[] = $role;
}
public function visitUser(User $role)
{
$this->visited[] = $role;
}
/**
* @return Role[]
*/
public function getVisited(): array
{
return $this->visited;
}
}

View File

@@ -3,29 +3,12 @@
namespace DesignPatterns\Behavioral\Visitor; namespace DesignPatterns\Behavioral\Visitor;
/** /**
* Visitor Pattern. * Note: the visitor must not choose itself which method to
* * invoke, it is the Visitee that make this decision
* The contract for the visitor.
*
* Note 1 : in C++ or Java, with method polymorphism based on type-hint, there are many
* methods visit() with different type for the 'role' parameter.
*
* Note 2 : the visitor must not choose itself which method to
* invoke, it is the Visitee that make this decision.
*/ */
interface RoleVisitorInterface interface RoleVisitorInterface
{ {
/**
* Visit a User object.
*
* @param \DesignPatterns\Behavioral\Visitor\User $role
*/
public function visitUser(User $role); public function visitUser(User $role);
/**
* Visit a Group object.
*
* @param \DesignPatterns\Behavioral\Visitor\Group $role
*/
public function visitGroup(Group $role); public function visitGroup(Group $role);
} }

View File

@@ -4,42 +4,34 @@ namespace DesignPatterns\Tests\Visitor\Tests;
use DesignPatterns\Behavioral\Visitor; use DesignPatterns\Behavioral\Visitor;
/**
* VisitorTest tests the visitor pattern.
*/
class VisitorTest extends \PHPUnit_Framework_TestCase class VisitorTest extends \PHPUnit_Framework_TestCase
{ {
protected $visitor; /**
* @var Visitor\RoleVisitor
*/
private $visitor;
protected function setUp() protected function setUp()
{ {
$this->visitor = new Visitor\RolePrintVisitor(); $this->visitor = new Visitor\RoleVisitor();
} }
public function getRole() public function provideRoles()
{ {
return array( return [
array(new Visitor\User('Dominik'), 'Role: User Dominik'), [new Visitor\User('Dominik')],
array(new Visitor\Group('Administrators'), 'Role: Group: Administrators'), [new Visitor\Group('Administrators')],
); ];
} }
/** /**
* @dataProvider getRole * @dataProvider provideRoles
*
* @param Visitor\Role $role
*/ */
public function testVisitSomeRole(Visitor\Role $role, $expect) public function testVisitSomeRole(Visitor\Role $role)
{ {
$this->expectOutputString($expect);
$role->accept($this->visitor); $role->accept($this->visitor);
} $this->assertSame($role, $this->visitor->getVisited()[0]);
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Mock
*/
public function testUnknownObject()
{
$mock = $this->getMockForAbstractClass('DesignPatterns\Behavioral\Visitor\Role');
$mock->accept($this->visitor);
} }
} }

View File

@@ -2,31 +2,25 @@
namespace DesignPatterns\Behavioral\Visitor; namespace DesignPatterns\Behavioral\Visitor;
/** class User implements Role
* Visitor Pattern.
*
* One example for a visitee. Each visitee has to extends Role
*/
class User extends Role
{ {
/** /**
* @var string * @var string
*/ */
protected $name; private $name;
/** public function __construct(string $name)
* @param string $name
*/
public function __construct($name)
{ {
$this->name = (string) $name; $this->name = $name;
} }
/** public function getName(): string
* @return string
*/
public function getName()
{ {
return 'User '.$this->name; return sprintf('User %s', $this->name);
}
public function accept(RoleVisitorInterface $visitor)
{
$visitor->visitUser($this);
} }
} }

View File

@@ -3,38 +3,10 @@
namespace DesignPatterns\Creational\AbstractFactory; namespace DesignPatterns\Creational\AbstractFactory;
/** /**
* class AbstractFactory.
*
* Sometimes also known as "Kit" in a GUI libraries.
*
* This design pattern implements the Dependency Inversion Principle since
* it is the concrete subclass which creates concrete components.
*
* In this case, the abstract factory is a contract for creating some components * In this case, the abstract factory is a contract for creating some components
* for the web. There are two components : Text and Picture. There are two ways * for the web. There are two ways of rendering text: HTML and JSON
* of rendering : HTML or JSON.
*
* Therefore 4 concrete classes, but the client just needs to know this contract
* to build a correct HTTP response (for a HTML page or for an AJAX request).
*/ */
abstract class AbstractFactory abstract class AbstractFactory
{ {
/** abstract public function createText(string $content): Text;
* Creates a text component.
*
* @param string $content
*
* @return Text
*/
abstract public function createText($content);
/**
* Creates a picture component.
*
* @param string $path
* @param string $name
*
* @return Picture
*/
abstract public function createPicture($path, $name = '');
} }

View File

@@ -1,23 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory\Html;
use DesignPatterns\Creational\AbstractFactory\Picture as BasePicture;
/**
* Class Picture.
*
* Picture is a concrete image for HTML rendering
*/
class Picture extends BasePicture
{
/**
* some crude rendering from HTML output.
*
* @return string
*/
public function render()
{
return sprintf('<img src="%s" title="%s"/>', $this->path, $this->name);
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory\Html;
use DesignPatterns\Creational\AbstractFactory\Text as BaseText;
/**
* Class Text.
*
* Text is a concrete text for HTML rendering
*/
class Text extends BaseText
{
/**
* some crude rendering from HTML output.
*
* @return string
*/
public function render()
{
return '<div>'.htmlspecialchars($this->text).'</div>';
}
}

View File

@@ -2,35 +2,10 @@
namespace DesignPatterns\Creational\AbstractFactory; namespace DesignPatterns\Creational\AbstractFactory;
/**
* Class HtmlFactory.
*
* HtmlFactory is a concrete factory for HTML component
*/
class HtmlFactory extends AbstractFactory class HtmlFactory extends AbstractFactory
{ {
/** public function createText(string $content): Text
* Creates a picture component.
*
* @param string $path
* @param string $name
*
* @return Html\Picture|Picture
*/
public function createPicture($path, $name = '')
{ {
return new Html\Picture($path, $name); return new HtmlText($content);
}
/**
* Creates a text component.
*
* @param string $content
*
* @return Html\Text|Text
*/
public function createText($content)
{
return new Html\Text($content);
} }
} }

View File

@@ -0,0 +1,8 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class HtmlText extends Text
{
// do something here
}

View File

@@ -1,23 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory\Json;
use DesignPatterns\Creational\AbstractFactory\Picture as BasePicture;
/**
* Class Picture.
*
* Picture is a concrete image for JSON rendering
*/
class Picture extends BasePicture
{
/**
* some crude rendering from JSON output.
*
* @return string
*/
public function render()
{
return json_encode(array('title' => $this->name, 'path' => $this->path));
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory\Json;
use DesignPatterns\Creational\AbstractFactory\Text as BaseText;
/**
* Class Text.
*
* Text is a text component with a JSON rendering
*/
class Text extends BaseText
{
/**
* some crude rendering from JSON output.
*
* @return string
*/
public function render()
{
return json_encode(array('content' => $this->text));
}
}

View File

@@ -2,36 +2,10 @@
namespace DesignPatterns\Creational\AbstractFactory; namespace DesignPatterns\Creational\AbstractFactory;
/**
* Class JsonFactory.
*
* JsonFactory is a factory for creating a family of JSON component
* (example for ajax)
*/
class JsonFactory extends AbstractFactory class JsonFactory extends AbstractFactory
{ {
/** public function createText(string $content): Text
* Creates a picture component.
*
* @param string $path
* @param string $name
*
* @return Json\Picture|Picture
*/
public function createPicture($path, $name = '')
{ {
return new Json\Picture($path, $name); return new JsonText($content);
}
/**
* Creates a text component.
*
* @param string $content
*
* @return Json\Text|Text
*/
public function createText($content)
{
return new Json\Text($content);
} }
} }

View File

@@ -0,0 +1,8 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class JsonText extends Text
{
// do something here
}

View File

@@ -1,19 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory;
/**
* Interface MediaInterface.
*
* This contract is not part of the pattern, in general case, each component
* are not related
*/
interface MediaInterface
{
/**
* some crude rendering from JSON or html output (depended on concrete class).
*
* @return string
*/
public function render();
}

View File

@@ -1,29 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory;
/**
* Class Picture.
*/
abstract class Picture implements MediaInterface
{
/**
* @var string
*/
protected $path;
/**
* @var string
*/
protected $name;
/**
* @param string $path
* @param string $name
*/
public function __construct($path, $name = '')
{
$this->name = (string) $name;
$this->path = (string) $path;
}
}

View File

@@ -39,45 +39,21 @@ HtmlFactory.php
:language: php :language: php
:linenos: :linenos:
MediaInterface.php
.. literalinclude:: MediaInterface.php
:language: php
:linenos:
Picture.php
.. literalinclude:: Picture.php
:language: php
:linenos:
Text.php Text.php
.. literalinclude:: Text.php .. literalinclude:: Text.php
:language: php :language: php
:linenos: :linenos:
Json/Picture.php JsonText.php
.. literalinclude:: Json/Picture.php .. literalinclude:: JsonText.php
:language: php :language: php
:linenos: :linenos:
Json/Text.php HtmlText.php
.. literalinclude:: Json/Text.php .. literalinclude:: HtmlText.php
:language: php
:linenos:
Html/Picture.php
.. literalinclude:: Html/Picture.php
:language: php
:linenos:
Html/Text.php
.. literalinclude:: Html/Text.php
:language: php :language: php
:linenos: :linenos:

View File

@@ -2,43 +2,24 @@
namespace DesignPatterns\Creational\AbstractFactory\Tests; namespace DesignPatterns\Creational\AbstractFactory\Tests;
use DesignPatterns\Creational\AbstractFactory\AbstractFactory;
use DesignPatterns\Creational\AbstractFactory\HtmlFactory; use DesignPatterns\Creational\AbstractFactory\HtmlFactory;
use DesignPatterns\Creational\AbstractFactory\JsonFactory; use DesignPatterns\Creational\AbstractFactory\JsonFactory;
/**
* AbstractFactoryTest tests concrete factories.
*/
class AbstractFactoryTest extends \PHPUnit_Framework_TestCase class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
{ {
public function getFactories() public function testCanCreateHtmlText()
{ {
return array( $factory = new HtmlFactory();
array(new JsonFactory()), $text = $factory->createText('foobar');
array(new HtmlFactory()),
); $this->assertInstanceOf('DesignPatterns\Creational\AbstractFactory\HtmlText', $text);
} }
/** public function testCanCreateJsonText()
* This is the client of factories. Note that the client does not
* care which factory is given to him, he can create any component he
* wants and render how he wants.
*
* @dataProvider getFactories
*/
public function testComponentCreation(AbstractFactory $factory)
{ {
$article = array( $factory = new JsonFactory();
$factory->createText('Lorem Ipsum'), $text = $factory->createText('foobar');
$factory->createPicture('/image.jpg', 'caption'),
$factory->createText('footnotes'),
);
$this->assertContainsOnly('DesignPatterns\Creational\AbstractFactory\MediaInterface', $article); $this->assertInstanceOf('DesignPatterns\Creational\AbstractFactory\JsonText', $text);
/* this is the time to look at the Builder pattern. This pattern
* helps you to create complex object like that article above with
* a given Abstract Factory
*/
} }
} }

View File

@@ -2,21 +2,15 @@
namespace DesignPatterns\Creational\AbstractFactory; namespace DesignPatterns\Creational\AbstractFactory;
/** abstract class Text
* Class Text.
*/
abstract class Text implements MediaInterface
{ {
/** /**
* @var string * @var string
*/ */
protected $text; private $text;
/** public function __construct(string $text)
* @param string $text
*/
public function __construct($text)
{ {
$this->text = (string) $text; $this->text = $text;
} }
} }

View File

@@ -1,38 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Diagram> <Diagram>
<ID>PHP</ID> <ID>PHP</ID>
<OriginalElement>\DesignPatterns\Creational\AbstractFactory\AbstractFactory</OriginalElement> <OriginalElement>\DesignPatterns\Creational\AbstractFactory\AbstractFactory</OriginalElement>
<nodes> <nodes>
<node x="281.0" y="228.0">\DesignPatterns\Creational\AbstractFactory\Html\Text</node> <node x="214.0" y="101.0">\DesignPatterns\Creational\AbstractFactory\JsonFactory</node>
<node x="150.0" y="229.0">\DesignPatterns\Creational\AbstractFactory\Html\Picture</node> <node x="107.0" y="0.0">\DesignPatterns\Creational\AbstractFactory\AbstractFactory</node>
<node x="223.0" y="117.0">\DesignPatterns\Creational\AbstractFactory\HtmlFactory</node> <node x="0.0" y="101.0">\DesignPatterns\Creational\AbstractFactory\HtmlFactory</node>
<node x="0.0" y="117.0">\DesignPatterns\Creational\AbstractFactory\JsonFactory</node> <node x="111.0" y="298.0">\DesignPatterns\Creational\AbstractFactory\JsonText</node>
<node x="126.0" y="0.0">\DesignPatterns\Creational\AbstractFactory\AbstractFactory</node> <node x="0.0" y="298.0">\DesignPatterns\Creational\AbstractFactory\HtmlText</node>
<node x="0.0" y="229.0">\DesignPatterns\Creational\AbstractFactory\MediaInterface</node> <node x="51.5" y="197.0">\DesignPatterns\Creational\AbstractFactory\Text</node>
</nodes> </nodes>
<notes /> <notes />
<edges> <edges>
<edge source="\DesignPatterns\Creational\AbstractFactory\HtmlFactory" target="\DesignPatterns\Creational\AbstractFactory\AbstractFactory"> <edge source="\DesignPatterns\Creational\AbstractFactory\HtmlText" target="\DesignPatterns\Creational\AbstractFactory\Text">
<point x="0.0" y="-33.5" /> <point x="0.0" y="-14.5" />
<point x="324.5" y="92.0" /> <point x="45.5" y="273.0" />
<point x="256.5" y="92.0" /> <point x="75.75" y="273.0" />
<point x="43.5" y="33.5" /> <point x="-24.25" y="25.5" />
</edge> </edge>
<edge source="\DesignPatterns\Creational\AbstractFactory\JsonFactory" target="\DesignPatterns\Creational\AbstractFactory\AbstractFactory"> <edge source="\DesignPatterns\Creational\AbstractFactory\JsonText" target="\DesignPatterns\Creational\AbstractFactory\Text">
<point x="0.0" y="-33.5" /> <point x="0.0" y="-14.5" />
<point x="101.5" y="92.0" /> <point x="154.5" y="273.0" />
<point x="169.5" y="92.0" /> <point x="124.25" y="273.0" />
<point x="-43.5" y="33.5" /> <point x="24.25" y="25.5" />
</edge> </edge>
</edges> <edge source="\DesignPatterns\Creational\AbstractFactory\JsonFactory" target="\DesignPatterns\Creational\AbstractFactory\AbstractFactory">
<settings layout="Hierarchic Group" zoom="1.0" x="213.0" y="138.0" /> <point x="0.0" y="-25.5" />
<SelectedNodes /> <point x="311.0" y="76.0" />
<Categories> <point x="252.5" y="76.0" />
<Category>Fields</Category> <point x="48.5" y="25.5" />
<Category>Constants</Category> </edge>
<Category>Constructors</Category> <edge source="\DesignPatterns\Creational\AbstractFactory\HtmlFactory" target="\DesignPatterns\Creational\AbstractFactory\AbstractFactory">
<Category>Methods</Category> <point x="0.0" y="-25.5" />
</Categories> <point x="97.0" y="76.0" />
<VISIBILITY>private</VISIBILITY> <point x="155.5" y="76.0" />
</Diagram> <point x="-48.5" y="25.5" />
</edge>
</edges>
<settings layout="Hierarchic Group" zoom="1.0" x="117.0" y="130.5" />
<SelectedNodes>
<node>\DesignPatterns\Creational\AbstractFactory\AbstractFactory</node>
</SelectedNodes>
<Categories>
<Category>Methods</Category>
<Category>Constants</Category>
<Category>Fields</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>

Some files were not shown because too many files have changed in this diff Show More