Padrões de Projeto em TypeScript: quando e por que
\n\nImagine que você está construindo uma casa. Pode-se colocar tijolos de qualquer jeito, e ela provavelmente não desabará. Mas será confortável morar nela? Dificilmente. A arquitetura de software é a mesma construção, só que em vez de tijolos, temos classes e funções. Padrões de projeto em TypeScript são plantas testadas para problemas arquiteturais típicos. Mas, como qualquer ferramenta, eles precisam ser usados com sabedoria.
\n\nNeste artigo, não vamos recontar a teoria seca dos livros do GoF (Gang of Four). Em vez disso, vamos analisar quais padrões de projeto TypeScript são realmente necessários no desenvolvimento moderno, quando eles salvam o projeto e quando só trazem dor de cabeça. Vamos nessa!
\n\nPor que o TypeScript muda as regras do jogo?
\n\nAntes de mergulhar nos padrões, vamos lembrar a principal vantagem do TypeScript — a tipagem estática. Em JavaScript, frequentemente usávamos padrões para compensar a natureza dinâmica da linguagem. Por exemplo, o Factory Method (Método Fábrica) ajudava a esconder a complexidade da criação de objetos. Em TypeScript, essa complexidade muitas vezes é resolvida simplesmente com interfaces e genéricos.
\n\nMas isso não significa que os padrões morreram. Pelo contrário, o TypeScript os torna mais expressivos e seguros. O compilador verifica se você implementou a estrutura corretamente e não permite que você cometa erros bobos. É aqui que o TypeScript revela o poder dos padrões GoF.
\n\nTop 5 padrões que você usará com mais frequência
\n\n1. Singleton — quando um é bom, e dois são demais
\n\nQuando usar: Você precisa de exatamente uma instância de uma classe para toda a aplicação. Exemplos clássicos são logger, configuração da aplicação, conexão com o banco de dados.
\n\nPor que em TypeScript: O TypeScript permite tornar o Singleton thread-safe e type-safe. Sem ele, você corre o risco de criar dezenas de objetos que desperdiçarão memória e confundirão o estado.
\n\nExemplo de código:
\n\nclass DatabaseConnection {\n private static instance: DatabaseConnection;\n private connectionString: string;\n\n private constructor(connectionString: string) {\n this.connectionString = connectionString;\n // Simulação de conexão\n console.log(`Conectando a ${connectionString}`);\n }\n\n static getInstance(connectionString: string): DatabaseConnection {\n if (!DatabaseConnection.instance) {\n DatabaseConnection.instance = new DatabaseConnection(connectionString);\n }\n return DatabaseConnection.instance;\n }\n\n query(sql: string): void {\n console.log(`Executando consulta: ${sql}`);\n }\n}\n\n// Uso\nconst db = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');\ndb.query('SELECT * FROM users');\n\n// Tentar criar outra conexão retornará a mesma instância\nconst db2 = DatabaseConnection.getInstance('postgres://localhost:5432/mydb');\nconsole.log(db === db2); // true\n\nAntipadrão: Não crie um Singleton por criar. Se o objeto pode existir em várias instâncias com estados diferentes (por exemplo, conexões diferentes para bancos diferentes), então Singleton é um mal.
\n\n2. Factory Method (Método Fábrica) — criação de objetos sem dor
\n\nQuando usar: Você não sabe de antemão qual objeto exato precisa criar. Ou o processo de criação é complexo e requer lógica.
\n\nPor que em TypeScript: O Factory Method combina perfeitamente com interfaces e genéricos. Você ganha flexibilidade sem perder a tipagem.
\n\nExemplo de código:
\n\n// Produto abstrato\ninterface Notification {\n send(message: string): void;\n}\n\n// Produtos concretos\nclass EmailNotification implements Notification {\n send(message: string): void {\n console.log(`Enviando email: ${message}`);\n }