Padrão de projeto Prototype em PHP

Fabio Hiroki - Oct 10 '21 - - Dev Community

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

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:

GitHub logo 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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';
    }
}
Enter fullscreen mode Exit fullscreen mode

Explicando Prototype com testes unitários

Gif de uma casa se multiplicando

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);
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

Gif do bob esponja se multiplicando

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!

. . . . . . . . . . . . . . . . . . . . .