DEV Community

Cover image for 🧠Padrão de Projeto Factory em TypeScript: Exemplo Didático com Contas Bancárias

🧠Padrão de Projeto Factory em TypeScript: Exemplo Didático com Contas Bancárias

Você já se deparou com a necessidade de criar diferentes tipos de objetos em sua aplicação e ficou preocupado em encher o código de condicionais ou duplicações? Em desenvolvimento de software, os Padrões de Projeto surgem exatamente para oferecer soluções elegantes a problemas comuns. Um desses padrões essenciais é o padrão Factory, parte dos chamados padrões criacionais. Em essência, o padrão Factory permite criar objetos sem expor ao código cliente a lógica de criação desses objetos. Em outras palavras, seu principal objetivo é instanciar classes concretas sem que o cliente precise saber exatamente qual classe está sendo usada, delegando essa decisão para um componente central ou subclasses especializadas. Isso traz mais flexibilidade e mantém nosso código limpo e extensível.

Para ilustrar de forma simples, imagine que você está desenvolvendo um sistema bancário que precisa lidar com diferentes tipos de contas, como conta corrente, conta poupança e conta salário. Cada tipo de conta possui comportamentos e regras próprias (por exemplo, a poupança pode ter cálculo de juros, a conta salário pode ter restrições de depósito, etc). Se fossemos criar manualmente cada tipo de conta espalhado pelo código, teríamos muitos condicionais if/else ou instâncias diretas de classes, tornando o código menos organizado. Além disso, repetir esse processo manualmente em vários lugares seria repetitivo e propenso a erros. É exatamente esse problema que o padrão Factory vem resolver, oferecendo uma maneira padronizada de criar esses objetos.


🛠️ O que é e por que usar o Padrão Factory?

O padrão Factory (também conhecido como Factory Method, ou Método Fábrica) faz parte do conjunto dos padrões criacionais definidos pela GoF. A ideia central é simples: centralizar a lógica de instanciação de objetos em um único lugar, em vez de espalhar chamadas de construção (new) por todo o programa. Em vez de criar um objeto diretamente com new no código cliente, você chama um método fábrica dedicado, responsável por instanciar o tipo correto e devolvê-lo já abstracionado na interface comum. Essa abordagem traz diversos benefícios de design:

  • Desacoplamento: o código que solicita o objeto não precisa conhecer qual classe concreta foi instanciada. A fábrica decide e fornece a instância apropriada, reduzindo dependências diretas entre partes do código.
  • Reutilização de código: a lógica de criação fica encapsulada em um único lugar, evitando duplicação. Se houver alguma mudança nesse processo (por exemplo, parâmetros adicionais para criar uma conta poupança), basta alterar na fábrica e todos os chamadores se beneficiam da atualização.
  • Facilidade de manutenção: adicionar ou modificar tipos de contas se torna mais simples. Quando for necessário introduzir um novo tipo de conta bancária, você pode fazê-lo implementando uma nova classe de conta e atualizando a fábrica, sem alterar o código cliente que usa essas contas. Isso segue o princípio de aberto/fechado, onde estendemos comportamentos sem modificar os existentes.
  • Legibilidade e segurança: o uso de um método fábrica com um nome claro (por exemplo, criarConta) torna o código mais legível e autoexplicativo. Além disso, reduzimos a chance de erros, já que evitamos espalhar detalhes de instância (como qual classe usar) por vários locais do sistema.

O padrão Factory nos ajuda a substituir a criação direta de objetos por uma chamada indireta, organizada e polimórfica. O código cliente passa a depender de uma interface ou classe abstrata, e não de implementações concretas específicas, ganhando flexibilidade.


🧪 Exemplo Prático: Criando Contas Bancárias com Factory

Vamos aplicar o padrão Factory em um exemplo prático utilizando TypeScript. Nosso cenário será a criação de diferentes tipos de contas bancárias: ContaCorrente, ContaPoupanca e ContaSalario. Primeiro, definiremos uma interface (ou classe abstrata) comum para todas as contas, estabelecendo operações básicas que cada conta deve ter. Em seguida, implementaremos cada tipo de conta concreta com suas particularidades. Por fim, construiremos uma classe fábrica que, dado um tipo de conta, decide qual classe instanciar e retorna uma referência do tipo base.

Acompanhe o passo a passo no código abaixo, que está totalmente escrito em TypeScript e contém comentários para facilitar o entendimento:

