Skip to main content

Command Palette

Search for a command to run...

Design Pattern: Factory Method

Published
•7 min read•View as Markdown

Factory Method

Como o próprio nome já diz, ele é a nossa fábrica. De que? De objetos uai.

Este é um padrão criacional que fornece um interface de criação de objetos numa superclasse, mas que seja possível alternar o tipo do objeto que retorna dessa criação.

Explicando o problema

Vamos imaginar que você está desenvolvendo um e-commerce. No primeiro momento sua aplicação apenas aceitará cartão de crédito, portanto sempre que vier um pedido no seu sistema toda a regra de pagamentos e validações ficará dentro da classe CreditCard.

<?php
class Cart 
{
    public function checkout() 
    {
        $payment = new CreditCard;
        return $payment?->pay();
    }
}

class CreditCard 
{
    public function pay()
    {
        return 'Transação concluída com sucesso' . PHP_EOL;
    }
}

$cart = new Cart();
echo $cart->checkout(); //Transação concluída com sucesso

Agora sua loja está indo muito bem e crescendo, mas seus clientes começam a reclamar e pedem por mais meios de pagamento. Para evitar que o número de vendas caia, logo você começa a desenvolver melhorias adicionando um novo meio de pagamento Invoice (Boletos).

É uma boa noticia certo? Mas se você não tomar cuidado no inicio do desenvolvimento, é possível que tudo esteja acoplado a uma única classe, e adicionar qualquer coisa pode alterar toda a base de código. Além disso sempre que for criado um meio de pagamento novo, provavelmente passará por isso tudo novamente.

Vejamos um exemplo adicionando a classe Invoices num universo simplificado.

<?php
class Cart 
{
    private $cart;

    public function __construct ($cart) 
    {
        $this->cart = $cart;
    }

    public function checkout() 
    {

        $payment = null;

        if ($this->cart->payment === 'boleto') {
            $payment = new Invoice;
        } 

        if ($this->cart->payment === 'credit') {
            $payment = new CreditCard;
        }

        return $payment?->pay() ?? 'Erro';
    }
}

class Invoice 
{
    public function pay()
    {
        return 'Boleto para ser pago'. PHP_EOL;
    }
}

class CreditCard 
{
    public function pay()
    {
        return 'Transação concluída com sucesso' . PHP_EOL;
    }
}

