
2024-05-10
Прочие шаблоны проектирования (Others)
2023-10-24
Этот шаблон считается анти-паттерном!
Некоторые считают Локатор Служб анти-паттерном. Он нарушает принцип инверсии зависимостей (Dependency Inversion principle) из набора принципов SOLID. Локатор Служб скрывает зависимости данного класса вместо их совместного использования, как в случае шаблона Внедрение Зависимости (Dependency Injection). В случае изменения данных зависимостей мы рискуем сломать функционал классов, которые их используют, вследствие чего затрудняется поддержка системы.
Используется для реализации слабосвязанной архитектуры, чтобы получить хорошо тестируемый, сопровождаемый и расширяемый код. Паттерн Инъекция зависимостей (DI) и паттерн Локатор Служб — это реализация паттерна Инверсия управления (Inversion of Control, IoC).
С Локатором Служб вы можете зарегистрировать сервис для определенного интерфейса. С помощью интерфейса вы можете получить зарегистрированный сервис и использовать его в классах приложения, не зная его реализацию. Вы можете настроить и внедрить объект Service Locator на начальном этапе сборки приложения.
Service.php
<?php
namespace DesignPatterns\More\ServiceLocator;
interface Service
{
}
ServiceLocator.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\ServiceLocator;
use OutOfRangeException;
use InvalidArgumentException;
class ServiceLocator
{
/**
* @var string[][]
*/
private array $services = [];
/**
* @var Service[]
*/
private array $instantiated = [];
public function addInstance(string $class, Service $service)
{
$this->instantiated[$class] = $service;
}
public function addClass(string $class, array $params)
{
$this->services[$class] = $params;
}
public function has(string $interface): bool
{
return isset($this->services[$interface]) || isset($this->instantiated[$interface]);
}
public function get(string $class): Service
{
if (isset($this->instantiated[$class])) {
return $this->instantiated[$class];
}
$object = new $class(...$this->services[$class]);
if (!$object instanceof Service) {
throw new InvalidArgumentException('Could not register service: is no instance of Service');
}
$this->instantiated[$class] = $object;
return $object;
}
}
LogService.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\ServiceLocator;
class LogService implements Service
{
}
Tests/ServiceLocatorTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\ServiceLocator\Tests;
use DesignPatterns\More\ServiceLocator\LogService;
use DesignPatterns\More\ServiceLocator\ServiceLocator;
use PHPUnit\Framework\TestCase;
class ServiceLocatorTest extends TestCase
{
private ServiceLocator $serviceLocator;
public function setUp(): void
{
$this->serviceLocator = new ServiceLocator();
}
public function testHasServices()
{
$this->serviceLocator->addInstance(LogService::class, new LogService());
$this->assertTrue($this->serviceLocator->has(LogService::class));
$this->assertFalse($this->serviceLocator->has(self::class));
}
public function testGetWillInstantiateLogServiceIfNoInstanceHasBeenCreatedYet()
{
$this->serviceLocator->addClass(LogService::class, []);
$logger = $this->serviceLocator->get(LogService::class);
$this->assertInstanceOf(LogService::class, $logger);
}
}
Посредник между уровнями области определения (хранилище) и распределения данных. Использует интерфейс, похожий на коллекции, для доступа к объектам области определения. Репозиторий инкапсулирует набор объектов, сохраняемых в хранилище данных, и операции выполняемые над ними, обеспечивая более объектно-ориентированное представление реальных данных. Репозиторий также преследует цель достижения полного разделения и односторонней зависимости между уровнями области определения и распределения данных.
Примеры:
Post.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\Repository\Domain;
class Post
{
public static function draft(PostId $id, string $title, string $text): Post
{
return new self(
$id,
PostStatus::fromString(PostStatus::STATE_DRAFT),
$title,
$text
);
}
public static function fromState(array $state): Post
{
return new self(
PostId::fromInt($state['id']),
PostStatus::fromInt($state['statusId']),
$state['title'],
$state['text']
);
}
private function __construct(
private PostId $id,
private PostStatus $status,
private string $title,
private string $text
) {
}
public function getId(): PostId
{
return $this->id;
}
public function getStatus(): PostStatus
{
return $this->status;
}
public function getText(): string
{
return $this->text;
}
public function getTitle(): string
{
return $this->title;
}
}
PostId.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\Repository\Domain;
use InvalidArgumentException;
/**
* This is a perfect example of a value object that is identifiable by it's value alone and
* is guaranteed to be valid each time an instance is created. Another important property of value objects
* is immutability.
*
* Notice also the use of a named constructor (fromInt) which adds a little context when creating an instance.
*/
class PostId
{
public static function fromInt(int $id): PostId
{
self::ensureIsValid($id);
return new self($id);
}
private function __construct(private int $id)
{
}
public function toInt(): int
{
return $this->id;
}
private static function ensureIsValid(int $id)
{
if ($id <= 0) {
throw new InvalidArgumentException('Invalid PostId given');
}
}
}
PostStatus.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\Repository\Domain;
use InvalidArgumentException;
/**
* Like PostId, this is a value object which holds the value of the current status of a Post. It can be constructed
* either from a string or int and is able to validate itself. An instance can then be converted back to int or string.
*/
class PostStatus
{
public const STATE_DRAFT_ID = 1;
public const STATE_PUBLISHED_ID = 2;
public const STATE_DRAFT = 'draft';
public const STATE_PUBLISHED = 'published';
private static array $validStates = [
self::STATE_DRAFT_ID => self::STATE_DRAFT,
self::STATE_PUBLISHED_ID => self::STATE_PUBLISHED,
];
public static function fromInt(int $statusId)
{
self::ensureIsValidId($statusId);
return new self($statusId, self::$validStates[$statusId]);
}
public static function fromString(string $status)
{
self::ensureIsValidName($status);
$state = array_search($status, self::$validStates);
if ($state === false) {
throw new InvalidArgumentException('Invalid state given!');
}
return new self($state, $status);
}
private function __construct(private int $id, private string $name)
{
}
public function toInt(): int
{
return $this->id;
}
/**
* there is a reason that I avoid using __toString() as it operates outside of the stack in PHP
* and is therefore not able to operate well with exceptions
*/
public function toString(): string
{
return $this->name;
}
private static function ensureIsValidId(int $status)
{
if (!in_array($status, array_keys(self::$validStates), true)) {
throw new InvalidArgumentException('Invalid status id given');
}
}
private static function ensureIsValidName(string $status)
{
if (!in_array($status, self::$validStates, true)) {
throw new InvalidArgumentException('Invalid status name given');
}
}
}
PostRepository.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\Repository;
use OutOfBoundsException;
use DesignPatterns\More\Repository\Domain\Post;
use DesignPatterns\More\Repository\Domain\PostId;
/**
* This class is situated between Entity layer (class Post) and access object layer (Persistence).
*
* Repository encapsulates the set of objects persisted in a data store and the operations performed over them
* providing a more object-oriented view of the persistence layer
*
* Repository also supports the objective of achieving a clean separation and one-way dependency
* between the domain and data mapping layers
*/
class PostRepository
{
public function __construct(private Persistence $persistence)
{
}
public function generateId(): PostId
{
return PostId::fromInt($this->persistence->generateId());
}
public function findById(PostId $id): Post
{
try {
$arrayData = $this->persistence->retrieve($id->toInt());
} catch (OutOfBoundsException $e) {
throw new OutOfBoundsException(sprintf('Post with id %d does not exist', $id->toInt()), 0, $e);
}
return Post::fromState($arrayData);
}
public function save(Post $post)
{
$this->persistence->persist([
'id' => $post->getId()->toInt(),
'statusId' => $post->getStatus()->toInt(),
'text' => $post->getText(),
'title' => $post->getTitle(),
]);
}
}
Persistence.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\Repository;
interface Persistence
{
public function generateId(): int;
public function persist(array $data);
public function retrieve(int $id): array;
public function delete(int $id);
}
InMemoryPersistence.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\Repository;
use OutOfBoundsException;
class InMemoryPersistence implements Persistence
{
private array $data = [];
private int $lastId = 0;
public function generateId(): int
{
$this->lastId++;
return $this->lastId;
}
public function persist(array $data)
{
$this->data[$this->lastId] = $data;
}
public function retrieve(int $id): array
{
if (!isset($this->data[$id])) {
throw new OutOfBoundsException(sprintf('No data found for ID %d', $id));
}
return $this->data[$id];
}
public function delete(int $id)
{
if (!isset($this->data[$id])) {
throw new OutOfBoundsException(sprintf('No data found for ID %d', $id));
}
unset($this->data[$id]);
}
}
Tests/PostRepositoryTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\Repository\Tests;
use OutOfBoundsException;
use DesignPatterns\More\Repository\Domain\PostId;
use DesignPatterns\More\Repository\Domain\PostStatus;
use DesignPatterns\More\Repository\InMemoryPersistence;
use DesignPatterns\More\Repository\Domain\Post;
use DesignPatterns\More\Repository\PostRepository;
use PHPUnit\Framework\TestCase;
class PostRepositoryTest extends TestCase
{
private PostRepository $repository;
protected function setUp(): void
{
$this->repository = new PostRepository(new InMemoryPersistence());
}
public function testCanGenerateId()
{
$this->assertEquals(1, $this->repository->generateId()->toInt());
}
public function testThrowsExceptionWhenTryingToFindPostWhichDoesNotExist()
{
$this->expectException(OutOfBoundsException::class);
$this->expectExceptionMessage('Post with id 42 does not exist');
$this->repository->findById(PostId::fromInt(42));
}
public function testCanPersistPostDraft()
{
$postId = $this->repository->generateId();
$post = Post::draft($postId, 'Repository Pattern', 'Design Patterns PHP');
$this->repository->save($post);
$this->repository->findById($postId);
$this->assertEquals($postId, $this->repository->findById($postId)->getId());
$this->assertEquals(PostStatus::STATE_DRAFT, $post->getStatus()->toString());
}
}
Шаблон Сущность-Атрибут-Значение используется для реализации модели EAV на PHP
Модель Сущность-Атрибут-Значение (EAV) - это модель данных, предназначенная для описания сущностей, в которых количество атрибутов (свойств, параметров), характеризующих их, потенциально огромно, но то количество, которое реально будет использоваться в конкретной сущности, относительно мало.
Entity.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\EAV;
use SplObjectStorage;
class Entity implements \Stringable
{
/**
* @var SplObjectStorage<Value,Value>
*/
private $values;
/**
* @param Value[] $values
*/
public function __construct(private string $name, array $values)
{
$this->values = new SplObjectStorage();
foreach ($values as $value) {
$this->values->attach($value);
}
}
public function __toString(): string
{
$text = [$this->name];
foreach ($this->values as $value) {
$text[] = (string) $value;
}
return join(', ', $text);
}
}
Attribute.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\EAV;
use SplObjectStorage;
class Attribute implements \Stringable
{
private SplObjectStorage $values;
public function __construct(private string $name)
{
$this->values = new SplObjectStorage();
}
public function addValue(Value $value): void
{
$this->values->attach($value);
}
public function getValues(): SplObjectStorage
{
return $this->values;
}
public function __toString(): string
{
return $this->name;
}
}
Value.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\EAV;
class Value implements \Stringable
{
public function __construct(private Attribute $attribute, private string $name)
{
$attribute->addValue($this);
}
public function __toString(): string
{
return sprintf('%s: %s', (string) $this->attribute, $this->name);
}
}
Tests/EAVTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\More\EAV\Tests;
use DesignPatterns\More\EAV\Attribute;
use DesignPatterns\More\EAV\Entity;
use DesignPatterns\More\EAV\Value;
use PHPUnit\Framework\TestCase;
class EAVTest extends TestCase
{
public function testCanAddAttributeToEntity(): void
{
$colorAttribute = new Attribute('color');
$colorSilver = new Value($colorAttribute, 'silver');
$colorBlack = new Value($colorAttribute, 'black');
$memoryAttribute = new Attribute('memory');
$memory8Gb = new Value($memoryAttribute, '8GB');
$entity = new Entity('MacBook Pro', [$colorSilver, $colorBlack, $memory8Gb]);
$this->assertEquals('MacBook Pro, color: silver, color: black, memory: 8GB', (string) $entity);
}
}
Порождающие шаблоны проектирования (Creational)
Ура! Я наконец-то дописал статью как собирать собственные бандлы на Symfony 6!!!
Статья про EasyAdmin всё ещё в процессе )))
Не, ну мне же надо на чем-то тестировать твиттер локальный...
Я тут еще много полезного буду выкладывать, так что заходите обязательно почитать.
Сайтик пока что в разработке - это далеко не окончательная версия - по сути это то что удалось слепить за 8 часов.
Комментарии