Vamos imaginar que você está trabalhando em um eCommerce e recebeu a responsabilidade de implementar a seguinte funcionalidade:
Na tela do carrinho, o usuário deve ser capaz de adicionar mais de uma unidade de um produto já presente, e também alterar a sua cor.
Uma solução intuitiva nesse contexto poderia ser instanciar um novo objeto produto e setar todos os atributos iguais ao produto original. Mas isso parece ser muito verboso, e você precisaria saber todo o funcionamento interno da classe produto, o que parece quebrar o encapsulamento.
Prototype ao resgate!
Do refactoring.guru:
O Prototype é um padrão de projeto criacional que permite copiar objetos existentes sem fazer seu código ficar dependente de suas classes.
Isso significa que você não precisa usar o operador new
nem se preocupar em como configurar uma instância copiada perfeitamente. Só precisa o usar o operador clone
e o PHP vai resolver tudo sozinho.
Aplicação de exemplo
Versão final do código está no Github:
fabiothiroki / php-design-patterns
A collection of design patterns written in PHP
A classe Cart
não faz parte do padrão de projeto, mas está presente aqui somente para demonstrar como o padrão se encaixaria numa aplicação real:
final class Cart
{
/**
* @var ProductPrototype[]
*/
private array $products;
public function addProduct(ProductPrototype $product): void
{
$this->products[] = $product;
}
/**
* @return ProductPrototype[]
*/
public function getProducts(): array
{
return $this->products;
}
}
Como podemos observar, somos capazes de adicionar um novo produto e verificar quais produtos já estão adicionados.
abstract class ProductPrototype
{
protected int $id;
protected string $name;
protected string $color;
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function getColor(): string
{
return $this->color;
}
public function setColor(string $color): void
{
$this->color = $color;
}
}
PHP já implementa Prototype
nativamente porque todo objeto já aceita o operador clone
por padrão. Nesse exemplo, criei a classe ProductPrototype
só para ilustrar um caso real: para a classe Cart
não importa qual o tipo específico de produto será adicionado, contanto que tenha os atributos id
, name
e color
.
Apenas para simplificar esse exemplo, vou mostrar dois tipos de produto: Smartphone
e Laptop
:
final class Smartphone extends ProductPrototype
{
public function __construct()
{
$this->id = 1;
$this->name = 'Smartphone';
$this->color = 'Default color';
}
}
final class Laptop extends ProductPrototype
{
public function __construct()
{
$this->id = 2;
$this->name = 'Smartphone';
$this->color = 'Default color';
}
}
Explicando Prototype com testes unitários
O que o operador clone
realmente faz? Ele copia uma instância com os mesmos atributos. Então no nosso exemplo, o produto clonado terá os mesmos valores de id
, nome
e cor
do objeto original.
public function testSmartphoneClone(): void
{
$smartphone = new Smartphone();
$clonedSmartphone = clone $smartphone;
self::assertEquals($clonedSmartphone->getId(), $smartphone->getId());
self::assertEquals($clonedSmartphone->getName(), $smartphone->getName());
self::assertEquals($clonedSmartphone->getColor(), $smartphone->getColor());
self::assertEquals($clonedSmartphone, $smartphone);
self::assertNotSame($clonedSmartphone, $smartphone);
}
Como podemos observar, se tivéssemos que clonar um objeto manualmente, teríamos que saber todas as propriedades a serem copiadas e como setá-las.
Perceba que esse teste não precisaria ser escrito numa aplicação real porque podemos assumir que o operador clone
funciona corretamente, já que é provido pelo próprio PHP.
De volta ao nosso caso de uso
Finalmente podemos dar ao Cart
a funcionalidade de aumentar a quantidade de um produto existente e alterar sua cor. Cart
ficará ainda mais feliz porque não precisa se preocupar se estará recebendo uma instância de Laptop
ou de Smartphone
.
É só fazer uso do operador clone e adicionar o produto recém clonado:
public function testCartCanAddClonedProducts(): void
{
$laptop = new Laptop();
$cart = new Cart();
$cart->addProduct($laptop);
$clonedLaptop = clone $laptop;
$clonedLaptop->setColor('White');
$cart->addProduct($clonedLaptop);
self::assertCount(2, $cart->getProducts());
self::assertEquals($cart->getProducts()[1]->getId(), $cart->getProducts()[0]->getId());
self::assertEquals($cart->getProducts()[1]->getName(), $cart->getProducts()[0]->getName());
self::assertEquals('White', $cart->getProducts()[1]->getColor());
self::assertEquals('Default color', $cart->getProducts()[0]->getColor());
}
Obrigado por ler até aqui, espero que você tenha entendido melhor sobre esse padrão de projeto e lembre-se de usá-lo quando necessário!