O Padrão de Projeto Builder é muito útil quando você tem objetos complexos que precisam ser criados parte a parte. Ou seja, você tem a estrutura da classe e consegue criar os objetos como se fossem um passo a passo.
O padrão Builder é composto por quatro componentes básicos que são: a Interface (ou classe abstrata) Builder, o Concrete Builder (construtor concreto), o Director (Diretor) e o Product (produto).
Resumidamente Director é responsável por chamar o método de construção do Builder, que chama sua implementações especializadas, Concrete Builder, que possui em sua implementação a lógica para construir nossa classe final que é o Product;
No seu dia a dia como programador, você vai encontrar situações em que você precisará criar uma classe que vai representar a construção de um determinado objeto, só que esse objeto terá pequenas partes, o que os tornam complexos, no sentido de muitas partes. Haverá também situações em que você vai precisar criar uma instância desse objeto, porém não vai precisar carregar todas as suas características.
Como normalmente era feita esta implementação? Criava-se um objeto com um construtor enorme, com permissão para entradas com valores null, ou valores default também nulos. E na instanciação, desconsiderava o que precisava. Em algumas linguagens como Java, permite criar múltiplos construtores o que facilitava um pouco neste quesito, porém, utilizando o padrão Builder o resultado esperado é mais desejável.
Implementação do Padrão Builder Passo a Passo
Vou fazer neste artigo duas variações do padrão do Builder em PHP. Para exemplo, o nosso problema será criar um construtor de hambúrguer, pelo fato de um hambúrguer tem vários componentes, vários ingredientes, então fica fácil para criarmos este exemplo. No final do artigo vou mostrar outros exemplos práticos para que você consiga contextualizar melhor a importância de uso deste pattern.
Representação do produto
O próprio hambúrguer é o produto. Ele representa o modelo do produto que vamos construir
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Creational\Builder\Burger; class Burger { private string $bread; private string $patty; private string $veggies; private string $sauces; private bool $withExtraCheese = false; public function setBread(string $bread): void { $this->bread = $bread; } public function setPatty(string $patty): void { $this->patty = $patty; } public function setVeggies(string $veggies): void { $this->veggies = $veggies; } public function setSauces(string $sauces): void { $this->sauces = $sauces; } public function setWithExtraCheese(bool $withExtraCheese): void { $this->withExtraCheese = $withExtraCheese; } public function getBread(): string { return $this->bread; } public function getPatty(): string { return $this->patty; } public function getveggies(): string { return $this->veggies; } public function getSauces(): string { return $this->sauces; } public function getWithExtraCheese(): bool { return $this->withExtraCheese; } }
Interface para criação do produto
Vamos fazer um contrato de criação de Burger e fazer a composição no Builder
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Creational\Builder\Burger; interface BurgerBuilderInterface { public function addBread(string $bread): self; public function addPatty(string $patty): self; public function addVeggies(string $veggies): self; public function addSauces(string $saouces): self; public function addWithExtraCheese(): self; public function build(): Burger; }
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Creational\Builder\Burger; class BurgerBuilder implements BurgerBuilderInterface { private Burger $burger; public function __construct() { $this->burger = new Burger(); } public function addBread(string $bread): self { $this->burger->setBread($bread); return $this; } public function addPatty(string $patty): self { $this->burger->setPatty($patty); return $this; } public function addVeggies(string $veggies): self { $this->burger->setVeggies($veggies); return $this; } public function addSauces(string $sauces): self { $this->burger->setSauces($sauces); return $this; } public function addWithExtraCheese(): self { $this->burger->setWithExtraCheese(true); return $this; } public function build(): Burger { return $this->burger; } }
Criando o Director
Segundo a solução proposta no livro GOF, temos que utilizar um personagem chamado Diretor, que é o cara que de fato “cria” o Product. Porém, no próximo exemplo vamos ver que esse participante do diagrama pode ser desconsiderado tranquilamente.
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Creational\Builder\Burger; final class BurgerDirector { public function buildBurger(BurgerBuilderInterface $builder): Burger { return $builder->build(); } }
Veja no Test que criamos a instância do Burger, e adicionamos as partes que compõem um hamburger de forma passo a passo, no segundo Test, você pode notar que podemos tranquilamente não utilizar partes para construir nosso objeto.
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Tests\Creational\Builder\Burger; use Growthdev\DesignPatterns\Creational\Builder\Burger\BurgerBuilder; use Growthdev\DesignPatterns\Creational\Builder\Burger\BurgerDirector; use PHPUnit\Framework\TestCase; final class BurgerBuilderTest extends TestCase { public function testCanCreateBurger(): void { $burgerBuilder = (new BurgerBuilder()) ->addBread("Brown Bread") ->addPatty("Beef") ->addVeggies("Pickles") ->addSauces("All") ->addWithExtraCheese(); $burgerDirector = new BurgerDirector(); $burger = $burgerDirector->buildBurger($burgerBuilder); $this->assertEquals("Brown Bread", $burger->getBread()); $this->assertEquals("Beef", $burger->getPatty()); $this->assertEquals("Pickles", $burger->getVeggies()); $this->assertEquals("All", $burger->getSauces()); $this->assertTrue($burger->getWithExtraCheese()); } public function testCanCreatePartialBurger(): void { $burgerBuilder = (new BurgerBuilder()) ->addBread("Brown Bread") ->addPatty("Beef") ->addSauces("All"); $burgerDirector = new BurgerDirector(); $burger = $burgerDirector->buildBurger($burgerBuilder); $this->assertEquals("Brown Bread", $burger->getBread()); $this->assertEquals("Beef", $burger->getPatty()); $this->assertEquals("All", $burger->getSauces()); } }
Padrão Builder simplificado
Observando bem os participantes, veja que temos apenas propriedades que geram as características do modelo e os métodos para atribuir e retornar os seus valores. Com isso, você consegue facilmente modelar o Padrão de Projeto Builder de forma diferente e ter o mesmo resultado.
No Livro Java Efetivo do Joshua Bloch ele trás um exemplo super inteligente de uso do Padrão Builder utilizando em combinação com o padrão Static Method Factory (uma alternativa ao uso dos construtores) com uma peculiaridade de utilizar uma classe aninhada.
Como o PHP não permite este recurso, de classes aninhadas, fiz uma variação utilizando Trait e classe anônima, que vai produzir o mesmo efeito. Vamos encapsular as propriedades e métodos e acesso aos valores na classe BurderTrait para ser utilizado, tanto na classe Burger quanto na BurguerBuilder
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Creational\Builder; trait BurgerTrait { private string $bread = ''; private string $patty = ''; private string $veggies = ''; private string $sauces = ''; private bool $withExtraCheese = false; public function getBread(): string { return $this->bread; } public function getPatty(): string { return $this->patty; } public function getVeggies(): string { return $this->veggies; } public function getSauces(): string { return $this->sauces; } public function getWithExtraCheese(): bool { return $this->withExtraCheese; } }
Precisamos criar dois contratos, um de acesso às propriedades da classe Burger e outro para garantir os métodos necessários para construção do Builder
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Creational\Builder; interface BurgetInterface { public function getBread(): string; public function getPatty(): string; public function getVeggies(): string; public function getSauces(): string; public function getWithExtraCheese(): bool; }
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Creational\Builder; interface BurgerBuilderInterface { public function addBread(string $bread): self; public function addPatty(string $patty): self; public function addVeggies(string $veggies): self; public function addSauces(string $saouces): self; public function addWithExtraCheese(): self; public function build(): Burger; }
Definições prontas, podemos juntar tudo na classe Burger e utilizar o Static Method Factory para implementar os recursos para o Builder. Para isso, vamos chamar a classe anônima através deste método para efetuar a ligação com a classe concreta, Burger.
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Creational\Builder; class Burger implements BurgetInterface { use BurgerTrait; public static function builder(): BurgerBuilderInterface { return new class implements BurgerBuilderInterface, BurgetInterface { use BurgerTrait; public function addBread(string $bread): self { $this->bread = $bread; return $this; } public function addPatty(string $patty): self { $this->patty = $patty; return $this; } public function addVeggies(string $veggies): self { $this->veggies = $veggies; return $this; } public function addSauces(string $sauces): self { $this->sauces = $sauces; return $this; } public function addWithExtraCheese(): self { $this->withExtraCheese = true; return $this; } public function build(): Burger { return new Burger($this); } }; } public function __construct(BurgetInterface $builder) { $this->bread = $builder->getBread(); $this->patty = $builder->getPatty(); $this->veggies = $builder->getVeggies(); $this->sauces = $builder->getSauces(); $this->withExtraCheese = $builder->getWithExtraCheese(); } }
Esta implementação ficou um pouco “verbosa”, porém, com o uso do Trait, deu para economizar boas linhas de código. Mas tudo indica que quando estivermos com o PHP 8.1, ao utilizarmos readonly nas propriedades, vamos conseguir garantir a imutabilidade sem tanto esforço.
Enfim, mas pode notar que o comportamento esperado para nossa solução permanece. Veja como ficou mais interessante desta forma:
<?php declare(strict_types=1); namespace Growthdev\DesignPatterns\Tests\Creational\Builder; use Growthdev\DesignPatterns\Creational\Builder\Burger; use PHPUnit\Framework\TestCase; final class BuilderTest extends TestCase { public function testCanCreateBurger(): void { $burger = Burger::builder() ->addBread("Brown Bread") ->addPatty("Beef") ->addVeggies("Pickles") ->addSauces("All") ->addWithExtraCheese() ->build(); $this->assertEquals("Brown Bread", $burger->getBread()); $this->assertEquals("Beef", $burger->getPatty()); $this->assertEquals("Pickles", $burger->getVeggies()); $this->assertEquals("All", $burger->getSauces()); $this->assertTrue($burger->getWithExtraCheese()); } public function testCanCreatePartialBurger(): void { $burger = Burger::builder() ->addBread("Brown Bread") ->addPatty("Beef") ->addSauces("All") ->build(); $this->assertEquals("Brown Bread", $burger->getBread()); $this->assertEquals("Beef", $burger->getPatty()); $this->assertEquals("All", $burger->getSauces()); } }
O código completo você encontra no meu Github: https://github.com/growthdev-repo/design-patterns
E aí? De que forma você costuma implementar o Padrão de Projeto Builder? Você conhecia o padrão Static Method Factory? Deixa aqui seu comentário que ajudará bastante este site a disseminar conhecimento de qualidade e acessível a todos. Até o próximo artigo!
Confiança Sempre!!!
Fontes:
- [1] GAMMA, Erich et al. Padrões de Projeto: Soluções reutilizáveis de software orientado a objetos.
- https://stacktraceguru.com/builder-pattern/
- https://designpatternsphp.readthedocs.io/en/latest/Creational/Builder/README.html
- https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java
Seja o primeiro a comentar