Ir ao conteúdo

Padrão de Projeto Composite em PHP com exemplo

Atualizado pela última vez em 30 de novembro de 2021

O Padrão Composite é um padrão estrutural que nos permite criar objetos em estruturas individuais ou compostas similares a uma árvore. Onde cada objeto, tanto individual (leaf) como composto(composite), serão tratados da mesma forma. Em outras palavras, você consegue criar componentes que ignoram as diferenças entre objetos  individuais ou compostos.

Diagrama de Classe UML do Padrão de Projeto Composite
Diagrama de Classe UML do Padrão de Projeto Composite

Quando me refiro a componentes compostos, é que o padrão Composite faz a ligação hierárquica ligando vários objetos a um único através da recursividade. Ou seja, ele permite que você componha objetos em uma estrutura similar a um árvore para representar hierarquias todo-parte.

Diagrama em Grafo para representar o Composite em arvore
Diagrama em Grafo para representar o Composite em arvore

Exemplo de uso do Padrão de Projeto Composite

Quando modelamos um problema fica mais fácil compreende-lo. Imagine que você vai construir um componente de um catálogo de produtos. Você precisará de dois objetos, um Produto que apresenta as folhas(leaf) da árvore e um catálogo (Composite) para criar a coleção de folhas. Utilizando o diagrama anterior, a representação dessa ideia seria essa:

Diagrama com relação entre os produtos e os catálogos
Diagrama com relação entre os produtos e os catálogos

Vamos modelar esta ideia. Primeiro vamos criar o contrato que define o Catalog Component. Só que para implementar o padrão Composite, temos um problema para resolver em nossas classes. Observe que tanto o objeto simples (leaf) quanto o composto (composite), estendem as características de Component,  só que na leaf, não deve ter os comportamentos de add, remove e getChild. Para contornar isso, você pode definir os métodos em Component com um chamada explícita de exceção. Com isso, você força seus filhos a não utilizarem os métodos e forçar o Composite a subscrever estes métodos por métodos concretos.

<?php

declare(strict_types=1);    

namespace Growthdev\DesignPatterns\Structural\Composite;

use BadMethodCallException;

abstract class CatalogComponent
{
    protected function add(CatalogComponent $catalogComponent): void
    {
        throw new BadMethodCallException('Method not implemented');
    }

    protected function remove(CatalogComponent $catalogComponent): void
    {
        throw new BadMethodCallException('Method not implemented');
    }

    protected function getChild(CatalogComponent $catalogComponent): CatalogComponent
    {
        throw new BadMethodCallException('Method not implemented');
    }

    abstract protected function display(): void;
}

Segundo o diagrama de classe do Composite, os componentes filhos precisam implementar um método comum, no nosso exemplo é o método é o display assinado como abstrato para forçar sua implementação;

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Composite;

final class Product extends CatalogComponent
{
    private string $name;
    private float $price;

    public function __construct(string $name, float $price)
    {
        $this->name = $name;
        $this->price = $price;
    }

