Adeus, Classes? Padrões de Design funcionais em TypeScript e React

Rocketseat

Rocketseat

5 min de leitura
typescript
Se você já trabalhou em uma codebase moderna de React, sabe como é: componentes funcionais, hooks, composição por todo lado. Aí chega aquele momento em que você precisa implementar um padrão de design clássico e se pergunta:
Como faço isso sem criar um monte de classes? 🤔
A verdade é que existe uma lacuna entre os padrões orientados a objetos que aprendemos nos livros e a realidade do código funcional que escrevemos todo dia. A boa notícia? TypeScript oferece alternativas funcionais elegantes para os padrões clássicos, aproveitando funções de primeira classe, composição e closures.
Bora explorar como implementar três dos padrões mais comuns (Strategy, Factory e Singleton) de forma funcional, sem forçar o uso de classes onde elas não fazem sentido! 🚀
👉
Antes de prosseguir, se liga nesse vídeo incrível, que pode te ajudar muito com o material:
3 padrões de projeto para devs: Strategy, Factory Method e Singleton
🚀 Aproveite os cursos pagos da Rocketseat, liberados gratuitamente até 06/10 https://rseat.in/cursos-liberados- • Criando SaaS com Next.js e RBAC  • Node e React • Liderança Técnica • IA para Dados • UX/UI com Figma Links importantes para seu desenvolvimento 👇 • Aproveite os cursos pagos da Rocketseat, liberados gratuitamente até 29/09 https://rseat.in/lPtKZZI-o • Navegação com Expo Router - React Native • Comunicação assertiva • Microsserviços com Spring Cloud • Engenharia de Prompt • Novidades do mundo tech com curadoria Rocketseat https://rseat.in/jC1FT1tgs • Modelo de currículo gratuito https://rseat.in/_HRcY_Tp_ Você já se viu em um projeto onde o código se tornou um emaranhado de `if/else` aninhados, lógica repetida e testes impossíveis? A dor de manter um software que cresce sem estrutura é real, tornando a manutenção lenta e cara para qualquer desenvolvedor. Mas existe uma solução comprovada para esses desafios: os Padrões de Projeto. Neste vídeo, vamos desvendar três dos padrões de design mais poderosos e amplamente utilizados que todo desenvolvedor precisa dominar: Strategy, Factory Method e Singleton. Aprenda a aplicar essas soluções recorrentes para problemas recorrentes, transformando seu código em algo mais limpo, flexível e fácil de escalar. Ao assistir, você vai aprender a: • Strategy Pattern: Trocar comportamentos de algoritmos em tempo de execução de forma dinâmica, sem alterar o código principal. • Factory Method Pattern: Centralizar a criação de objetos, desacoplando o cliente dos detalhes de implementação e facilitando a extensão de produtos. • Singleton Pattern: Garantir uma única instância de uma classe e fornecer um ponto de acesso global controlado para recursos específicos. • Melhorar significativamente a manutenibilidade e a testabilidade do seu software. • Reduzir a complexidade e o acoplamento, eliminando condicionais gigantes e código espalhado. • Escrever código mais robusto, extensível e alinhado às melhores práticas de engenharia de software. Assista agora e comece a aplicar esses padrões essenciais nos seus projetos 👇 00:00 - 00:10 - Por que padrões de design são essenciais? 00:10 - 00:28 - O que são Padrões de Design? 00:28 - 00:57 - Quais os benefícios de usar Padrões? 00:57 - 01:16 - O que é o Padrão Strategy? 01:16 - 01:59 - Como aplicar o Padrão Strategy? 01:59 - 02:09 - Quando devo usar o Padrão Strategy? 02:09 - 02:29 - Quais prós e contras do Strategy? 02:29 - 02:49 - O que é o Padrão Factory Method? 02:49 - 03:31 - Como aplicar o Factory Method no código? 03:31 - 03:41 - Quando usar o Padrão Factory Method? 03:41 - 04:03 - Quais prós e contras do Factory Method? 04:03 - 04:12 - O que é o Padrão Singleton? 04:12 - 04:54 - Como implementar o Padrão Singleton? 04:54 - 05:04 - Quando devo usar o Padrão Singleton? 05:04 - 05:27 - Quais prós e contras do Singleton? 05:27 - 06:07 - Como aplicar Padrões de Design na prática? 06:07 - 06:27 - Resumo dos Padrões e Próximos Passos. #DesignPatterns,#PadroesDeProjeto,#EngenhariaDeSoftware,#Programacao,#Desenvolvimento,#CodigoLimpo,#StrategyPattern,#FactoryMethod,#Singleton,#Rocketseat ----- Conecte-se a 500mil devs e avance para o próximo nível com a nossa plataforma: https://rseat.in/rocketseat_ Cadastre-se na nossa plataforma: https://rseat.in/rocketseat_ Junte-se a mais de 392mil devs em nossa comunidade no Discord: https://discord.gg/rocketseat Acompanhe a Rocketseat nas redes sociais: TikTok: @rocketseat Facebook: @rocketseat Instagram: @rocketseat
3 padrões de projeto para devs: Strategy, Factory Method e Singleton

