DEV Community

luanloose
luanloose

Posted on

Design Pattern: 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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.

Discussion (0)