//Case 1
$myCart = ['payment' => 'boleto'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Boleto para ser pago

//Case 2
$myCart = ['payment' => 'credit'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Transação concluída com sucesso

//Case 3 - Método de pagamento não implementado
$myCart = ['payment' => 'Outro meio de pagamento'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Erro

Nesse pequeno exemplo parece até simples (só adicionar uns ifs e já era), mas nem sempre é assim, adicionar uma nova classe num código que depende de classes concretas pode dar uma certa dor de cabeça.

Pense comigo, como ficaria esse código com 5 meios de pagamentos? Terão 5 ifs? Um Switch? Parece meio ruim né, além de deixar o sódigo bastante sujo, ficará cheio de condicionais que mudam o comportamento da aplicação, podendo até criar um erro inesperado.

Solução

O padrão Factory sugere que você não chame diretamente o operador new. Você pode estar se perguntando como irá criar os objetos, mas calma, ainda será com o operador new, mas ele será chamado dentro desse método chamado de fábrica, esses objetos gerados e retornados por esse método serão chamados de produtos.

Usando nosso exemplo anterior, vamos aplicar o conceito para entender a ideia dele:

<?php
class Cart 
{
    private $factoryPayment;
    private $cart;

    public function __construct ($cart) 
    {
        $this->factoryPayment = new FactoryPayment;
        $this->cart = $cart;
    }

    public function checkout() 
    {
        $payment = $this->factoryPayment->createPayment($this->cart->payment);
        return $payment?->pay() ?? 'Erro';
    }
}

class FactoryPayment 
{
    public function createPayment($payment) : Payment|null
    {
        return match ($payment) {
            'boleto' => new Invoice,
            'credit' => new CreditCard,
            default => null
        };
    }
}

interface Payment {
    public function pay();
}

class Invoice implements Payment
{
    public function pay()
    {
        return 'Boleto para ser pago'. PHP_EOL;
    }
}

class CreditCard implements Payment
{
    public function pay()
    {
        return 'Transação concluída com sucesso' . PHP_EOL;
    }
}

//Case 1
$myCart = ['payment' => 'boleto'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Boleto para ser pago

//Case 2
$myCart = ['payment' => 'credit'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Transação concluída com sucesso

//Case 3 - Método de pagamento não implementado
$myCart = ['payment' => 'Outro meio de pagamento'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Erro

Olhando de início pode parecer sem sentido, pois apenas mudamos a chamada de criação para outra classe. Mas isso ajuda bastante na hora de incluir novos meios de pagamento por exemplo, basta implementar a interface Payment com o método pay e se tiver tudo correto com a nova classe vai funcionar tudo corretamente sem precisar de alterar nada no código principal, pois está tudo abstraído.

Outro caso muito maneiro é a possibilidade de criar outro factory antes do FactoryPayment, para criar gateways de pagamento. Suponha que vc decida pagar boletos (Invoice) com o gateway X e o cartão de crédito com o Y (CreditCard) com esse padrão isso é possível, irei explicar no código cada ponto, veja:

<?php

//Classe principal que será o código cliente que receberá os produtos
class Cart 
{
    private $factoryGateway;
    private $cart;

    public function __construct ($cart) 
    {
        $this->factoryGateway = new FactoryGateway;
        $this->cart = $cart;
    }

    public function checkout() 
    {
        $payment = $this->factoryGateway->createGateway($this->cart->payment);
        return $payment?->pay() ?? 'Erro';
    }
}

// Classe fábrica de gateways que será chamada pelo código cliente
// usando o método de pagamento para decisão.
class FactoryGateway 
{
    public function createGateway($payment) : Payment|null
    {
        $gateway = match ($payment) {
            'boleto' => new GatewayX,
            'credit' => new GatewayY,
            default => null
        };
        return $gateway?->createPayment();
    }
}

// Classe abstrata que será usada como fábrica de pagamentos
abstract class AbstractPayment 
{
    abstract public function createPayment(): Payment;

    protected function authenticate()
    {
        echo 'Autenticado no ' . get_class($this) . PHP_EOL;
    }
}

// Agora temos que implementar os gateways e os métodos que retornarão os produtos
// que no nosso caso são meios de pagamento
class GatewayX extends AbstractPayment
{
    public function createPayment(): Payment
    {
        $this->authenticate();
        return new Invoice;
    }
}

class GatewayY extends AbstractPayment
{
    public function createPayment() : Payment
    {
        $this->authenticate();
        return new CreditCard;
    }
}

// Abaixo temos a interface e os produtos que a implementam. 
interface Payment {
    public function pay();
}

class Invoice implements Payment
{
    public function pay()
    {
        //cria boleto
        return 'Boleto para ser pago'. PHP_EOL;
    }
}

class CreditCard implements Payment
{
    public function pay()
    {
        //valida dados do cartão, limite e etc
        return 'Transação concluída com sucesso' . PHP_EOL;
    }
}

//Case 1
$myCart = ['payment' => 'boleto'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Boleto para ser pago com sucesso no GatewayX

//Case 2
$myCart = ['payment' => 'credit'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Transação concluída com sucesso no GatewayY

//Case 3 - Método de pagamento não implementado
$myCart = ['payment' => 'Outro meio de pagamento'];
$cart = new Cart((object) $myCart);
echo $cart->checkout(); //Erro

OBS: Ele fica bem similar ao próximo padrão que vou mostrar em um artigo futuro, que é o Abstract Factory.

Pontos de atenção

  1. Todos os produtos devem implementar a mesma classe ou interface.

    Ex: Payment é implementado em todos os produtos.

  2. Produtos concretos são implementações diferentes da mesma interface.

    Ex: Invoice , CreditCard.

  3. Na classe criador, deve ser declarado o método fábrica que retornará os novos objetos produto. Esse retorno deve corresponder à interface do produto.

    Ex: Criador AbstractPayment possui o método fábrica createPayment e é retornado um produto do tipo Payment.

  4. Apesar de ter criador no nome, essa não é a principal responsabilidade dela, é possível ter alguma regra de negócio relacionada aos seus produtos.

    Ex: Método authenticate da classe criadora AbstractPayment.

  5. Criadores concretos sobrescrevem o método fábrica base para criar um tipo diferente de produto.

    Ex: GatewayX e GatewayY.

SOLID

Aqui temos menção a 2 itens do SOLID:

Princípio de responsabilidade única (S): Você pode mover o código criador para um único ponto, facilitando a manutenção.

Principio aberto/fechado (O): Você pode adicionar novos produtos na aplicação sem quebrar o código cliente.

Conclusão

Use esse padrão sempre que você não souber os tipos e dependências dos objetos que seu código irá utilizar, ele separa o código construtor de produtos do código principal. Isso deixa o código de construção do produto mais simples de ser extendido e independente do resto.

Por exemplo, caso queira adicionar mais métodos de pagamento ou gateways, basta implementar as classes correspondentes, adicionar nos métodos fábrica e pronto, seu código está com a nova funcionalidade.

É isso pessoal, chegamos ao fim de mais um artigo, até o próximo.

K

Cara, o artigo está sensacional, parabéns. Eu só achei o exemplo meio confuso, principalmente na parte de gateways. Talvez pra quem nunca tenha lidado com pagamentos fique mais simples.

Eu vejo a forma de pagamento como um factory de PaymentMethod e o gateway como um SDK/uma infraestrutura independente disso, sendo que no exemplo a forma de pagamento tá vindo do Gateway, daí a confusão. Mas entendi a mensagem sobre Factory, talvez fosse uma boa dar uma reorganizada nisso se concordar.

Outra sugestão: acho que fica melhor escrever os blocos de cada classe de forma separada, fica mais fácil de ler onde cada classe termina. Mas entendo que deixar tudo junto facilita para rodar em outro lugar (copiando tudo de uma vez só).

Se for a intenção, convém construir cada trecho separadamente e mostrar o código final... no fim, hehe. Assim dá pra acompanhar a construção de cada coisa.

Assim você pode focar em mostrar só os arquivos alterados em cada evolução.

Tá aí o super feedback e continua jogando duro, seus conhecimentos estão ajudando outros devs a começar a desenvolver do jeito certo.

Design Pattern

Part 2 of 4

Estou estudando mais a fundo sobre padrões de projeto e vou compartilhar aqui as minhas anotações.

Up next

Design Pattern: SOLID

Exemplificando cada letra =)