O desafio: padrões OO em código funcional

Os padrões de design clássicos nasceram no mundo da programação orientada a objetos. O livro "Design Patterns" (Gang of Four) foi escrito quando Smalltalk e C++ dominavam o mercado. Naturalmente, todos os exemplos usam classes, herança e polimorfismo.
O problema surge quando você trabalha em uma codebase que prioriza funções. No ecossistema JavaScript/TypeScript moderno, especialmente com React, a programação funcional é a norma. Functional components, hooks, estado imutável, funções puras. Aliás, dominar essa abordagem é o que separa as interfaces reativas medianas das excelentes. Se você quer levar suas habilidades em React para esse nível, construir projetos do zero ao avançado é o caminho ideal.
Tentar aplicar padrões OO clássicos nesse contexto cria uma dissonância. Você acaba escrevendo classes apenas para satisfazer o padrão, adicionando boilerplate desnecessário e tornando o código menos idiomático. É como tentar encaixar uma peça quadrada em um buraco redondo.
A questão não é se os padrões são úteis (são!), mas como adaptá-los para o paradigma funcional que TypeScript e JavaScript abraçam tão bem.

Alternativa funcional: strategy pattern

O Strategy Pattern tradicional usa interfaces e classes para encapsular algoritmos intercambiáveis. Em TypeScript funcional, isso se torna muito mais simples: strategies são apenas funções.

A versão orientada a objetos

interface PaymentStrategy { processPayment(amount: number): void; } class PayPalPayment implements PaymentStrategy { processPayment(amount: number): void { console.log(`Processando pagamento PayPal de R$${amount}`); } } class CreditCardPayment implements PaymentStrategy { processPayment(amount: number): void { console.log(`Processando pagamento com cartão de R$${amount}`); } } class OnlineStore { private paymentStrategy: PaymentStrategy; constructor(paymentStrategy: PaymentStrategy) { this.paymentStrategy = paymentStrategy; } setPaymentStrategy(strategy: PaymentStrategy): void { this.paymentStrategy = strategy; } checkout(amount: number): void { this.paymentStrategy.processPayment(amount); } } // Uso: muitas linhas de setup const store = new OnlineStore(new PayPalPayment()); store.checkout(100); store.setPaymentStrategy(new CreditCardPayment()); store.checkout(200);

A versão funcional

// Estratégias são apenas funções type PaymentStrategy = (amount: number) => void; const processPayPalPayment: PaymentStrategy = (amount) => { console.log(`Processando pagamento PayPal de R$${amount}`); }; const processCreditCardPayment: PaymentStrategy = (amount) => { console.log(`Processando pagamento com cartão de R$${amount}`); }; // Higher-order function para processar pagamento const processPayment = ( strategy: PaymentStrategy, amount: number ): void => { strategy(amount); }; // Uso: simples e direto processPayment(processPayPalPayment, 100); processPayment(processCreditCardPayment, 200); // Ou com seleção dinâmica const paymentStrategies: Record<string, PaymentStrategy> = { paypal: processPayPalPayment, creditCard: processCreditCardPayment, }; const selectedMethod = 'paypal'; processPayment(paymentStrategies[selectedMethod], 150);
A diferença é notável: redução significativa de código, zero boilerplate, e a mesma funcionalidade. Além disso, a versão funcional aproveita as características nativas do JavaScript: funções são cidadãos de primeira classe, podem ser armazenadas em objetos, arrays, passadas como argumentos. Não precisamos simular callbacks com objetos.

Quando usar a abordagem funcional para strategy

