
2024-05-10
Структурные шаблоны проектирования (Structural)
2023-10-24
Структурные паттерны проектирования ( Structural ) - упрощают проектирование путем выявления простого способа реализовать отношения между субъектами.
Адаптер призван привести нестандартный или неудобный интерфейс какого-то класса в интерфейс, совместимый с вашим кодом. Адаптер позволяет классам работать вместе стандартным образом, что обычно не получается из-за несовместимых интерфейсов, предоставляя для этого прослойку с интерфейсом, удобным для клиентов, самостоятельно используя оригинальный интерфейс.
Book.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Adapter;
interface Book
{
public function turnPage();
public function open();
public function getPage(): int;
}
PaperBook.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Adapter;
class PaperBook implements Book
{
private int $page;
public function open(): void
{
$this->page = 1;
}
public function turnPage(): void
{
$this->page++;
}
public function getPage(): int
{
return $this->page;
}
}
EBook.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Adapter;
interface EBook
{
public function unlock();
public function pressNext();
/**
* returns current page and total number of pages, like [10, 100] is page 10 of 100
*
* @return int[]
*/
public function getPage(): array;
}
EBookAdapter.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Adapter;
/**
* This is the adapter here. Notice it implements Book,
* therefore you don't have to change the code of the client which is using a Book
*/
class EBookAdapter implements Book
{
public function __construct(protected EBook $eBook)
{
}
/**
* This class makes the proper translation from one interface to another.
*/
public function open()
{
$this->eBook->unlock();
}
public function turnPage()
{
$this->eBook->pressNext();
}
/**
* notice the adapted behavior here: EBook::getPage() will return two integers, but Book
* supports only a current page getter, so we adapt the behavior here
*/
public function getPage(): int
{
return $this->eBook->getPage()[0];
}
}
Kindle.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Adapter;
/**
* this is the adapted class. In production code, this could be a class from another package, some vendor code.
* Notice that it uses another naming scheme and the implementation does something similar but in another way
*/
class Kindle implements EBook
{
private int $page = 1;
private int $totalPages = 100;
public function pressNext()
{
$this->page++;
}
public function unlock()
{
}
/**
* returns current page and total number of pages, like [10, 100] is page 10 of 100
*
* @return int[]
*/
public function getPage(): array
{
return [$this->page, $this->totalPages];
}
}
Tests/AdapterTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Adapter\Tests;
use DesignPatterns\Structural\Adapter\PaperBook;
use DesignPatterns\Structural\Adapter\EBookAdapter;
use DesignPatterns\Structural\Adapter\Kindle;
use PHPUnit\Framework\TestCase;
class AdapterTest extends TestCase
{
public function testCanTurnPageOnBook()
{
$book = new PaperBook();
$book->open();
$book->turnPage();
$this->assertSame(2, $book->getPage());
}
public function testCanTurnPageOnKindleLikeInANormalBook()
{
$kindle = new Kindle();
$book = new EBookAdapter($kindle);
$book->open();
$book->turnPage();
$this->assertSame(2, $book->getPage());
}
}
Призван отделить абстракцию от её реализации так, что они могут изменяться независимо друг от друга.
Formatter.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
interface Formatter
{
public function format(string $text): string;
}
PlainTextFormatter.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
class PlainTextFormatter implements Formatter
{
public function format(string $text): string
{
return $text;
}
}
HtmlFormatter.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
class HtmlFormatter implements Formatter
{
public function format(string $text): string
{
return sprintf('<p>%s</p>', $text);
}
}
Service.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
abstract class Service
{
public function __construct(protected Formatter $implementation)
{
}
final public function setImplementation(Formatter $printer)
{
$this->implementation = $printer;
}
abstract public function get(): string;
}
HelloWorldService.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
class HelloWorldService extends Service
{
public function get(): string
{
return $this->implementation->format('Hello World');
}
}
PingService.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge;
class PingService extends Service
{
public function get(): string
{
return $this->implementation->format('pong');
}
}
Tests/BridgeTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Bridge\Tests;
use DesignPatterns\Structural\Bridge\HelloWorldService;
use DesignPatterns\Structural\Bridge\HtmlFormatter;
use DesignPatterns\Structural\Bridge\PlainTextFormatter;
use PHPUnit\Framework\TestCase;
class BridgeTest extends TestCase
{
public function testCanPrintUsingThePlainTextFormatter()
{
$service = new HelloWorldService(new PlainTextFormatter());
$this->assertSame('Hello World', $service->get());
}
public function testCanPrintUsingTheHtmlFormatter()
{
$service = new HelloWorldService(new HtmlFormatter());
$this->assertSame('<p>Hello World</p>', $service->get());
}
}
Предназначен для взаимодействия с иерархической группой объектов также, как и с отдельно взятым экземпляром.
Например экземпляр класса Form обрабатывает все свои элементы формы, как будто это один экземпляр. И когда вызывается метод render(), он перебирает все дочерние элементы и вызывает их собственный render().
Renderable.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Composite;
interface Renderable
{
public function render(): string;
}
Form.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Composite;
/**
* The composite node MUST extend the component contract. This is mandatory for building
* a tree of components.
*/
class Form implements Renderable
{
/**
* @var Renderable[]
*/
private array $elements;
/**
* runs through all elements and calls render() on them, then returns the complete representation
* of the form.
*
* from the outside, one will not see this and the form will act like a single object instance
*/
public function render(): string
{
$formCode = '<form>';
foreach ($this->elements as $element) {
$formCode .= $element->render();
}
return $formCode . '</form>';
}
public function addElement(Renderable $element)
{
$this->elements[] = $element;
}
}
InputElement.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Composite;
class InputElement implements Renderable
{
public function render(): string
{
return '<input type="text" />';
}
}
TextElement.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Composite;
class TextElement implements Renderable
{
public function __construct(private string $text)
{
}
public function render(): string
{
return $this->text;
}
}
Tests/CompositeTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Composite\Tests;
use DesignPatterns\Structural\Composite\Form;
use DesignPatterns\Structural\Composite\TextElement;
use DesignPatterns\Structural\Composite\InputElement;
use PHPUnit\Framework\TestCase;
class CompositeTest extends TestCase
{
public function testRender()
{
$form = new Form();
$form->addElement(new TextElement('Email:'));
$form->addElement(new InputElement());
$embed = new Form();
$embed->addElement(new TextElement('Password:'));
$embed->addElement(new InputElement());
$form->addElement($embed);
// This is just an example, in a real world scenario it is important to remember that web browsers do not
// currently support nested forms
$this->assertSame(
'<form>Email:<input type="text" /><form>Password:<input type="text" /></form></form>',
$form->render()
);
}
}
Преобразователь Данных — это паттерн, который выступает в роли посредника для двунаправленной передачи данных между постоянным хранилищем данных (часто, реляционной базы данных) и представления данных в памяти (слой домена, то что уже загружено и используется для логической обработки). Цель паттерна в том, чтобы держать представление данных в памяти и постоянное хранилище данных независимыми друг от друга и от самого преобразователя данных. Слой состоит из одного или более mapper-а (или объектов доступа к данным), отвечающих за передачу данных. Реализации mapper-ов различаются по назначению. Общие mapper-ы могут обрабатывать всевозоможные типы сущностей доменов, а выделенные mapper-ы будет обрабатывать один или несколько конкретных типов.
Ключевым моментом этого паттерна, в отличие от Активной Записи (Active Records) является то, что модель данных следует Принципу Единой Обязанности SOLID.
Пример - DB Object Relational Mapper (ORM) : Doctrine2 использует DAO под названием «EntityRepository»
User.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\DataMapper;
class User
{
public static function fromState(array $state): User
{
// validate state before accessing keys!
return new self(
$state['username'],
$state['email']
);
}
public function __construct(private string $username, private string $email)
{
}
public function getUsername(): string
{
return $this->username;
}
public function getEmail(): string
{
return $this->email;
}
}
UserMapper.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\DataMapper;
use InvalidArgumentException;
class UserMapper
{
public function __construct(private StorageAdapter $adapter)
{
}
/**
* finds a user from storage based on ID and returns a User object located
* in memory. Normally this kind of logic will be implemented using the Repository pattern.
* However the important part is in mapRowToUser() below, that will create a business object from the
* data fetched from storage
*/
public function findById(int $id): User
{
$result = $this->adapter->find($id);
if ($result === null) {
throw new InvalidArgumentException("User #$id not found");
}
return $this->mapRowToUser($result);
}
private function mapRowToUser(array $row): User
{
return User::fromState($row);
}
}
StorageAdapter.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\DataMapper;
class StorageAdapter
{
public function __construct(private array $data)
{
}
/**
* @return array|null
*/
public function find(int $id)
{
if (isset($this->data[$id])) {
return $this->data[$id];
}
return null;
}
}
Tests/DataMapperTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\DataMapper\Tests;
use InvalidArgumentException;
use DesignPatterns\Structural\DataMapper\StorageAdapter;
use DesignPatterns\Structural\DataMapper\User;
use DesignPatterns\Structural\DataMapper\UserMapper;
use PHPUnit\Framework\TestCase;
class DataMapperTest extends TestCase
{
public function testCanMapUserFromStorage()
{
$storage = new StorageAdapter([1 => ['username' => 'someone', 'email' => 'someone@example.com']]);
$mapper = new UserMapper($storage);
$user = $mapper->findById(1);
$this->assertInstanceOf(User::class, $user);
}
public function testWillNotMapInvalidData()
{
$this->expectException(InvalidArgumentException::class);
$storage = new StorageAdapter([]);
$mapper = new UserMapper($storage);
$mapper->findById(1);
}
}
Динамически добавляет новую функциональность в экземпляры классов.
Пример - Web Service Layer: Декораторы JSON и XML для REST сервисов (в этом случае, конечно, только один из них может быть разрешен).
Booking.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Decorator;
interface Booking
{
public function calculatePrice(): int;
public function getDescription(): string;
}
BookingDecorator.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Decorator;
abstract class BookingDecorator implements Booking
{
public function __construct(protected Booking $booking)
{
}
}
DoubleRoomBooking.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Decorator;
class DoubleRoomBooking implements Booking
{
public function calculatePrice(): int
{
return 40;
}
public function getDescription(): string
{
return 'double room';
}
}
ExtraBed.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Decorator;
class ExtraBed extends BookingDecorator
{
private const PRICE = 30;
public function calculatePrice(): int
{
return $this->booking->calculatePrice() + self::PRICE;
}
public function getDescription(): string
{
return $this->booking->getDescription() . ' with extra bed';
}
}
WiFi.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Decorator;
class WiFi extends BookingDecorator
{
private const PRICE = 2;
public function calculatePrice(): int
{
return $this->booking->calculatePrice() + self::PRICE;
}
public function getDescription(): string
{
return $this->booking->getDescription() . ' with wifi';
}
}
Tests/DecoratorTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Decorator\Tests;
use DesignPatterns\Structural\Decorator\DoubleRoomBooking;
use DesignPatterns\Structural\Decorator\ExtraBed;
use DesignPatterns\Structural\Decorator\WiFi;
use PHPUnit\Framework\TestCase;
class DecoratorTest extends TestCase
{
public function testCanCalculatePriceForBasicDoubleRoomBooking()
{
$booking = new DoubleRoomBooking();
$this->assertSame(40, $booking->calculatePrice());
$this->assertSame('double room', $booking->getDescription());
}
public function testCanCalculatePriceForDoubleRoomBookingWithWiFi()
{
$booking = new DoubleRoomBooking();
$booking = new WiFi($booking);
$this->assertSame(42, $booking->calculatePrice());
$this->assertSame('double room with wifi', $booking->getDescription());
}
public function testCanCalculatePriceForDoubleRoomBookingWithWiFiAndExtraBed()
{
$booking = new DoubleRoomBooking();
$booking = new WiFi($booking);
$booking = new ExtraBed($booking);
$this->assertSame(72, $booking->calculatePrice());
$this->assertSame('double room with wifi with extra bed', $booking->getDescription());
}
}
Используется для реализации слабосвязанной архитектуры. Чтобы получить более тестируемый, сопровождаемый и расширяемый код.
Объект DatabaseConfiguration внедряется в DatabaseConnection и последний получает всё, что ему необходимо из переменной $ config. Без DI, конфигурация будет создана непосредственно в Connection, что не очень хорошо для тестирования и расширения Connection, так как связывает эти классы напрямую.
DatabaseConfiguration.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\DependencyInjection;
class DatabaseConfiguration
{
public function __construct(
private string $host,
private int $port,
private string $username,
private string $password
) {
}
public function getHost(): string
{
return $this->host;
}
public function getPort(): int
{
return $this->port;
}
public function getUsername(): string
{
return $this->username;
}
public function getPassword(): string
{
return $this->password;
}
}
DatabaseConnection.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\DependencyInjection;
class DatabaseConnection
{
public function __construct(private DatabaseConfiguration $configuration)
{
}
public function getDsn(): string
{
// this is just for the sake of demonstration, not a real DSN
// notice that only the injected config is used here, so there is
// a real separation of concerns here
return sprintf(
'%s:%s@%s:%d',
$this->configuration->getUsername(),
$this->configuration->getPassword(),
$this->configuration->getHost(),
$this->configuration->getPort()
);
}
}
Tests/DependencyInjectionTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\DependencyInjection\Tests;
use DesignPatterns\Structural\DependencyInjection\DatabaseConfiguration;
use DesignPatterns\Structural\DependencyInjection\DatabaseConnection;
use PHPUnit\Framework\TestCase;
class DependencyInjectionTest extends TestCase
{
public function testDependencyInjection()
{
$config = new DatabaseConfiguration('localhost', 3306, 'user', '1234');
$connection = new DatabaseConnection($config);
$this->assertSame('user:1234@localhost:3306', $connection->getDsn());
}
}
Основная цель шаблона фасада — не избавить вас от необходимости читать руководство по сложному API. Это всего лишь побочный эффект. Первая цель — уменьшить связь и следовать Закону Деметры.
Фасад предназначен для разделения клиента и подсистемы путем внедрения многих (но иногда только одного) интерфейсов, и, конечно, уменьшения общей сложности.
Вот почему хороший фасад не содержит созданий экземпляров классов (new) внутри. Если внутри фасада создаются объекты для реализации каждого метода, это не Фасад, это Строитель или [Абстрактная|Статическая|Простая] Фабрика [или Фабричный Метод].
Лучший фасад не содержит new или конструктора с type-hinted параметрами. Если вам необходимо создавать новые экземпляры классов, в таком случае лучше использовать Фабрику в качестве аргумента.
Facade.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Facade;
class Facade
{
public function __construct(private Bios $bios, private OperatingSystem $os)
{
}
public function turnOn()
{
$this->bios->execute();
$this->bios->waitForKeyPress();
$this->bios->launch($this->os);
}
public function turnOff()
{
$this->os->halt();
$this->bios->powerDown();
}
}
OperatingSystem.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Facade;
interface OperatingSystem
{
public function halt();
public function getName(): string;
}
Bios.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Facade;
interface Bios
{
public function execute();
public function waitForKeyPress();
public function launch(OperatingSystem $os);
public function powerDown();
}
Tests/FacadeTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Facade\Tests;
use DesignPatterns\Structural\Facade\Bios;
use DesignPatterns\Structural\Facade\Facade;
use DesignPatterns\Structural\Facade\OperatingSystem;
use PHPUnit\Framework\TestCase;
class FacadeTest extends TestCase
{
public function testComputerOn()
{
$os = $this->createMock(OperatingSystem::class);
$os->method('getName')
->will($this->returnValue('Linux'));
$bios = $this->createMock(Bios::class);
$bios->method('launch')
->with($os);
/** @noinspection PhpParamsInspection */
$facade = new Facade($bios, $os);
$facade->turnOn();
$this->assertSame('Linux', $os->getName());
}
}
Писать код, который легко читается, как предложения в естественном языке (вроде русского или английского).
Примеры:
Sql.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\FluentInterface;
class Sql implements \Stringable
{
private array $fields = [];
private array $from = [];
private array $where = [];
public function select(array $fields): Sql
{
$this->fields = $fields;
return $this;
}
public function from(string $table, string $alias): Sql
{
$this->from[] = $table . ' AS ' . $alias;
return $this;
}
public function where(string $condition): Sql
{
$this->where[] = $condition;
return $this;
}
public function __toString(): string
{
return sprintf(
'SELECT %s FROM %s WHERE %s',
join(', ', $this->fields),
join(', ', $this->from),
join(' AND ', $this->where)
);
}
}
Tests/FluentInterfaceTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\FluentInterface\Tests;
use DesignPatterns\Structural\FluentInterface\Sql;
use PHPUnit\Framework\TestCase;
class FluentInterfaceTest extends TestCase
{
public function testBuildSQL()
{
$query = (new Sql())
->select(['foo', 'bar'])
->from('foobar', 'f')
->where('f.bar = ?');
$this->assertSame('SELECT foo, bar FROM foobar AS f WHERE f.bar = ?', (string) $query);
}
}
Для уменьшения использования памяти Приспособленец разделяет как можно больше памяти между аналогичными объектами. Это необходимо, когда используется большое количество объектов, состояние которых не сильно отличается. Обычной практикой является хранение состояния во внешних структурах и передавать их в объект-приспособленец, когда необходимо.
Text.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Flyweight;
/**
* This is the interface that all flyweights need to implement
*/
interface Text
{
public function render(string $extrinsicState): string;
}
Word.php
<?php
namespace DesignPatterns\Structural\Flyweight;
class Word implements Text
{
public function __construct(private string $name)
{
}
public function render(string $extrinsicState): string
{
return sprintf('Word %s with font %s', $this->name, $extrinsicState);
}
}
Charachter.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Flyweight;
/**
* Implements the flyweight interface and adds storage for intrinsic state, if any.
* Instances of concrete flyweights are shared by means of a factory.
*/
class Character implements Text
{
/**
* Any state stored by the concrete flyweight must be independent of its context.
* For flyweights representing characters, this is usually the corresponding character code.
*/
public function __construct(private string $name)
{
}
public function render(string $extrinsicState): string
{
// Clients supply the context-dependent information that the flyweight needs to draw itself
// For flyweights representing characters, extrinsic state usually contains e.g. the font.
return sprintf('Character %s with font %s', $this->name, $extrinsicState);
}
}
TextFactory.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Flyweight;
use Countable;
/**
* A factory manages shared flyweights. Clients should not instantiate them directly,
* but let the factory take care of returning existing objects or creating new ones.
*/
class TextFactory implements Countable
{
/**
* @var Text[]
*/
private array $charPool = [];
public function get(string $name): Text
{
if (!isset($this->charPool[$name])) {
$this->charPool[$name] = $this->create($name);
}
return $this->charPool[$name];
}
private function create(string $name): Text
{
if (strlen($name) == 1) {
return new Character($name);
}
return new Word($name);
}
public function count(): int
{
return count($this->charPool);
}
}
Tests/FlyweightTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Flyweight\Tests;
use DesignPatterns\Structural\Flyweight\TextFactory;
use PHPUnit\Framework\TestCase;
class FlyweightTest extends TestCase
{
private array $characters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
private array $fonts = ['Arial', 'Times New Roman', 'Verdana', 'Helvetica'];
public function testFlyweight()
{
$factory = new TextFactory();
for ($i = 0; $i <= 10; $i++) {
foreach ($this->characters as $char) {
foreach ($this->fonts as $font) {
$flyweight = $factory->get($char);
$rendered = $flyweight->render($font);
$this->assertSame(sprintf('Character %s with font %s', $char, $font), $rendered);
}
}
}
foreach ($this->fonts as $word) {
$flyweight = $factory->get($word);
$rendered = $flyweight->render('foobar');
$this->assertSame(sprintf('Word %s with font foobar', $word), $rendered);
}
// Flyweight pattern ensures that instances are shared
// instead of having hundreds of thousands of individual objects
// there must be one instance for every char that has been reused for displaying in different fonts
$this->assertCount(count($this->characters) + count($this->fonts), $factory);
}
}
Создать интерфейс взаимодействия с любым классом, который трудно или невозможно использовать в оригинальном виде.
Doctrine2 использует прокси для реализации магии фреймворка (например, для ленивой инициализации), в то время как пользователь работает со своими собственными классами сущностей и никогда не будет использовать прокси.
BankAccount.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Proxy;
interface BankAccount
{
public function deposit(int $amount);
public function getBalance(): int;
}
HeavyBankAccount.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Proxy;
class HeavyBankAccount implements BankAccount
{
/**
* @var int[]
*/
private array $transactions = [];
public function deposit(int $amount)
{
$this->transactions[] = $amount;
}
public function getBalance(): int
{
// this is the heavy part, imagine all the transactions even from
// years and decades ago must be fetched from a database or web service
// and the balance must be calculated from it
return array_sum($this->transactions);
}
}
BankAccountProxy.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Proxy;
class BankAccountProxy extends HeavyBankAccount implements BankAccount
{
private ?int $balance = null;
public function getBalance(): int
{
// because calculating balance is so expensive,
// the usage of BankAccount::getBalance() is delayed until it really is needed
// and will not be calculated again for this instance
if ($this->balance === null) {
$this->balance = parent::getBalance();
}
return $this->balance;
}
}
Tests/ProxyTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Proxy\Tests;
use DesignPatterns\Structural\Proxy\BankAccountProxy;
use PHPUnit\Framework\TestCase;
class ProxyTest extends TestCase
{
public function testProxyWillOnlyExecuteExpensiveGetBalanceOnce()
{
$bankAccount = new BankAccountProxy();
$bankAccount->deposit(30);
// this time balance is being calculated
$this->assertSame(30, $bankAccount->getBalance());
// inheritance allows for BankAccountProxy to behave to an outsider exactly like ServerBankAccount
$bankAccount->deposit(50);
// this time the previously calculated balance is returned again without re-calculating it
$this->assertSame(30, $bankAccount->getBalance());
}
}
Для реализации централизованного хранения объектов, часто используемых во всем приложении, как правило, реализуется с помощью абстрактного класса только c статическими методами (или с помощью шаблона Singleton). Помните, что это вводит глобальное состояние, которого следует избегать. Используйте Dependency Injection вместо Registry.
Registry.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Registry;
use InvalidArgumentException;
abstract class Registry
{
public const LOGGER = 'logger';
/**
* this introduces global state in your application which can not be mocked up for testing
* and is therefor considered an anti-pattern! Use dependency injection instead!
*
* @var Service[]
*/
private static array $services = [];
private static array $allowedKeys = [
self::LOGGER,
];
final public static function set(string $key, Service $value)
{
if (!in_array($key, self::$allowedKeys)) {
throw new InvalidArgumentException('Invalid key given');
}
self::$services[$key] = $value;
}
final public static function get(string $key): Service
{
if (!in_array($key, self::$allowedKeys) || !isset(self::$services[$key])) {
throw new InvalidArgumentException('Invalid key given');
}
return self::$services[$key];
}
}
Service.php
<?php
namespace DesignPatterns\Structural\Registry;
class Service
{
}
Tests/RegistryTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Structural\Registry\Tests;
use InvalidArgumentException;
use DesignPatterns\Structural\Registry\Registry;
use DesignPatterns\Structural\Registry\Service;
use PHPUnit\Framework\TestCase;
class RegistryTest extends TestCase
{
private Service $service;
protected function setUp(): void
{
$this->service = $this->getMockBuilder(Service::class)->getMock();
}
public function testSetAndGetLogger()
{
Registry::set(Registry::LOGGER, $this->service);
$this->assertSame($this->service, Registry::get(Registry::LOGGER));
}
public function testThrowsExceptionWhenTryingToSetInvalidKey()
{
$this->expectException(InvalidArgumentException::class);
Registry::set('foobar', $this->service);
}
/**
* notice @runInSeparateProcess here: without it, a previous test might have set it already and
* testing would not be possible. That's why you should implement Dependency Injection where an
* injected class may easily be replaced by a mockup
*
* @runInSeparateProcess
*/
public function testThrowsExceptionWhenTryingToGetNotSetKey()
{
$this->expectException(InvalidArgumentException::class);
Registry::get(Registry::LOGGER);
}
}
Порождающие шаблоны проектирования (Creational)
Ура! Я наконец-то дописал статью как собирать собственные бандлы на Symfony 6!!!
Статья про EasyAdmin всё ещё в процессе )))
Не, ну мне же надо на чем-то тестировать твиттер локальный...
Я тут еще много полезного буду выкладывать, так что заходите обязательно почитать.
Сайтик пока что в разработке - это далеко не окончательная версия - по сути это то что удалось слепить за 8 часов.
Комментарии