    public function display(): void
    {
        printf("Product: %s R$ %.2f\n", $this->name, $this->price);
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Composite;

use SplObjectStorage;

final class ProductCatalog extends CatalogComponent
{
    private SplObjectStorage $products;

    public function __construct(private string $name)
    {
        $this->products = new SplObjectStorage();
    }

    public function add(CatalogComponent $catalogComponent): void
    {
        $this->products->attach($catalogComponent);
    }

    public function remove(CatalogComponent $catalogComponent): void
    {
        $this->products->detach($catalogComponent);
    }

    public function getChild(CatalogComponent $catalogComponent): CatalogComponent
    {
        if (!$this->products->contains($catalogComponent)) {
            throw new \InvalidArgumentException('CatalogComponent not found');
        }
        return $catalogComponent;
    }

    public function display(): void
    {
        echo "Catalog: {$this->name}\n";
        foreach ($this->products as $product) {
            $product->display();
        }
    }
}

Observe que o catálogo de produto por ser um componente, consegue adicionar hierarquicamente produtos para o compor:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Structural\Composite;

use Growthdev\DesignPatterns\Structural\Composite\Product;
use Growthdev\DesignPatterns\Structural\Composite\ProductCatalog;
use PHPUnit\Framework\TestCase;

final class CompositeTest extends TestCase
{
    public function testCanCreateProductCatalog(): void
    {
        $catalog = new ProductCatalog('Catalog 1');
        $catalog2 = new ProductCatalog('Catalog 2');

        $catalog->add(new Product('Product 1', 10.0));
        $catalog->add(new Product('Product 2', 20.0));
        $catalog->add($catalog2);
        $catalog->getChild($catalog2)->add(new Product('Product 3', 30.0));
        $catalog->getChild($catalog2)->add(new Product('Product 4', 40.0));
        $catalog->getChild($catalog2)->add(new Product('Product 5', 50.0));

        $catalog->display();
        
        $this->expectOutputString(
            "Catalog: Catalog 1\n" .
            "Product: Product 1 R$ 10.00\n" .
            "Product: Product 2 R$ 20.00\n" .
            "Catalog: Catalog 2\n" .
            "Product: Product 3 R$ 30.00\n" .
            "Product: Product 4 R$ 40.00\n" .
            "Product: Product 5 R$ 50.00\n"
        );
    }
}

Resultado do teste do padrão
Resultado do teste do padrão

Este exemplo ilustra muito bem o uso deste padrão de projetos . Não estava previsto para este artigo, mas para deixá-lo um pouco mais didático, vou modelar mais um problema para resolvermos com o uso do Composite Pattern.

Modelando mais um resultado com o Padrão de Projeto Composite

Certamente em algum momento você teve que manipular HTML. Se você observar, ele carrega todos os princípios do padrão Composite. Ele tem tags, como <img/> ou <h1></h1> que representam a leaf e outras tags que que conseguem adicionar outras em uma estrutura hierarquizada como uma árvore, como exemplo a tag <div> que pode receber diversas outras tags, etc…

A nossa classe para representar nosso componente principal será TagComponent. Seguido da classe Tag que representa leaf e da classe TagComposite.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Composite\Html;

abstract class TagComponent
{
    public function add(TagComponent $tagComponent): void
    {
        throw new \BadMethodCallException('Method not implemented');
    }

    public function getChild(TagComponent $tagComponent): TagComponent
    {
        throw new \BadMethodCallException('Method not implemented');
    }

    abstract public function display(): void;
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Composite\Html;

class Tag extends TagComponent
{
    private string $name;

    public function __construct(string $name, private ?string $content = null) 
    {
        $this->name = $name;
    }

    public function display(): void
    {
        if ($this->content) {
            echo "<{$this->name}>{$this->content}</{$this->name}>\n";
        } else {
            echo "<{$this->name} />\n";
        }
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Composite\Html;

use SplObjectStorage;

class TagComposite extends TagComponent
{
    private SplObjectStorage $components;

    public function __construct(private string $name)
    {
        $this->components = new SplObjectStorage();
    }

    public function add(TagComponent $tagComponent): void
    {
        $this->components->attach($tagComponent);
    }

    public function getChild(TagComponent $tagComponent): TagComponent
    {
        if (!$this->components->contains($tagComponent)) {
            throw new \InvalidArgumentException('TagComponent not found');
        }
        return $tagComponent;
    }

    public function display(): void
    {
        echo "<{$this->name}>\n";
        foreach ($this->components as $component) {
            $component->display();
        }
        echo "</{$this->name}>\n";
    }
}

Veja como fica mais fácil visualizar o uso deste padrão quando conseguimos fazer uma comparação com algo que já conhecemos que tem comportamento semelhante.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Structural\Composite\Html;

use Growthdev\DesignPatterns\Structural\Composite\Html\Tag;
use Growthdev\DesignPatterns\Structural\Composite\Html\TagComposite;
use PHPUnit\Framework\TestCase;

final class TagCompositeTest extends TestCase
{
    public function testCanCreateHtmlComposite(): void
    {
        $html = new TagComposite('html');
        $head = new TagComposite('head');
        $head->add(new Tag('title', 'Hello World'));
        $html->add($head);

        $body = new TagComposite('body');
        $div = new TagComposite('div');
        
        $body->add($div);
        $body->getChild($div)->add(new Tag('img'));
        $body->getChild($div)->add(new Tag('h1', 'Growth Dev'));

        $html->add($body);
        $html->display();

        $this->expectOutputString(
            "<html>\n"
            . "<head>\n"
            .    "<title>Hello World</title>\n"
            . "</head>\n"
            . "<body>\n"
            .    "<div>\n"
            .      "<img />\n"
            .        "<h1>Growth Dev</h1>\n"
            .    "</div>\n"
            . "</body>\n"
            . "</html>\n"
        );
    }
}

Resultado do segundo teste
Resultado do segundo teste

Chegamos ao fim de mais um artigo, se você gostou, deixe seu comentário e compartilhe com o máximo de pessoas que você puder. Isso vai me ajudar enormemente!

Os códigos fontes de todos os exemplos estão disponíveis em meu Github

Resumo dos Padrões de Projetos (Design Patterns)

Se você quiser se aprofundar um pouco mais sobre Padrões de Projetos, não deixe de acesso a página 

Até o próximo artigo!

Confiança Sempre!!!

Fontes:

Publicado emDesign PatternPadrões de ProjetosPHP

Seja o primeiro a comentar

    Deixe um comentário

    O seu endereço de e-mail não será publicado.