Design Patterns: Simple Factory, Factory Method e Abstract Factory (Parte 3)

·7 min de leitura

Contexto

Seguindo a linha da Parte 2, você já viu como Strategy ajuda a separar comportamentos. Quando esse tipo de design começa a amadurecer, surge uma pergunta inevitável: quem decide qual implementação concreta deve ser instanciada em cada cenário?

Na prática, esse problema aparece quando o projeto começa a repetir if/else, switch ou match para criar objetos. No início parece inofensivo, mas depois de algumas regras novas, feature flags e variações por ambiente, a criação fica espalhada e difícil de manter. É exatamente aqui que os padrões de fábrica entram: eles tiram o foco do new e colocam o foco em uma forma consistente de criação.

Este artigo cobre três abordagens que parecem semelhantes, mas têm propósitos diferentes: Simple Factory, Factory Method e Abstract Factory. A melhor forma de entender é evoluir o mesmo exemplo, saindo de um caso simples até um cenário com famílias de objetos que precisam ser coerentes entre si.

Simple Factory

Simple Factory não é um padrão formal do GoF, mas é amplamente usado porque resolve bem o primeiro estágio do problema: centralizar decisões de criação em um único ponto. Em vez de cada parte do sistema saber qual classe concreta deve ser instanciada, o código pede para a fábrica criar e recebe a abstração pronta.

No exemplo abaixo, a fábrica recebe um tipo e devolve a estratégia de frete correspondente:

<?php

declare(strict_types=1);

interface FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float;
}

final readonly class StandardFreight implements FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float
    {
        return 10.0 + ($weightKg * 2.5);
    }
}

final readonly class ExpressFreight implements FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float
    {
        return 25.0 + ($weightKg * 5.0);
    }
}

enum FreightType: string
{
    case Standard = 'standard';
    case Express = 'express';
}

final class FreightStrategyFactory
{
    public function make(FreightType $type): FreightStrategy
    {
        return match ($type) {
            FreightType::Standard => new StandardFreight(),
            FreightType::Express => new ExpressFreight(),
        };
    }
}

O uso fica direto:

$factory = new FreightStrategyFactory();
$freight = $factory->make(FreightType::Express);
$price = $freight->quote(3.2, '01310-000');

Esse formato melhora legibilidade e reduz acoplamento, porque o restante da aplicação deixa de conhecer classes concretas. O cuidado é que, com o tempo, a fábrica pode virar um ponto de concentração excessivo de regras e crescer sem controle. Quando isso começa a acontecer, normalmente é sinal de que você precisa dar o próximo passo de modelagem.

Factory Method

Factory Method é o próximo passo quando não basta ter uma única fábrica central. A ideia aqui é deslocar a decisão de criação para “criadores” diferentes, usando polimorfismo. Em vez de um match enorme, você passa a ter classes criadoras especializadas, cada uma responsável por criar sua variação.

No exemplo, o objeto retornado continua sendo FreightStrategy, mas quem escolhe a implementação concreta é cada FreightCreator:

<?php

declare(strict_types=1);

interface FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float;
}

final readonly class StandardFreight implements FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float
    {
        return 10.0 + ($weightKg * 2.5);
    }
}

final readonly class ExpressFreight implements FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float
    {
        return 25.0 + ($weightKg * 5.0);
    }
}

abstract class FreightCreator
{
    abstract protected function createFreight(): FreightStrategy;

    public function quote(float $weightKg, string $zipCode): float
    {
        return $this->createFreight()->quote($weightKg, $zipCode);
    }
}

final class StandardFreightCreator extends FreightCreator
{
    protected function createFreight(): FreightStrategy
    {
        return new StandardFreight();
    }
}

final class ExpressFreightCreator extends FreightCreator
{
    protected function createFreight(): FreightStrategy
    {
        return new ExpressFreight();
    }
}

Uso:

$creator = new ExpressFreightCreator();
$price = $creator->quote(3.2, '01310-000');

Esse modelo funciona muito bem quando você quer substituir comportamento trocando o criador, e também quando precisa estender sem editar uma fábrica única o tempo todo. O trade-off é estrutural: aparecem mais classes no projeto. Em contextos pequenos, pode ser excesso; em contextos com muitas variações, costuma compensar.

Abstract Factory

