Software · Design Patterns

Padrões Criacionais na Prática: Factory Method, Builder e o Uso do Singleton

Como encapsular a instanciação de objetos complexos, eliminar construtores telescópicos e integrar padrões clássicos do GoF a contêineres modernos de injeção de dependência.

Diagrama conceitual de padrões criacionais GoF ilustrando Factory Method Builder construtor em etapas e ciclo de vida de Singleton

A criação de objetos em sistemas orientados a objetos parece, à primeira vista, uma tarefa trivial: basta utilizar a palavra-chave new da linguagem e passar os parâmetros necessários. No entanto, quando essa instanciação direta se espalha indiscriminadamente por todas as camadas de uma aplicação corporativa, o código torna-se fortemente acoplado às classes concretas, impossível de ser testado isoladamente com mocks e extremamente vulnerável a mudanças nos construtores.

Os Padrões Criacionais, catalogados pelo clássico grupo Gang of Four (GoF), têm como propósito fundamental abstrair o processo de instanciação. Eles encapsulam a lógica de decisão sobre qual objeto criar, como ele deve ser montado e quando ele deve ser disponibilizado, promovendo sistemas flexíveis e verdadeiramente extensíveis.

O Espectro dos Padrões Criacionais

Padrão Criacional Propósito Principal Quando Utilizar
Factory Method Criação polimórfica delegada a subclasses ou métodos especialistas. Quando o tipo exato do objeto a ser instanciado depende de configurações ou contexto dinâmico.
Abstract Factory Criação de famílias completas de objetos compatíveis entre si. Quando o sistema precisa ser independente de como seus múltiplos produtos relacionados são criados.
Builder Montagem passo a passo de objetos complexos e imutáveis. Quando um objeto possui múltiplos parâmetros opcionais ou regras rígidas de construção incremental.
Singleton Garantia de instância única global com ponto único de acesso. Quando um recurso compartilhado (como pool de conexões ou coletor de métricas) deve ter apenas uma instância no processo.
Prototype Criação de novos objetos por clonagem de instâncias existentes. Quando o custo de instanciação direta é proibitivo ou quando objetos diferem minimamente de um template base.

Dentre esses padrões, três destacam-se pelo impacto direto e diário no desenvolvimento de software moderno: Factory Method, Builder e Singleton.

1. Factory Method: Delegação e Desacoplamento da Criação

O padrão Factory Method define uma interface para criar um objeto, mas deixa para subclasses ou métodos especializados decidirem qual classe concreta instanciar. Em vez de o código cliente invocar diretamente new PaymentGatewayBoleto() ou new PaymentGatewayPix(), ele requisita a criação através de uma fábrica abstrata.

Cenário de Aplicação: Provedores de Integração

Imagine um sistema de faturamento que precisa processar cobranças via múltiplos provedores bancários (Itaú, Bradesco, Banco do Brasil). Se a camada de aplicação instanciar as classes de cada banco diretamente, qualquer inclusão de uma nova instituição bancária exigirá alterações e recompilações nos controladores e serviços de negócio:

// Abstração do produto criado
public interface IPaymentGateway 
{
    Task<TransactionResult> ChargeAsync(decimal amount, Customer customer);
}

// Fábrica encapsulada
public static class PaymentGatewayFactory 
{
    public static IPaymentGateway Create(PaymentMethod method, IConfiguration config) => method switch
    {
        PaymentMethod.Pix    => new PixPaymentGateway(config["Pix:Key"]),
        PaymentMethod.Boleto => new BoletoPaymentGateway(config["Boleto:Account"]),
        PaymentMethod.Card   => new CreditCardPaymentGateway(config["Card:Secret"]),
        _ => throw new ArgumentOutOfRangeException(nameof(method), "Método não suportado.")
    };
}

O cliente interage exclusivamente com a interface IPaymentGateway, permitindo plugar novos gateways sem modificar uma única linha do fluxo de checkout.

2. Builder: O Fim dos Construtores Telescópicos

Um dos piores anti-patterns em linguagens tipadas é o construtor telescópico: classes complexas de negócio (como relatórios analíticos, requisições de contrato ou planos de cobrança) que possuem construtores com dezenas de parâmetros, muitos deles nulos ou booleanos ininteligíveis:

