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
-
Todos os
produtos
devem implementar a mesma classe ou interface.Ex:
Payment
é implementado em todos os produtos. -
Produtos concretos
são implementações diferentes da mesma interface.Ex:
Invoice
,CreditCard
. -
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ábricacreatePayment
e é retornado um produto do tipoPayment
. -
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 criadoraAbstractPayment
. -
Criadores concretos sobrescrevem o método fábrica base para criar um tipo diferente de produto.
Ex:
GatewayX
eGatewayY
.
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.
Top comments (0)