Abstract Factory entra em cena quando o problema já não é “qual objeto criar”, e sim “qual conjunto coerente de objetos criar”. Aqui, a preocupação principal é garantir compatibilidade entre objetos que pertencem à mesma família.

No exemplo seguinte, o checkout precisa combinar frete e impostos dentro do mesmo contexto (Brasil ou Internacional), sem misturar componentes incompatíveis:

<?php

declare(strict_types=1);

interface FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float;
}

final readonly class BrFreight implements FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float
    {
        return 12.0 + ($weightKg * 2.2);
    }
}

final readonly class IntlFreight implements FreightStrategy
{
    public function quote(float $weightKg, string $zipCode): float
    {
        return 60.0 + ($weightKg * 9.0);
    }
}

interface TaxCalculator
{
    public function tax(float $subtotal): float;
}

final readonly class BrTaxCalculator implements TaxCalculator
{
    public function tax(float $subtotal): float
    {
        return $subtotal * 0.12;
    }
}

final readonly class IntlTaxCalculator implements TaxCalculator
{
    public function tax(float $subtotal): float
    {
        return $subtotal * 0.20;
    }
}

interface CheckoutFactory
{
    public function freight(): FreightStrategy;
    public function taxes(): TaxCalculator;
}

final class BrCheckoutFactory implements CheckoutFactory
{
    public function freight(): FreightStrategy
    {
        return new BrFreight();
    }

    public function taxes(): TaxCalculator
    {
        return new BrTaxCalculator();
    }
}

final class IntlCheckoutFactory implements CheckoutFactory
{
    public function freight(): FreightStrategy
    {
        return new IntlFreight();
    }

    public function taxes(): TaxCalculator
    {
        return new IntlTaxCalculator();
    }
}

Uso:

final class CheckoutService
{
    public function __construct(private CheckoutFactory $factory) {}

    public function total(float $subtotal, float $weightKg, string $zipCode): float
    {
        $freight = $this->factory->freight()->quote($weightKg, $zipCode);
        $taxes = $this->factory->taxes()->tax($subtotal);

        return $subtotal + $freight + $taxes;
    }
}

Com isso, trocar de “mundo” vira troca de uma dependência só: a fábrica concreta. Esse é o grande ganho do Abstract Factory. Ele reduz o risco de combinações inválidas e deixa explícito que certas peças devem nascer juntas.

Comparação direta (sem ambiguidades)

Se você quiser uma regra mental curta, pense assim: no Simple Factory você centraliza new em um ponto único; no Factory Method você desloca a criação para criadores especializados; no Abstract Factory você cria famílias inteiras e coerentes de objetos.

Em termos de decisão prática, a diferença está no tamanho e no tipo do problema. Se você só quer parar de espalhar criação, Simple Factory geralmente resolve. Se a criação varia por contexto e você quer extensão por polimorfismo, Factory Method tende a encaixar melhor. Se existem objetos que sempre devem vir em conjunto e combinar entre si, Abstract Factory normalmente é o caminho mais seguro.

Como escolher (regra prática)

Uma forma saudável de evoluir é começar simples e sofisticar só quando necessário. Você pode iniciar com Simple Factory para organizar a criação. Se esse ponto central começar a inflar e as variações crescerem, Factory Method ajuda a distribuir responsabilidades. Quando o domínio exigir kits coerentes de implementações, Abstract Factory fecha o problema com mais segurança semântica.

Armadilhas comuns (as três têm)

O erro mais comum é chamar qualquer mecanismo de criação de “factory” sem deixar claro qual problema ele resolve. Outro erro recorrente é esconder dependências demais e deixar o fluxo difícil de testar. E, por fim, vale cuidado com a mistura entre fábricas e Service Locator global, porque isso costuma mascarar acoplamentos em vez de reduzi-los.

Conclusão

A confusão entre os três nomes é comum, mas fica simples quando você foca no objetivo de cada um. Simple Factory organiza criação em um ponto único. Factory Method usa polimorfismo para variar quem cria. Abstract Factory garante que objetos relacionados nasçam juntos e consistentes. A escolha certa, no fim, não é sobre “qual é mais elegante”, e sim sobre qual estrutura torna seu domínio mais claro e sustentável conforme o sistema cresce.

Referências

  • Design Patterns (GoF) — Factory Method e Abstract Factory.
  • Refactoring Guru – Factory Comparison: https://refactoring.guru/design-patterns/factory-comparison

Links