// Classe base abstrata representando uma Conta genérica abstract class Conta { // Cada conta terá um saldo e métodos abstratos para operações básicas protected saldo: number = 0; abstract getSaldo(): number; abstract sacar(valor: number): void; abstract depositar(valor: number): void; } // Conta Corrente: herda de Conta e implementa as operações class ContaCorrente extends Conta { constructor(saldoInicial: number) { super(); this.saldo = saldoInicial; } // Retorna o saldo atual da conta corrente getSaldo(): number { return this.saldo; } // Saca um valor, não permite saldo negativo sacar(valor: number): void { if (valor > this.saldo) { throw new Error("Saldo insuficiente para saque na conta corrente"); } this.saldo -= valor; } // Deposita um valor na conta corrente depositar(valor: number): void { this.saldo += valor; } } // Conta Poupança: herda de Conta e tem taxa de juros class ContaPoupanca extends Conta { private taxaJuros: number; // taxa de juros (% ao mês, por exemplo) constructor(saldoInicial: number, taxaJuros: number) { super(); this.saldo = saldoInicial; this.taxaJuros = taxaJuros; } // Retorna o saldo + juros acumulados (simulação simples de rendimento) getSaldo(): number { return this.saldo + this.calcularJuros(); } // Saca um valor, similar à ContaCorrente sacar(valor: number): void { if (valor > this.saldo) { throw new Error("Saldo insuficiente para saque na conta poupança"); } this.saldo -= valor; } // Deposita um valor, similar à ContaCorrente depositar(valor: number): void { this.saldo += valor; } // Método específico da poupança para calcular juros com base no saldo atual private calcularJuros(): number { return this.saldo * (this.taxaJuros / 100); } } // Conta Salário: herda de Conta, geralmente apenas recebe depósitos de empregador class ContaSalario extends Conta { constructor(saldoInicial: number) { super(); this.saldo = saldoInicial; } getSaldo(): number { return this.saldo; } sacar(valor: number): void { if (valor > this.saldo) { throw new Error("Saldo insuficiente para saque na conta salário"); } this.saldo -= valor; } depositar(valor: number): void { // Em uma conta salário real, depósitos podem ser restritos ao empregador. // Aqui, permitimos depositar normalmente para simplificar. this.saldo += valor; } } // FabricaContas: classe fábrica que cria contas de acordo com o tipo solicitado class FabricaContas { static criarConta(tipo: string, saldoInicial: number, taxaJuros?: number): Conta { switch (tipo) { case "corrente": return new ContaCorrente(saldoInicial); case "poupanca": // Conta poupança requer informar a taxa de juros if (taxaJuros === undefined) { throw new Error("Taxa de juros deve ser fornecida para conta poupança"); } return new ContaPoupanca(saldoInicial, taxaJuros); case "salario": return new ContaSalario(saldoInicial); default: throw new Error("Tipo de conta inválido"); } } } // --- Código de uso (cliente) --- // Agora vamos usar a FabricaContas para criar diferentes contas: const contaCorrente = FabricaContas.criarConta("corrente", 1000); contaCorrente.depositar(500); // depositando em conta corrente console.log("Saldo conta corrente:", contaCorrente.getSaldo()); // Saída esperada: Saldo conta corrente: 1500 const contaPoupanca = FabricaContas.criarConta("poupanca", 500, 1); // 1% de juros contaPoupanca.sacar(100); // sacando da conta poupança console.log("Saldo conta poupança:", contaPoupanca.getSaldo()); // Saída: Saldo conta poupança: 404 (por exemplo, 400 saldo + 4 de juros) const contaSalario = FabricaContas.criarConta("salario", 3000); console.log("Saldo conta salário:", contaSalario.getSaldo()); // Saída: Saldo conta salário: 3000 (depósitos apenas do empregador, neste exemplo) 
Enter fullscreen mode Exit fullscreen mode

No código acima, podemos observar passo a passo a aplicação do padrão Factory:

  • Definimos uma classe base abstrata Conta, que declara métodos que todas as contas concretas devem implementar (getSaldo(), sacar(), depositar()). Essa é a interface comum que permite ao código cliente tratar diferentes contas de forma genérica.
  • Em seguida, implementamos as classes concretas: ContaCorrente, ContaPoupanca e ContaSalario. Cada uma estende Conta e fornece sua própria lógica para saque, depósito e cálculo de saldo. Note que a conta poupança possui um atributo adicional taxaJuros e um método privado calcularJuros() para exemplificar um comportamento específico (acréscimo de juros sobre o saldo). Já a conta salário, neste exemplo simplificado, funciona de forma similar à conta corrente, mas poderíamos restringir depósitos diretos nela.
  • Criamos então a classe FabricaContas, que contém o método estático criarConta(). É aqui que acontece a "mágica" do Factory: esse método recebe um parâmetro indicando o tipo de conta desejado (uma string, no nosso caso) e, através de um simples switch, instancia a classe apropriada para aquele tipo. Se o tipo for "corrente", cria ContaCorrente; se for "poupanca", cria ContaPoupanca (exigindo também o parâmetro da taxa de juros); se for "salario", cria ContaSalario. Caso um tipo inválido seja passado, lançamos um erro – assim o cliente sabe que aquele tipo não é suportado. Toda a lógica de decisão de qual classe instanciar está encapsulada dentro de FabricaContas.
  • Por fim, no código cliente (a parte "// --- Código de uso (cliente) ---"), vemos como é simples utilizar a fábrica. Basta chamar FabricaContas.criarConta() passando o tipo desejado e os parâmetros necessários, e ele nos retorna uma instância de Conta. Repare que, do ponto de vista do código cliente, não estamos usando new ContaCorrente() ou new ContaPoupanca() diretamente – quem faz isso é a fábrica. Nós apenas recebemos de volta um objeto do tipo abstrato Conta e o utilizamos chamando métodos como depositar, sacar ou getSaldo(). O cliente não se importa se por trás é uma ContaCorrente ou ContaPoupanca; ele só lida com Conta, o que demonstra o baixo acoplamento. Se amanhã adicionarmos uma ContaInvestimento, por exemplo, basta criar a nova classe e ajustar o método fábrica, sem alterar nada no código que usa FabricaContas para obter as contas.

