Padrão de projeto Prototype em PHP
Tags: braziliandevs, php, webdev, oo
Published: 2021-10-10 16:38:09

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!
![Gif de alguém sendo clonado](https://media.giphy.com/media/3oKHWnC0GztmsjVLCE/giphy.gif)

Do [refactoring.guru](https://refactoring.guru/pt-br/design-patterns/prototype):
> 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](https://github.com/fabiothiroki/php-design-patterns):
{% github fabiothiroki/php-design-patterns no-readme %} 

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:
```php
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.

```php
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`:
```php
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
![Gif de uma casa se multiplicando](https://media.giphy.com/media/l4FB2YVpNjtedlFWE/giphy.gif)

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.

```php
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:
```php
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());
}
```

![Gif do bob esponja se multiplicando](https://media.giphy.com/media/xT3i1faW3SLPwdoVs4/giphy.gif)

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!