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.
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:
- 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. - 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.
- 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.