🧾 Diagrama de Sequência do Factory Pattern

Para deixar ainda mais claro como funciona o padrão Factory, vamos visualizar um diagrama de sequência. Ele mostra o passo a passo da interação entre o código cliente, a fábrica e a conta que será criada (neste caso, uma conta poupança).

Factory Pattern

O que acontece nesse fluxo:

  1. O cliente solicita à FabricaContas a criação de uma conta poupança.
  2. A fábrica instancia internamente uma ContaPoupanca com os parâmetros informados.
  3. A instância é retornada ao cliente como uma abstração do tipo Conta.
  4. O cliente interage com a conta criada sem precisar saber que se trata de uma ContaPoupanca.

Esse encapsulamento da criação e a separação entre o código que usa e o que cria os objetos é justamente o que torna o padrão Factory tão útil e poderoso.

Como vimos, o padrão Factory nos permite encapsular a criação de objetos e delegar essa responsabilidade a uma única classe (ou método), facilitando a vida do desenvolvedor. Isso traz várias vantagens práticas, já destacadas anteriormente e resumidas a seguir:

  • Baixo acoplamento: o código cliente não precisa conhecer as classes concretas das contas. Ele pede uma conta para a fábrica e usa através da interface comum (Conta), tornando o sistema mais flexível a mudanças.
  • Centralização da lógica de criação (Reutilização): toda a lógica para instanciar diferentes tipos de conta está em um único método (criarConta). Isso evita duplicação de código de criação espalhada pelo sistema, facilitando reutilização e garantindo consistência.
  • Facilidade de manutenção: se for necessário modificar como uma conta é criada (por exemplo, exigir mais parâmetros) ou adicionar um novo tipo de conta, fazemos a alteração somente na fábrica. O impacto é isolado, reduzindo a chance de erros e tornando a manutenção mais simples.
  • Extensibilidade: novos tipos de contas podem ser adicionados com impacto mínimo. O padrão Factory segue o princípio Open/Closed (aberto para extensão, fechado para modificação) em boa parte do código – o núcleo do programa não precisa ser alterado para acomodar novos produtos, apenas a fábrica é estendida ou adaptada para conhecê-los.

🏁 Conclusão

Neste artigo, exploramos o padrão de projeto Factory de maneira didática, usando um exemplo do mundo bancário para facilitar o entendimento. Vimos o problema de criar objetos manualmente e como o Factory oferece uma solução elegante ao abstrair o processo de criação e centralizá-lo. Com uma implementação prática em TypeScript, ficou claro como o código cliente passa a depender apenas de uma interface genérica (Conta), enquanto uma classe fábrica se encarrega de instanciar os objetos corretos de acordo com a necessidade.

O resultado é um código mais robusto, flexível e fácil de manter, especialmente em cenários que lidam com vários tipos de objetos com comportamentos distintos. O padrão Factory é uma ferramenta valiosa no arsenal de um desenvolvedor e, embora simples de aplicar, traz um grande ganho em organização do código. Como qualquer padrão de projeto, deve ser usado com bom senso – nem todo caso de instanciamento requer uma fábrica – mas em sistemas maiores ou em evolução, ele certamente ajuda a reduzir acoplamento e melhorar a escalabilidade das soluções.


📚 Referências


💡Curtiu?

Se quiser trocar ideia sobre IA, cloud e arquitetura, me segue nas redes:

Publico conteúdos técnicos direto do campo de batalha. E quando descubro uma ferramenta que economiza tempo e resolve bem, como essa, você fica sabendo também.

Top comments (0)