// Anti-pattern do construtor telescópico (ilegível e frágil)
var report = new AuditReport("Fiscal", null, true, false, 0, DateTime.UtcNow, null, true);

O padrão Builder decompõe a construção do objeto complexo em etapas fluentes e autoexplicativas, permitindo validar regras de integridade antes de entregar a instância final imutável:

public class AuditReportBuilder 
{
    private string _title;
    private DateTime _generatedAt = DateTime.UtcNow;
    private readonly List<string> _sections = new();
    private bool _includeConfidential;

    public AuditReportBuilder WithTitle(string title) 
    {
        if (string.IsNullOrWhiteSpace(title))
            throw new ArgumentException("O título é obrigatório.");
        _title = title;
        return this;
    }

    public AuditReportBuilder AddSection(string sectionName) 
    {
        _sections.Add(sectionName);
        return this;
    }

    public AuditReportBuilder IncludeConfidentialData() 
    {
        _includeConfidential = true;
        return this;
    }

    public AuditReport Build() 
    {
        if (string.IsNullOrEmpty(_title))
            throw new InvalidOperationException("Não é possível construir o relatório sem título.");

        return new AuditReport(_title, _sections.AsReadOnly(), _includeConfidential, _generatedAt);
    }
}

O uso torna-se transparente e à prova de falhas:

var report = new AuditReportBuilder()
    .WithTitle("Auditoria Financeira Q3")
    .AddSection("Fluxo de Caixa")
    .AddSection("Conformidade Fiscal")
    .IncludeConfidentialData()
    .Build();

3. Singleton: Da Armadilha Global ao Gerenciamento por Contêiner

O padrão Singleton garante que uma classe tenha uma única instância em todo o ciclo de vida da aplicação, fornecendo um ponto global de acesso a ela. Tradicionalmente, sua implementação clássica utiliza um construtor privado e uma propriedade estática com inicialização preguiçosa (lazy):

// Singleton clássico thread-safe com Lazy<T>
public sealed class HardwareMetricsCollector 
{
    private static readonly Lazy<HardwareMetricsCollector> _instance =
        new(() => new HardwareMetricsCollector());

    private HardwareMetricsCollector() { /* Inicialização de buffers nativos */ }

    public static HardwareMetricsCollector Instance => _instance.Value;
}

O Risco do Anti-Pattern

O Singleton clássico estático ganhou má reputação na indústria porque, se for utilizado para manter estado mutável compartilhado, ele reintroduz os perigos de variáveis globais: acoplamento oculto, condições de corrida entre threads e pesadelos para a execução de testes unitários em paralelo.

A Cura: Contêineres de Injeção de Dependência (IoC)

Em arquiteturas corporativas modernas, o Singleton manual é praticamente abandonado em favor do ciclo de vida gerenciado por Injeção de Dependência:

builder.Services.AddSingleton<IMetricsCollector, HardwareMetricsCollector>();

O contêiner de IoC se encarrega de garantir uma única instância do serviço durante toda a vida do processo, mas as classes consumidoras recebem a interface via injeção de dependência no construtor. Isso preserva a testabilidade do sistema com mocks e mantém a rastreabilidade das dependências.

Boas Práticas para Adoção de Padrões Criacionais

Em bases de código escaláveis, a instanciação de objetos e a escolha de padrões criacionais costumam seguir diretrizes claras:

  1. Construtores de Domínio Protegidos: Nossas entidades de domínio ricas (Rich Domain Models) não expõem construtores públicos desprovidos de validação. A criação de agregados ocorre através de métodos de fábrica estáticos explícitos (Order.Create(...)) que garantem invariantes de negócio desde o primeiro milissegundo de existência.
  2. Builders para Configurações e Pipelines: Adotamos builders fluentes para construir pipelines complexos de dados, políticas de retry e orquestrações de regras de negócio, assegurando tipagem forte e legibilidade para toda a equipe.
  3. Delegação ao Contêiner de IoC: Nenhuma classe de infraestrutura ou aplicação implementa singletons estáticos legados. Todo gerenciamento de ciclo de vida (Singleton, Scoped ou Transient) é confiado ao contêiner de injeção de dependência oficial da plataforma.