A abordagem funcional brilha quando suas estratégias são stateless (sem estado interno). Se você precisa apenas de algoritmos intercambiáveis que operam sobre os dados que recebem, funções são perfeitas.
Exemplo prático: cálculo de frete com diferentes transportadoras.
type ShippingCalculator = (weightInKg: number, distanceInKm: number) => number; const calculateFedExShipping: ShippingCalculator = (weightInKg, distanceInKm) => { const weightCost = weightInKg * 0.5; const distanceCost = distanceInKm * 0.1; return weightCost + distanceCost; }; const calculateUPSShipping: ShippingCalculator = (weightInKg, distanceInKm) => { const weightCost = weightInKg * 0.45; const distanceCost = distanceInKm * 0.12; return weightCost + distanceCost; }; const calculateShippingCost = ( calculator: ShippingCalculator, weightInKg: number, distanceInKm: number ) => calculator(weightInKg, distanceInKm); // Uso const shippingCost = calculateShippingCost(calculateFedExShipping, 10, 500); console.log(`Custo de envio: R$${shippingCost}`);

Alternativa funcional: factory pattern

O Factory Pattern tradicional cria objetos através de classes especializadas. Factory functions em TypeScript fazem o mesmo, mas usando closures para encapsulamento.

A versão orientada a objetos

interface Notification { send(message: string): void; } class EmailNotification implements Notification { send(message: string): void { console.log(`Enviando email: ${message}`); } } class SMSNotification implements Notification { send(message: string): void { console.log(`Enviando SMS: ${message}`); } } class NotificationFactory { createNotification(type: string): Notification { if (type === 'EMAIL') { return new EmailNotification(); } else if (type === 'SMS') { return new SMSNotification(); } throw new Error('Tipo de notificação não suportado'); } } // Uso const factory = new NotificationFactory(); const emailNotif = factory.createNotification('EMAIL'); emailNotif.send('Olá Mayk!');

A versão funcional

Factory functions são funções que criam e retornam objetos. Simples assim. Elas usam closures para criar variáveis verdadeiramente privadas, sem necessidade de new, this ou herança baseada em protótipos.
// Factory function simples function createUser(name: string) { // Closure mantém dados privados const createdAt = new Date(); return { name, greet() { console.log(`Olá, eu sou ${name}`); }, getAccountAge() { const now = new Date(); const ageInYears = now.getFullYear() - createdAt.getFullYear(); return ageInYears; } }; } // Uso - sem new! const user = createUser('Rodrigo'); user.greet(); // Olá, eu sou Rodrigo
Um exemplo mais completo com composição de funcionalidades:
// Factory base para personagem function createCharacter(name: string, initialHitPoints: number) { let currentHitPoints = initialHitPoints; return { name, getHitPoints() { return currentHitPoints; }, takeDamage(damageAmount: number) { currentHitPoints -= damageAmount; console.log(`${name} sofreu ${damageAmount} de dano`); return this; }, isAlive() { return currentHitPoints > 0; } }; } // Factory para jogador que compõe o character function createPlayer(name: string, weaponName: string) { const character = createCharacter(name, 100); return { ...character, weapon: weaponName, attack(target: ReturnType<typeof createCharacter>) { const attackDamage = 20; console.log(`${name} ataca com ${weaponName}`); target.takeDamage(attackDamage); return this; } }; } // Uso com method chaining const player = createPlayer('Diego', 'Espada'); const enemy = createCharacter('Orc', 80); player.attack(enemy).attack(enemy); console.log(`Inimigo vivo? ${enemy.isAlive()}`);

As vantagens das factory functions

