2024-05-10
Порождающие шаблоны проектирования ( Creational ) - это паттерны, которые имеют дело с механизмом создания объекта и пытаются создать объекты в порядке, подходящем к ситуации. Обычная форма создания объекта может привести к проблемам проектирования или увеличивать сложность конструкции. Порождающие шаблоны проектирования решают эту проблему, определённым образом контролируя процесс создания объекта.
Абстрактная фабрика ( Abstract Factory )
Абстрактная фабрика призвана создавать серии связанных или зависимых объектов без указания их конкретных классов. Обычно все созданные классы реализуют один и тот же интерфейс. Клиент абстрактной фабрики не заботится о том, как создаются эти объекты, он просто знает, как они сочетаются друг с другом.
WriterFactory.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
interface WriterFactory
{
public function createCsvWriter(): CsvWriter;
public function createJsonWriter(): JsonWriter;
}
CsvWriter.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
interface CsvWriter
{
public function write(array $line): string;
}
JsonWriter.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
interface JsonWriter
{
public function write(array $data, bool $formatted): string;
}
UnixCsvWriter.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class UnixCsvWriter implements CsvWriter
{
public function write(array $line): string
{
return join(',', $line) . "\n";
}
}
UnixJsonWriter.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class UnixJsonWriter implements JsonWriter
{
public function write(array $data, bool $formatted): string
{
$options = 0;
if ($formatted) {
$options = JSON_PRETTY_PRINT;
}
return json_encode($data, $options);
}
}
UnixWriterFactory.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class UnixWriterFactory implements WriterFactory
{
public function createCsvWriter(): CsvWriter
{
return new UnixCsvWriter();
}
public function createJsonWriter(): JsonWriter
{
return new UnixJsonWriter();
}
}
WinCsvWriter.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class WinCsvWriter implements CsvWriter
{
public function write(array $line): string
{
return join(',', $line) . "\r\n";
}
}
WinJsonWriter.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class WinJsonWriter implements JsonWriter
{
public function write(array $data, bool $formatted): string
{
$options = 0;
if ($formatted) {
$options = JSON_PRETTY_PRINT;
}
return json_encode($data, $options);
}
}
WinWriterFactory.php
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class WinWriterFactory implements WriterFactory
{
public function createCsvWriter(): CsvWriter
{
return new WinCsvWriter();
}
public function createJsonWriter(): JsonWriter
{
return new WinJsonWriter();
}
}
Tests/AbstractFactoryTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\AbstractFactory\Tests;
use DesignPatterns\Creational\AbstractFactory\CsvWriter;
use DesignPatterns\Creational\AbstractFactory\JsonWriter;
use DesignPatterns\Creational\AbstractFactory\UnixWriterFactory;
use DesignPatterns\Creational\AbstractFactory\WinWriterFactory;
use DesignPatterns\Creational\AbstractFactory\WriterFactory;
use PHPUnit\Framework\TestCase;
class AbstractFactoryTest extends TestCase
{
public function provideFactory()
{
return [
[new UnixWriterFactory()],
[new WinWriterFactory()]
];
}
/**
* @dataProvider provideFactory
*/
public function testCanCreateCsvWriterOnUnix(WriterFactory $writerFactory)
{
$this->assertInstanceOf(JsonWriter::class, $writerFactory->createJsonWriter());
$this->assertInstanceOf(CsvWriter::class, $writerFactory->createCsvWriter());
}
}
Строитель ( Builder )
Строитель — это интерфейс для производства частей сложного объекта. Иногда, если Строитель лучше знает о том, что он строит, этот интерфейс может быть абстрактным классом с методами по-умолчанию (адаптер). Если у вас есть сложное дерево наследования для объектов, логично иметь сложное дерево наследования и для их строителей.
Director.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder;
use DesignPatterns\Creational\Builder\Parts\Vehicle;
/**
* Director is part of the builder pattern. It knows the interface of the builder
* and builds a complex object with the help of the builder
*
* You can also inject many builders instead of one to build more complex objects
*/
class Director
{
public function build(Builder $builder): Vehicle
{
$builder->createVehicle();
$builder->addDoors();
$builder->addEngine();
$builder->addWheel();
return $builder->getVehicle();
}
}
Builder.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder;
use DesignPatterns\Creational\Builder\Parts\Vehicle;
interface Builder
{
public function createVehicle(): void;
public function addWheel(): void;
public function addEngine(): void;
public function addDoors(): void;
public function getVehicle(): Vehicle;
}
TruckBuilder.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder;
use DesignPatterns\Creational\Builder\Parts\Door;
use DesignPatterns\Creational\Builder\Parts\Engine;
use DesignPatterns\Creational\Builder\Parts\Wheel;
use DesignPatterns\Creational\Builder\Parts\Truck;
use DesignPatterns\Creational\Builder\Parts\Vehicle;
class TruckBuilder implements Builder
{
private Truck $truck;
public function addDoors(): void
{
$this->truck->setPart('rightDoor', new Door());
$this->truck->setPart('leftDoor', new Door());
}
public function addEngine(): void
{
$this->truck->setPart('truckEngine', new Engine());
}
public function addWheel(): void
{
$this->truck->setPart('wheel1', new Wheel());
$this->truck->setPart('wheel2', new Wheel());
$this->truck->setPart('wheel3', new Wheel());
$this->truck->setPart('wheel4', new Wheel());
$this->truck->setPart('wheel5', new Wheel());
$this->truck->setPart('wheel6', new Wheel());
}
public function createVehicle(): void
{
$this->truck = new Truck();
}
public function getVehicle(): Vehicle
{
return $this->truck;
}
}
CarBuilder.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder;
use DesignPatterns\Creational\Builder\Parts\Door;
use DesignPatterns\Creational\Builder\Parts\Engine;
use DesignPatterns\Creational\Builder\Parts\Wheel;
use DesignPatterns\Creational\Builder\Parts\Car;
use DesignPatterns\Creational\Builder\Parts\Vehicle;
class CarBuilder implements Builder
{
private Car $car;
public function addDoors(): void
{
$this->car->setPart('rightDoor', new Door());
$this->car->setPart('leftDoor', new Door());
$this->car->setPart('trunkLid', new Door());
}
public function addEngine(): void
{
$this->car->setPart('engine', new Engine());
}
public function addWheel(): void
{
$this->car->setPart('wheelLF', new Wheel());
$this->car->setPart('wheelRF', new Wheel());
$this->car->setPart('wheelLR', new Wheel());
$this->car->setPart('wheelRR', new Wheel());
}
public function createVehicle(): void
{
$this->car = new Car();
}
public function getVehicle(): Vehicle
{
return $this->car;
}
}
Parts/Vehicle.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder\Parts;
abstract class Vehicle
{
final public function setPart(string $key, object $value)
{
}
}
Parts/Truck.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder\Parts;
class Truck extends Vehicle
{
}
Parts/Car.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder\Parts;
class Car extends Vehicle
{
}
Parts/Engine.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder\Parts;
class Engine
{
}
Parts/Wheel.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder\Parts;
class Wheel
{
}
Parts/Door.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder\Parts;
class Door
{
}
Tests/DirectorTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Builder\Tests;
use DesignPatterns\Creational\Builder\Parts\Car;
use DesignPatterns\Creational\Builder\Parts\Truck;
use DesignPatterns\Creational\Builder\TruckBuilder;
use DesignPatterns\Creational\Builder\CarBuilder;
use DesignPatterns\Creational\Builder\Director;
use PHPUnit\Framework\TestCase;
class DirectorTest extends TestCase
{
public function testCanBuildTruck()
{
$truckBuilder = new TruckBuilder();
$newVehicle = (new Director())->build($truckBuilder);
$this->assertInstanceOf(Truck::class, $newVehicle);
}
public function testCanBuildCar()
{
$carBuilder = new CarBuilder();
$newVehicle = (new Director())->build($carBuilder);
$this->assertInstanceOf(Car::class, $newVehicle);
}
}
Фабричный метод ( Factory Method )
Выгодное отличие от SimpleFactory в том, что вы можете вынести реализацию создания объектов в подклассы. В простых случаях, этот абстрактный класс может быть только интерфейсом. Этот паттерн является «настоящим» Шаблоном Проектирования, потому что он следует «Принципу инверсии зависимостей» также известному как «D» в S.O.L.I.D. Это означает, что класс FactoryMethod зависит от абстракций, а не от конкретных классов. Это существенный плюс в сравнении с SimpleFactory или StaticFactory.
Logger.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
interface Logger
{
public function log(string $message);
}
StdoutLogger.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
class StdoutLogger implements Logger
{
public function log(string $message)
{
echo $message;
}
}
FileLogger.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
class FileLogger implements Logger
{
public function __construct(private string $filePath)
{
}
public function log(string $message)
{
file_put_contents($this->filePath, $message . PHP_EOL, FILE_APPEND);
}
}
LoggerFactory.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
interface LoggerFactory
{
public function createLogger(): Logger;
}
StdoutLoggerFactory.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
class StdoutLoggerFactory implements LoggerFactory
{
public function createLogger(): Logger
{
return new StdoutLogger();
}
}
FileLoggerFactory.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod;
class FileLoggerFactory implements LoggerFactory
{
public function __construct(private string $filePath)
{
}
public function createLogger(): Logger
{
return new FileLogger($this->filePath);
}
}
Tests/FactoryMethodTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\FactoryMethod\Tests;
use DesignPatterns\Creational\FactoryMethod\FileLogger;
use DesignPatterns\Creational\FactoryMethod\FileLoggerFactory;
use DesignPatterns\Creational\FactoryMethod\StdoutLogger;
use DesignPatterns\Creational\FactoryMethod\StdoutLoggerFactory;
use PHPUnit\Framework\TestCase;
class FactoryMethodTest extends TestCase
{
public function testCanCreateStdoutLogging()
{
$loggerFactory = new StdoutLoggerFactory();
$logger = $loggerFactory->createLogger();
$this->assertInstanceOf(StdoutLogger::class, $logger);
}
public function testCanCreateFileLogging()
{
$loggerFactory = new FileLoggerFactory(sys_get_temp_dir());
$logger = $loggerFactory->createLogger();
$this->assertInstanceOf(FileLogger::class, $logger);
}
}
Объектный пул ( Pool )
Порождающий паттерн, который предоставляет набор заранее инициализированных объектов, готовых к использованию («пул»), что не требует каждый раз создавать и уничтожать их.
Хранение объектов в пуле может заметно повысить производительность в ситуациях, когда стоимость и скорость инициализации экземпляра класса высоки, а количество одновременно используемых экземпляров в любой момент времени является низким. Время на получение объекта из пула легко прогнозируется, тогда как создание нового объекта (особенно с сетевым оверхедом) может занимать неопределенное время.
Однако эти преимущества в основном относятся к объектам, которые изначально являются дорогостоящими по времени создания. Например, соединения с базой данных, соединения сокетов, потоков или инициализация больших графических объектов, таких как шрифты или растровые изображения. В некоторых ситуациях, использование простого пула объектов (которые не зависят от внешних ресурсов, а только занимают память) может оказаться неэффективным и приведёт к снижению производительности.
WorkerPool.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Pool;
use Countable;
class WorkerPool implements Countable
{
/**
* @var StringReverseWorker[]
*/
private array $occupiedWorkers = [];
/**
* @var StringReverseWorker[]
*/
private array $freeWorkers = [];
public function get(): StringReverseWorker
{
if (count($this->freeWorkers) === 0) {
$worker = new StringReverseWorker();
} else {
$worker = array_pop($this->freeWorkers);
}
$this->occupiedWorkers[spl_object_hash($worker)] = $worker;
return $worker;
}
public function dispose(StringReverseWorker $worker): void
{
$key = spl_object_hash($worker);
if (isset($this->occupiedWorkers[$key])) {
unset($this->occupiedWorkers[$key]);
$this->freeWorkers[$key] = $worker;
}
}
public function count(): int
{
return count($this->occupiedWorkers) + count($this->freeWorkers);
}
}
StringReverseWorker.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Pool;
class StringReverseWorker
{
public function run(string $text): string
{
return strrev($text);
}
}
Tests/PoolTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Pool\Tests;
use DesignPatterns\Creational\Pool\WorkerPool;
use PHPUnit\Framework\TestCase;
class PoolTest extends TestCase
{
public function testCanGetNewInstancesWithGet()
{
$pool = new WorkerPool();
$worker1 = $pool->get();
$worker2 = $pool->get();
$this->assertCount(2, $pool);
$this->assertNotSame($worker1, $worker2);
}
public function testCanGetSameInstanceTwiceWhenDisposingItFirst()
{
$pool = new WorkerPool();
$worker1 = $pool->get();
$pool->dispose($worker1);
$worker2 = $pool->get();
$this->assertCount(1, $pool);
$this->assertSame($worker1, $worker2);
}
}
Прототип ( Prototype )
Помогает избежать затрат на создание объектов стандартным способом (new Foo()), а вместо этого создаёт прототип и затем клонирует его. Большие объемы данных (например, создать 1000000 строк в базе данных сразу через ORM).
BookPrototype.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Prototype;
abstract class BookPrototype
{
protected string $title;
protected string $category;
abstract public function __clone();
final public function getTitle(): string
{
return $this->title;
}
final public function setTitle(string $title): void
{
$this->title = $title;
}
}
BarBookPrototype.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Prototype;
class BarBookPrototype extends BookPrototype
{
protected string $category = 'Bar';
public function __clone()
{
}
}
FooBookPrototype.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Prototype;
class FooBookPrototype extends BookPrototype
{
protected string $category = 'Foo';
public function __clone()
{
}
}
Tests/PrototypeTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Prototype\Tests;
use DesignPatterns\Creational\Prototype\BarBookPrototype;
use DesignPatterns\Creational\Prototype\FooBookPrototype;
use PHPUnit\Framework\TestCase;
class PrototypeTest extends TestCase
{
public function testCanGetFooBook()
{
$fooPrototype = new FooBookPrototype();
$barPrototype = new BarBookPrototype();
for ($i = 0; $i < 10; $i++) {
$book = clone $fooPrototype;
$book->setTitle('Foo Book No ' . $i);
$this->assertInstanceOf(FooBookPrototype::class, $book);
}
for ($i = 0; $i < 5; $i++) {
$book = clone $barPrototype;
$book->setTitle('Bar Book No ' . $i);
$this->assertInstanceOf(BarBookPrototype::class, $book);
}
}
}
Простая фабрика ( Simple factory )
Она отличается от Статической Фабрики тем, что собственно не является статической. Таким образом, вы можете иметь множество фабрик с разными параметрами. Простая фабрика всегда должна быть предпочтительнее Статической фабрики!
SimpleFactory.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\SimpleFactory;
class SimpleFactory
{
public function createBicycle(): Bicycle
{
return new Bicycle();
}
}
Bicycle.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\SimpleFactory;
class Bicycle
{
public function driveTo(string $destination)
{
}
}
Использование
$factory = new SimpleFactory();
$bicycle = $factory->createBicycle();
$bicycle->driveTo('Paris');
Tests/SimpleFactoryTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\SimpleFactory\Tests;
use DesignPatterns\Creational\SimpleFactory\Bicycle;
use DesignPatterns\Creational\SimpleFactory\SimpleFactory;
use PHPUnit\Framework\TestCase;
class SimpleFactoryTest extends TestCase
{
public function testCanCreateBicycle()
{
$bicycle = (new SimpleFactory())->createBicycle();
$this->assertInstanceOf(Bicycle::class, $bicycle);
}
}
Одиночка ( Singleton )
Это считается анти-паттерном! Для лучшей тестируемости и сопровождения кода используйте Инъекцию Зависимости (Dependency Injection)!
Позволяет содержать только один экземпляр объекта в приложении, которое будет обрабатывать все обращения, запрещая создавать новый экземпляр.
DB Connector для подключения к базе данных
Logger
Config Manager
Threads Handling
Блокировка файла в приложении (есть только один в файловой системе с одновременным доступом к нему)
Singleton.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Singleton;
use Exception;
final class Singleton
{
private static ?Singleton $instance = null;
/**
* gets the instance via lazy initialization (created on first usage)
*/
public static function getInstance(): Singleton
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* is not allowed to call from outside to prevent from creating multiple instances,
* to use the singleton, you have to obtain the instance from Singleton::getInstance() instead
*/
private function __construct()
{
}
/**
* prevent the instance from being cloned (which would create a second instance of it)
*/
private function __clone()
{
}
/**
* prevent from being unserialized (which would create a second instance of it)
*/
public function __wakeup()
{
throw new Exception("Cannot unserialize singleton");
}
}
Tests/SingletonTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\Singleton\Tests;
use DesignPatterns\Creational\Singleton\Singleton;
use PHPUnit\Framework\TestCase;
class SingletonTest extends TestCase
{
public function testUniqueness()
{
$firstCall = Singleton::getInstance();
$secondCall = Singleton::getInstance();
$this->assertInstanceOf(Singleton::class, $firstCall);
$this->assertSame($firstCall, $secondCall);
}
}
Статическая фабрика ( Static factory )
Подобно AbstractFactory, этот паттерн используется для создания ряда связанных или зависимых объектов. Разница между этим шаблоном и Абстрактной Фабрикой заключается в том, что Статическая Фабрика использует только один статический метод, чтобы создать все допустимые типы объектов. Этот метод, обычно, называется factory или build.
StaticFactory.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\StaticFactory;
use InvalidArgumentException;
/**
* Note1: Remember, static means global state which is evil because it can't be mocked for tests
* Note2: Cannot be subclassed or mock-upped or have multiple different instances.
*/
final class StaticFactory
{
public static function factory(string $type): Formatter
{
return match ($type) {
'number' => new FormatNumber(),
'string' => new FormatString(),
default => throw new InvalidArgumentException('Unknown format given'),
};
}
}
Formatter.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\StaticFactory;
interface Formatter
{
public function format(string $input): string;
}
FormatString.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\StaticFactory;
class FormatString implements Formatter
{
public function format(string $input): string
{
return $input;
}
}
FormatNumber.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\StaticFactory;
class FormatNumber implements Formatter
{
public function format(string $input): string
{
return number_format((int) $input);
}
}
Tests/StaticFactoryTest.php
<?php
declare(strict_types=1);
namespace DesignPatterns\Creational\StaticFactory\Tests;
use InvalidArgumentException;
use DesignPatterns\Creational\StaticFactory\FormatNumber;
use DesignPatterns\Creational\StaticFactory\FormatString;
use DesignPatterns\Creational\StaticFactory\StaticFactory;
use PHPUnit\Framework\TestCase;
class StaticFactoryTest extends TestCase
{
public function testCanCreateNumberFormatter()
{
$this->assertInstanceOf(FormatNumber::class, StaticFactory::factory('number'));
}
public function testCanCreateStringFormatter()
{
$this->assertInstanceOf(FormatString::class, StaticFactory::factory('string'));
}
public function testException()
{
$this->expectException(InvalidArgumentException::class);
StaticFactory::factory('object');
}
}
Структурные шаблоны проектирования (Structural)
Комментарии