Factory functions oferecem encapsulamento real através de closures. Diferente de propriedades privadas de classes (que são apenas convenção ou syntax sugar), variáveis dentro de closures são verdadeiramente inacessíveis:
function createBankAccount(initialBalance: number) { // Variável REALMENTE privada via closure let balance = initialBalance; return { deposit(amount: number) { balance += amount; }, withdraw(amount: number) { if (amount <= balance) { balance -= amount; } }, getBalance() { return balance; } // balance não é acessível diretamente! }; } const account = createBankAccount(1000); account.deposit(500); console.log(account.getBalance()); // 1500 console.log(account.balance); // undefined - privado de verdade!
Além disso, factory functions favorecem composição sobre herança. Você pode facilmente combinar funcionalidades de múltiplas fontes sem criar hierarquias complexas de classes:
// Mixin funcional function withMagicAbilities<T extends { name: string }>(character: T) { let currentMana = 100; return { ...character, getMana() { return currentMana; }, castSpell(spellName: string) { const manaCost = 20; if (currentMana >= manaCost) { console.log(`${character.name} conjura ${spellName}!`); currentMana -= manaCost; } else { console.log(`${character.name} não tem mana suficiente!`); } return this; } }; } // Compondo funcionalidades const player = createPlayer('Laís', 'Cajado'); const mage = withMagicAbilities(player); mage.attack(enemy).castSpell('Bola de Fogo');

Considerações sobre closures

Vale mencionar que closures, apesar de poderosas, têm algumas considerações importantes:
Performance: cada instância criada via factory function mantém suas próprias cópias de funções na memória. Para aplicações que criam milhares de objetos similares, classes podem ser mais eficientes em termos de memória, já que os métodos são compartilhados via prototype chain.
Debugging: closures podem tornar o debugging um pouco mais desafiador, já que variáveis privadas não aparecem no console ou em ferramentas de inspeção. Em classes, propriedades privadas são mais visíveis durante o debug.
Dito isso, para a maioria das aplicações modernas, esses trade-offs são aceitáveis. A simplicidade e clareza do código funcional geralmente superam essas pequenas desvantagens.

Alternativa funcional: singleton pattern

Aqui está uma revelação: você provavelmente não precisa implementar o Singleton Pattern em TypeScript. Módulos ES6 já são singletons por natureza! 🎉

A versão orientada a objetos

class DatabaseConnection { private static instance: DatabaseConnection; private connection: any; private constructor() { this.connection = {}; // lógica de conexão } public static getInstance(): DatabaseConnection { if (!DatabaseConnection.instance) { DatabaseConnection.instance = new DatabaseConnection(); } return DatabaseConnection.instance; } public query(sql: string) { console.log(`Executando query: ${sql}`); } } // Uso const db1 = DatabaseConnection.getInstance(); const db2 = DatabaseConnection.getInstance(); console.log(db1 === db2); // true - mesma instância

A versão com módulos ES6

Módulos ES6/TypeScript são executados apenas uma vez, independentemente de quantas vezes sejam importados. O sistema de cache do Node.js/V8 garante que a mesma instância seja retornada. Isso torna os módulos singletons naturais!
// databaseModule.ts class DatabaseConnection { private connection: any; constructor() { console.log('Criando conexão com banco...'); this.connection = {}; // lógica de conexão } query(sql: string) { console.log(`Executando query: ${sql}`); } } // Cria e exporta a instância única export const database = new DatabaseConnection(); // Ou sem classe, puramente funcional: const connection = createDatabaseConnection(); export function executeQuery(sql: string) { return connection.execute(sql); }
Agora, em qualquer arquivo que importar esse módulo, você terá acesso à mesma instância:
// arquivo1.ts import { database } from './databaseModule'; database.query('SELECT * FROM users'); // Usa a mesma instância// arquivo2.ts import { database } from './databaseModule'; database.query('SELECT * FROM products'); // Mesma instância!
Essa abordagem com módulos para gerenciar uma instância única, como a de um banco de dados, é extremamente poderosa no back-end. Afinal, essa é apenas uma peça no quebra-cabeça de uma API escalável. Se você tem interesse em construir toda a arquitetura por trás de uma aplicação — desde a lógica de negócio até a exposição de endpoints seguros — aplicar estes e outros padrões em um ambiente como o Node.js é o seu campo de jogo.

Por que singleton é considerado um anti-pattern

O Singleton clássico tem problemas bem documentados:
  1. Quebra o princípio da responsabilidade única: a classe gerencia tanto sua lógica quanto seu próprio ciclo de vida
  1. Dificulta testes: todos os testes compartilham a mesma instância, causando contaminação entre testes
  1. Dependências ocultas: hard-coded references tornam o código fortemente acoplado
  1. Impede dependency injection: não pode injetar mocks ou implementações alternativas
Módulos ES6 resolvem esses problemas de forma mais elegante:
// configService.ts interface AppConfig { apiUrl: string; apiKey: string; timeoutInMs: number; } let config: AppConfig = { apiUrl: 'https://api.example.com', apiKey: '12345', timeoutInMs: 5000 }; export function getConfig(): AppConfig { return { ...config }; // retorna cópia para evitar mutação externa } export function updateConfig(newConfig: Partial<AppConfig>): void { config = { ...config, ...newConfig }; } // Para testes, você pode facilmente resetar ou mockar export function resetConfig(): void { config = { apiUrl: 'https://api.example.com', apiKey: '12345', timeoutInMs: 5000 }; }

Quando usar cada abordagem

A escolha entre programação funcional e orientada a objetos não é binária. TypeScript permite ambos os paradigmas, e você pode (e deve!) usar o melhor de cada mundo.

Prefira a abordagem funcional quando

Transformação de dados: pipelines de processamento, validações, formatações
Estratégias stateless: algoritmos que operam apenas sobre os dados recebidos
Composição é prioritária: quando você quer combinar comportamentos de forma flexível
Testabilidade é crítica: funções puras são triviais de testar, sem setup complexo
Codebase funcional: React, Redux, ou qualquer projeto que já usa paradigma funcional

Considere OO quando

🔷 Modelagem de domínio complexo: entidades com comportamento rico e regras de negócio encapsuladas
🔷 Estado mutável otimizado: quando performance de memória é crítica e você precisa criar muitas instâncias
🔷 Frameworks orientados a objetos: Angular, NestJS, ou bibliotecas que esperam classes
🔷 Hierarquias bem definidas: quando polimorfismo e herança realmente fazem sentido

A abordagem híbrida (recomendada!)

Na prática, a maioria das codebases TypeScript beneficia de uma abordagem híbrida:
class UserService { constructor(private readonly apiClient: ApiClient) {} // Método puro - testável e previsível validateUser(user: User): ValidationResult { return { isValid: this.isValidEmail(user.email) && this.isValidAge(user.age), errors: this.collectValidationErrors(user) }; } // Método impuro - side effect isolado async saveUser(user: User): Promise<void> { const validation = this.validateUser(user); if (!validation.isValid) { throw new Error('Usuário inválido'); } await this.apiClient.post('/users', user); } private isValidEmail(email: string): boolean { return /\S+@\S+\.\S+/.test(email); } private isValidAge(age: number): boolean { const minimumAge = 18; return age >= minimumAge; } private collectValidationErrors(user: User): string[] { const errors: string[] = []; if (!this.isValidEmail(user.email)) { errors.push('Email inválido'); } if (!this.isValidAge(user.age)) { errors.push('Idade mínima: 18 anos'); } return errors; } }
Use classes para agrupar funcionalidade relacionada, mas mantenha métodos puros sempre que possível. Separe claramente lógica pura de side effects. Favoreça composição sobre herança.

Conclusão e próximos passos

A programação funcional em TypeScript não é sobre dogmatismo ou seguir regras rígidas. É sobre escolher as ferramentas certas para cada trabalho. Os padrões de design clássicos continuam valiosos, mas suas implementações podem (e devem!) ser adaptadas para o paradigma que você está usando.
Quando você abraça funções de primeira classe, composição e imutabilidade, o código fica mais simples, testável e idiomático. Strategy Pattern vira funções intercambiáveis. Factory Pattern vira factory functions com closures. Singleton vira módulos ES6.
A boa notícia? Você não precisa reescrever toda sua codebase. Comece pequeno: use const ao invés de let, extraia funções puras, experimente factory functions em novos recursos. A transição para código mais funcional é gradual e pragmática.
O ecossistema TypeScript moderno, especialmente com React e frameworks similares, já abraçou a programação funcional. E é exatamente aqui que frameworks como o Next.js brilham. Se você quer levar essa filosofia funcional para o próximo nível e construir aplicações completas, desde o roteamento até a renderização no servidor, entender como o Next.js organiza esses conceitos é fundamental. Adaptar seus padrões de design para esse contexto não é apenas possível, é a abordagem mais natural e produtiva. 💜
E você, qual outro padrão de design tem dificuldade em aplicar no paradigma funcional? Conta para a gente lá na comunidade!

Conheça o Rocketseat Para Empresas

Oferecemos soluções personalizadas para empresas de todos os portes.

Rocketseat

Rocketseat

Ecossistema de educação contínua referência em programação e Inteligência Artificial.

Artigos_

Explore conteúdos relacionados

Descubra mais artigos que complementam seu aprendizado e expandem seu conhecimento.

Imagem contendo uma carta e um símbolo de check
NewsletterReceba conteúdos inéditos e novidades gratuitamente